利用canvas的getImageData方法 提取图片的像素,然后再计算颜色的出现频率,然后再进行频率的排序

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style>
			.test{
				padding: 20px 20px;
			}
			#mianColor{
				display: flex;
				width: 100px;
				justify-content: space-between;
				margin-top: 20px;
			}
			.mianColorCss{
				width: 10px;
				height: 10px;
				border-radius: 50%;
			}
		</style>
	</head>
	<body>
		<img src="https://grocery-cdn.huaban.com/file/6ccf5020-b653-11ed-ab85-ef9549ca1204.jpg" width="100px"/>
		<div class="test">
			<div id="show"></div>
			<div><input type="color" id="htmlColorEl"></div>
			<div id="mianColor"></div>
		</div>
	</body>
	<script>
		getColor();
		function getColor(){
			//input在html5中有新增了type=color的属性有一个直接值的取色器加拾色器,点击滴管进入取色,监听input事件追踪值的变化 input的value值会返回十六进制,所以写了一个内置conversion函数转成rgb
			//vue中document.getElementById("htmlColorEl").value也可以用refs获取,把事件监听写在生命周期的钩子里
			document.getElementById("htmlColorEl").addEventListener("input", (e) => {
				let conversion = function(value) {
					// 16进制颜色值的正则
					let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
					// 把颜色值变成小写
					let color = value.toLowerCase();
					if (reg.test(color)) {
						// 如果只有三位的值,需变成六位,如:#fff => #ffffff
						if (color.length === 4) {
							let colorNew = "#";
						for (let i = 1; i < 4; i += 1) {
								colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1));
							}
							color = colorNew;
						}
						// 处理六位的颜色值,转为RGB
						let colorChange = [];
						for (let j = 1; j < 7; j += 2) {
							colorChange.push(parseInt("0x" + color.slice(j, j + 2)));
						}
						return "RGB(" + colorChange.join(",") + ")";
					} else {
						return color;
					}
				};
			
				var test = conversion(document.getElementById("htmlColorEl").value)
				console.log(document.getElementById("htmlColorEl").value, 'test', test)
				document.getElementById("show").innerHTML = test
			})
		}
		
		setMainColor(
			"https://grocery-cdn.huaban.com/file/6ccf5020-b653-11ed-ab85-ef9549ca1204.jpg"
			);

		function setMainColor(image){
			console.log("sss")
			getMainColor(image).then(v=>{
				var test1 = ''
				for(let i=0; i<5; i++){
					  test1 += '<div class="mianColorCss" style="background-color: '+v[i].color+';"></div>'
				}
				document.getElementById("mianColor").innerHTML = test1
				console.log(v);//*
			})
		}
		function getMainColor(image) {
			return new Promise((resolve, reject) => {
				try {
					const canvas = document.createElement("canvas");
					const img = new Image(); // 创建img元素
					img.src = image; // 设置图片源地址
					img.onload = () => {
						let color = getImageColor(canvas, img);
						resolve(color);
					};
				} catch (e) {
					reject(e);
				}
			});
		}

		function getImageColor(canvas, img) {
			const context = canvas.getContext("2d");
			context.drawImage(img, 0, 0);

			// 获取像素数据
			let pixelData = context.getImageData(0,0,canvas.width,canvas.height).data;
			// console.log("pixelData", pixelData);

			return getCountsArr(pixelData);
		}

		function getCountsArr(pixelData) {
			let colorList = [];
			let rgba = [];
			let rgbaStr = "";
			// 分组循环
			for (let i = 0; i < pixelData.length; i += 4) {
				rgba[0] = pixelData[i];
				rgba[1] = pixelData[i + 1];
				rgba[2] = pixelData[i + 2];
				rgba[3] = pixelData[i + 3];

				if (rgba.indexOf(undefined) !== -1 || pixelData[i + 3] === 0) {
					continue;
				}
				// console.log("rgba", rgba);
				rgbaStr = rgba.join(",");
				//设置颜色出现频率
				if (rgbaStr in colorList) {
					++colorList[rgbaStr];
				} else {
					colorList[rgbaStr] = 1;
				}
			}
			console.log(colorList)
			let arr = []
			for (let prop in colorList) {
				arr.push({
					// 如果只获取rgb,则为`rgb(${prop})`
					color: `rgba(${prop})`,
					count: colorList[prop],
				});
			}
			// 给提取中的颜色出现频率排序
			arr.sort((a, b) => {
				return b.count - a.count;
			});

			// console.log("arr", arr);
			return arr;
		}
	</script>
