识别图片颜色
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}
180Demo
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