diff --git a/src/views/api/aplayer/components/aplayer-thumbnail.vue b/src/views/api/aplayer/components/aplayer-thumbnail.vue index d19a40e..dd95e24 100644 --- a/src/views/api/aplayer/components/aplayer-thumbnail.vue +++ b/src/views/api/aplayer/components/aplayer-thumbnail.vue @@ -1,10 +1,11 @@ \ No newline at end of file diff --git a/src/views/api/aplayer/default.jpg b/src/views/api/aplayer/default.jpg deleted file mode 100644 index 69d274c..0000000 Binary files a/src/views/api/aplayer/default.jpg and /dev/null differ diff --git a/src/views/api/aplayer/utils.ts b/src/views/api/aplayer/utils.ts index e17358e..1b2d54d 100644 --- a/src/views/api/aplayer/utils.ts +++ b/src/views/api/aplayer/utils.ts @@ -66,3 +66,157 @@ export function getElementViewTop (element: HTMLElement): number { elementScrollTop = document.body.scrollTop + document.documentElement.scrollTop return actualTop - elementScrollTop } + +interface Color { + r: number + g: number + b: number +} +class OctreeNode { + isLeaf: boolean = false + pixelCount: number = 0 + red: number = 0 + green: number = 0 + blue: number = 0 + children: OctreeNode[] = [] + next: OctreeNode | null = null +} + +let leafNum = 0, + reducible: {[propName: number]: OctreeNode | null} = {} + +function createNode(level: number) { + const node = new OctreeNode() + if (level === 7) { + node.isLeaf = true + leafNum++ + } else { + // 将其丢到第 level 层的 reducible 链表中 + node.next = reducible[level] + reducible[level] = node + } + return node +} + +function addColor(node: OctreeNode, color: Color, level: number) { + if (node.isLeaf) { + node.pixelCount += 1 + node.red += color.r + node.green += color.g + node.blue += color.b + } + else { + let str = '' + let r = color.r.toString(2) + let g = color.g.toString(2) + let b = color.b.toString(2) + while (r.length < 8) r = '0' + r + while (g.length < 8) g = '0' + g + while (b.length < 8) b = '0' + b + + str += r[level] + str += g[level] + str += b[level] + + const index = parseInt(str, 2) + if (!node.children[index]) { + node.children[index] = createNode(level + 1) + } + + if (!node.children[index]) { + console.log(index, level, color.r.toString(2)) + } + addColor(node.children[index], color, level + 1) + } +} + +function reduceTree() { + // 找到最深层次的并且有可合并节点的链表 + let level = 6 + while (!reducible[level]) { + level -= 1 + } + + // 取出链表头并将其从链表中移除 + const node = reducible[level] + if (!node) return + reducible[level] = (node).next + + // 合并子节点 + let r = 0; + let g = 0; + let b = 0; + let count = 0; + for (let i = 0; i < 8; i++) { + if (!node.children[i]) continue + r += node.children[i].red + g += node.children[i].green + b += node.children[i].blue + count += node.children[i].pixelCount + leafNum-- + } + + // 赋值 + node.isLeaf = true + node.red = r + node.green = g + node.blue = b + node.pixelCount = count + leafNum++ +} + +function buidOctree(root: OctreeNode, imageData: Uint8ClampedArray, maxColors: number) { + const total = imageData.length / 4 + for (let i = 0; i < total; i++) { + // 添加颜色 + addColor(root, { + r: imageData[i * 4], + g: imageData[i * 4 + 1], + b: imageData[i * 4 + 2] + }, 0) + // 合并叶子节点 + while (leafNum > maxColors) reduceTree() + } +} + +function colorsStats(node: OctreeNode, colorMap: {[propName: string]: number}) { + if (node.isLeaf) { + const r = Math.floor(node.red / node.pixelCount) + const g = Math.floor(node.green / node.pixelCount) + const b = Math.floor(node.blue / node.pixelCount) + const color = `${r},${g},${b}` + if (colorMap[color]) colorMap[color] += node.pixelCount + else colorMap[color] = node.pixelCount + return + } + + for (let i = 0 ; i < 8 ; i++) { + if (node.children[i]) { + colorsStats(node.children[i], colorMap) + } + } +} + +export const themeColor = function (img: HTMLImageElement): Color { + const canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + width = canvas.width = img.width, + height = canvas.height = img.height + ctx.drawImage(img, 0, 0, width, height) + const imageData = ctx.getImageData(0, 0, width, height).data + const colorMap: {[propName: string]: number} = {} + const root = new OctreeNode() + reducible = {} + leafNum = 0 + buidOctree(root, imageData, 8) + colorsStats(root, colorMap) + const arr: string[] = [] + for (let key in colorMap) { + arr.push(key) + } + arr.sort(function (a, b) { + return colorMap[a] - colorMap[b] + }) + const rgb = arr[arr.length - 1].split(',') + return {r: parseInt(rgb[0]), g: parseInt(rgb[1]), b: parseInt(rgb[2])} +} diff --git a/src/views/api/aplayer/vue-aplayer.vue b/src/views/api/aplayer/vue-aplayer.vue index 72b2fe1..05ee68a 100644 --- a/src/views/api/aplayer/vue-aplayer.vue +++ b/src/views/api/aplayer/vue-aplayer.vue @@ -15,10 +15,11 @@ :pic="currentMusic.pic" :playing="isPlaying" :enable-drag="isFloatMode" - :theme="currentTheme" + :theme="currentMusic.theme || theme" @toggleplay="toggle" @dragbegin="onDragBegin" @dragging="onDragAround" + @adaptingTheme="setSelfAdaptingTheme" />
@@ -67,12 +68,6 @@ import Lyrics from './components/aplayer-lrc.vue' import { warn } from './utils' - /** - * memorize self-adapting theme for cover image urls - * @type {Object.} - */ - const picThemeCache = {} - // mutex playing instance let activeMutex = null @@ -226,7 +221,7 @@ // handle Promise returned from audio.play() // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play audioPlayPromise: Promise.resolve(), - + rejectPlayPromise: Promise.reject(), // @since 1.2.0 float mode @@ -655,28 +650,9 @@ this.audio.src = this.currentMusic.src || this.currentMusic.url } }, - - setSelfAdaptingTheme () { - // auto theme according to current music cover image - if ((this.currentMusic.theme || this.theme) === 'pic') { - const pic = this.currentMusic.pic - // use cache - if (picThemeCache[pic]) { - this.selfAdaptingTheme = picThemeCache[pic] - } else { - try { - new ColorThief().getColorAsync(pic, ([r, g, b]) => { - picThemeCache[pic] = `rgb(${r}, ${g}, ${b})` - this.selfAdaptingTheme = `rgb(${r}, ${g}, ${b})` - }) - } catch (e) { - warn('color-thief is required to support self-adapting theme') - } - } - } else { - this.selfAdaptingTheme = null - } - }, + setSelfAdaptingTheme (color) { + this.selfAdaptingTheme = `rgb(${color.r},${color.g},${color.b})` + } }, watch: { music (music) { @@ -685,9 +661,6 @@ currentMusic: { handler (music) { - // async - this.setSelfAdaptingTheme() - const src = music.src || music.url // HLS support if (/\.m3u8(?=(#|\?|$))/.test(src)) { @@ -753,7 +726,6 @@ }, mounted () { this.initAudio() - this.setSelfAdaptingTheme() if (this.autoplay) this.play() }, beforeDestroy () {