</html>

vue的图片主色调提取

主要工具组件  canvas-straw.vue 

<template>
    <div class="color-content" :style="{ '--size': boxSize + 'px', '--pix-size': '10px' }" v-if="value">
        <div class="close-icon" @click="close">close</div>
        <div class="img-box">
            <img :src="src" ref="img" crossOrigin @load="initCanvas()" alt="origin-img">
        </div>
        <div class="pix-box" :style="pixPos">
            <div class="center" :style="{ 'borderColor': `rgb(${color})` }"></div>
            <div class="htmls" v-html="innerVal"></div>
        </div>
    </div>
</template>
<script>
export default {
    name: 'canvas-straw',
    model: {
        event: 'on-change',
        prop: 'value'
    },
    props: {
        boxSize: {
            type: Number,
            default: 100
        },
        value: {
            type: Boolean,
            default: false
        },
        src: {
            type: String,
            default: ''
        }
    },
    data() {
        return {
            color: '153, 153, 153',
            innerVal: '',
            mouseInfo: {
                clientY: 0,
                clientX: 0,
                space: 20,
                size: 100
            }
        }
    },
    computed: {
        pixPos() {
            const width = window.innerWidth
            const height = window.innerHeight
            let { clientY, clientX, space, size } = this.mouseInfo
            let left = clientX
            let top = clientY
            if ((clientY + size) > height) {
                top = clientY - size - space
            } else {
                top += space
            }
            if ((clientX + size) > width) {
                left = clientX - size - space
            } else {
                left += space
            }
            return `left: ${left}px; top:${top}px`
        }
    },
    methods: {
        close() {
            this.$emit('on-change', false)
        },
        initCanvas() {
            let oImg = this.$refs.img
            let canvas = this.draw(oImg)
            oImg.addEventListener('click', (e) => {
                const [r, g, b] = this.color.split(',')
                console.log({ r, g, b })
                this.$emit('on-change', { r, g, b })
            })
            oImg.addEventListener('mousemove', (e) => {
                this.mouseInfo.clientY = e.clientY
                this.mouseInfo.clientX = e.clientX
                let x = e.offsetX
                let y = e.offsetY
                this.color = this.getPix(x, y, canvas.ctx)
            })
        },
        // 画图
        draw(img) {
            let style = window.getComputedStyle(img)
            let width = parseInt(style.width)
            let height = parseInt(style.height)
            img.style.width = width + 'px'
            img.style.height = height + 'px'
            img.style.maxWidth = width + 'px'
            img.style.maxHeight = height + 'px'
            let canvas = document.createElement('canvas')
            canvas.width = width
            canvas.height = height
            let ctx = canvas.getContext('2d')
            ctx.drawImage(img, 0, 0, width, height) // 这里一定要写上获取到图片的宽高,否则生成的图片和原图片大小不一样,吸取到的颜色不准
            return {
                ctx,
                canvas
            }
        },
        // 获取16进制颜色
        gethex(r, g, b) {
            r = r.toString(16)
            g = g.toString(16)
            b = b.toString(16)
            // 补0
            if (r.length === 1) r = '0' + r
            if (g.length === 1) g = '0' + g
            if (b.length === 1) b = '0' + b
            let hex = r + g + b
            // 简化处理,如 FFEEDD 可以写为 FED
            if (r.slice(0, 1) === r.slice(1, 1) && g.slice(0, 1) === g.slice(1, 1) && b.slice(0, 1) === b.slice(1, 1)) {
                hex = r.slice(0, 1) + g.slice(0, 1) + b.slice(0, 1)
            }
            return hex
        },
        // 获取像素颜色
        getPix(x, y, context) {
            const size = 10
            const num = this.boxSize / 2 / size // boxSize (必须是偶数 盒子大小) / 一半 / 每个像素大小
            x = x - num // 减掉自身像素个数的开始坐标
            y = y - num
            // 读取图片像素信息
            const w = num * 2 + 1 // 图片大小是 像素个数的2倍 (并多一行一列像素 为了视觉上的中心点 所以必须是奇数)
            const h = num * 2 + 1
            const centerPos = Math.ceil(w / 2) // 获取中心点坐标 向上取整
            let imageData = context.getImageData(x, y, w, h) // 截取 当前坐标下 w,h大小画图的数据
            const pos = this.getPos(imageData.data, w) // 计算当前截取画布下的像素信息(读取像素长度控制在千万以内 否则易导致浏览器崩溃)
            // 生成矩阵数据
            let arr = []
            Array(w).fill().map((item, index) => {
                let tx = index + 1
                const inners = Array(h).fill().map((item2, index2) => {
                    let ty = index2 + 1
                    const color = pos.get(`${tx},${ty}`)
                    // 创建 10 * 10 px大小为单位的像素块
                    arr.push(`<div data-set="${tx},${ty}" style="left:${index * 10}px; top:${index2 * 10}px; background: rgb(${color})"></div>`)
                    return '#' + color
                })
                return inners
            })
            // 更新数据
            this.innerVal = arr.join('')
            // 返回当前中心坐标的 颜色值
            return pos.get(`${centerPos},${centerPos}`)
        },

        // 计算像素信息并返回
        getPos(data, imgWidth) {
            let pos = new Map()
            let length = data.length

            for (let i = 0; i < length; i++) {
                if (i % 4 === 0) { // 每四个元素为一个像素数据 r,g,b,alpha
                    let x = i / 4 % imgWidth + 1 // 横坐标
                    let y = Math.floor(i / 4 / imgWidth) + 1 // 纵坐标
                    let alpha = Math.round(data[i + 3] / 255 * 100) / 100 // alpha 值

                    if (data[i + 3] === 255) { // 没有alpha 值
                        // let hex = this.gethex(data[i], data[i + 1], data[i + 2])
                        let hex = `${data[i]}, ${data[i + 1]}, ${data[i + 2]}`
                        pos.set(`${x},${y}`, hex)
                    } else if (alpha > 0) { // 有alpha 值
                        let rgba = `${data[i]}, ${data[i + 1]}, ${data[i + 2]}, ${alpha}`
                        pos.set(`${x},${y}`, rgba)
                    }
                }
            }
            return pos
        }
    }
}
</script>
<style lang='less' scoped>
.color-content {
    z-index: 9999;
    position: fixed;
    left: 0px;
    top: 0px;
    width: 100%;
    height: 100%;
    text-align: center;
    padding: 20px;

    .img-box {
        display: flex;
        width: 100%;
        height: 100%;
        justify-content: center;
        align-items: center;
    }

    img {
        cursor: crosshair;
        max-width: 100%;
        max-height: 100%;
    }
}

