useThemeColor
首先创建一个canvas容器 将图片绘制到容器中 使用getImageData方法获取rgba, 查看getImageData 4通过中位数切分算法切割并提取颜色 筛选掉相似的颜色
使用方法:
const {colors, extract} = useThemeColor()
ts
// src/hooks/useThemeColor.ts
import { ref } from 'vue'
class ColorBox {
constructor(
public colorRange: number[][],
public total: number,
public data: Uint8ClampedArray
) {
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
}
volume: number
rank: number
getColor(): number[] {
let red = 0, green = 0, blue = 0
for (let i = 0; i < this.total; i++) {
red += this.data[i * 4]
green += this.data[i * 4 + 1]
blue += this.data[i * 4 + 2]
}
return [Math.round(red / this.total), Math.round(green / this.total), Math.round(blue / this.total)]
}
}
function getCutSide(colorRange: number[][]) {
const diff = colorRange.map(([min, max]) => max - min)
return diff.indexOf(Math.max(...diff))
}
function cutRange(colorRange: number[][], side: number, cut: number): number[][][] {
const left = colorRange.map(r => [...r])
const right = colorRange.map(r => [...r])
left[side][1] = cut
right[side][0] = cut
return [left, right]
}
function quickSort(arr: { color: number; count: number }[]): typeof arr {
if (arr.length <= 1) return arr
const pivot = arr.splice(Math.floor(arr.length / 2), 1)[0]
const left = arr.filter(i => i.count <= pivot.count)
const right = arr.filter(i => i.count > pivot.count)
return [...quickSort(left), pivot, ...quickSort(right)]
}
function getMedianColor(map: Record<string, number>) {
const arr = Object.entries(map).map(([color, count]) => ({
color: Number(color),
count
}))
const sorted = quickSort(arr)
const index = Math.floor(sorted.length / 2)
let total = 0
for (let i = 0; i <= index; i++) total += sorted[i].count
return { color: sorted[index].color, count: total }
}
function cutBox(box: ColorBox): ColorBox[] {
const side = getCutSide(box.colorRange)
const map: Record<string, number> = {}
for (let i = 0; i < box.total; i++) {
const color = box.data[i * 4 + side]
map[color] = (map[color] || 0) + 1
}
const median = getMedianColor(map)
const cut = median.color
const count = median.count
const ranges = cutRange(box.colorRange, side, cut)
const leftData = box.data.slice(0, count * 4)
const rightData = box.data.slice(count * 4)
return [
new ColorBox(ranges[0], count, leftData),
new ColorBox(ranges[1], box.total - count, rightData)
]
}
function queueCut(boxes: ColorBox[], target: number): ColorBox[] {
while (boxes.length < target) {
boxes.sort((a, b) => b.rank - a.rank)
const box = boxes.shift()
if (!box) break
boxes.push(...cutBox(box))
}
return boxes
}
function filterSimilarColors(arr: number[][], diff: number): number[][] {
const result: number[][] = []
for (let i = 0; i < arr.length; i++) {
const [r1, g1, b1] = arr[i]
if (
!result.some(
([r2, g2, b2]) =>
Math.abs(r1 - r2) < diff &&
Math.abs(g1 - g2) < diff &&
Math.abs(b1 - b2) < diff
)
) {
result.push(arr[i])
}
}
return result
}
/**
* useThemeColor - Vue 3 hook to extract dominant colors from image
*/
export function useThemeColor() {
const colors = ref<number[][]>([])
const extract = async (
source: string | HTMLImageElement,
options?: {
colorNumber?: number
difference?: number
}
): Promise<number[][]> => {
const { colorNumber = 8, difference = 30 } = options || {}
return new Promise((resolve, reject) => {
const img = typeof source === 'string' ? new Image() : source
if (typeof source === 'string') {
img.crossOrigin = 'anonymous'
img.src = source
}
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
canvas.width = img.width as number
canvas.height = img.height as number
ctx.drawImage(img, 0, 0)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.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 r = imageData[i * 4]
const g = imageData[i * 4 + 1]
const b = imageData[i * 4 + 2]
if (r < rMin) rMin = r
if (r > rMax) rMax = r
if (g < gMin) gMin = g
if (g > gMax) gMax = g
if (b < bMin) bMin = b
if (b > bMax) bMax = b
}
const range = [[rMin, rMax], [gMin, gMax], [bMin, bMax]]
const rootBox = new ColorBox(range, total, imageData)
const boxes = queueCut([rootBox], colorNumber)
let colorArr = boxes.map(box => box.getColor())
colorArr = filterSimilarColors(colorArr, difference)
colors.value = colorArr
resolve(colorArr)
}
img.onerror = reject
})
}
return {
colors,
extract
}
}
Demo
<template>
<div :style="backgroundStyle">
<div
id="extract-color-id"
class="extract-color"
style="display: flex;padding: 0 20px; justify-content:end;">
</div>
</div>
</template>
<script setup lang="ts">
import {useThemeColor} from '@/hooks/useThemeColor'
import {computed, onMounted} from "vue";
const {colors, extract} = useThemeColor()
const backgroundStyle = computed(() => {
if (!colors.value || colors.value.length === 0) return {}
const stops = colors.value
.map(c => `rgb(${c.map(v => Math.round(v)).join(',')})`)
.join(', ')
return {
background: `linear-gradient(135deg, ${stops})`,
animation: 'gradientMove 15s ease infinite',
backgroundSize: '300% 300%',
filter: 'blur(0px)',
}
})
const SetColor = (colorArr: number[][]) => {
// 初始化删除多余子节点
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 colorBlock = document.createElement('div') as HTMLElement;
colorBlock.id = `color-block-id${index}`;
colorBlock.style.cssText = 'height: 50px;width: 50px;margin-right: 10px;border-radius: 50%;';
colorBlock.style.backgroundColor = `rgb${bgc}`;
extractColor.appendChild(colorBlock);
}
};
onMounted(async () => {
const url = 'https://fastly.picsum.photos/id/1039/800/400.jpg?hmac=SKX6rgTRDAY5YFLTjO2eIGoSKhJl1KDQsT13NLejas4';
const img = new Image();
img.src = url;
img.crossOrigin = 'anonymous';
img.onload = () => {
extract(url).then(colors => {
SetColor(colors)
})
};
console.log(colors.value)
})
</script>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
/* 禁止垂直方向回弹 */
overscroll-behavior-y: none;
}
#app {
width: 100%;
height: 100%;
}
/* 动态背景动画 */
@keyframes gradientMove {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
</style>
想法或问题?在 GitHub Issue 下方参与讨论
去评论