From c0291d2404c641b33253d73951e2a7acf237a526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=8C=E7=B3=96=E5=8C=85=E5=AD=90?= Date: Sat, 28 Jan 2023 03:30:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=99=A8=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E8=89=B2=E6=A0=B9=E6=8D=AE=E4=B8=93=E8=BE=91=E5=B0=81=E9=9D=A2?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aplayer/components/aplayer-thumbnail.vue | 31 ++-- src/views/api/aplayer/default.jpg | Bin 3203 -> 0 bytes src/views/api/aplayer/utils.ts | 154 ++++++++++++++++++ src/views/api/aplayer/vue-aplayer.vue | 40 +---- 4 files changed, 177 insertions(+), 48 deletions(-) delete mode 100644 src/views/api/aplayer/default.jpg 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 69d274cb2e07433db635fd9be71ab60de8d5bd34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3203 zcmah|2UJsM8@>T#2t^W*B_o89h=eQ>AQB*i6(DQ{0mYC+2#`S%rme~l42m*E5KtLH zK!%p-UMK<;WvGDK0wSYzRkYTXKlr=;b9(we_uO;7``-6?-uX%INv8l^w-oj^0H9JW z0cGGDq<;Wl*F;8KDgXi$WM(4(kWR@P$B0CHA_~P7TGN@lD3&#Y$3dmg`6!Gv8U;8w zrSR#DIF<++#foNgNw8;UF2JB{CJ7dTr=e+l3M+=~nI>QbrTGRk(&8BQOqi1+)FFkK z!r^mRB04mM6VDYAQ%JB+;zZeb*^GigKe>qFNHFqpRA?y8A4=f~SWvt*0m(oUY@xRH z))>64t&Ob}6pO~-QD__rhel#?L>n~G1`GWRm@Jxrxs4d;>i#*F?282ZTvT#$vUM`f znkR@xVeIYgQD`g*i$%&jkit~1h@OJv3QfN#xUz%{0h=#k^SIDuMS2u3QAC2tGW~A} z96pWqRrnvf#o;X1^~qZ(3S@mt;~%|+!Kr)}Dv%}QB?=fUnY-y1vaEOi9%$K7mJQLB z$4KO`xFV`62_~~xGuccchCr~f#k-Sn?sl#ij2pqu7VF|_>q2mMA=}tsapW&tDpx3? za~Z5JT=qAv{l9XF6akAa;t7Izy!bEE^N-<)c)}PSA4;KoniiBsXRx`;w&nJI&Xy%$ zC$X6B0v-qYsXs*acRASGQC!hvH+P&39*ZYnY;0UzUEJ*4?J@3XGy!KzfPLmLzpMHG za!|71sO4dPH{35K*~Tm%zpkq65?ZN|qB4~x2yn+%)hVRX?LgX}LuYv+dMNwX9#ixHDIe7rA zu)+ibQ6vY@6Vi{JglTFOU|j;ER-00^FWsJcAt*fcJfj*X1TX6?**vd zD$^G+eAcIZ{jMVxC(gBv=#()uHOySEIlRD-6*Qa^kk=;331bQ-u-L$TyTIh2?!`Fc zrKD}6zvsL(-*=u}0kIzpdg}dvlt``ft)cmPs**3oPPg#w+@6khr4h}83wy?>RF}3w zcaIi}Ms`wvM97h3(U?O5(k%bpg5#gnFAkQ~$J_c&gvB$9fr4>}+RSFR&7^f(IF$5S zU*{@#zB;_x()LiF%h<-(UFdVlth=5w8A|L5&asC{r%UCzb)x=&!r&e)pAXc2iz{lH zvZjvsM{3<{tTfpG>d+?lwNghWjd+`c-N)OybL-O~!_^(~n5x?6xIu5_Fwdpuo|y3D zw(zIFG~IOO9^bOESZrn7^wd;+bzY$t(JNeNMkl!FMrw)=5Nd|!J= z>*n=)gJ=z92P?@1eoGakY6HelT%-=|T2^2sIWev9Sf34S=fvpDt>Y@f6a?YTe@sf=X9w4Vh{YXc4OOXL#5qKv z7V@gi!~@X+r@f>(LhLz8*KTP!^->U9xpMdeRlY2HY!Anw(084Kug2{jL*%`t;EH}o zp?z_C?Q56LAxG8{GPHFdfl@04>>?BBmosj}AKi)_QQlOVcNbD3iLPicW?FD3EW_4c zK3wO_?pj01iHTEh&e06b5&eGfunrD%%{jG>22lxkl9q{bpN*EpVW?e zMIVFlYaKUfcZ|;$PZ3A^E&SXK^G}{DtACaL$Q+S~h$lJ&-SysCv9l(K9E)%Y&7@Yv zq&=&?Y%K&l*JWAqux09?PcP-8=F)G&q&+@wr#zo_j{k+nYvS*mPOp2ELvn0}8`lki zj`%PlPq)U<-z72-Q4M*je7TZ3OWi3f#(-Y_GJ$&|_NBgjnDe$0_~YXAc~8yVq?2sh zy?aaW%!Tl=?$(rf}vy2^`f4=t8F6(~S*&AIWIlNB{>EBolT`?9Grz;m1?uD2ObYMU6 zfpI3T20UC_wP|=l>~f=TMV{UDn%y3Ue_dcNSdXa%A=WmBwI3L;?4?P8PV=`Xc|FN0 z%^{P>N6m#tOE*?{H&!Y8jX1?`e$u4eF;W@{+&Qaw-Z5+k`CU|I%!O_BsoXnOuucKX zcWYMtf_>~lcKMkn#|x@wN@xw~nVkHNhUfV{@Z0sGh~5>FtF`^r$KyNt<#O^iQ=jD; z7I3Y{dibI5i3zjqsS%;q?^V5NZAhEyd3w=C{3Ei)=G5qiS)(^Qy8Ln6n%IH^2`6*A zB_690ONX}mU4H$Z{VM48S&&s{T^*BIVg>hk%VZp3J(!n3UKx4~_gDCbbJr;}sc03c zs}m-hCH4NL`k}+^h*+=mSw)0p67AURYHSy~{<+!inx-wJwwv=kX-_zG;{7Jp+K+A9 z711T9TSZ;b_qYWLU49Kpo&}pC+B|QDy8XOe{b9)t^PmBPbc6>l*P8QMmDas`1nqFU zsKqdD(Q=^CP;&XNg?Bj%PH^fgN1kL!3TTVjBdGGwer3VTH9qAB{*YdRxXj+58l;Ks zV+`o|Wd$@!WW`+Y$5?fC0drnd!?wdQ)lp~o$lRlb?Bb@t4S(nQWfcwAt)`qhOlfY; z`H|LLV#3&8@&ml>@|wlw$-8ZDMFKfY11dquVv?ok-BiD^7hSHt_I zF%enI!ineg4yot)yUhJ=85vU3?^qsK)!O^9Sbb>t4gfV)Gpu#i=*h0WWQUd&X7daJ`gF0Ik`*G8ZwB44^Bcv1g+ z=ofjq;nKN*ZcffXH+rL@@xF}RJ5w%RIXYN21yLI&Yx?ykfN|3c?7UYl$KVV1$@U8- zl-C|%$$9zvf~Qk+)k4q2cAVe#WR2?Ft^Dog26Nndz81UBrmoBp92=c+>8i|E)0(a; z&fgg?Af2Bt!ua{j zbct7oG>L@x@BAgjT{KCY3Y|NW)CjE@E_!VBGS*I+Tk&p}*!ZSy?9WTRMZ;-4ADg+y zK7SX@-c<-27*`0`f_+;zu2*kzV@N>*QiWC-P>}-4&|QPRn_mfQsEHueHzYw%FqmX>G*#DZ6pxe 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 () {