.close-icon {
    position: fixed;
    right: 10px;
    top: 10px;
    cursor: pointer;
    transition: ease-in-out 0.3s;
    color: #ebebeb;

    &:hover {
        color: #ff0000;
    }
}
</style>
<style lang="less">
@pix-size: 10px;

.pix-box {
    z-index: 999;
    position: absolute;
    // left: 20px;
    top: 30px;
    width: calc(var(--size) + @pix-size + 2px);
    height: calc(var(--size) + @pix-size + 2px);
    box-sizing: border-box;
    border: #fff solid 1px;
    border-radius: 50%;
    overflow: hidden;
    box-shadow: 0px 0px 5px #999;

    div.htmls {
        position: absolute;
        left: 0px;
        top: 0px;
        width: 100%;
        height: 100%;

        &>div {
            width: @pix-size;
            height: @pix-size;
            position: absolute;
            border: none;
        }
    }

    div.center {
        position: absolute;
        width: @pix-size;
        height: @pix-size;
        left: calc(var(--size) / 2);
        top: calc(var(--size) / 2);
        box-sizing: border-box;
        border: #999 solid 1px;
        z-index: 10;
        filter: invert(100%);
    }

    background: url('./images/bg-pixel.jpg')
}</style>

自动提取主色调配置文件 index.ts 

/**
 * 颜色盒子类
 *
 * @param {Array} colorRange    [[rMin, rMax],[gMin, gMax], [bMin, bMax]] 颜色范围
 * @param {any} total   像素总数, imageData / 4
 * @param {any} data    像素数据集合
 */
class ColorBox {
    colorRange: unknown[];
    total: number;
    data: Uint8ClampedArray;
    volume: number;
    rank: number;
    constructor(colorRange: any[], total: number, data: Uint8ClampedArray) {
        this.colorRange = colorRange;
        this.total = total;
        this.data = data;
        this.volume = (colorRange[0][1] - colorRange[0][0]) * (colorRange[1][1] - colorRange[1][0]) * (colorRange[2][1] - colorRange[2][0]);
        this.rank = total * this.volume;
    }
    getColor() {
        const total = this.total;
        const data = this.data;
        let redCount = 0,
            greenCount = 0,
            blueCount = 0;

        for (let i = 0; i < total; i++) {
            redCount += data[i * 4];
            greenCount += data[i * 4 + 1];
            blueCount += data[i * 4 + 2];
        }
        return [redCount / total, greenCount / total, blueCount / total];
    }
}

