使用原生js和vue提取图片主色调
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
·
利用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>
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 年前
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)