项目概况

识别图片颜色

2026-01-295 min read未分类
css

useThemeColor

首先创建一个canvas容器 将图片绘制到容器中 使用getImageData方法获取rgba, 查看getImageData 4通过中位数切分算法切割并提取颜色 筛选掉相似的颜色

使用方法:

const {colors, extract} = useThemeColor()

ts
1// src/hooks/useThemeColor.ts 2import { ref } from 'vue' 3class ColorBox { 4 constructor( 5 public colorRange: number[][], 6 public total: number, 7 public data: Uint8ClampedArray 8 ) { 9 this.volume = 10 (colorRange[0][1] - colorRange[0][0]) * 11 (colorRange[1][1] - colorRange[1][0]) * 12 (colorRange[2][1] - colorRange[2][0]) 13 this.rank = total * this.volume 14 } 15 volume: number 16 rank: number 17 getColor(): number[] { 18 let red = 0, green = 0, blue = 0 19 for (let i = 0; i < this.total; i++) { 20 red += this.data[i * 4] 21 green += this.data[i * 4 + 1] 22 blue += this.data[i * 4 + 2] 23 } 24 return [Math.round(red / this.total), Math.round(green / this.total), Math.round(blue / this.total)] 25 } 26} 27 28function getCutSide(colorRange: number[][]) { 29 const diff = colorRange.map(([min, max]) => max - min) 30 return diff.indexOf(Math.max(...diff)) 31} 32 33function cutRange(colorRange: number[][], side: number, cut: number): number[][][] { 34 const left = colorRange.map(r => [...r]) 35 const right = colorRange.map(r => [...r]) 36 left[side][1] = cut 37 right[side][0] = cut 38 return [left, right] 39} 40 41function quickSort(arr: { color: number; count: number }[]): typeof arr { 42 if (arr.length <= 1) return arr 43 const pivot = arr.splice(Math.floor(arr.length / 2), 1)[0] 44 const left = arr.filter(i => i.count <= pivot.count) 45 const right = arr.filter(i => i.count > pivot.count) 46 return [...quickSort(left), pivot, ...quickSort(right)] 47} 48 49function getMedianColor(map: Record<string, number>) { 50 const arr = Object.entries(map).map(([color, count]) => ({ 51 color: Number(color), 52 count 53 })) 54 const sorted = quickSort(arr) 55 const index = Math.floor(sorted.length / 2) 56 let total = 0 57 for (let i = 0; i <= index; i++) total += sorted[i].count 58 return { color: sorted[index].color, count: total } 59} 60 61function cutBox(box: ColorBox): ColorBox[] { 62 const side = getCutSide(box.colorRange) 63 const map: Record<string, number> = {} 64 65 for (let i = 0; i < box.total; i++) { 66 const color = box.data[i * 4 + side] 67 map[color] = (map[color] || 0) + 1 68 } 69 70 const median = getMedianColor(map) 71 const cut = median.color 72 const count = median.count 73 const ranges = cutRange(box.colorRange, side, cut) 74 75 const leftData = box.data.slice(0, count * 4) 76 const rightData = box.data.slice(count * 4) 77 78 return [ 79 new ColorBox(ranges[0], count, leftData), 80 new ColorBox(ranges[1], box.total - count, rightData) 81 ] 82} 83 84function queueCut(boxes: ColorBox[], target: number): ColorBox[] { 85 while (boxes.length < target) { 86 boxes.sort((a, b) => b.rank - a.rank) 87 const box = boxes.shift() 88 if (!box) break 89 boxes.push(...cutBox(box)) 90 } 91 return boxes 92} 93 94function filterSimilarColors(arr: number[][], diff: number): number[][] { 95 const result: number[][] = [] 96 for (let i = 0; i < arr.length; i++) { 97 const [r1, g1, b1] = arr[i] 98 if ( 99 !result.some( 100 ([r2, g2, b2]) => 101 Math.abs(r1 - r2) < diff && 102 Math.abs(g1 - g2) < diff && 103 Math.abs(b1 - b2) < diff 104 ) 105 ) { 106 result.push(arr[i]) 107 } 108 } 109 return result 110} 111 112/** 113 * useThemeColor - Vue 3 hook to extract dominant colors from image 114 */ 115export function useThemeColor() { 116 const colors = ref<number[][]>([]) 117 118 const extract = async ( 119 source: string | HTMLImageElement, 120 options?: { 121 colorNumber?: number 122 difference?: number 123 } 124 ): Promise<number[][]> => { 125 const { colorNumber = 8, difference = 30 } = options || {} 126 127 return new Promise((resolve, reject) => { 128 const img = typeof source === 'string' ? new Image() : source 129 130 if (typeof source === 'string') { 131 img.crossOrigin = 'anonymous' 132 img.src = source 133 } 134 135 img.onload = () => { 136 const canvas = document.createElement('canvas') 137 const ctx = canvas.getContext('2d')! 138 139 canvas.width = img.width as number 140 canvas.height = img.height as number 141 ctx.drawImage(img, 0, 0) 142 143 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data 144 const total = imageData.length / 4 145 146 let rMin = 255, rMax = 0, 147 gMin = 255, gMax = 0, 148 bMin = 255, bMax = 0 149 150 for (let i = 0; i < total; i++) { 151 const r = imageData[i * 4] 152 const g = imageData[i * 4 + 1] 153 const b = imageData[i * 4 + 2] 154 if (r < rMin) rMin = r 155 if (r > rMax) rMax = r 156 if (g < gMin) gMin = g 157 if (g > gMax) gMax = g 158 if (b < bMin) bMin = b 159 if (b > bMax) bMax = b 160 } 161 162 const range = [[rMin, rMax], [gMin, gMax], [bMin, bMax]] 163 const rootBox = new ColorBox(range, total, imageData) 164 const boxes = queueCut([rootBox], colorNumber) 165 let colorArr = boxes.map(box => box.getColor()) 166 colorArr = filterSimilarColors(colorArr, difference) 167 colors.value = colorArr 168 resolve(colorArr) 169 } 170 171 img.onerror = reject 172 }) 173 } 174 175 return { 176 colors, 177 extract 178 } 179} 180