// 获取切割边
const getCutSide = (colorRange: number[][]) => {   // r:0,g:1,b:2
    const arr = [];
    for (let i = 0; i < 3; i++) {
        arr.push(colorRange[i][1] - colorRange[i][0]);
    }
    return arr.indexOf(Math.max(arr[0], arr[1], arr[2]));
}

// 切割颜色范围
const cutRange = (colorRange: number[][], colorSide: number, cutValue: any) => {
    const arr1: number[][] = [];
    const arr2: number[][] = [];
    colorRange.forEach(function (item) {
        arr1.push(item.slice());
        arr2.push(item.slice());
    })
    arr1[colorSide][1] = cutValue;
    arr2[colorSide][0] = cutValue;

    return [arr1, arr2];
}

// 找到出现次数为中位数的颜色
const __quickSort = (arr: any[]): any => {
    if (arr.length <= 1) {
        return arr;
    }
    const pivotIndex = Math.floor(arr.length / 2);
    const pivot = arr.splice(pivotIndex, 1)[0];
    const left = [];
    const right = [];
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].count <= pivot.count) {
            left.push(arr[i]);
        }
        else {
            right.push(arr[i]);
        }
    }
    return __quickSort(left).concat([pivot], __quickSort(right));
}

const getMedianColor = (colorCountMap: Record<string, number>, total: number) => {

    const arr = [];
    for (const key in colorCountMap) {
        arr.push({
            color: parseInt(key),
            count: colorCountMap[key]
        })
    }

    const sortArr = __quickSort(arr);
    let medianCount = 0;
    const medianIndex = Math.floor(sortArr.length / 2)

    for (let i = 0; i <= medianIndex; i++) {
        medianCount += sortArr[i].count;
    }

    return {
        color: parseInt(sortArr[medianIndex].color),
        count: medianCount
    }
}

// 切割颜色盒子
const cutBox = (colorBox: { colorRange: number[][]; total: number; data: Uint8ClampedArray }) => {

    const colorRange = colorBox.colorRange;
    const cutSide = getCutSide(colorRange);
    const colorCountMap: Record<string, number> = {};
    const total = colorBox.total;
    const data = colorBox.data;

    // 统计出各个值的数量
    for (let i = 0; i < total; i++) {
        const color = data[i * 4 + cutSide];

        if (colorCountMap[color]) {
            colorCountMap[color] += 1;
        }
        else {
            colorCountMap[color] = 1;
        }
    }

    const medianColor = getMedianColor(colorCountMap, total);
    const cutValue = medianColor.color;
    const cutCount = medianColor.count;
    const newRange = cutRange(colorRange, cutSide, cutValue);
    const box1 = new ColorBox(newRange[0], cutCount, data.slice(0, cutCount * 4));
    const box2 = new ColorBox(newRange[1], total - cutCount, data.slice(cutCount * 4));
    return [box1, box2];
}

// 队列切割
const queueCut = (queue: any[], num: number) => {
    while (queue.length < num) {
        queue.sort((a: { rank: number }, b: { rank: number }) => {
            return a.rank - b.rank
        });
        const colorBox = queue.pop();
        const result = cutBox(colorBox);
        queue = queue.concat(result);
    }
    return queue.slice(0, num)
}

// 颜色去重
const colorFilter = (colorArr: number[][], difference: number) => {
    for (let i = 0; i < colorArr.length; i++) {
        for (let j = i + 1; j < colorArr.length; j++) {
            if (Math.abs(colorArr[i][0] - colorArr[j][0]) < difference && Math.abs(colorArr[i][1] - colorArr[j][1]) < difference && Math.abs(colorArr[i][2] - colorArr[j][2]) < difference) {
                colorArr.splice(j, 1)
                j--
            }
        }
    }
    return colorArr
}

/**
 * 提取颜色
 * @param colorNumber 提取最大颜色数量
 * @param img 需要提取的图片
 * @param difference 图片颜色筛选精准度
 * @param callback 回调函数
 */
