UNPKG

lazy-js-utils

Version:

A collection of lazy-loaded JavaScript utilities for efficient development

1,507 lines (1,473 loc) 189 kB
import { DYNAMIC_IMPORT_RE$1 as DYNAMIC_IMPORT_RE, ESM_STATIC_IMPORT_RE$1 as ESM_STATIC_IMPORT_RE, EXPORT_DECAL_RE$1 as EXPORT_DECAL_RE, EXPORT_DEFAULT_RE$1 as EXPORT_DEFAULT_RE, EXPORT_NAMED_RE$1 as EXPORT_NAMED_RE, EXPORT_STAR_RE$1 as EXPORT_STAR_RE, _toString$1 as _toString, isAbsolute$1 as isAbsolute, isActive$1 as isActive, isBase64$1 as isBase64, isBlob$1 as isBlob, isBottom$1 as isBottom, isBrowser$1 as isBrowser, isComment$1 as isComment, isContainCn$1 as isContainCn, isDate$1 as isDate, isDef$1 as isDef, isDivElement$1 as isDivElement, isESModule$1 as isESModule, isElement$1 as isElement, isEqual$1 as isEqual, isFalse$1 as isFalse, isFile$1 as isFile, isFileType$1 as isFileType, isFn$1 as isFn, isIFrameElement$1 as isIFrameElement, isIPv4$1 as isIPv4, isIPv6$1 as isIPv6, isIdCard$1 as isIdCard, isImageElement$1 as isImageElement, isLeapYear$1 as isLeapYear, isMap$1 as isMap, isMobile$1 as isMobile, isNaN$1 as isNaN, isNameCn$1 as isNameCn, isNameEn$1 as isNameEn, isNil$1 as isNil, isNode$1 as isNode, isNum$1 as isNum, isObject$1 as isObject, isPlainObject$1 as isPlainObject, isPostCode$1 as isPostCode, isPromise$1 as isPromise, isProxyDocument$1 as isProxyDocument, isReg$1 as isReg, isRelative$1 as isRelative, isSameDay$1 as isSameDay, isScriptElement$1 as isScriptElement, isSet$1 as isSet, isShadowRoot$1 as isShadowRoot, isSocketUrl$1 as isSocketUrl, isSoldierId$1 as isSoldierId, isStyleElement$1 as isStyleElement, isSupportCamera$1 as isSupportCamera, isSupportWebp$1 as isSupportWebp, isSymbol$1 as isSymbol, isTrainNumber$1 as isTrainNumber, isTrue$1 as isTrue, isType$1 as isType, isUndef$1 as isUndef, isUrl$1 as isUrl, isVersion$1 as isVersion, isVideo$1 as isVideo, isVue$1 as isVue, isWeakMap$1 as isWeakMap, isWeakSet$1 as isWeakSet, isWin$1 as isWin, parallel$1 as parallel } from "./parallel-DHAKJPw_.js"; import { isArray$1 as isArray, isBool$1 as isBool, isNull$1 as isNull, isStr$1 as isStr } from "./isBool-Bi9uSocl.js"; import { createElement$1 as createElement, findElement$1 as findElement, mount$1 as mount, unmount$1 as unmount, useMutationObserver$1 as useMutationObserver } from "./createElement-D3MQi5A0.js"; import { createChunk$1 as createChunk } from "./createChunk-CBgu98Zd.js"; import { toAbsolutePath$1 as toAbsolutePath } from "./toAbsolutePath-Coj-fxcV.js"; import process from "node:process"; import { DomHandler, Parser } from "htmlparser2"; import QRCode from "qrcode"; //#region src/array/diff.ts /** * * @param { any[] } array1 数组1 * @param { any[] } array2 数组2 * @param { Options } [options] {} * @param { 'same' | 'different' } [options.compare] 'same' | 'different' * @param { 'value' | 'index' } [options.compresultare] 'value' | 'index' * @returns 返回相同项或不同项的索引或值 */ function diff(array1, array2, options = { compare: "same", result: "value" }) { const same = array1.filter((item) => array2.includes(item)); const diff$1 = array1.filter((item) => !array2.includes(item)).concat(array2.filter((item) => !array1.includes(item))); const { compare = "same", result = "value" } = options; if (compare === "same") return result === "value" ? same : same.map((item) => array1.indexOf(item)); return result === "value" ? splitDiff(diff$1) : diff$1.map((item) => array1.indexOf(item)).filter((i) => i >= 0); } function splitDiff(arr) { if (!arr.length) return arr; const mid = Math.floor(arr.length / 2); const left = arr.slice(0, mid); const right = arr.slice(mid); const result = []; for (let i = 0; i < mid; i++) result.push([left[i], right[i]]); return result; } //#endregion //#region src/array/forEach.ts /** * * @param { any[] } array 数组 * @param { Function } callback ForEachCallback * @returns */ function forEach(array, callback) { for (let i = 0; i < array.length; i++) { const res = callback(array[i], i, array); if (!isUndef(res)) return res; } return void 0; } //#endregion //#region src/array/getAverage.ts /** * * @param { number[] } array 数字数组 * @param { number } fraction 保留几位小数 * @returns 平均值 */ function getAverage(array, fraction = 2) { return (array.reduce((pre, cur) => pre + cur) / array.length).toFixed(fraction); } //#endregion //#region src/js/executeStr.ts function executeStr(str) { return new Function(`return (${str})`)(); } //#endregion //#region src/array/quickFilter.ts /** * * @param { any[] } array 数组 * @param { string | Array<string> } key 过滤条件 * @returns */ function quickFilter(array, key) { const reg = /\/[\w. $]+\/[gims]*/; return array.filter((item) => { if (isArray(key)) return key.some((k) => findItem(item, k)); else return findItem(item, key); }); function findItem(item, key$1) { const [_key, _value] = key$1.split("="); if (isUndef(item[_key])) return false; return isDef(_value) ? reg.test(_value) ? new RegExp(executeStr(_value)).test(item[_key]) : _value === item[_key] : /.*/.test(item[key$1]); } } //#endregion //#region src/array/quickFind.ts /** * * @param { T[] } array 数组 * @param { string | number } id 主键 * @returns */ function quickFind(array, id) { const indexMap = new Map(); array.forEach((item, i) => indexMap.set(item[id], i)); return new QuickFind(array, indexMap, id); } var QuickFind = class { constructor(array, indexMap, id) { this.array = array; this.indexMap = indexMap; this.id = id; this.array = array; this.indexMap = indexMap; this.id = id; } find(id) { const index = this.indexMap.get(id); if (isUndef(index)) return void 0; return this.array[index]; } _update(id, key, value) { if (isUndef(key)) { const index = this.indexMap.get(id); if (isUndef(index)) throw new Error("当前id不存在"); if (value[this.id] !== id) throw new Error("不可修改唯一id"); this.array[index] = value; } else { const target = this.find(id); if (isUndef(target)) return this.array; target[key] = value; } return this.array; } delete(id) { const index = this.indexMap.get(id); if (isUndef(index)) return; this.array.splice(index, 1); this.indexMap.delete(id); return this.array; } set(id, key, value) { let tempValue = value; const index = this.indexMap.get(id); if (isUndef(value)) { if (isUndef(key)) return this.array; tempValue = key; } if (isDef(index)) return this._update(id, key, tempValue); else { if (!value) value = key; if (isUndef(value[this.id])) throw new Error("新增的数据必须包含唯一id"); if (value[this.id] !== id) throw new Error("新增的数据id必须与当前id一致"); this.indexMap.set(id, this.array.length); this.array.push(value); return this.array; } } }; //#endregion //#region src/array/sort.ts /** * * @param { any[] } array 数组 * @param { Array<string | number> | number | string } match 匹配条件 * @returns */ function sort(array, match) { if (isType(match, "s|n")) match = [`${match}`]; return match.reduce((result, cur) => { let flag = false; if (cur[0] === "-") { flag = true; cur = cur.slice(1); } return result.sort((a, b) => { if (cur !== "1" && b[cur] === a[cur]) return 0; if (flag) { if (cur === "1") return b > a ? 1 : -1; return b[cur] > a[cur] ? 1 : -1; } if (cur === "1") return a > b ? 1 : -1; return b[cur] > a[cur] ? -1 : 1; }); }, array); } //#endregion //#region src/array/sortByOrder.ts /** * * @param { any[] } sortArr 数组 * @param { string[] } order 顺序 * @param { string } prop 按照哪个属性 * @returns */ function sortByOrder(sortArr, order, prop) { if (!order) return sortArr; const result = []; let insertIndex; const _prop = prop ? prop.split(".") : void 0; order.forEach((key, idx) => { if (key === "*") return insertIndex = idx; const index = sortArr.findIndex((item) => getDepthVal(_prop, item) === key); if (index !== -1) { result.push(sortArr[index]); sortArr.splice(index, 1); } }); if (isDef(insertIndex)) result.splice(insertIndex, 0, ...sortArr); else result.concat(sortArr); return result; function getDepthVal(_prop$1, item) { return _prop$1 ? _prop$1.reduce((result$1, cur) => { return result$1 === null || result$1 === void 0 ? void 0 : result$1[cur]; }, item) : item; } } //#endregion //#region src/array/uniqueArray.ts /** * * @param { any[] } array 数组 * @returns 去重后的数组 */ function uniqueArray(array) { return array.reduce((result, item) => { if (isType(item, "o|a") && !isHave(result, item) || !result.includes(item)) result.push(item); return result; }, []); } function equals(a, b) { if (Object.keys(a).length !== Object.keys(b).length) return false; for (const key in a) if (isType(a[key], "o|a") && isType(b[key], "o|a") && !equals(a[key], b[key]) || a[key] !== b[key]) return false; return true; } function isHave(result, item) { return result.some((i) => isType(i, "o|a") && equals(item, i)); } //#endregion //#region src/array/chunk.ts /** * * @param { T[] } arr 数组 * @param { number } size 以多少为基准分割 * @returns 分割后的数组 */ function chunk(arr, size = 1) { if (size < 1) return []; const result = []; for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size)); return result; } //#endregion //#region src/array/countBy.ts /** * * @param { any[] } array 数组 * @param { Function } iterator 迭代数组函数 * @returns 结果的个数 */ function countBy(array, iterator) { return array.reduce((result, item) => { const val = iterator(item); if (!result[val]) result[val] = 1; else result[val]++; return result; }, {}); } //#endregion //#region src/array/flatten.ts /** * * @param { Record<string, any> | Record<string, any>[] } o 对象或者数组 * @param { string }flattenProps 展开的属性默认为children * @param { boolean }onlyLastNode 只保留最后一层级的数据 * @returns 一层的数组 */ function flatten(o, flattenProps = "children", onlyLastNode = false, result = []) { o = isArray(o) ? o : [o]; o.forEach((node) => { const children = node[flattenProps]; if (!onlyLastNode) result.push(node); if (children) flatten(children, flattenProps, onlyLastNode, result); else if (onlyLastNode) result.push(node); }); return result; } //#endregion //#region src/array/filterEmpty.ts /** * * @param { any[] } array 数组 * @returns 过滤空值后的数组 */ function filterEmpty(array) { return array.filter(Boolean); } //#endregion //#region src/canvas/Canvas.ts var Canvas = class { canvas = document.createElement("canvas"); ctx = this.canvas.getContext("2d"); constructor(width, height) { if (width) this.canvas.width = width * devicePixelRatio; if (height) this.canvas.height = height * devicePixelRatio; return this; } }; //#endregion //#region src/event/insertElement.ts /** * 插入元素 * @param { HTMLElement | string } parent 父元素 * @param { HTMLElement | DocumentFragment | string } element 将被插入的元素 * @param { HTMLElement } target 插入在这个元素之前 * @returns */ function insertElement(parent$1, element, target) { return mount(parent$1, element, (parent$2, element$1) => parent$2.insertBefore(element$1, isNull(target) ? null : target || parent$2.firstChild)); } //#endregion //#region src/event/removeElement.ts /** * 删除元素 * @param { HTMLElement | ChildNode | DocumentFragment | string } el 待被删除的节点 * @returns 父节点 */ function removeElement(el) { if (isStr(el)) el = findElement(el) || el; if (isStr(el)) throw new Error(`${el} is not a element`); const p = el.parentElement; if (p) p.removeChild(el); return p; } //#endregion //#region src/event/useEventListener.ts /** * 事件监听 * @param { Window | Document | Element | string } target 元素 * @param { T } eventName 事件名 * @param { (e: (WindowEventMap & DocumentEventMap)[T]) => void } callback 回调 * @param { boolean | AddEventListenerOptions } useCapture 捕获 * @param { boolean } autoRemove 是否自动被移除 * @returns 停止 */ function useEventListener(target, eventName, callback, useCapture, autoRemove) { let stopped = false; let stop; let animationStop; if (eventName === "DOMContentLoaded") stopped = true; function event(e) { try { var _callback$call; callback === null || callback === void 0 || (_callback$call = callback.call) === null || _callback$call === void 0 || _callback$call.call(callback, e.target, e); } catch (error) { animationStop === null || animationStop === void 0 || animationStop(); throw new Error(error); } if (autoRemove) stop(); } unmount(() => stop === null || stop === void 0 ? void 0 : stop()); mount(target, (target$1) => { const originCall = target$1 === null || target$1 === void 0 ? void 0 : target$1[eventName]; const eventFunction = (e) => { if (stopped) stop === null || stop === void 0 || stop(); try { const isRawEvent = originCall && originCall.toString().includes("() { [native code] }"); if (!isRawEvent && originCall) originCall === null || originCall === void 0 || originCall(); } catch (error) { console.error(error); } event(e); }; target$1.addEventListener(eventName, eventFunction, useCapture); let mutationStop; if (target$1 instanceof Element && target$1.parentNode) mutationStop = useMutationObserver(target$1.parentNode, (mutations) => { for (const mutation of mutations) for (const node of Array.from(mutation.removedNodes)) if (node === target$1) { stop === null || stop === void 0 || stop(); mutationStop === null || mutationStop === void 0 || mutationStop(); } }); stop = () => { target$1.removeEventListener(eventName, eventFunction, useCapture); mutationStop === null || mutationStop === void 0 || mutationStop(); }; }); return () => { if (!stop) return stopped = true; stop === null || stop === void 0 || stop(); }; } //#endregion //#region src/event/useKeyBoard.ts /** * 检测指定按键 * @param { string } c 按键字符串 * @param { Function } callback 按键与案件字符串一致时的回调 * @returns 停止 */ function useKeyBoard(c, callback) { return useEventListener(document, "keydown", (e) => { let code = ""; const key = e.key; if (e.shiftKey && key !== "Shift") code += "Shift+"; if (e.ctrlKey && key !== "Control") code += "Ctrl+"; if (e.altKey && key !== "Alt") code += "Alt+"; if (e.metaKey && key !== "Meta") code += "Meta+"; if (e.code === "Space") code += "Space"; code += key; if (code === c) callback(code); }); } //#endregion //#region src/canvas/CreateSignatureCanvas.ts var CreateSignatureCanvas = class { canvas = document.createElement("canvas"); ctx = this.canvas.getContext("2d"); stop = []; active = false; historyStack = []; resetStack = []; color = "#000000"; bg = "#eee"; constructor(lineWidth = 2, w = 400, h = 400, color = "#000000", bg = "#eee") { this.color = color; this.bg = bg; this.createCanvas(lineWidth, w, h); window.onunload = () => this.unmount(); } createCanvas(lineWidth = 2, w, h) { this.canvas.width = w; this.canvas.height = h; this.ctx.fillStyle = this.bg; this.ctx.fillRect(0, 0, w, h); this.ctx.strokeStyle = this.color; this.ctx.lineWidth = lineWidth; this.ctx.lineCap = "round"; let offsetY = 0; let offsetX = 0; this.stop.push(useEventListener(this.canvas, "touchstart", (e) => { offsetY = this.canvas.offsetTop; offsetX = this.canvas.offsetLeft; this.ctx.beginPath(); this.ctx.moveTo(e.changedTouches[0].pageX + 2 - offsetX, e.changedTouches[0].pageY - offsetY); }, false)); let down = false; this.stop.push(useEventListener(this.canvas, "mousedown", (e) => { offsetY = this.canvas.offsetTop; offsetX = this.canvas.offsetLeft; down = true; this.ctx.beginPath(); this.ctx.moveTo(e.pageX + 2 - offsetX, e.pageY - offsetY); }, false)); this.stop.push(useEventListener(this.canvas, "mousemove", (e) => { if (!down) return; this.ctx.lineTo(e.pageX + 2 - offsetX, e.pageY - offsetY); this.ctx.stroke(); }, false)); this.stop.push(useEventListener(this.canvas, "mouseup", () => down = false)); this.stop.push(useEventListener(this.canvas, "mouseout", () => down = false)); this.stop.push(useEventListener(this.canvas, "touchmove", (e) => { this.ctx.lineTo(e.changedTouches[0].pageX + 2 - offsetX, e.changedTouches[0].pageY - offsetY); this.ctx.stroke(); }, false)); } clearCanvas() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = this.bg; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } mount(el) { insertElement(el, this.canvas, null); this.listen(); return this; } setColor(color) { this.color = color; this.ctx.strokeStyle = color; } setBgColor(color) { this.bg = color; const tempCanvas = document.createElement("canvas"); tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height; const tempCtx = tempCanvas.getContext("2d"); tempCtx.drawImage(this.canvas, 0, 0); this.clearCanvas(); this.ctx.fillStyle = color; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(tempCanvas, 0, 0); } unmount() { removeElement(this.canvas); this.stop.forEach((s) => s()); } listen() { useEventListener(this.canvas, "mousedown", () => { this.active = true; }); useEventListener(this.canvas, "mouseup", () => { this.active = false; const { width, height } = this.canvas; const imageData = this.ctx.getImageData(0, 0, width, height); this.historyStack.push(imageData); }); useEventListener(this.canvas, "mouseout", () => { this.active = false; }); useKeyBoard("Ctrl+z", () => this.undo()); useKeyBoard("Ctrl+x", () => this.redo()); } undo() { if (this.historyStack.length === 0) return; this.clearCanvas(); this.resetStack.push(this.historyStack.pop()); this.historyStack.forEach((imageData) => this.ctx.putImageData(imageData, 0, 0)); } redo() { if (this.resetStack.length === 0) return; this.clearCanvas(); this.historyStack.push(this.resetStack.pop()); this.historyStack.forEach((imageData) => this.ctx.putImageData(imageData, 0, 0)); } erase(lineWidth = 2) { this.ctx.lineWidth = lineWidth; this.ctx.strokeStyle = "rgba(255, 255, 255, 1)"; this.ctx.globalCompositeOperation = "destination-out"; } unerased() { this.ctx.strokeStyle = this.color; this.ctx.globalCompositeOperation = "source-over"; } save(type = "image/png", quality = 1) { return this.canvas.toDataURL(type, quality); } }; //#endregion //#region src/perf/memorizeFn.ts /** * 函数缓存结果 * @param { Function } fn 函数 * @param { Map<string, any> } cache 缓存对象 * @param { number } maxSize 最大缓存条目数,默认为100 * @returns { Function } 带缓存的函数 */ function memorizeFn(fn, cache = new Map(), maxSize = 100) { return function(...args) { const _args = JSON.stringify(args); if (cache.has(_args)) { const value = cache.get(_args); cache.delete(_args); cache.set(_args, value); return value; } if (cache.size >= maxSize) { const firstKey = cache.keys().next().value; cache.delete(firstKey); } const result = fn.apply(fn, args); cache.set(_args, result); return result; }; } //#endregion //#region src/perf/useRic.ts /** * 浏览器空闲时期被调用 * @param { Function[] } tasks 函数队列 * @param { number } timeRemaining 剩余时间大于多少才继续执行 * @param { number } timeout 超时时间 * @param { Function } callback 回调 * @returns */ function useRic(tasks, options) { const { timeRemaining = 0, timeout: timeout$1 = 2e3, callback } = options || {}; let work = true; const idleCallback = window.requestIdleCallback || function(handler) { const startTime = Date.now(); return setTimeout(() => handler({ didTimeout: false, timeRemaining() { return Math.max(0, 50 - (Date.now() - startTime)); } }), 1); }; const taskResult = []; const idleCancel = window.cancelIdleCallback || clearTimeout; const animationId = idleCallback(async function animationCallback(deadline) { if (!work) return; if ((deadline.timeRemaining() > +timeRemaining || deadline.didTimeout) && tasks.length > 0) { var _tasks$shift; taskResult.push((_tasks$shift = tasks.shift()) === null || _tasks$shift === void 0 ? void 0 : _tasks$shift()); } if (tasks.length > 0) idleCallback(animationCallback); else { callback === null || callback === void 0 || callback(taskResult); stop(); } }, { timeout: timeout$1 }); function stop() { work = false; idleCancel(animationId); } return stop; } //#endregion //#region src/perf/debounce.ts /** * 防抖函数 * @param { Function } fn 函数 * @param { number } time 时间 * @returns */ function debounce(fn, time) { let timer = null; return function(e) { if (!isNull(timer)) clearTimeout(timer); timer = setTimeout(() => { const result = fn.call(this, e); timer = null; return result; }, time); }; } //#endregion //#region src/perf/fileSplice.ts /** * * @param { File } _file 文件 * @param { number } _chunkSize 切分大小 默认 1024 * 100 * @returns */ async function fileSplice(options) { return new Promise((resolve) => { const { file, chunkSize = 5 * 1024 * 1024, callback } = options || {}; const THREAD_COUNT = navigator.hardwareConcurrency || 4; const result = []; const chunkCount = Math.ceil(file.size / chunkSize); const workerChunkCount = Math.ceil(chunkCount / THREAD_COUNT); let finishCount = 0; for (let i = 0; i < THREAD_COUNT; i++) { const worker = new Worker("./dist/worker/fileSpliceWorker.js", { type: "module" }); const startIndex = i * workerChunkCount; let endIndex = startIndex + workerChunkCount; if (endIndex > chunkCount) endIndex = chunkCount; worker.postMessage({ file, i, chunkSize, startIndex, endIndex }); worker.onmessage = ({ data }) => { finishCount++; const params = { ...data, fileName: file.name, type: file.type, size: file.size, lastModified: file.lastModified, isDone: finishCount === THREAD_COUNT, remaining: THREAD_COUNT - finishCount }; result.push(params); callback && callback(params); worker.terminate(); if (finishCount === THREAD_COUNT) resolve(result); }; } }); } //#endregion //#region src/perf/getLru.ts /** * * @param max 最大存储量 默认 50 * @returns */ function getLru(max = 50) { return { set(key, value) { if (this.cache.has(key)) this.cache.delete(key); this.cache.set(key, value); if (this.cache.size > this.max) this.cache.delete(this.cache.keys().next().value); }, get(key) { if (this.cache.has(key)) { const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } return void 0; }, cache: new Map(), max, size() { return this.cache.size; } }; } //#endregion //#region src/event/useIntersectionObserver.ts /** * 检测物体可见 * @param { Element | string } element 元素 * @param { (entries: IntersectionObserverEntry[]) => void } callback 元素可见回调 * @param { IntersectionObserverOptions } options {} * @param { Element | Document | string | null } options.root 相对容器节点 * @param { string } options.rootMargin 相对容器节点位置"10px 20px 30px 40px" * @param { number | number[] } options.threshold 相对容器节点百分比 [0, 0.25, 0.5, 0.75, 1] * @returns */ function useIntersectionObserver(element, callback, options) { let stopped = false; let stop; unmount(() => stop === null || stop === void 0 ? void 0 : stop()); mount(element, (element$1) => { if ((options === null || options === void 0 ? void 0 : options.root) && isStr(options.root)) options.root = findElement(options.root); const ob = new IntersectionObserver(callback, options); ob.observe(element$1); stop = () => ob.disconnect(); if (stopped) stop(); }); return () => { if (!stop) return stopped = true; stop === null || stop === void 0 || stop(); }; } //#endregion //#region src/perf/lazyLoad.ts const loadingDefault = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHdpZHRoPSI0MHB4IiBoZWlnaHQ9IjQwcHgiIHZpZXdCb3g9IjAgMCA0MCA0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEuNDE0MjE7IiB4PSIwcHgiIHk9IjBweCI+CiAgICA8ZGVmcz4KICAgICAgICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWwogICAgICAgICAgICBALXdlYmtpdC1rZXlmcmFtZXMgc3BpbiB7CiAgICAgICAgICAgICAgZnJvbSB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoLTM1OWRlZykKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgQGtleWZyYW1lcyBzcGluIHsKICAgICAgICAgICAgICBmcm9tIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIHRvIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC0zNTlkZWcpCiAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIHN2ZyB7CiAgICAgICAgICAgICAgICAtd2Via2l0LXRyYW5zZm9ybS1vcmlnaW46IDUwJSA1MCU7CiAgICAgICAgICAgICAgICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiAxLjVzIGxpbmVhciBpbmZpbml0ZTsKICAgICAgICAgICAgICAgIC13ZWJraXQtYmFja2ZhY2UtdmlzaWJpbGl0eTogaGlkZGVuOwogICAgICAgICAgICAgICAgYW5pbWF0aW9uOiBzcGluIDEuNXMgbGluZWFyIGluZmluaXRlOwogICAgICAgICAgICB9CiAgICAgICAgXV0+PC9zdHlsZT4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJvdXRlciI+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwwQzIyLjIwNTgsMCAyMy45OTM5LDEuNzg4MTMgMjMuOTkzOSwzLjk5MzlDMjMuOTkzOSw2LjE5OTY4IDIyLjIwNTgsNy45ODc4MSAyMCw3Ljk4NzgxQzE3Ljc5NDIsNy45ODc4MSAxNi4wMDYxLDYuMTk5NjggMTYuMDA2MSwzLjk5MzlDMTYuMDA2MSwxLjc4ODEzIDE3Ljc5NDIsMCAyMCwwWiIgc3R5bGU9ImZpbGw6YmxhY2s7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44NTc4Niw1Ljg1Nzg2QzcuNDE3NTgsNC4yOTgxNSA5Ljk0NjM4LDQuMjk4MTUgMTEuNTA2MSw1Ljg1Nzg2QzEzLjA2NTgsNy40MTc1OCAxMy4wNjU4LDkuOTQ2MzggMTEuNTA2MSwxMS41MDYxQzkuOTQ2MzgsMTMuMDY1OCA3LjQxNzU4LDEzLjA2NTggNS44NTc4NiwxMS41MDYxQzQuMjk4MTUsOS45NDYzOCA0LjI5ODE1LDcuNDE3NTggNS44NTc4Niw1Ljg1Nzg2WiIgc3R5bGU9ImZpbGw6cmdiKDIxMCwyMTAsMjEwKTsiLz4KICAgICAgICA8L2c+CiAgICAgICAgPGc+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMCwzMi4wMTIyQzIyLjIwNTgsMzIuMDEyMiAyMy45OTM5LDMzLjgwMDMgMjMuOTkzOSwzNi4wMDYxQzIzLjk5MzksMzguMjExOSAyMi4yMDU4LDQwIDIwLDQwQzE3Ljc5NDIsNDAgMTYuMDA2MSwzOC4yMTE5IDE2LjAwNjEsMzYuMDA2MUMxNi4wMDYxLDMzLjgwMDMgMTcuNzk0MiwzMi4wMTIyIDIwLDMyLjAxMjJaIiBzdHlsZT0iZmlsbDpyZ2IoMTMwLDEzMCwxMzApOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksMjguNDkzOUMzMC4wNTM2LDI2LjkzNDIgMzIuNTgyNCwyNi45MzQyIDM0LjE0MjEsMjguNDkzOUMzNS43MDE5LDMwLjA1MzYgMzUuNzAxOSwzMi41ODI0IDM0LjE0MjEsMzQuMTQyMUMzMi41ODI0LDM1LjcwMTkgMzAuMDUzNiwzNS43MDE5IDI4LjQ5MzksMzQuMTQyMUMyNi45MzQyLDMyLjU4MjQgMjYuOTM0MiwzMC4wNTM2IDI4LjQ5MzksMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxMDEsMTAxLDEwMSk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMy45OTM5LDE2LjAwNjFDNi4xOTk2OCwxNi4wMDYxIDcuOTg3ODEsMTcuNzk0MiA3Ljk4NzgxLDIwQzcuOTg3ODEsMjIuMjA1OCA2LjE5OTY4LDIzLjk5MzkgMy45OTM5LDIzLjk5MzlDMS43ODgxMywyMy45OTM5IDAsMjIuMjA1OCAwLDIwQzAsMTcuNzk0MiAxLjc4ODEzLDE2LjAwNjEgMy45OTM5LDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoMTg3LDE4NywxODcpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTUuODU3ODYsMjguNDkzOUM3LjQxNzU4LDI2LjkzNDIgOS45NDYzOCwyNi45MzQyIDExLjUwNjEsMjguNDkzOUMxMy4wNjU4LDMwLjA1MzYgMTMuMDY1OCwzMi41ODI0IDExLjUwNjEsMzQuMTQyMUM5Ljk0NjM4LDM1LjcwMTkgNy40MTc1OCwzNS43MDE5IDUuODU3ODYsMzQuMTQyMUM0LjI5ODE1LDMyLjU4MjQgNC4yOTgxNSwzMC4wNTM2IDUuODU3ODYsMjguNDkzOVoiIHN0eWxlPSJmaWxsOnJnYigxNjQsMTY0LDE2NCk7Ii8+CiAgICAgICAgPC9nPgogICAgICAgIDxnPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYuMDA2MSwxNi4wMDYxQzM4LjIxMTksMTYuMDA2MSA0MCwxNy43OTQyIDQwLDIwQzQwLDIyLjIwNTggMzguMjExOSwyMy45OTM5IDM2LjAwNjEsMjMuOTkzOUMzMy44MDAzLDIzLjk5MzkgMzIuMDEyMiwyMi4yMDU4IDMyLjAxMjIsMjBDMzIuMDEyMiwxNy43OTQyIDMzLjgwMDMsMTYuMDA2MSAzNi4wMDYxLDE2LjAwNjFaIiBzdHlsZT0iZmlsbDpyZ2IoNzQsNzQsNzQpOyIvPgogICAgICAgIDwvZz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggZD0iTTI4LjQ5MzksNS44NTc4NkMzMC4wNTM2LDQuMjk4MTUgMzIuNTgyNCw0LjI5ODE1IDM0LjE0MjEsNS44NTc4NkMzNS43MDE5LDcuNDE3NTggMzUuNzAxOSw5Ljk0NjM4IDM0LjE0MjEsMTEuNTA2MUMzMi41ODI0LDEzLjA2NTggMzAuMDUzNiwxMy4wNjU4IDI4LjQ5MzksMTEuNTA2MUMyNi45MzQyLDkuOTQ2MzggMjYuOTM0Miw3LjQxNzU4IDI4LjQ5MzksNS44NTc4NloiIHN0eWxlPSJmaWxsOnJnYig1MCw1MCw1MCk7Ii8+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K"; /** * * @param { MaybeElement } element 元素 * @param { string } loadingUrl 懒加载的loading图 * @returns { Function } 停止懒加载的函数 */ function lazyLoad(element, loadingUrl = loadingDefault) { return mount(element, (el) => { findElement(["img", "video"], true, el).forEach((child) => { const datasrc = child.dataset.src; const src = datasrc || child.src; if (!datasrc) child.src = loadingUrl; const stop = useIntersectionObserver(child, (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { stop(); entry.target.src = src; } }); }); }); }); } //#endregion //#region src/perf/preload.ts /** * 图片视频预加载函数 * @param { string[] } list 图片数组 * @param { string } style 设置样式 * @returns { HTMLImageElement[] } 图片数组 */ function preload(list, style) { let imageNode; let videoNode; if (!isArray(list)) list = [list]; return list.map((src) => isVideo(src) ? createVideo(src) : createImage(src)); function createImage(src) { if (!imageNode) imageNode = new Image(); const image = imageNode.cloneNode(); image.src = src; if (style) image.setAttribute("style", style); return image; } function createVideo(src) { if (!videoNode) videoNode = createElement("video"); const video = videoNode.cloneNode(); video.src = src; if (style) video.setAttribute("style", style); return video; } } //#endregion //#region src/perf/prefetch.ts /** * 借助浏览器空闲时间去加载一些图片资源 * @param { string[] } list 图片或视频地址数组 * @param { number } timeRemaining 浏览器空闲时间大于多少去加载 * @returns { Function } 停止加载的函数 */ function prefetch(list, timeRemaining = 0) { const imageNode = new Image(); const videoNode = createElement("video"); return useRic(list.map((src) => { if (isVideo(src)) return () => { videoNode.src = src; }; return () => { imageNode.src = src; }; }, timeRemaining)); } //#endregion //#region src/perf/throttle.ts /** * 截流函数 * @param { Function } fn 函数 * @param { number } stop 时间 * @returns */ function throttle(fn, stop) { let start = 0; return function(...args) { const end = Date.now(); if (end - start >= stop) { const result = fn.call(this, ...args); start = end; return result; } }; } //#endregion //#region src/perf/useRaf.ts /** * 使用 requestAnimationFrame 执行一个函数,并提供停止执行的功能。 * * @param {function(number): void} fn - 在每一帧调用的函数,参数是时间戳。 * @param {object} [options] - 配置选项。 * @param {number} [options.delta] - 两次调用之间的最小时间间隔(毫秒)。 * @param {boolean} [options.autoStop] - 是否在首次调用后自动停止。 * @param {boolean} [options.immediate] - 是否在首次调用时立即执行。 * @returns {function(): void} - 停止执行的函数。 */ function useRaf(fn, options = {}) { let start; let isStopped = false; let rafId; const { immediate, delta = 0, autoStop } = options; const animationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || ((fn$1) => setTimeout(fn$1, 1e3 / 60)); const cancelAnimation = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame || clearTimeout; const stop = () => { isStopped = true; cancelAnimation(rafId); }; rafId = animationFrame(function myFrame(timestamp = Date.now()) { if (isUndef(start)) { start = timestamp; if (immediate) fn === null || fn === void 0 || fn(timestamp); } else if (isStopped) return; else if (timestamp - start > delta) { fn === null || fn === void 0 || fn(timestamp); start = timestamp; if (autoStop) { stop(); return; } } rafId = animationFrame(myFrame); }); return stop; } //#endregion //#region src/perf/once.ts /** * 只执行一次函数 * @param { Function } fn 函数 * @returns { Function } 函数 */ function once(fn) { let called = false; return function(...args) { if (!called) { called = true; return fn.apply(this, args); } }; } //#endregion //#region src/perf/createHybridMap.ts var HybridMap = class { objectStore; primitiveStore; constructor(entries) { this.objectStore = new WeakMap(); this.primitiveStore = new Map(); if (entries) for (const [k, v] of entries) this.set(k, v); } isObjectKey(key) { return typeof key === "object" && key !== null; } set(key, value) { if (this.isObjectKey(key)) this.objectStore.set(key, value); else this.primitiveStore.set(key, value); return this; } get(key) { if (this.isObjectKey(key)) return this.objectStore.get(key); else return this.primitiveStore.get(key); } has(key) { if (this.isObjectKey(key)) return this.objectStore.has(key); else return this.primitiveStore.has(key); } delete(key) { if (this.isObjectKey(key)) return this.objectStore.delete(key); else return this.primitiveStore.delete(key); } clear() { this.primitiveStore.clear(); this.objectStore = new WeakMap(); } get size() { throw new Error("HybridMap does not support size due to WeakMap limitations."); } forEach() { throw new Error("HybridMap does not support forEach due to WeakMap limitations."); } keys() { throw new Error("HybridMap does not support iteration due to WeakMap limitations."); } values() { throw new Error("HybridMap does not support iteration due to WeakMap limitations."); } entries() { throw new Error("HybridMap does not support iteration due to WeakMap limitations."); } [Symbol.iterator]() { throw new Error("HybridMap does not support iteration due to WeakMap limitations."); } }; function createHybridMap(entries) { return new HybridMap(entries); } //#endregion //#region src/canvas/DotImageCanvas.ts /** * DotImageCanvas 将图片转换为点阵图形式展示,并提供动画绘制效果 * * 支持多种绘制方向,颜色控制,背景设置,以及动画效果控制 * @class */ var DotImageCanvas = class { /** 用于绘制的 Canvas 元素 */ canvas = document.createElement("canvas"); /** Canvas 的绘制上下文 */ ctx = this.canvas.getContext("2d"); /** 缓存已处理图像的点阵数据 */ points = new Map(); /** 原始图片源地址 */ originSrc = ""; /** 点阵图颜色,设置后将覆盖原图颜色 */ color = ""; /** 点阵粗细,影响绘制的圆点大小 */ fontWeight = 1; /** 绘制状态:pending-绘制中,success-完成,fail-失败,reverted-已撤销 */ status = "pending"; /** 背景颜色 */ bgColor = "#fff"; /** 停止当前动画的函数 */ stop = () => {}; /** 绘制方向 */ direction = "horizontal"; /** 所有绘制任务 */ allTasks = []; /** 已完成的任务索引 */ completedTaskIndex = 0; /** 是否正在撤销绘制 */ isReverting = false; /** 每个绘制任务中的点坐标记录 */ drawnPoints = []; /** 清除任务列表 */ clearTasks = []; /** 是否使用优先渲染模式 (RAF) */ isPreferred = false; /** 是否已挂载到DOM */ mounted = false; /** * 创建 DotImageCanvas 实例 * * @param {string | DotImageCanvasOptions} srcOrOptions - 图片URL或选项对象 * @param {string} [color] - 绘制颜色,为空时保留原图颜色 * @param {number} [fontWeight] - 点阵粗细 * @param {string} [bgColor] - 背景颜色 * @param {Direction} [direction] - 绘制方向 */ constructor(srcOrOptions, color, fontWeight, bgColor = "#fff", direction) { if (typeof srcOrOptions === "string") this.initOptions(srcOrOptions, color, fontWeight, bgColor, direction); else { const options = srcOrOptions; this.initOptions(options.src, options.color, options.fontWeight, options.bgColor || "#fff", options.direction); if (options.isPreferred !== void 0) this.isPreferred = options.isPreferred; } this.executor(); } createDotImage(img) { this.canvas.width = img.width * devicePixelRatio; this.canvas.height = img.height * devicePixelRatio; this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height); const width = this.canvas.width; const height = this.canvas.height; const imageDataObj = this.ctx.getImageData(0, 0, width, height); const rawData = imageDataObj.data; const pixelView = new Uint32Array(rawData.buffer); const pixelBitmap = new Uint8Array(width * height); const colorMap = new Map(); const imagePointSet = new Array(height); for (let i = 0; i < height; i++) imagePointSet[i] = new Array(width); const WHITE_THRESHOLD = 15132390; for (let i = 0; i < height; i++) for (let j = 0; j < width; j++) { const pixelIndex = i * width + j; const pixelValue = pixelView[pixelIndex]; const r = pixelValue >> 0 & 255; const g = pixelValue >> 8 & 255; const b = pixelValue >> 16 & 255; const a = pixelValue >> 24 & 255; if (a === 0 || r > 230 && g > 230 && b > 230) { pixelBitmap[pixelIndex] = 0; imagePointSet[i][j] = this.color ? 0 : this.bgColor; } else { pixelBitmap[pixelIndex] = 1; const color = `rgba(${r},${g},${b},${a / 255})`; colorMap.set(pixelIndex, color); imagePointSet[i][j] = color; } } this.points.set(this.originSrc, { width, height, imagePointSet, pixelBitmap, colorMap }); return imagePointSet; } createImage() { if (this.hasImage()) { const { imagePointSet, width, height } = this.points.get(this.originSrc); const pRatio = window.devicePixelRatio || 1; this.canvas.width = width * pRatio; this.canvas.height = height * pRatio; this.getCanvas(imagePointSet); return; } const img = createElement("img", { crossOrigin: "anonymous", src: this.originSrc }); return new Promise((resolve) => { img.onload = () => { this.getCanvas(this.createDotImage(img)); resolve(img); }; img.onerror = () => { this.status = "fail"; }; }); } hasImage() { return this.points.has(this.originSrc); } async executor() { try { await this.createImage(); } catch (error) {} return this; } /** * 创建绘制和清除任务的辅助方法 - 保持原有实现方式 */ createDrawAndClearTasks(pointsGenerator, size) { const batchPoints = []; const drawTask = () => { const points = pointsGenerator(); for (const { x, y, color } of points) { batchPoints.push({ x, y, color }); this.ctx.beginPath(); this.ctx.arc(x, y, size, 0, Math.PI * 2); this.ctx.fillStyle = color; this.ctx.fill(); } }; const clearTask = () => { for (const { x, y, color } of batchPoints) { this.ctx.beginPath(); this.ctx.fillStyle = color; this.ctx.clearRect(x - size, y - size, size * 2.5, size * 2.5); } }; return { drawTask, clearTask }; } getCanvas(imagePointSet) { var _imagePointSet$; this.clearCanvas(); const h = imagePointSet.length; const w = (_imagePointSet$ = imagePointSet[0]) === null || _imagePointSet$ === void 0 ? void 0 : _imagePointSet$.length; const oneTempLength = this.canvas.width * 1 / h; const size = this.fontWeight * 50 / this.canvas.width; const getPoint = memorizeFn((i) => oneTempLength * (i + .5)); const tasks = []; const clearTasks = []; if (this.direction === "horizontal-reverse") for (let i = h - 1; i >= 0; i--) { const row = i; const { drawTask, clearTask } = this.createDrawAndClearTasks(() => { const points = new Array(w); let pointCount = 0; for (let j = w - 1; j >= 0; j--) { const color = imagePointSet[row][j]; if (color) points[pointCount++] = { x: getPoint(j), y: getPoint(row), color: this.color || `${color}` }; } return pointCount < w ? points.slice(0, pointCount) : points; }, size); tasks.push(drawTask); clearTasks.push(clearTask); } else if (this.direction === "horizontal") for (let i = 0; i < h; i++) { const row = i; const { drawTask, clearTask } = this.createDrawAndClearTasks(() => { const points = []; for (let j = 0; j < w; j++) { const color = imagePointSet[row][j]; if (color) points.push({ x: getPoint(j), y: getPoint(row), color: this.color || `${color}` }); } return points; }, size); tasks.push(drawTask); clearTasks.push(clearTask); } else if (this.direction === "vertical") for (let j = 0; j < w; j++) { const col = j; const { drawTask, clearTask } = this.createDrawAndClearTasks(() => { const points = []; for (let i = 0; i < h; i++) { const color = imagePointSet[i][col]; if (color) points.push({ x: getPoint(col), y: getPoint(i), color: this.color || `${color}` }); } return points; }, size); tasks.push(drawTask); clearTasks.push(clearTask); } else if (this.direction === "vertical-reverse") for (let j = w - 1; j >= 0; j--) { const col = j; const { drawTask, clearTask } = this.createDrawAndClearTasks(() => { const points = []; for (let i = h - 1; i >= 0; i--) { const color = imagePointSet[i][col]; if (color) points.push({ x: getPoint(col), y: getPoint(i), color: this.color || `${color}` }); } return points; }, size); tasks.push(drawTask); clearTasks.push(clearTask); } else if (this.direction === "center-out" || this.direction === "out-center") { const centerX = Math.floor(w / 2); const centerY = Math.floor(h / 2); const { pixelBitmap, colorMap } = this.points.get(this.originSrc) || {}; const validPixels = pixelBitmap ? Array.from(pixelBitmap).filter((p) => p === 1).length : Math.ceil(w * h * .3); let distances = new Float32Array(validPixels); const pointData = { indices: new Uint32Array(validPixels), x: new Uint16Array(validPixels), y: new Uint16Array(validPixels) }; let pointCount = 0; for (let i = 0; i < h; i++) for (let j = 0; j < w; j++) { const pixelIndex = i * w + j; if (pixelBitmap && pixelBitmap[pixelIndex] === 0) continue; const color = imagePointSet[i][j]; if (!color) continue; pointData.indices[pointCount] = pixelIndex; pointData.x[pointCount] = j; pointData.y[pointCount] = i; distances[pointCount] = Math.sqrt((j - centerX) ** 2 + (i - centerY) ** 2); pointCount++; } if (pointCount < validPixels) { const newIndices = new Uint32Array(pointCount); const newX = new Uint16Array(pointCount); const newY = new Uint16Array(pointCount); const newDistances = new Float32Array(pointCount); for (let i = 0; i < pointCount; i++) { newIndices[i] = pointData.indices[i]; newX[i] = pointData.x[i]; newY[i] = pointData.y[i]; newDistances[i] = distances[i]; } pointData.indices = newIndices; pointData.x = newX; pointData.y = newY; distances = newDistances; } const indexOrder = new Uint32Array(pointCount); for (let i = 0; i < pointCount; i++) indexOrder[i] = i; if (this.direction === "center-out") indexOrder.sort((a, b) => distances[a] - distances[b]); else indexOrder.sort((a, b) => distances[b] - distances[a]); const batchSize = Math.max(1, Math.min(500, Math.floor(pointCount / 100))); for (let i = 0; i < pointCount; i += batchSize) { const end = Math.min(i + batchSize, pointCount); const { drawTask, clearTask } = this.createDrawAndClearTasks(() => { const result = []; for (let j = i; j < end; j++) { const pointIndex = indexOrder[j]; const x = getPoint(pointData.x[pointIndex]); const y = getPoint(pointData.y[pointIndex]); const pixelIndex = pointData.indices[pointIndex]; const color = colorMap ? colorMap.get(pixelIndex) || this.color : this.color || imagePointSet[pointData.y[pointIndex]][pointData.x[pointIndex]]; result.push({ x, y, color }); } return result; }, size); tasks.push(drawTask); clearTasks.push(clearTask); } } this.allTasks = tasks; this.clearTasks = clearTasks; this.completedTaskIndex = 0; this.isReverting = false; this.startAnimation(); } startAnimation() { this.stop(); const tasksToRun = this.isReverting ? Array.from({ length: this.completedTaskIndex }, (_, i) => { const idx = this.completedTaskIndex - 1 - i; return () => { const clearTask = this.clearTasks[idx]; if (clearTask) { clearTask(); this.completedTaskIndex = idx; } return true; }; }).filter((task) => task !== null) : Array.from({ length: this.allTasks.length - this.completedTaskIndex }, (_, i) => { const idx = this.completedTaskIndex + i; return () => { const drawTask = this.allTasks[idx]; if (drawTask) { drawTask(); this.completedTaskIndex = idx + 1; } return true; }; }); if (this.isPreferred) { let currentIndex = 0; this.stop = useRaf(() => { const tasksPerFrame = 5; let count = 0; while (currentIndex < tasksToRun.length && count < tasksPerFrame) { tasksToRun[currentIndex](); currentIndex++; count++; } if (currentIndex >= tasksToRun.length) { this.stop(); this.status = this.isReverting ? "reverted" : "success"; if (this.isReverting) this.completedTaskIndex = 0; else this.completedTaskIndex = this.allTasks.length; } }); } else this.stop = useRic(tasksToRun, { callback: () => { this.status = this.isReverting ? "reverted" : "success"; if (this.isReverting) this.completedTaskIndex = 0; else this.completedTaskIndex = this.allTasks.length; this.stop(); } }); return this; } revert() { if (this.completedTaskIndex <= 0) return this; this.isReverting = true; this.startAnimation(); return this; } continue() { if (this.completedTaskIndex >= this.allTasks.length && !this.isReverting) return this; this.isReverting = false; this.startAnimation(); return this; } initOptions(src, color, fontWeight, bgColor, direction = "horizontal") { this.originSrc = src; this.color = color; this.fontWeight = fontWeight; this.bgColor = bgColor; this.direction = direction; } /** * 重新绘制图像,可以更新配置 * * @param {string | Partial<DotImageCanvasOptions>} srcOrOptions - 图片URL或选项对象 * @param {string} [color] - 点阵颜色 * @param {number} [fontWeight] - 点阵粗细 * @param {string} [bgColor] - 背景颜色 * @param {Direction} [direction] - 绘制方向 * @returns {Promise<DotImageCanvas>} 更新后的实例 */ async repaint(srcOrOptions, color, fontWeight, bgColor = "#fff", direction) { this.stop(); const p = removeElement(this.canvas); this.status = "pending"; if (typeof srcOrOptions === "string") this.initOptions(srcOrOptions, color, fontWeight, bgColor, direction); else { const options = srcOrOptions; this.initOptions(options.src || this.originSrc, options.color || this.color, options.fontWeight !== void 0 ? options.fontWeight : this.fontWeight, options.bgColor || this.bgColor || "#fff", options.direction || this.direction); if (options.isPreferred !== void 0) this.isPreferred = options.isPreferred; } if (!p) throw new Error("repaint error not found canvas container or has been removed"); return Object.assign(this, await this.executor()).append(p); } clearCanvas() { this.stop(); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.completedTaskIndex = 0; this.isReverting = false; } append(container) { insertElement(container, this.canvas); this.mounted = true; return this; } destory() { this.stop(); this.mounted = false; removeElement(this.canvas); } }; //#endregion //#region src/canvas/DotTextCanvas.ts /** * DotTextCanvas 将文字转换为点阵形式展示,并提供动画绘制效果 * * 支持多种绘制方向、颜色控制、间距设置以及动画效果 * @class */ var DotTextCanvas = class DotTextCanvas { /** 用于绘制的 Canvas 元素 */ canvas = document.createElement("canvas"); /** Canvas 的绘制上下文 */ ctx = this.canvas.getContext("2d"); /** 缓存已处理字符的点阵数据 */ points = new Map(); /** 原始文本内容 */ originText; /** 字体大小 */ fontSize; /** 点阵颜色 */ color; /** 点阵粗细,影响绘制的圆点大小 */ fontWeight; /** 绘制方向 */ direction; /** 全局字符间距,默认为0 */ charSpacing = 0; /** 每个字符对之间的间距数组,优先级高于全局间距 */ charSpacings = []; /** 存储最终合成的点阵数据 */ textPointSet = []; /** 绘制状态:pending-绘制中,success-完成 */ status = "pending"; /** 容器元素 */ container; /** 停止当前动画的函数 */ stop = () => {}; /** 是否已挂载到DOM */ mounted = false; /** 是否使用优先渲染模式(RAF) */ isPreferred = false; /** 用于文本绘制的临时Canvas元素 */ _textCanvas; /** 临时Canvas的绘制上下文 */ _textCtx; /** * 创建 DotTextCanvas 实例 * * @param {string | DotTextCanvasOptions} textOrOptions - 文本内容或选项对象 * @param {number} [fontSize] - 字体大小 * @param {string} [color] - 点阵颜色 * @param {number} [fontWeight] - 点阵粗细 * @param {Direction} [direction] - 绘制方向 * @param {boolean} [isPreferred] - 是否使用优先渲染模式 * @param {number} [charSpacing] - 全局字符间距 * @param {number[]} [charSpacings] - 每个字符对之间的间距数组 */ constructor(textOrOptions, fontSize, color, fontWeight, direction = "vertical", isPreferred = false, charSpacing = 0, charSpacings = []) { if (typeof textOrOptions === "string") { this.originText = textOrOptions; this.fontSize = fontSize; this.color = color; this.fontWeight = fontWeight; this.direction = direction; this.isPreferred = isPreferred; this.charSpacing = Math.max(0, charSpacing); this.charSpacings = charSpacings.map((s) => Math.max(0, s)); } else { const options = textOrOptions; this.originText = options.text; this.fontSize = options.fontSize; this.color = options.color; this.fontWeight = options.fontWeight; this.direction = options.direction || "vertical"; this.isPreferred = options.isPreferred || false; this.charSpacing = options.charSpacing !== void 0 ? Math.max(0, options.charSpacing) : 0; this.charSpacings = options.charSpacings ? options.charSpacings.map((s) => Math.max(0, s)) : []; } this.executor(); } /** * 为单个字符创建点阵数据 * * @param {string} text - 要转换为点阵的字符 * @returns {Array<number[]>} 字符的点阵数据 */ createTextPoint(text) { if (!this._textCanvas) { this._textCanvas = document.createElement("canvas"); this._textCtx = this._textCanvas.getContext("2d"); } const canvas = this._textCanvas; const ctx = this._textCtx; const pRatio = window.devicePixelRatio || 1; const size = 16 * pRatio; canvas.width = canvas.height = size; ctx.font = `${size}px SimSun`; ctx.fillText(text, 0, 14 * pRatio); const { data: imageData, width, height } = ctx.getImageData(0, 0, size, size); const textPointSet = []; for (let i = 0; i < height; i++) { const temp = []; textPointSet.push(temp); for (let j = 0; j < width; j++) { const pxStartIndex = i * width * 4 + j * 4; temp.push(imageData[pxStartIndex + 3] ? 1 : 0); } } this.points.set(text, textPointSet); return textPointSet; } /** * 执行文字点阵转换流程 */ executor() { this.originText.split("").forEach((text) => this.getText(text)); this.textPointSet = this.combineText(); this.getCanvas(); } /** * 获取字符的点阵数据,如果不存在则创建 * * @param {str