From 8bada69218415f005a7b0a08f29b27d2a228985c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E4=BA=91=E8=8B=8D=E7=8B=97?= Date: Wed, 20 Mar 2024 15:56:35 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20banner=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BC=B9=E5=B9=95=E5=8A=A8=E7=94=BB=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hexo-theme-async-ts/global.d.ts | 25 ++ packages/hexo-theme-async-ts/src/init.ts | 4 +- .../hexo-theme-async-ts/src/plugins/danmu.ts | 265 ++++++++++++++++++ packages/hexo-theme-async/_config.yml | 4 + .../layout/_partial/script.ejs | 8 +- .../layout/_third-party/plugin.ejs | 4 + .../scripts/helper/async_config.js | 1 + 7 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 packages/hexo-theme-async-ts/src/plugins/danmu.ts diff --git a/packages/hexo-theme-async-ts/global.d.ts b/packages/hexo-theme-async-ts/global.d.ts index 056a1e5a..5e8cb7b4 100644 --- a/packages/hexo-theme-async-ts/global.d.ts +++ b/packages/hexo-theme-async-ts/global.d.ts @@ -80,6 +80,7 @@ declare interface Window { start_time?: string; prefix?: string; }; + danmu: DanMuOptions; }; PAGE_CONFIG: { @@ -98,6 +99,7 @@ declare interface Window { changeGiscusTheme: () => void; show_date_time: () => void; + danMu: (fun: DanMuFun) => void; // 三方插件 Fancybox: any; @@ -105,3 +107,26 @@ declare interface Window { Swup: any; fjGallery: any; } + +declare type DanMuData = { + id: string | number; + text: string; + url?: string; + avatar?: string; +}; + +declare type DanMuMode = 'half' | 'top' | 'full'; + +declare type DanMuOptions = { + el: string; + speed?: number; + gapWidth?: number; + gapHeight?: number; + avatar?: boolean; + height?: number; + delayRange?: number; + mode?: DanMuMode; + align?: string; +}; + +declare type DanMuFun = () => Promise; diff --git a/packages/hexo-theme-async-ts/src/init.ts b/packages/hexo-theme-async-ts/src/init.ts index 4bc547ad..33ab4efe 100644 --- a/packages/hexo-theme-async-ts/src/init.ts +++ b/packages/hexo-theme-async-ts/src/init.ts @@ -627,12 +627,12 @@ export function ShowDateTime() { } } +window.asyncFun = globalFun; + /** * 初始化 */ export function ready() { - window.asyncFun = globalFun; - PrintCopyright(); /* loading animate */ diff --git a/packages/hexo-theme-async-ts/src/plugins/danmu.ts b/packages/hexo-theme-async-ts/src/plugins/danmu.ts new file mode 100644 index 00000000..ec9c8c4b --- /dev/null +++ b/packages/hexo-theme-async-ts/src/plugins/danmu.ts @@ -0,0 +1,265 @@ +/** + * 弹幕滚动 + */ +class DanMuKu { + box = null; // 弹幕容器 + boxSize = { + width: 0, // 容器宽度 + height: 0, // 容器高度 + }; + + rows = 0; // 行数 + dataList: Array> = []; // 弹幕数据 二维数组 + indexs: number[] = []; // 最新弹幕下标 + idMap = {}; // 记录已出现 id + + avatar = false; // 是否显示头像 + speed = 20; // 弹幕每秒滚动距离 + height = 36; // 弹幕高度 + gapWidth = 20; // 弹幕前后间隔 + gapHeight = 20; // 弹幕上下间隔 + delayRange = 5000; // 延时范围 + align = 'center'; // 轨道纵轴对齐方式 + + animates = new Map(); // 动画执行元素 + stageAnimates = new Map(); + + constructor(options: DanMuOptions) { + this.update(options); + } + + _divisor(mode: DanMuMode) { + let divisor = 0.5; + switch (mode) { + case 'half': + divisor = 0.5; + break; + case 'top': + divisor = 1 / 3; + break; + case 'full': + divisor = 1; + break; + default: + break; + } + return divisor; + } + + /** + * 初始化生成弹道 + */ + _initBarrageList() { + if (!this.box.querySelector(`.barrage-list`)) { + const barrage = document.createElement('div'); + barrage.className = 'barrage-list'; + barrage.setAttribute('style', `gap:${this.gapHeight}px;justify-content: ${this.align};display: flex;height: 100%;flex-direction: column;overflow: hidden;`); + + for (let i = 0; i < this.rows; i++) { + const item = document.createElement('div'); + item.className = `barrage-list-${i}`; + item.setAttribute('style', `height:${this.height}px;position:relative;`); + barrage.appendChild(item); + this.dataList[i] = []; + } + this.box.appendChild(barrage); + } + } + + _pushOne(data: DanMuData) { + const lens = this.dataList.map(item => item.length); + const min = Math.min(...lens); + const index = lens.findIndex(i => i === min); + this.dataList[index].push(data); + } + + _pushList(data: DanMuData[]) { + const list = this._sliceRowList(this.rows, data); + list.forEach((item, index) => { + if (this.dataList[index]) { + this.dataList[index] = this.dataList[index].concat(...item); + } else { + this.dataList[index] = item; + } + }); + } + + _sliceRowList(rows: number, list: DanMuData[]) { + const sliceList = []; + const perNum = Math.round(list.length / rows); + for (let i = 0; i < rows; i++) { + let arr = []; + if (i === rows - 1) { + arr = list.slice(i * perNum); + } else { + i === 0 ? (arr = list.slice(0, perNum)) : (arr = list.slice(i * perNum, (i + 1) * perNum)); + } + sliceList.push(arr); + } + return sliceList; + } + + /** + * 创建弹幕,并执行动画 + * @param {*} item 弹幕数据 + * @param {*} barrageIndex 弹幕轨道下标 + * @param {*} dataIndex 弹幕数据下标 + * @returns + */ + _dispatchItem(item: DanMuData, barrageIndex: number, dataIndex: number) { + if (!item || this.idMap[item.id]) { + return; + } + + this.idMap[item.id] = item.id; + + const parent = this.box.querySelector(`.barrage-list-${barrageIndex}`); + let danmuEl = document.createElement('div'); + danmuEl.className = 'danmu-item'; + danmuEl.setAttribute( + 'style', + ` + height:${this.height}px; + display:inline-flex; + position: absolute; + right: 0; + background-color: var(--trm-danmu-bg-color,#fff); + color: var(--trm-danmu-color,#000); + border-radius: 32px; + padding: ${this.avatar ? `4px 8px 4px 4px` : '4px 8px'}; + align-items: center; `, + ); + danmuEl.innerHTML = ` + ${this.avatar ? `` : ''} +
${item.text}
`; + parent.appendChild(danmuEl); + + const { width } = danmuEl.getBoundingClientRect(); + danmuEl.style.width = `${width}px`; + const allTime = ((this.boxSize.width + width) / this.speed) * 1000; + const pastTime = ((width + this.gapWidth) / (this.boxSize.width + width)) * allTime; + + if (dataIndex < this.dataList[barrageIndex].length) { + const animate = danmuEl.animate({ transform: ['translateX(100%)', `translateX(-${this.gapWidth}px)`] }, { duration: pastTime, fill: 'forwards' }); + + this.stageAnimates.set(animate, animate); + + animate.onfinish = () => { + this.stageAnimates.delete(animate); + this.indexs[barrageIndex] = dataIndex + 1; + this._dispatchItem(this.dataList[barrageIndex][dataIndex + 1], barrageIndex, dataIndex + 1); + }; + } + + const animate = danmuEl.animate({ transform: ['translateX(100%)', `translateX(-${this.boxSize.width}px)`] }, { duration: allTime, fill: 'forwards' }); + + animate.onfinish = () => { + this.animates.delete(animate); + danmuEl.remove(); + danmuEl = null; + }; + + this.animates.set(animate, animate); + } + + /** + * 开始滚动弹幕 + */ + _run() { + const len = this.dataList.length; + for (let barrageIndex = 0; barrageIndex < len; barrageIndex++) { + const row = this.dataList[barrageIndex]; + let dataIndex = this.indexs[barrageIndex]; + + if (!dataIndex && dataIndex !== 0) { + dataIndex = this.indexs[barrageIndex] = 0; + } + + row[dataIndex] && setTimeout(() => this._dispatchItem(row[dataIndex], barrageIndex, dataIndex), Math.random() * this.delayRange); + } + } + + /** + * 暂停滚动 + */ + pause() { + this.animates.forEach(item => item.pause()); + this.stageAnimates.forEach(item => item.pause()); + return this; + } + + /** + * 开始滚动 + */ + play() { + this.animates.forEach(item => item.play()); + this.stageAnimates.forEach(item => item.play()); + return this; + } + + /** + * 清除弹幕 + */ + clear() { + this.dataList = []; + this.indexs = []; + this.idMap = {}; + this.animates.clear(); + this.stageAnimates.clear(); + if (this.box.querySelector('.barrage-list')) { + this.box.removeChild(this.box.querySelector('.barrage-list')); + } + return this; + } + + update(options: DanMuOptions) { + const box = document.querySelector(options.el); + if (box) { + const size = box.getBoundingClientRect(); + this.box = box; + this.boxSize = size; + this.speed = options.speed ?? this.speed; + this.gapWidth = options.gapWidth ?? this.gapWidth; + this.gapHeight = options.gapHeight ?? this.gapHeight; + this.avatar = options.avatar ?? this.avatar; + this.height = options.height ?? this.height; + this.delayRange = options.delayRange ?? this.delayRange; + this.align = options.align ?? this.align; + this.rows = parseInt(((size.height * this._divisor(options.mode ?? 'half')) / (this.height + this.gapHeight)).toString()); + this.clear(); + } else { + throw new Error(`未找到容器 ${options.el}`); + } + return this; + } + + /** + * 添加弹幕数据 + * @param {*} data + */ + pushData(data: DanMuData | DanMuData[]) { + this._initBarrageList(); + switch (Object.prototype.toString.apply(data)) { + case '[object Object]': + this._pushOne(data as DanMuData); + break; + case '[object Array]': + this._pushList(data as DanMuData[]); + break; + } + this._run(); + return this; + } +} + +const danmuku = new DanMuKu(window.ASYNC_CONFIG.danmu); + +window.danMu = async (fun: DanMuFun) => { + danmuku.update(window.ASYNC_CONFIG.danmu).pushData(await fun()); + + if (window.ASYNC_CONFIG.swup) { + document.addEventListener('swup:contentReplaced', async function () { + danmuku.update(window.ASYNC_CONFIG.danmu).pushData(await fun()); + }); + } +}; diff --git a/packages/hexo-theme-async/_config.yml b/packages/hexo-theme-async/_config.yml index 0d768d8a..aea01d18 100644 --- a/packages/hexo-theme-async/_config.yml +++ b/packages/hexo-theme-async/_config.yml @@ -36,6 +36,7 @@ assets: rowcss: css/plugins/bootstrap.row.css typing: js/plugins/typing.js search: js/plugins/local_search.js + danmu: js/plugins/danmu.js main: js/main.js # Icon configuration icons: @@ -98,6 +99,9 @@ top_bars: # Banner settings banner: use_cover: false + danmu: + enable: false + el: .trm-banner default: type: img bgurl: /img/banner.png diff --git a/packages/hexo-theme-async/layout/_partial/script.ejs b/packages/hexo-theme-async/layout/_partial/script.ejs index dce78043..04afa6ef 100644 --- a/packages/hexo-theme-async/layout/_partial/script.ejs +++ b/packages/hexo-theme-async/layout/_partial/script.ejs @@ -1,9 +1,6 @@ <%- partial('../_third-party/plugin',{ loadtype: 'js' }) %> - -<%- partial('../_third-party/cdn',{ cdntype: 'body' }) %> - <% if(hexo_env('cmd') !== 'server'){ %> <%- partial('../_third-party/sw') %> @@ -11,4 +8,7 @@ <%- partial('../_third-party/seo/baidu-push') %> <% } %> - \ No newline at end of file + + + +<%- partial('../_third-party/cdn',{ cdntype: 'body' }) %> \ No newline at end of file diff --git a/packages/hexo-theme-async/layout/_third-party/plugin.ejs b/packages/hexo-theme-async/layout/_third-party/plugin.ejs index 8a97133a..52d0a90d 100644 --- a/packages/hexo-theme-async/layout/_third-party/plugin.ejs +++ b/packages/hexo-theme-async/layout/_third-party/plugin.ejs @@ -83,4 +83,8 @@ plugin = theme.assets.plugin; <% } %> <% } %> + + <% if(theme.banner.danmu.enable) {%> + + <% } %> <% } %> diff --git a/packages/hexo-theme-async/scripts/helper/async_config.js b/packages/hexo-theme-async/scripts/helper/async_config.js index 22583ca1..89d39709 100644 --- a/packages/hexo-theme-async/scripts/helper/async_config.js +++ b/packages/hexo-theme-async/scripts/helper/async_config.js @@ -61,6 +61,7 @@ hexo.extend.helper.register('async_config', function () { start_time: theme.footer?.live_time?.enable ? theme.footer.live_time.start_time : '', prefix: __(theme.footer.live_time.prefix), }, + danmu: theme.banner.danmu, }; // 随便封面