const themeColor = (colorNumber: number, img: CanvasImageSource, difference: number, callback: (arg0: number[][]) => void) => {
    const canvas = document.createElement('canvas') as HTMLCanvasElement;
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
    let width = 0
    let height = 0
    let imageData = null

    canvas.width = img.width as number;
    width = canvas.width as number
    canvas.height = img.height as number
    height = canvas.height

    ctx.drawImage(img, 0, 0, width, height);

    imageData = ctx.getImageData(0, 0, width, height).data;

    const total = imageData.length / 4;

    let rMin = 255,
        rMax = 0,
        gMin = 255,
        gMax = 0,
        bMin = 255,
        bMax = 0;

    // 获取范围
    for (let i = 0; i < total; i++) {
        const red = imageData[i * 4];
        const green = imageData[i * 4 + 1];
        const blue = imageData[i * 4 + 2];

        if (red < rMin) {
            rMin = red;
        }

        if (red > rMax) {
            rMax = red;
        }

        if (green < gMin) {
            gMin = green;
        }

        if (green > gMax) {
            gMax = green;
        }

        if (blue < bMin) {
            bMin = blue;
        }

        if (blue > bMax) {
            bMax = blue;
        }
    }

    const colorRange = [[rMin, rMax], [gMin, gMax], [bMin, bMax]];
    const colorBox = new ColorBox(colorRange, total, imageData);
    const colorBoxArr = queueCut([colorBox], colorNumber);
    let colorArr = [];

    for (let j = 0; j < colorBoxArr.length; j++) {
        colorBoxArr[j].total && colorArr.push(colorBoxArr[j].getColor())
    }

    colorArr = colorFilter(colorArr, difference)

    callback(colorArr);
}

export default themeColor

使用方式

<template>
  <div>
    <div  style="width: 20%;">
      <div>
        <canvas style="display: none" id="canvas"></canvas>
        <i-button @click="show = !show">打开</i-button>
    <canvas-straw :src="imgSrc" v-model="show"></canvas-straw>

      </div>
      <div id="extract-color-id" class="extract-color" style="display: flex;padding: 0 10px; justify-content:space-around;">
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import themeColor from './index';
import canvasStraw from './canvas-straw.vue'

const name = `index`;

export default Vue.extend({
  name,
  components: {
    canvasStraw
  },
  data() {
    return {
      show: false,
      colorArr: [],
      imgSrc: 'https://grocery-cdn.huaban.com/file/6ccf5020-b653-11ed-ab85-ef9549ca1204.jpg'
    };
  },
  computed: {

  },
  watch: {},
  created() {},
  destroyed() {},
  mounted() {
    //将rgb转化成hex
    const rgbToHex = (r, g, b) => {
    const toHex = num => {
        const hex = num.toString(16);
        return hex.lenght === 1 ? `0${hex}` : hex;
    };
    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
    /**
     * 设置颜色方法
     */
    const SetColor = (colorArr: number[][]) => {
      console.log(colorArr)
      // 初始化删除多余子节点
      const extractColor = document.querySelector('#extract-color-id') as HTMLElement;
      while (extractColor.firstChild) {
        extractColor.removeChild(extractColor.firstChild);
      }
      // 创建子节点
      for (let index = 0; index < colorArr.length; index++) {
        const bgc = '(' + colorArr[index][0] + ',' + colorArr[index][1] + ',' + colorArr[index][2] + ')';
        const hex = rgbToHex(Math.round(colorArr[index][0]),Math.round(colorArr[index][1]),Math.round(colorArr[index][2]))
        const colorBlock = document.createElement('div') as HTMLElement;
        colorBlock.addEventListener('click', ()=>{
          var input = document.createElement("input"); // 创建input对象
          input.value = hex; // 设置复制内容
          document.body.appendChild(input); // 添加临时实例
          input.select(); // 选择实例内容
          document.execCommand("Copy"); // 执行复制
          document.body.removeChild(input); // 删除临时实例
          this.$message.success('复制成功!');
        });
        colorBlock.style.cssText = 'height: 20px;width: 20px;margin-right: 5px;border-radius: 50%;';
        colorBlock.style.backgroundColor = `rgb${bgc}`;
        extractColor.appendChild(colorBlock);
      }
    };
    const img = new Image();
    img.src = this.imgSrc;
    img.crossOrigin = 'anonymous';
    img.onload = () => {
      themeColor(30, img, 20, SetColor);
    };
  },
  methods: {

  },
});
</script>
<style lang="less" scoped></style>

GitHub 加速计划 / vu / vue
108
18
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
9e887079 [skip ci] 1 年前
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 1 年前
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