UNPKG

web-video-creator

Version:

A framework for creating videos based on Node.js + Puppeteer + FFmpeg.

954 lines (914 loc) 42.8 kB
import ____SvgAnimation from "../media/SvgAnimation.js"; import ____VideoCanvas from "../media/VideoCanvas.js"; import ____DynamicImage from "../media/DynamicImage.js"; import ____LottieCanvas from "../media/LottieCanvas.js"; export default class CaptureContext { /** 媒体选择器 */ SVG_SELECTOR = "svg"; AUDIO_SELECTOR = 'audio[src$=".mp3"],audio[src$=".ogg"],audio[src$=".acc"],audio[src*=".mp3?"],audio[src*=".ogg?"],audio[src*=".aac?"],audio[capture]'; VIDEO_SELECTOR = 'video[src$=".mp4"],video[src$=".webm"],video[src$=".mkv"],video[src*=".mp4?"],video[src*=".webm?"],video[src*=".mkv?"],video[capture],canvas[video-capture]'; DYNAMIC_IMAGE_SELECTOR = 'img[src$=".gif"],img[src$=".webp"],img[src$=".apng"],img[src*=".gif?"],img[src*=".webp?"],img[src*=".apng?"],img[capture],canvas[dyimage-capture]'; LOTTIE_SELECTOR = "lottie,canvas[lottie-capture]"; /** @type {number} - 启动时间点(毫秒) */ startTime = Date.now(); /** @type {number} - 当前时间点(毫秒) */ currentTime = 0; /** @type {number} - 当前帧指针 */ frameIndex = 0; /** @type {number} - 帧间隔时间(毫秒) */ frameInterval = 0; /** @type {boolean} - 准备完毕标志 */ readyFlag = false; /** @type {boolean} - 启动标志 */ startFlag = false; /** @type {boolean} - 停止标志 */ stopFlag = false; /** @type {boolean} - 暂停标志 */ pauseFlag = false; /** @type {number} - 准备完毕时间点 */ readyTime; /** @type {Function} - 准备完毕回调 */ readyCallback; /** @type {Function} - 恢复回调 */ resumeCallback = null; /** @type {Function[]} - 间隔回调列表 */ intervalCallbacks = []; /** @type {Function[]} - 超时回调列表 */ timeoutCallbacks = []; /** @type {number} - 计时器自增ID */ timerId = 0; /** @type {number} - 自增音频ID */ audioId = 0; /** @type {number} - 应用于Date对象的时间偏移HACK(处理mojs动画) */ timeOffset = 0; /** @type {Object} - 配置对象 */ config = { /** @type {number} - 渲染帧率 */ fps: null, /** @type {number} - 开始时间点 */ startTime: 0, /** @type {number} - 总时长 */ duration: null, /** @type {number} - 目标总帧数 */ frameCount: null }; /** @type {SvgAnimation[]|VideoCanvas[]|DynamicImage[]|LottieCanvas[]} - 媒体调度列表 */ dispatchMedias = []; /** * 构造函数 */ constructor() { // 控制台输出重写 this._consoleRewrite(); // 元素行为重写 this._elementRewrite(); // 时间虚拟化重写 this._timeVirtualizationRewrite(); } /** * 初始化上下文 */ init() { // 自动触发超时和间隔回调 this._callTimeoutCallbacks(); this._callIntervalCallbacks(); } /** * 准备完毕 * * @return {boolean} - 是否已启动 */ ready() { // 设置准备完毕标志为true this.readyFlag = true; // 设置准备完毕时的时间点 this.readyTime = performance.____now(); // 如果存在准备前的启动则调用 if (this.readyCallback) { this.readyCallback(); return true; } return false; } /** * 是否已经启动捕获 */ isCapturing() { return this.currentTime >= (this.config.startTime || 0); } /** * 开始捕获 */ start() { // 如果在准备完毕前启动则延迟到准备完毕再启动 if (!this.readyFlag) { this.readyCallback = this.start; return; } // 检查配置 this._checkConfig(); // 插入捕获辅助元素 this._insertCaptureHelper(); // 转换元素为媒体元素 this._convertElementsToMedias(); // 监听媒体插入 this._observMediaInsert(); // 更新开始时间 this.startTime = Date.now(); // 计算帧间隔时间 this.frameInterval = 1000 / this.config.fps; // 递归捕获帧 (function nextFrame() { (async () => { // 设置启动标志位 if (!this.startFlag) this.startFlag = true; // 如果已停止则跳出 if (this.stopFlag) return ____screencastCompleted(); // 媒体调度 const mediaRenderPromises = this.dispatchMedias.map(media => (async () => { // 媒体可销毁时执行销毁 if (media.canDestory(this.currentTime)) return media.destory(); // 如媒体不可播放则跳过调度 if (!media.canPlay(this.currentTime)) return; // 媒体未准备完毕时调用加载 if (!media.isReady()) { // 加载媒体,如加载失败则跳过 if (!await media.load()) return; }; const mediaCurrentTime = this.currentTime - media.startTime - (media.offsetTime || 0); await media.seek(mediaCurrentTime > 0 ? mediaCurrentTime : 0); })()); await Promise.all(mediaRenderPromises); // CSS动画调度 await ____seekCSSAnimations(this.currentTime); // 动作序列调度 await ____seekTimeActions(this.currentTime); // 根据帧间隔推进当前时间 this.currentTime += this.frameInterval; // 时间偏移HACK重置(处理mojs动画) this.timeOffset = 0; // 触发轮询回调列表 this._callIntervalCallbacks(this.currentTime); // 触发超时回调列表 this._callTimeoutCallbacks(this.currentTime); // 是否处于捕获中状态 if (this.isCapturing()) { // 捕获帧图 - 此函数请见Page.js的#envInit的exposeFunction if (!await ____captureFrame()) { this.stopFlag = true; return; } // 遇到暂停标志时等待恢复 if (this.pauseFlag) await new Promise(resolve => this.resumeCallback = resolve); // 捕获帧数到达目标帧数时终止捕获 if (++this.frameIndex >= this.config.frameCount) { this.stopFlag = true; // 完成录制回调 - 此函数请见Page.js的#envInit的exposeFunction return ____screencastCompleted(); } // 如果未到达目标帧数但已被停止也触发录制完成 else if (this.stopFlag) return ____screencastCompleted(); } // 跳过无需捕获的帧 else await ____skipFrame(); // 开始捕获下一帧 nextFrame.bind(this)(); })() .catch(err => console.error(err)); }).bind(this)(); } /** * 检查配置 */ _checkConfig() { const { fps, duration, frameCount } = this.config; if (isNaN(fps) || fps <= 0) throw new Error(`config fps ${fps} is invalid`); if (isNaN(duration) || duration <= 0) throw new Error(`config duration ${duration} is invalid`); if (isNaN(frameCount) || frameCount <= 0) throw new Error(`config frameCount ${frameCount} is invalid`); } /** * 插入捕获辅助元素 * BeginFrame可能会陷入假死,这个元素会不断旋转确保总是产生新的帧 */ _insertCaptureHelper() { const captureHelper = document.createElement("capture-helper"); // 设置几乎看不见的样式 Object.assign(captureHelper.style, { width: "0.1px", height: "0.1px", opacity: 0.1, position: "fixed", top: 0, left: 0, zIndex: 999, backgroundColor: "#fff", transform: "rotate(0deg)" }); // 加入到body中 (document.body || document).appendChild(captureHelper); let rotate = 0; (function update() { rotate = rotate >= 360 ? 0 : (rotate + 0.1); captureHelper.style.transform = `rotate(${rotate}deg)`; // 如果已启动则高频更新,未启动时摸鱼 if (this.startFlag) ____setTimeout(update.bind(this), 0); else ____setTimeout(update.bind(this), 1000); }).bind(this)(); } /** * 转换元素为媒体对象 * * @private */ _convertElementsToMedias() { const svgs = document.querySelectorAll(this.SVG_SELECTOR); const audios = document.querySelectorAll(this.AUDIO_SELECTOR); const videos = document.querySelectorAll(this.VIDEO_SELECTOR); const dynamicImages = document.querySelectorAll(this.DYNAMIC_IMAGE_SELECTOR); const lotties = document.querySelectorAll(this.LOTTIE_SELECTOR); svgs.forEach(e => captureCtx.convertToSvgAnimation(e)); audios.forEach(e => captureCtx.convertToInnerAudio(e)); videos.forEach(e => captureCtx.convertToVideoCanvas(e)); dynamicImages.forEach(e => captureCtx.convertToDynamicImage(e)); lotties.forEach(e => captureCtx.convertToLottieCanvas(e)); } /** * 监听媒体插入 * * @private */ _observMediaInsert() { const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === "childList") { if (mutation.addedNodes.length > 0) { for (const addedNode of mutation.addedNodes) { if (!addedNode.matches) return; if (addedNode.matches("canvas")) break; else if (addedNode.matches(this.SVG_SELECTOR)) this.convertToSvgAnimation(addedNode); else if (addedNode.matches(this.DYNAMIC_IMAGE_SELECTOR)) this.convertToDynamicImage(addedNode); else if (addedNode.matches(this.AUDIO_SELECTOR)) this.convertToInnerAudio(addedNode); else if (addedNode.matches(this.VIDEO_SELECTOR)) this.convertToVideoCanvas(addedNode); else if (addedNode.matches(this.LOTTIE_SELECTOR)) this.convertToLottieCanvas(addedNode); } } if (mutation.removedNodes.length > 0) { for (const removedNode of mutation.removedNodes) { // 通知节点移除 removedNode.____onRemoved && removedNode.____onRemoved(); } } } } }); observer.observe(document.body || document, { childList: true, subtree: true, attributes: false, characterData: false }); } /** * 控制台输出重写 */ _consoleRewrite() { const getPrintFun = fn => ((...args) => fn.bind(console)(args.reduce((t, v) => { if (v instanceof Error) return `${t}\n${v.stack} `; else if (v instanceof Object) return `${t}${JSON.stringify(v)} `; return `${t}${v} `; }, ""))); console.____log = console.log; console.log = getPrintFun(console.____log); console.____warn = console.warn; console.warn = getPrintFun(console.____warn); console.____error = console.error; console.error = getPrintFun(console.____error); console.____debug = console.debug; console.debug = getPrintFun(console.____debug); } /** * 元素行为重写 */ _elementRewrite() { // 支持获取html元素布尔属性 HTMLElement.prototype.getBooleanAttribute = function (name) { const value = this.getAttribute(name); if (value == null) return undefined; return value == "false" ? false : true; } // 支持获取html元素数字属性 HTMLElement.prototype.getNumberAttribute = function (name) { const value = this.getAttribute(name); if (value == null) return undefined; return Number(value); } // 支持获取svg元素数字属性 SVGSVGElement.prototype.getNumberAttribute = function (name) { const value = this.getAttribute(name); if (value == null) return undefined; return Number(value); } } /** * 时间虚拟化重写 * * @private */ _timeVirtualizationRewrite() { // 暂存setInterval函数 window.____setInterval = window.setInterval; // 重写setInterval函数 window.setInterval = (fn, interval) => { if (typeof fn !== "function" || isNaN(interval)) return; this.timerId--; this.intervalCallbacks.push([this.timerId, this.currentTime, interval, fn]); return this.timerId; }; // 暂存clearInterval函数 window.____clearInterval = window.clearInterval; // 重写cleanInterval函数 window.clearInterval = timerId => { if (!timerId) return; if (timerId >= 0) return window.____clearInterval(timerId); this.intervalCallbacks = this.intervalCallbacks.filter(([_timerId]) => { if (_timerId == timerId) return false; return true; }); } // 暂存setTimeout函数 window.____setTimeout = window.setTimeout; // 重写setTimeout函数 window.setTimeout = (fn, timeout = 0) => { if (typeof fn !== "function" || isNaN(timeout)) return; this.timerId--; this.timeoutCallbacks.push([this.timerId, this.currentTime, timeout, fn]); return this.timerId; }; // 暂存clearTimeout函数 window.____clearTimeout = window.clearTimeout; // 重写clearTimeout函数 window.clearTimeout = timerId => { if (!timerId) return; if (timerId >= 0) return window.____clearTimeout(timerId); this.timeoutCallbacks = this.timeoutCallbacks.filter(([_timerId]) => { if (_timerId == timerId) return false; return true; }); } // 暂存requestAnimationFrame函数 window.____requestAnimationFrame = window.requestAnimationFrame; // 重写requestAnimationFrame,传递上下文提供的currentTime确保在非60fps捕获时实现帧率同步 window.requestAnimationFrame = fn => { if (!this.startFlag) return setTimeout(currentTime => fn(currentTime), 0); if (this.stopFlag) return; // 下一个事件循环再调用 return window.____requestAnimationFrame(() => fn(this.currentTime)); }; // 暂存Date对象 window.____Date = Date; const ctx = this; // 重写Date构造函数 window.Date = function Date(...args) { if (new.target === undefined) return new window.____Date(ctx.startTime + ctx.currentTime).toString(); if (args.length === 0) return new window.____Date(ctx.startTime + ctx.currentTime); return new window.____Date(...args); } // 将挂载的函数 Object.assign(window.Date, { prototype: window.____Date.prototype, now: () => Math.floor(this.startTime + this.currentTime) + (this.timeOffset += 0.01), parse: window.____Date.parse.bind(window.____Date), UTC: window.____Date.UTC.bind(window.____Date) }); // 重写performance.now函数 performance.____now = performance.now; performance.now = () => this.currentTime; // 启动前进行定时器调度,避免死锁 (async () => { (function dispatchBeforeStart() { // 如果已启动则不再调度,调度权交由nextFrame调度 if (this.startFlag) return; // 如果已准备完毕开始调度操作 if(this.readyFlag) { const currentTime = performance.____now() - this.readyTime; this._callTimeoutCallbacks(currentTime); this._callIntervalCallbacks(currentTime); ____setTimeout(dispatchBeforeStart.bind(this), 0); } // 如果还未准备完毕则进行轮询检查 else ____setTimeout(dispatchBeforeStart.bind(this), 1000); }).bind(this)(); })(); } /** * 拼接完整URL * * @private * @param {string} relativeUrl - 相对URL * @returns {string} - 绝对URL */ _currentUrlJoin(relativeUrl) { if (!relativeUrl || /^(https?:)?\/\//.test(relativeUrl)) return relativeUrl; const currentURL = window.location.href; return new URL(relativeUrl, currentURL).href; } /** * 触发轮询函数回调 * * @private */ _callIntervalCallbacks(currentTime) { if (this.intervalCallbacks.length == 0) return; for (let i = 0; i < this.intervalCallbacks.length; i++) { const [timerId, timestamp, interval, fn] = this.intervalCallbacks[i]; if (currentTime < timestamp + interval) continue; this.intervalCallbacks[i][1] = currentTime; // 下一个事件循环再调用 ____setTimeout(() => fn(currentTime), 0); } } /** * 触发超时函数回调 * * @private */ _callTimeoutCallbacks(currentTime) { if (this.timeoutCallbacks.length == 0) return; this.timeoutCallbacks = this.timeoutCallbacks.filter(([timerId, timestamp, timeout, fn]) => { if (currentTime < timestamp + timeout) return true; // 下一个事件循环再调用 ____setTimeout(() => fn(currentTime), 0); return false; }); } /** * 创建画布 * * @private */ _createCanvas(options) { const { id, class: _class, width, height } = options; const canvas = document.createElement("canvas"); id && canvas.setAttribute("id", id); _class && canvas.setAttribute("class", _class); canvas.width = width; canvas.height = height; return canvas; } /** * 添加音频 */ addAudio(options = {}) { const audioId = this.audioId++; const { url } = options; url && (options.url = this._currentUrlJoin(url)); ____addAudio({ audioId, ...options }); } /** * 添加多个音频 */ addAudios(audios = []) { audios.forEach(audio => this.addAudio(audio)); } /** * 转化为SVG动画对象 * * @param {SVGSVGElement} e - SVG元素 */ convertToSvgAnimation(e) { const hasAnimation = e.querySelector("animate, animateTransform, animateMotion, animateColor"); // 未找到任何动画元素则不做处理,这些SVG元素可能是静态的或者由其它动画库控制 if (!hasAnimation) return null; const options = { // SVG元素 target: e, // 动画播放开始时间点(毫秒) startTime: e.getNumberAttribute("start-time") || e.getNumberAttribute("startTime") || this.currentTime, // 动画播放结束时间点(毫秒) endTime: Math.min(e.getNumberAttribute("end-time") || e.getNumberAttribute("endTime") || Infinity, this.config.duration) }; // 实例化SVG动画对象 const svgAnimation = new ____SvgAnimation(options); // 将对象加入媒体调度列表 this.dispatchMedias.push(svgAnimation); return svgAnimation; } /** * 将HTML视频元素转换为内部合成音频 * * @param {HTMLAudioElement} e - 视频元素 */ convertToInnerAudio(e) { // 获取seek时间 const currentTimeAttribute = e.getNumberAttribute("currentTime"); const audioId = this.audioId++; const options = { // 内部音频唯一ID id: audioId, // 音频来源 url: this._currentUrlJoin(e.getAttribute("src")) || undefined, // 音频格式 format: e.getAttribute("format") || undefined, // 音频开始时间点(毫秒) startTime: e.getNumberAttribute("start-time") || e.getNumberAttribute("startTime") || this.currentTime, // 音频结束时间点(毫秒) endTime: Math.min(e.getNumberAttribute("end-time") || e.getNumberAttribute("endTime") || Infinity, this.config.duration), // 音频裁剪开始时间点(毫秒) seekStart: e.getNumberAttribute("seek-start") || e.getNumberAttribute("seekStart") || (currentTimeAttribute ? currentTimeAttribute * 1000 : undefined), // 音频裁剪结束时间点(毫秒) seekEnd: e.getNumberAttribute("seek-end") || e.getNumberAttribute("seekEnd"), // 音频淡入时长(毫秒) fadeInDuration: e.getNumberAttribute("fade-in-duration") || e.getNumberAttribute("fadeInDuration"), // 音频淡出时长(毫秒) fadeOutDuration: e.getNumberAttribute("fade-out-duration") || e.getNumberAttribute("fadeOutDuration"), // 音频音量 volume: (e.getNumberAttribute("volume") || e.volume || 1) * 100, // 音频是否循环播放 loop: e.getBooleanAttribute("loop"), // 音频是否自动播放 autoplay: e.getBooleanAttribute("autoplay"), // 音频是否静音 muted: e.getBooleanAttribute("muted"), // 拉取失败时重试拉取次数 retryFetchs: e.getNumberAttribute("retry-fetchs") || e.getNumberAttribute("retryFetchs"), // 是否忽略本地缓存 ignoreCache: e.getBooleanAttribute("ignore-cache") || e.getBooleanAttribute("ignoreCache") }; e.____onRemoved = () => ____updateAudioEndTime(audioId, this.currentTime); ____addAudio(options); } /** * 将HTML视频元素转换为视频画布 * * @param {HTMLVideoElement} e - 视频元素 */ convertToVideoCanvas(e) { // 获取seek时间 const currentTimeAttribute = e.getNumberAttribute("currentTime"); const options = { // 元素ID id: e.getAttribute("id") || undefined, // 元素类名 class: e.getAttribute("class") || undefined, // 内部音频唯一ID audioId: this.audioId++, // 视频来源 url: this._currentUrlJoin(e.getAttribute("src")) || undefined, // 蒙版视频来源 maskUrl: this._currentUrlJoin(e.getAttribute("_maskSrc") || e.getAttribute("maskSrc")) || undefined, // 视频格式 format: e.getAttribute("format") || undefined, // 视频宽度 width: e.getNumberAttribute("width") || e.width, // 视频高度 height: e.getNumberAttribute("height") || e.height, // 视频开始时间点(毫秒) startTime: e.getNumberAttribute("start-time") || e.getNumberAttribute("startTime") || this.currentTime, // 视频结束时间点(毫秒) endTime: Math.min(e.getNumberAttribute("end-time") || e.getNumberAttribute("endTime") || Infinity, this.config.duration), // 音频淡入时长(毫秒) fadeInDuration: e.getNumberAttribute("fade-in-duration") || e.getNumberAttribute("fadeInDuration"), // 音频淡出时长(毫秒) fadeOutDuration: e.getNumberAttribute("fade-out-duration") || e.getNumberAttribute("fadeOutDuration"), // 视频裁剪开始时间点(毫秒) seekStart: e.getNumberAttribute("seek-start") || e.getNumberAttribute("seekStart") || (currentTimeAttribute ? currentTimeAttribute * 1000 : undefined), // 视频裁剪结束时间点(毫秒) seekEnd: e.getNumberAttribute("seek-end") || e.getNumberAttribute("seekEnd"), // 视频是否循环播放 loop: e.getBooleanAttribute("loop"), // 视频音频音量 volume: (e.getNumberAttribute("volume") || e.volume || 1) * 100, // 视频是否自动播放 autoplay: e.getBooleanAttribute("autoplay"), // 视频是否静音 muted: e.getBooleanAttribute("muted"), // 拉取失败时重试拉取次数 retryFetchs: e.getNumberAttribute("retry-fetchs") || e.getNumberAttribute("retryFetchs"), // 是否忽略本地缓存 ignoreCache: e.getBooleanAttribute("ignore-cache") || e.getBooleanAttribute("ignoreCache"), }; let canvas; if (!(e instanceof HTMLCanvasElement)) { // 创建画布元素 canvas = this._createCanvas(options); // 复制目标元素样式 this._copyElementStyle(e, canvas); // 代理目标元素所有属性和行为 this._buildElementProxy(e, canvas); // 将目标元素替换为画布 e.replaceWith(canvas); } else { canvas = e; canvas.width = 0; canvas.height = 0; } // 实例化视频画布实例 const videoCanvas = new ____VideoCanvas(options); // 绑定画布元素 videoCanvas.bind(canvas); // 将对象加入媒体调度列表 this.dispatchMedias.push(videoCanvas); return videoCanvas; } /** * 将HTML图像元素转换为动态图像 * * @param {HTMLImageElement} e - 图像HTML元素 */ convertToDynamicImage(e) { const options = { // 元素ID id: e.getAttribute("id") || undefined, // 元素类名 class: e.getAttribute("class") || undefined, // 图像来源 url: this._currentUrlJoin(e.getAttribute("src")) || undefined, // 图像格式 format: e.getAttribute("format") || undefined, // 图像宽度 width: e.getNumberAttribute("width") || e.width, // 图像高度 height: e.getNumberAttribute("height") || e.height, // 图像播放开始时间点(毫秒) startTime: e.getNumberAttribute("start-time") || e.getNumberAttribute("startTime") || this.currentTime, // 图像播放结束时间点(毫秒) endTime: Math.min(e.getNumberAttribute("end-time") || e.getNumberAttribute("endTime") || Infinity, this.config.duration), // 是否循环播放 loop: e.getBooleanAttribute("loop"), // 拉取失败时重试拉取次数 retryFetchs: e.getNumberAttribute("retry-fetchs") || e.getNumberAttribute("retryFetchs") }; let canvas; if (!(e instanceof HTMLCanvasElement)) { // 创建画布元素 canvas = this._createCanvas(options); // 复制目标元素样式 this._copyElementStyle(e, canvas); // 代理目标元素所有属性和行为 this._buildElementProxy(e, canvas); // 将目标元素替换为画布 e.replaceWith(canvas); } else { canvas = e; canvas.width = 0; canvas.height = 0; } // 实例化动态图像实例 const dynamicImage = new ____DynamicImage(options); // 绑定画布元素 dynamicImage.bind(canvas); // 将对象加入媒体调度列表 this.dispatchMedias.push(dynamicImage); return dynamicImage; } /** * 将HTMLLottie元素转换为Lottie画布 * * @param {HTMLElement} e - LottieHTML元素 */ convertToLottieCanvas(e) { const options = { // 元素ID id: e.getAttribute("id") || undefined, // 元素类名 class: e.getAttribute("class") || undefined, // lottie来源 url: this._currentUrlJoin(e.getAttribute("src")) || undefined, // 动画宽度 width: parseInt(e.style.width) || e.getNumberAttribute("width"), // 动画宽度 height: parseInt(e.style.height) || e.getNumberAttribute("height"), // 动画播放开始时间点(毫秒) startTime: e.getNumberAttribute("start-time") || e.getNumberAttribute("startTime") || this.currentTime, // 动画播放结束时间点(毫秒) endTime: Math.min(e.getNumberAttribute("end-time") || e.getNumberAttribute("endTime") || Infinity, this.config.duration), // 是否循环播放 loop: e.getBooleanAttribute("loop"), // 拉取失败时重试拉取次数 retryFetchs: e.getNumberAttribute("retry-fetchs") || e.getNumberAttribute("retryFetchs") }; let canvas; if (!(e instanceof HTMLCanvasElement)) { // 创建画布元素 canvas = this._createCanvas(options); // 复制目标元素样式 this._copyElementStyle(e, canvas); // 代理目标元素所有属性和行为 this._buildElementProxy(e, canvas) // 将目标元素替换为画布 e.replaceWith(canvas); } else { canvas = e; canvas.width = 0; canvas.height = 0; } // 实例化Lottie动画实例 const lottieCanvas = new ____LottieCanvas(options); // 绑定画布元素 lottieCanvas.bind(canvas); // 将对象加入媒体调度列表 this.dispatchMedias.push(lottieCanvas); return lottieCanvas; } /** * 抛出错误中断捕获 * * @param {number} code 错误码 * @param {number} message 错误消息 */ throwError(code, message) { ____throwError(code, message); } /** * 复制元素样式 * * @private * @param {HTMLElement} source - 被复制HTML元素 * @param {HTMLElement} target - 新元素 */ _copyElementStyle(source, target) { const sourceStyle = window.getComputedStyle(source); for (var i = 0; i < sourceStyle.length; i++) { var property = sourceStyle[i]; var value = sourceStyle.getPropertyValue(property); target.style.setProperty(property, value); } } /** * 建立元素代理 * 将对旧元素的所有行为代理到新元素 * * @private * @param {HTMLElement} source - 被代理HTML元素 * @param {HTMLElement} target - 新元素 */ _buildElementProxy(source, target) { // 监听元素 Object.defineProperties(source, { textContent: { get: () => target.textContent, set: v => target.textContent = v }, innerHTML: { get: () => target.innerHTML, set: v => target.innerHTML = v }, innerText: { get: () => target.innerText, set: v => target.innerText = v }, setHTML: { get: () => target.setHTML, set: v => target.setHTML = v }, getInnerHTML: { get: () => target.getInnerHTML, set: v => target.getInnerHTML = v }, getRootNode: { get: () => target.getRootNode, set: v => target.getRootNode = v }, value: { get: () => target.value, set: v => target.value = v }, style: { get: () => target.style, set: v => target.style = v }, src: { get: () => target.src, set: v => target.src = v }, classList: { get: () => target.classList, set: v => target.classList = v }, className: { get: () => target.className, set: v => target.className = v }, hidden: { get: () => target.hidden, set: v => target.hidden = v }, animate: { get: () => target.animate, set: v => target.animate = v }, attributes: { get: () => target.attributes, set: v => target.attributes = v }, childNodes: { get: () => target.childNodes, set: v => target.childNodes = v }, children: { get: () => target.children, set: v => target.children = v }, addEventListener: { get: () => target.addEventListener, set: v => target.addEventListener = v }, removeEventListener: { get: () => target.removeEventListener, set: v => target.removeEventListener = v }, append: { get: () => target.append, set: v => target.append = v }, appendChild: { get: () => target.appendChild, set: v => target.appendChild = v }, prepend: { get: () => target.prepend, set: v => target.prepend = v }, replaceChild: { get: () => target.replaceChild, set: v => target.replaceChild = v }, replaceChildren: { get: () => target.replaceChildren, set: v => target.replaceChildren = v }, removeChild: { get: () => target.removeChild, set: v => target.removeChild = v }, blur: { get: () => target.blur, set: v => target.blur = v }, title: { get: () => target.title, set: v => target.title = v }, toString: { get: () => target.toString, set: v => target.toString = v }, autofocus: { get: () => target.autofocus, set: v => target.autofocus = v }, parentElement: { get: () => target.parentElement, set: v => target.parentElement = v }, parentNode: { get: () => target.parentNode, set: v => target.parentNode = v }, clientWidth: { get: () => target.clientWidth, set: v => target.clientWidth = v }, clientHeight: { get: () => target.clientHeight, set: v => target.clientHeight = v }, clientTop: { get: () => target.clientTop, set: v => target.clientTop = v }, clientLeft: { get: () => target.clientLeft, set: v => target.clientLeft = v }, removeAttribute: { get: () => target.removeAttribute, set: v => target.removeAttribute = v }, removeAttributeNode: { get: () => target.removeAttributeNode, set: v => target.removeAttributeNode = v }, removeAttributeNS: { get: () => target.removeAttributeNS, set: v => target.removeAttributeNS = v }, setAttribute: { get: () => target.setAttribute, set: v => target.setAttribute = v }, setAttributeNS: { get: () => target.setAttributeNS, set: v => target.setAttributeNS = v }, setAttributeNode: { get: () => target.setAttributeNode, set: v => target.setAttributeNode = v }, setAttributeNodeNS: { get: () => target.setAttributeNodeNS, set: v => target.setAttributeNodeNS = v }, getAttributeNames: { get: () => target.getAttributeNames, set: v => target.getAttributeNames = v }, getAttribute: { get: () => target.getAttribute, set: v => target.getAttribute = v }, getAttributeNS: { get: () => target.getAttributeNS, set: v => target.getAttributeNS = v }, getAttributeNode: { get: () => target.getAttributeNode, set: v => target.getAttributeNode = v }, getAttributeNodeNS: { get: () => target.getAttributeNodeNS, set: v => target.getAttributeNodeNS = v }, hasAttribute: { get: () => target.hasAttribute, set: v => target.hasAttribute = v }, hasAttributeNS: { get: () => target.hasAttributeNS, set: v => target.hasAttributeNS = v }, hasAttributes: { get: () => target.hasAttributes, set: v => target.hasAttributes = v }, hasChildNodes: { get: () => target.hasChildNodes, set: v => target.hasChildNodes = v }, hasOwnProperty: { get: () => target.hasOwnProperty, set: v => target.hasOwnProperty = v }, offsetParent: { get: () => target.offsetParent, set: v => target.offsetParent = v }, offsetTop: { get: () => target.offsetTop, set: v => target.offsetTop = v }, offsetLeft: { get: () => target.offsetLeft, set: v => target.offsetLeft = v }, offsetWidth: { get: () => target.offsetWidth, set: v => target.offsetWidth = v }, offsetHeight: { get: () => target.offsetHeight, set: v => target.offsetHeight = v }, hasChildNodes: { get: () => target.hasChildNodes, set: v => target.hasChildNodes = v }, getAnimations: { get: () => target.getAnimations, set: v => target.getAnimations = v }, scroll: { get: () => target.scroll, set: v => target.scroll = v }, scrollBy: { get: () => target.scrollBy, set: v => target.scrollBy = v }, scrollIntoView: { get: () => target.scrollIntoView, set: v => target.scrollIntoView = v }, scrollIntoViewIfNeeded: { get: () => target.scrollIntoViewIfNeeded, set: v => target.scrollIntoViewIfNeeded = v }, scrollTop: { get: () => target.scrollTop, set: v => target.scrollTop = v }, scrollLeft: { get: () => target.scrollLeft, set: v => target.scrollLeft = v }, scrollWidth: { get: () => target.scrollWidth, set: v => target.scrollWidth = v }, scrollHeight: { get: () => target.scrollHeight, set: v => target.scrollHeight = v }, dataset: { get: () => target.dataset, set: v => target.dataset = v }, insert: { get: () => target.insert, set: v => target.insert = v }, insertBefore: { get: () => target.insertBefore, set: v => target.insertBefore = v }, before: { get: () => target.before, set: v => target.before = v }, firstChild: { get: () => target.firstChild, set: v => target.firstChild = v }, firstElementChild: { get: () => target.firstElementChild, set: v => target.firstElementChild = v }, lastChild: { get: () => target.lastChild, set: v => target.lastChild = v }, lastElementChild: { get: () => target.lastElementChild, set: v => target.lastElementChild = v }, closest: { get: () => target.closest, set: v => target.closest = v }, valueOf: { get: () => target.valueOf, set: v => target.valueOf = v }, click: { get: () => target.click, set: v => target.click = v }, cloneNode: { get: () => target.cloneNode, set: v => target.cloneNode = v }, nodeName: { get: () => target.nodeName, set: v => target.nodeName = v }, nodeType: { get: () => target.nodeType, set: v => target.nodeType = v }, nodeValue: { get: () => target.nodeValue, set: v => target.nodeValue = v }, normalize: { get: () => target.normalize, set: v => target.normalize = v }, matches: { get: () => target.matches, set: v => target.matches = v }, play: { get: () => () => { } }, pause: { get: () => () => { } } }); source.remove = () => target.remove(); } /** * 拉取响应 * * @param {string} url - 拉取URL * @param {Object} options - 拉取选项 * @param {number} [options.method="GET"] - 请求方法 * @param {number} [options.body] - 请求体 * @param {number} [options.retryFetchs=2] - 重试次数 * @param {number} [options.retryDelay=500] - 重试延迟 * @returns {Response} - 响应对象 */ async fetch(url, options = {}, _retryIndex = 0) { const { retryFetchs = 2, retryDelay = 500, ...fetchOptions } = options; return await new Promise((resolve, reject) => { fetch(url, fetchOptions) .then(async response => { if (response.status >= 500) throw new Error(`Failed to load resource: [${fetchOptions.method || "GET"}] ${response.url} - [${response.status}] ${response.statusText}\n${await response.text()}`); else if (response.status >= 400) resolve(null); else resolve(response); }) .catch(err => { if (_retryIndex >= retryFetchs) reject(err); else ____setTimeout(() => this.fetch(url, options, _retryIndex + 1), retryDelay); }); }); } }