Demo

text
1<template> 2 <div :style="backgroundStyle"> 3 <div 4 id="extract-color-id" 5 class="extract-color" 6 style="display: flex;padding: 0 20px; justify-content:end;"> 7 </div> 8 </div> 9</template> 10<script setup lang="ts"> 11 12import {useThemeColor} from '@/hooks/useThemeColor' 13import {computed, onMounted} from "vue"; 14 15const {colors, extract} = useThemeColor() 16 17const backgroundStyle = computed(() => { 18 if (!colors.value || colors.value.length === 0) return {} 19 const stops = colors.value 20 .map(c => `rgb(${c.map(v => Math.round(v)).join(',')})`) 21 .join(', ') 22 return { 23 background: `linear-gradient(135deg, ${stops})`, 24 animation: 'gradientMove 15s ease infinite', 25 backgroundSize: '300% 300%', 26 filter: 'blur(0px)', 27 } 28}) 29 30const SetColor = (colorArr: number[][]) => { 31 // 初始化删除多余子节点 32 const extractColor = document.querySelector('#extract-color-id') as HTMLElement; 33 while (extractColor.firstChild) { 34 extractColor.removeChild(extractColor.firstChild); 35 } 36 // 创建子节点 37 for (let index = 0; index < colorArr.length; index++) { 38 const bgc = '(' + colorArr[index][0] + ',' + colorArr[index][1] + ',' + colorArr[index][2] + ')'; 39 const colorBlock = document.createElement('div') as HTMLElement; 40 colorBlock.id = `color-block-id${index}`; 41 colorBlock.style.cssText = 'height: 50px;width: 50px;margin-right: 10px;border-radius: 50%;'; 42 colorBlock.style.backgroundColor = `rgb${bgc}`; 43 extractColor.appendChild(colorBlock); 44 } 45}; 46 47onMounted(async () => { 48 const url = 'https://fastly.picsum.photos/id/1039/800/400.jpg?hmac=SKX6rgTRDAY5YFLTjO2eIGoSKhJl1KDQsT13NLejas4'; 49 const img = new Image(); 50 img.src = url; 51 img.crossOrigin = 'anonymous'; 52 img.onload = () => { 53 extract(url).then(colors => { 54 SetColor(colors) 55 }) 56 }; 57 console.log(colors.value) 58}) 59</script> 60 61<style> 62html, body { 63 margin: 0; 64 padding: 0; 65 width: 100%; 66 height: 100%; 67 /* 禁止垂直方向回弹 */ 68 overscroll-behavior-y: none; 69} 70 71#app { 72 width: 100%; 73 height: 100%; 74} 75 76/* 动态背景动画 */ 77@keyframes gradientMove { 78 0% { 79 background-position: 0% 50%; 80 } 81 50% { 82 background-position: 100% 50%; 83 } 84 100% { 85 background-position: 0% 50%; 86 } 87} 88</style> 89