UNPKG

octopus-svga

Version:

高性能SVGA动效播放器,支持Web/微信小程序/支付宝小程序/抖音小程序,设计目标是 解析速度更快、体积更小、性能更高、兼容性更高、功能更丰富。

1,721 lines (1,700 loc) 172 kB
import { OctopusPlatform, pluginSelector, pluginCanvas, pluginOfsCanvas, pluginDecode, pluginDownload, pluginFsm, pluginImage, pluginNow, pluginPath, pluginRAF, installPlugin } from 'octopus-platform'; class EnhancedPlatform extends OctopusPlatform { now; path; remote; local; decode; image; rAF; getSelector; getCanvas; getOfsCanvas; constructor() { super([ pluginSelector, pluginCanvas, pluginOfsCanvas, pluginDecode, pluginDownload, pluginFsm, pluginImage, pluginNow, pluginPath, pluginRAF, ], "1.2.2"); this.init(); } installPlugin(plugin) { installPlugin(this, plugin); } } const platform = new EnhancedPlatform(); class ResourceManager { painter; /** * 判断是否是 ImageBitmap * @param img * @returns */ static isBitmap(img) { return platform.globals.env === "h5" && img instanceof ImageBitmap; } /** * 释放内存资源(图片) * @param img */ static releaseOne(img) { if (ResourceManager.isBitmap(img)) { img.close(); } else if (img.src !== "") { // 【微信】将存在本地的文件删除,防止用户空间被占满 if (platform.globals.env === "weapp" && img.src.includes(platform.path.USER_DATA_PATH)) { platform.local.remove(img.src); } platform.image.release(img); } } // FIXME: 微信小程序创建调用太多createImage会导致微信/微信小程序崩溃 caches = []; /** * 动态素材 */ dynamicMaterials = new Map(); /** * 素材 */ materials = new Map(); /** * 已清理Image对象的坐标 */ point = 0; constructor(painter) { this.painter = painter; } /** * 创建图片标签 * @returns */ createImage() { let img = null; if (this.point > 0) { this.point--; img = this.caches.shift(); } if (!img) { img = platform.image.create(this.painter.F); } this.caches.push(img); return img; } /** * 将 ImageBitmap 插入到 caches * @param img */ inertBitmapIntoCaches(img) { if (ResourceManager.isBitmap(img)) { this.caches.push(img); } } /** * 加载额外的图片资源 * @param source 资源内容/地址 * @param filename 文件名称 * @returns */ loadExtImage(source, filename) { return platform.image .load(() => this.createImage(), source, platform.path.resolve(filename, "ext")) .then((img) => { this.inertBitmapIntoCaches(img); return img; }); } /** * 加载图片集 * @param images 图片数据 * @param filename 文件名称 * @returns */ async loadImagesWithRecord(images, filename, type = "normal") { const imageAwaits = []; const imageFilename = `${filename.replace(/\.svga$/g, "")}.png`; Object.entries(images).forEach(([name, image]) => { // 过滤 1px 透明图 if (image instanceof Uint8Array && image.byteLength < 70) { return; } const p = platform.image .load(() => this.createImage(), image, platform.path.resolve(imageFilename, type === "dynamic" ? `dyn_${name}` : name)) .then((img) => { this.inertBitmapIntoCaches(img); if (type === "dynamic") { this.dynamicMaterials.set(name, img); } else { this.materials.set(name, img); } }); imageAwaits.push(p); }); await Promise.all(imageAwaits); } /** * 释放图片资源 */ release() { // FIXME: 小程序 image 对象需要手动释放内存,否则可能导致小程序崩溃 for (const img of this.caches) { ResourceManager.releaseOne(img); } this.materials.clear(); this.dynamicMaterials.clear(); // FIXME: 支付宝小程序 image 修改 src 无法触发 onload 事件 platform.globals.env === "alipay" ? this.cleanup() : this.tidyUp(); } /** * 整理图片资源,将重复的图片资源移除 */ tidyUp() { // 通过 Set 的去重特性,保持 caches 元素的唯一性 this.caches = Array.from(new Set(this.caches)); this.point = this.caches.length; } /** * 清理图片资源 */ cleanup() { this.caches.length = 0; this.point = 0; } } function readFloatLEImpl() { // 使用静态DataView池 const DATA_VIEW_POOL_SIZE = 4; const dataViewPool = Array(DATA_VIEW_POOL_SIZE) .fill(0) .map(() => new DataView(new ArrayBuffer(8))); // 使用8字节支持double let currentViewIndex = 0; return function readFloatLE(buf, pos) { if (pos < 0 || pos + 4 > buf.length) throw new RangeError("Index out of range"); // 轮换使用DataView池中的实例 const view = dataViewPool[currentViewIndex]; currentViewIndex = (currentViewIndex + 1) % DATA_VIEW_POOL_SIZE; // 直接设置字节,避免创建subarray const u8 = new Uint8Array(view.buffer); u8[0] = buf[pos]; u8[1] = buf[pos + 1]; u8[2] = buf[pos + 2]; u8[3] = buf[pos + 3]; return view.getFloat32(0, true); }; } const readFloatLE = readFloatLEImpl(); /** * 简易的hash算法 * @param buff * @param start * @param end * @param step * @returns */ function calculateHash(buff, start, end, step) { // 使用简单的哈希算法 let hash = 0; for (let i = start; i < end; i += step) { // 简单的哈希算法,类似于字符串哈希 hash = (hash << 5) - hash + buff[i]; hash = hash & hash; // 转换为32位整数 } // 添加数据长度作为哈希的一部分,增加唯一性 hash = (hash << 5) - hash + end - start; hash = hash & hash; // 转换为字符串 return hash.toString(36); } class Preflight { caches = new Map(); count = 0; get size() { return this.caches.size; } get hitCount() { return this.count; } // get cache() { // return Object.fromEntries(this.caches); // } /** * 计算二进制数据的哈希值 * @param reader Reader对象 * @param end 结束位置 * @returns 哈希值 */ calculate(reader, end) { // 保存原始位置 const { pos: startPos, buf } = reader; const endPos = Math.min(end, reader.len); // 采样数据以加快计算速度,同时保持足够的唯一性 // 对于大数据,每隔几个字节采样一次 const step = Math.max(1, Math.floor((endPos - startPos) / 100)); return calculateHash(buf, startPos, endPos, step); } /** * 检查是否存在缓存数据 * @param key 键 * @returns 是否存在 */ has(key) { const hit = this.caches.has(key); if (hit) { this.count++; } return hit; // return this.caches.has(key); } /** * 获取缓存数据 * @param key 键 * @returns 缓存数据 */ get(key) { return this.caches.get(key); } /** * 设置缓存数据 * @param key 键 * @param value 缓存数据 */ set(key, value) { this.caches.set(key, value); } /** * 清空所有缓存数据 */ clear() { this.count = 0; this.caches.clear(); } } class Reader { // 添加静态缓存,用于常用的空数组 static EMPTY_UINT8ARRAY = new Uint8Array(0); /** * Read buffer. * @type {Uint8Array} */ buf; /** * Read buffer length. * @type {number} */ len; /** * Read buffer position. * @type {number} */ pos; preflight = new Preflight(); /** * Constructs a new reader instance using the specified buffer. * @classdesc Wire format reader using `Uint8Array` if available, otherwise `Array`. * @constructor * @param {Uint8Array} buffer Buffer to read from */ constructor(buffer) { this.buf = buffer; this.pos = 0; this.len = buffer.length; } indexOutOfRange(reader, writeLength) { return new RangeError("index out of range: " + reader.pos + " + " + (writeLength || 1) + " > " + reader.len); } /** * 将复杂逻辑分离到单独方法 * @returns */ readVarint32Slow() { let byte = this.buf[this.pos++]; let value = byte & 0x7f; let shift = 7; // 使用do-while循环减少条件判断 do { if (this.pos >= this.len) { throw this.indexOutOfRange(this); } byte = this.buf[this.pos++]; value |= (byte & 0x7f) << shift; shift += 7; } while (byte >= 128 && shift < 32); return value >>> 0; // 确保无符号 } /** * Reads a sequence of bytes preceded by its length as a varint. * @param length * @returns */ end(length) { return length === undefined ? this.len : this.pos + length; } /** * Reads a varint as an unsigned 32 bit value. * @function * @returns {number} Value read */ uint32() { // 快速路径:大多数情况下是单字节 const byte = this.buf[this.pos]; if (byte < 128) { this.pos++; return byte; } // 慢速路径:多字节处理 return this.readVarint32Slow(); } /** * Reads a varint as a signed 32 bit value. * @returns {number} Value read */ int32() { return this.uint32() | 0; } /** * Reads a float (32 bit) as a number. * @function * @returns {number} Value read */ float() { const pos = this.pos + 4; if (pos > this.len) { throw this.indexOutOfRange(this, 4); } const value = readFloatLE(this.buf, this.pos); this.pos = pos; return value; } /** * read bytes range * @returns */ getBytesRange() { const length = this.uint32(); const start = this.pos; const end = start + length; if (end > this.len) { throw this.indexOutOfRange(this, length); } return [start, end, length]; } /** * Reads a sequence of bytes preceded by its length as a varint. * @returns {Uint8Array} Value read */ bytes() { const [start, end, length] = this.getBytesRange(); this.pos += length; if (length === 0) { return Reader.EMPTY_UINT8ARRAY; } return this.buf.subarray(start, end); } /** * Reads a string preceeded by its byte length as a varint. * @returns {string} Value read */ string() { const [start, end] = this.getBytesRange(); // 直接在原始buffer上解码,避免创建中间bytes对象 const result = platform.decode.utf8(this.buf, start, end); this.pos = end; return result; } /** * Skips the specified number of bytes if specified, otherwise skips a varint. * @param {number} [length] Length if known, otherwise a varint is assumed * @returns {Reader} `this` */ skip(length) { if (typeof length === "number") { if (this.pos + length > this.len) { throw this.indexOutOfRange(this, length); } this.pos += length; return this; } // 变长整数跳过优化 - 使用位运算 const { buf, len } = this; let pos = this.pos; // 一次检查多个字节,减少循环次数 while (pos < len) { const byte = buf[pos++]; if ((byte & 0x80) === 0) { this.pos = pos; return this; } // 快速检查连续的高位字节 if (pos < len && (buf[pos] & 0x80) !== 0) { pos++; if (pos < len && (buf[pos] & 0x80) !== 0) { pos++; if (pos < len && (buf[pos] & 0x80) !== 0) { pos++; // 继续检查剩余字节 while (pos < len && (buf[pos] & 0x80) !== 0) { pos++; if (pos - this.pos >= 10) { throw Error("invalid varint encoding"); } } if (pos < len) { this.pos = pos + 1; return this; } } } } } throw this.indexOutOfRange(this); } /** * Skips the next element of the specified wire type. * @param {number} wireType Wire type received * @returns {Reader} `this` */ skipType(wireType) { switch (wireType) { case 0: this.skip(); break; case 1: this.skip(8); break; case 2: this.skip(this.uint32()); break; case 3: while ((wireType = this.uint32() & 7) !== 4) { this.skipType(wireType); } break; case 5: this.skip(4); break; /* istanbul ignore next */ default: throw Error("invalid wire type " + wireType + " at offset " + this.pos); } return this; } } class Layout { /** * Decodes a Layout message from the specified reader. * @function decode * @memberof com.opensource.svga.Layout * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.Layout} Layout * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const { preflight } = reader; const end = reader.end(length); const hash = preflight.calculate(reader, end); if (preflight.has(hash)) { reader.pos = end; return preflight.get(hash); } const message = new Layout(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.x = reader.float(); break; } case 2: { message.y = reader.float(); break; } case 3: { message.width = reader.float(); break; } case 4: { message.height = reader.float(); break; } default: reader.skipType(tag & 7); break; } } preflight.set(hash, Layout.format(message)); return preflight.get(hash); } static format(message) { const { x = 0, y = 0, width = 0, height = 0 } = message; return { x, y, width, height }; } /** * Layout x. * @member {number} x * @memberof com.opensource.svga.Layout * @instance */ x = 0; /** * Layout y. * @member {number} y * @memberof com.opensource.svga.Layout * @instance */ y = 0; /** * Layout width. * @member {number} width * @memberof com.opensource.svga.Layout * @instance */ width = 0; /** * Layout height. * @member {number} height * @memberof com.opensource.svga.Layout * @instance */ height = 0; } class Transform { /** * Decodes a Transform message from the specified reader. * @function decode * @memberof com.opensource.svga.Transform * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.Transform} Transform * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const end = reader.end(length); const message = new Transform(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.a = reader.float(); break; } case 2: { message.b = reader.float(); break; } case 3: { message.c = reader.float(); break; } case 4: { message.d = reader.float(); break; } case 5: { message.tx = reader.float(); break; } case 6: { message.ty = reader.float(); break; } default: reader.skipType(tag & 7); break; } } return message; } /** * Transform a. * @member {number} a * @memberof com.opensource.svga.Transform * @instance */ a = 0; /** * Transform b. * @member {number} b * @memberof com.opensource.svga.Transform * @instance */ b = 0; /** * Transform c. * @member {number} c * @memberof com.opensource.svga.Transform * @instance */ c = 0; /** * Transform d. * @member {number} d * @memberof com.opensource.svga.Transform * @instance */ d = 0; /** * Transform tx. * @member {number} tx * @memberof com.opensource.svga.Transform * @instance */ tx = 0; /** * Transform ty. * @member {number} ty * @memberof com.opensource.svga.Transform * @instance */ ty = 0; } class ShapeArgs { /** * Decodes a ShapeArgs message from the specified reader. * @function decode * @memberof com.opensource.svga.ShapeEntity.ShapeArgs * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.ShapeEntity.ShapeArgs} ShapeArgs * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const { preflight } = reader; const end = reader.end(length); const hash = preflight.calculate(reader, end); if (preflight.has(hash)) { reader.pos = end; return preflight.get(hash); } const message = new ShapeArgs(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.d = reader.string(); break; } default: reader.skipType(tag & 7); break; } } preflight.set(hash, message); return preflight.get(hash); } /** * ShapeArgs d. * @member {string} d * @memberof com.opensource.svga.ShapeEntity.ShapeArgs * @instance */ d = ""; } class RectArgs { /** * Decodes a RectArgs message from the specified reader. * @function decode * @memberof com.opensource.svga.ShapeEntity.RectArgs * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.ShapeEntity.RectArgs} RectArgs * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const { preflight } = reader; const end = reader.end(length); const hash = preflight.calculate(reader, end); if (preflight.has(hash)) { reader.pos = end; return preflight.get(hash); } const message = new RectArgs(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.x = reader.float(); break; } case 2: { message.y = reader.float(); break; } case 3: { message.width = reader.float(); break; } case 4: { message.height = reader.float(); break; } case 5: { message.cornerRadius = reader.float(); break; } default: reader.skipType(tag & 7); break; } } preflight.set(hash, message); return preflight.get(hash); } /** * RectArgs x. * @member {number} x * @memberof com.opensource.svga.ShapeEntity.RectArgs * @instance */ x = 0; /** * RectArgs y. * @member {number} y * @memberof com.opensource.svga.ShapeEntity.RectArgs * @instance */ y = 0; /** * RectArgs width. * @member {number} width * @memberof com.opensource.svga.ShapeEntity.RectArgs * @instance */ width = 0; /** * RectArgs height. * @member {number} height * @memberof com.opensource.svga.ShapeEntity.RectArgs * @instance */ height = 0; /** * RectArgs cornerRadius. * @member {number} cornerRadius * @memberof com.opensource.svga.ShapeEntity.RectArgs * @instance */ cornerRadius = 0; } class EllipseArgs { /** * Decodes an EllipseArgs message from the specified reader. * @function decode * @memberof com.opensource.svga.ShapeEntity.EllipseArgs * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.ShapeEntity.EllipseArgs} EllipseArgs * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const { preflight } = reader; const end = reader.end(length); const hash = preflight.calculate(reader, end); if (preflight.has(hash)) { reader.pos = end; return preflight.get(hash); } const message = new EllipseArgs(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.x = reader.float(); break; } case 2: { message.y = reader.float(); break; } case 3: { message.radiusX = reader.float(); break; } case 4: { message.radiusY = reader.float(); break; } default: reader.skipType(tag & 7); break; } } preflight.set(hash, message); return preflight.get(hash); } /** * EllipseArgs x. * @member {number} x * @memberof com.opensource.svga.ShapeEntity.EllipseArgs * @instance */ x = 0; /** * EllipseArgs y. * @member {number} y * @memberof com.opensource.svga.ShapeEntity.EllipseArgs * @instance */ y = 0; /** * EllipseArgs radiusX. * @member {number} radiusX * @memberof com.opensource.svga.ShapeEntity.EllipseArgs * @instance */ radiusX = 0; /** * EllipseArgs radiusY. * @member {number} radiusY * @memberof com.opensource.svga.ShapeEntity.EllipseArgs * @instance */ radiusY = 0; } class RGBAColor { /** * Decodes a RGBAColor message from the specified reader. * @function decode * @memberof com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor} RGBAColor * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const { preflight } = reader; const end = reader.end(length); const hash = preflight.calculate(reader, end); if (preflight.has(hash)) { reader.pos = end; return preflight.get(hash); } const message = new RGBAColor(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.r = reader.float(); break; } case 2: { message.g = reader.float(); break; } case 3: { message.b = reader.float(); break; } case 4: { message.a = reader.float(); break; } default: reader.skipType(tag & 7); break; } } preflight.set(hash, RGBAColor.format(message)); return preflight.get(hash); } static format(message) { const { r, g, b, a } = message; return `rgba(${(r * 255) | 0}, ${(g * 255) | 0}, ${(b * 255) | 0}, ${(a * 1) | 0})`; } /** * RGBAColor r. * @member {number} r * @memberof com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor * @instance */ r = 0; /** * RGBAColor g. * @member {number} g * @memberof com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor * @instance */ g = 0; /** * RGBAColor b. * @member {number} b * @memberof com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor * @instance */ b = 0; /** * RGBAColor a. * @member {number} a * @memberof com.opensource.svga.ShapeEntity.ShapeStyle.RGBAColor * @instance */ a = 0; } class ShapeStyle { /** * Decodes a ShapeStyle message from the specified reader. * @function decode * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.ShapeEntity.ShapeStyle} ShapeStyle * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const { preflight } = reader; const end = reader.end(length); const hash = preflight.calculate(reader, end); if (preflight.has(hash)) { reader.pos = end; return preflight.get(hash); } const message = new ShapeStyle(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.fill = RGBAColor.decode(reader, reader.uint32()); break; } case 2: { message.stroke = RGBAColor.decode(reader, reader.uint32()); break; } case 3: { message.strokeWidth = reader.float(); break; } case 4: { message.lineCap = reader.int32(); break; } case 5: { message.lineJoin = reader.int32(); break; } case 6: { message.miterLimit = reader.float(); break; } case 7: { message.lineDashI = reader.float(); break; } case 8: { message.lineDashII = reader.float(); break; } case 9: { message.lineDashIII = reader.float(); break; } default: reader.skipType(tag & 7); break; } } preflight.set(hash, ShapeStyle.format(message)); return preflight.get(hash); } static format(message) { const { fill, stroke, strokeWidth, miterLimit, lineDashI, lineDashII, lineDashIII } = message; const lineDash = []; let lineCap; let lineJoin; if (lineDashI > 0) { lineDash.push(lineDashI); } if (lineDashII > 0) { if (lineDash.length < 1) { lineDash.push(0); } lineDash.push(lineDashII); } if (lineDashIII > 0) { if (lineDash.length < 2) { lineDash.push(0, 0); } lineDash.push(lineDashIII); } switch (message.lineCap) { case 0 /* PlatformVideo.LINE_CAP_CODE.BUTT */: lineCap = "butt" /* PlatformVideo.LINE_CAP.BUTT */; break; case 1 /* PlatformVideo.LINE_CAP_CODE.ROUND */: lineCap = "round" /* PlatformVideo.LINE_CAP.ROUND */; break; case 2 /* PlatformVideo.LINE_CAP_CODE.SQUARE */: lineCap = "square" /* PlatformVideo.LINE_CAP.SQUARE */; break; } switch (message.lineJoin) { case 0 /* PlatformVideo.LINE_JOIN_CODE.MITER */: lineJoin = "miter" /* PlatformVideo.LINE_JOIN.MITER */; break; case 1 /* PlatformVideo.LINE_JOIN_CODE.ROUND */: lineJoin = "round" /* PlatformVideo.LINE_JOIN.ROUND */; break; case 2 /* PlatformVideo.LINE_JOIN_CODE.BEVEL */: lineJoin = "bevel" /* PlatformVideo.LINE_JOIN.BEVEL */; break; } return { lineDash, fill: fill ? fill : null, stroke: stroke ? stroke : null, lineCap, lineJoin, strokeWidth, miterLimit }; } /** * ShapeStyle fill. * @member {com.opensource.svga.ShapeEntity.ShapeStyle.IRGBAColor|null|undefined} fill * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ fill = null; /** * ShapeStyle stroke. * @member {com.opensource.svga.ShapeEntity.ShapeStyle.IRGBAColor|null|undefined} stroke * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ stroke = null; /** * ShapeStyle strokeWidth. * @member {number} strokeWidth * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ strokeWidth = 0; /** * ShapeStyle lineCap. * @member {com.opensource.svga.ShapeEntity.ShapeStyle.LineCap} lineCap * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ lineCap = 0; /** * ShapeStyle lineJoin. * @member {com.opensource.svga.ShapeEntity.ShapeStyle.LineJoin} lineJoin * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ lineJoin = 0; /** * ShapeStyle miterLimit. * @member {number} miterLimit * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ miterLimit = 0; /** * ShapeStyle lineDashI. * @member {number} lineDashI * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ lineDashI = 0; /** * ShapeStyle lineDashII. * @member {number} lineDashII * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ lineDashII = 0; /** * ShapeStyle lineDashIII. * @member {number} lineDashIII * @memberof com.opensource.svga.ShapeEntity.ShapeStyle * @instance */ lineDashIII = 0; } class ShapeEntity { /** * Decodes a ShapeEntity message from the specified reader. * @function decode * @memberof com.opensource.svga.ShapeEntity * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.ShapeEntity} ShapeEntity * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const end = reader.end(length); const message = new ShapeEntity(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.type = reader.int32(); break; } case 2: { message.shape = ShapeArgs.decode(reader, reader.uint32()); break; } case 3: { message.rect = RectArgs.decode(reader, reader.uint32()); break; } case 4: { message.ellipse = EllipseArgs.decode(reader, reader.uint32()); break; } case 10: { message.styles = ShapeStyle.decode(reader, reader.uint32()); break; } case 11: { message.transform = Transform.decode(reader, reader.uint32()); break; } default: reader.skipType(tag & 7); break; } } return ShapeEntity.format(message); } static format(message) { const { type, shape, rect, ellipse, styles, transform } = message; switch (type) { case 0 /* PlatformVideo.SHAPE_TYPE_CODE.SHAPE */: return { type: "shape" /* PlatformVideo.SHAPE_TYPE.SHAPE */, path: shape, styles: styles, transform: transform, }; case 1 /* PlatformVideo.SHAPE_TYPE_CODE.RECT */: return { type: "rect" /* PlatformVideo.SHAPE_TYPE.RECT */, path: rect, styles: styles, transform: transform, }; case 2 /* PlatformVideo.SHAPE_TYPE_CODE.ELLIPSE */: return { type: "ellipse" /* PlatformVideo.SHAPE_TYPE.ELLIPSE */, path: ellipse, styles: styles, transform: transform, }; } return null; } /** * ShapeEntity type. * @member {com.opensource.svga.ShapeEntity.ShapeType} type * @memberof com.opensource.svga.ShapeEntity * @instance */ type = 0; /** * ShapeEntity shape. * @member {com.opensource.svga.ShapeEntity.IShapeArgs|null|undefined} shape * @memberof com.opensource.svga.ShapeEntity * @instance */ shape = null; /** * ShapeEntity rect. * @member {com.opensource.svga.ShapeEntity.IRectArgs|null|undefined} rect * @memberof com.opensource.svga.ShapeEntity * @instance */ rect = null; /** * ShapeEntity ellipse. * @member {com.opensource.svga.ShapeEntity.IEllipseArgs|null|undefined} ellipse * @memberof com.opensource.svga.ShapeEntity * @instance */ ellipse = null; /** * ShapeEntity styles. * @member {com.opensource.svga.ShapeEntity.IShapeStyle|null|undefined} styles * @memberof com.opensource.svga.ShapeEntity * @instance */ styles = null; /** * ShapeEntity transform. * @member {com.opensource.svga.ITransform|null|undefined} transform * @memberof com.opensource.svga.ShapeEntity * @instance */ transform = null; } class FrameEntity { static HIDDEN_FRAME = { alpha: 0, }; /** * Decodes a FrameEntity message from the specified reader. * @function decode * @memberof com.opensource.svga.FrameEntity * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.FrameEntity} FrameEntity * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const end = reader.end(length); const message = new FrameEntity(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.alpha = reader.float(); break; } case 2: { message.layout = Layout.decode(reader, reader.uint32()); break; } case 3: { message.transform = Transform.decode(reader, reader.uint32()); break; } case 4: { message.clipPath = reader.string(); break; } case 5: { const shape = ShapeEntity.decode(reader, reader.uint32()); if (shape !== null) { message.shapes.push(shape); } break; } default: reader.skipType(tag & 7); break; } } if (message.shapes.length === 0) { message.shapes = reader.preflight.get("latest_shapes"); } else { reader.preflight.set("latest_shapes", message.shapes); } return FrameEntity.format(message); } static format(message) { // alpha值小于 0.05 将不展示,所以不做解析处理 if (message.alpha < 0.05) { return FrameEntity.HIDDEN_FRAME; } const { alpha, layout, transform, shapes } = message; return { alpha, layout: layout, transform, shapes, }; } /** * FrameEntity shapes. * @member {Array.<com.opensource.svga.IShapeEntity>} shapes * @memberof com.opensource.svga.FrameEntity * @instance */ shapes = []; /** * FrameEntity alpha. * @member {number} alpha * @memberof com.opensource.svga.FrameEntity * @instance */ alpha = 0; /** * FrameEntity layout. * @member {com.opensource.svga.ILayout|null|undefined} layout * @memberof com.opensource.svga.FrameEntity * @instance */ layout = null; /** * FrameEntity transform. * @member {com.opensource.svga.ITransform|null|undefined} transform * @memberof com.opensource.svga.FrameEntity * @instance */ transform = null; /** * FrameEntity clipPath. * @member {string} clipPath * @memberof com.opensource.svga.FrameEntity * @instance */ clipPath = ""; } class SpriteEntity { /** * Decodes a SpriteEntity message from the specified reader. * @function decode * @memberof com.opensource.svga.SpriteEntity * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.SpriteEntity} SpriteEntity * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const end = reader.end(length); const message = new SpriteEntity(); let tag; reader.preflight.set("latest_shapes", []); while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.imageKey = reader.string(); break; } case 2: { if (!(message.frames && message.frames.length)) { message.frames = []; } message.frames.push(FrameEntity.decode(reader, reader.uint32())); break; } case 3: { message.matteKey = reader.string(); break; } default: reader.skipType(tag & 7); break; } } return SpriteEntity.format(message); } static format(message) { return { imageKey: message.imageKey, frames: message.frames, }; } /** * SpriteEntity frames. * @member {Array.<com.opensource.svga.IFrameEntity>} frames * @memberof com.opensource.svga.SpriteEntity * @instance */ frames = []; /** * SpriteEntity imageKey. * @member {string} imageKey * @memberof com.opensource.svga.SpriteEntity * @instance */ imageKey = ""; /** * SpriteEntity matteKey. * @member {string} matteKey * @memberof com.opensource.svga.SpriteEntity * @instance */ matteKey = ""; } class MovieParams { /** * Decodes a MovieParams message from the specified reader. * @function decode * @memberof com.opensource.svga.MovieParams * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.MovieParams} MovieParams * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const end = reader.end(length); const message = new MovieParams(); let tag; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.viewBoxWidth = reader.float(); break; } case 2: { message.viewBoxHeight = reader.float(); break; } case 3: { message.fps = reader.int32(); break; } case 4: { message.frames = reader.int32(); break; } default: reader.skipType(tag & 7); break; } } return message; } /** * MovieParams viewBoxWidth. * @member {number} viewBoxWidth * @memberof com.opensource.svga.MovieParams * @instance */ viewBoxWidth = 0; /** * MovieParams viewBoxHeight. * @member {number} viewBoxHeight * @memberof com.opensource.svga.MovieParams * @instance */ viewBoxHeight = 0; /** * MovieParams fps. * @member {number} fps * @memberof com.opensource.svga.MovieParams * @instance */ fps = 0; /** * MovieParams frames. * @member {number} frames * @memberof com.opensource.svga.MovieParams * @instance */ frames = 0; } class MovieEntity { static EMPTY_U8 = new Uint8Array(0); /** * Decodes a MovieEntity message from the specified reader. * @function decode * @memberof com.opensource.svga.MovieEntity * @static * @param {$protobuf.Reader} reader Reader to decode from * @param {number} [length] Message length if known beforehand * @returns {com.opensource.svga.MovieEntity} MovieEntity * @throws {Error} If the payload is not a reader or valid buffer * @throws {$protobuf.util.ProtocolError} If required fields are missing */ static decode(reader, length) { const end = reader.end(length); const message = new MovieEntity(); let key; let value; let end2; let tag; let tag2; while (reader.pos < end) { tag = reader.uint32(); switch (tag >>> 3) { case 1: { message.version = reader.string(); break; } case 2: { message.params = MovieParams.decode(reader, reader.uint32()); break; } case 3: { end2 = reader.uint32() + reader.pos; key = ""; value = MovieEntity.EMPTY_U8; while (reader.pos < end2) { tag2 = reader.uint32(); switch (tag2 >>> 3) { case 1: key = reader.string(); break; case 2: value = reader.bytes(); break; default: reader.skipType(tag2 & 7); break; } } message.images[key] = value; break; } case 4: { message.sprites.push(SpriteEntity.decode(reader, reader.uint32())); break; } default: reader.skipType(tag & 7); break; } } return MovieEntity.format(message); } static format(message) { const { version, images, sprites } = message; const { fps, frames, viewBoxWidth, viewBoxHeight } = message.params; return { version, filename: "", locked: false, dynamicElements: {}, size: { width: viewBoxWidth, height: viewBoxHeight, }, fps, frames, images, sprites, }; } /** * MovieEntity version. * @member {string} version * @memberof com.opensource.svga.MovieEntity * @instance */ version = ""; /** * MovieEntity params. * @member {com.opensource.svga.IMovieParams|null|undefined} params * @memberof com.opensource.svga.MovieEntity * @instance */ params = null; /** * MovieEntity images. * @member {Object.<string,Uint8Array>} images * @memberof com.opensource.svga.MovieEntity * @instance */ images = {}; /** * MovieEntity sprites. * @member {Array.<com.opensource.svga.ISpriteEntity>} sprites * @memberof com.opensource.svga.MovieEntity * @instance */ sprites = []; } // import benchmark from "octopus-benchmark"; function createVideoEntity(data, filename) { if (data instanceof Uint8Array) { const reader = new Reader(data); const video = MovieEntity.decode(reader); // benchmark.log('preflight cache size', reader.preflight.size); // benchmark.log('preflight hit count', reader.preflight.hitCount); video.filename = filename; reader.preflight.clear(); return video; } throw new Error("Invalid data type"); } /** * CurrentPoint对象池,用于减少对象创建和GC压力 */ class PointPool { pool = []; acquire() { const { pool } = this; return pool.length > 0 ? pool.pop() : { x: 0, y: 0, x1: 0, y1: 0, x2: 0, y2: 0 }; } release(point) { // 重置点的属性 point.x = point.y = point.x1 = point.y1 = point.x2 = point.y2 = 0; this.pool.push(point); } } class Renderer2D { context; /** * https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths * 绘制路径的不同指令: * * 直线命令 * - M: moveTo,移动到指定点,不绘制直线。 * - L: lineTo,从起始点绘制一条直线到指定点。 * - H: horizontal lineTo,从起始点绘制一条水平线到指定点。 * - V: vertical lineTo,从起始点绘制一条垂直线到指定点。 * - Z: closePath,从起始点绘制一条直线到路径起点,形成一个闭合路径。 * * 曲线命令 * - C: bezierCurveTo,绘制三次贝塞尔曲线。 * - S: smooth curveTo,绘制平滑三次贝塞尔曲线。 * - Q: quadraticCurveTo,绘制两次贝塞尔曲线。 * - T: smooth quadraticCurveTo,绘制平滑两次贝塞尔曲线。 * * 弧线命令 * - A: arcTo,从起始点绘制一条弧线到指定点。 */ static SVG_PATH = new Set([ "M", "L", "H", "V", "Z", "C", "S", "Q", "m", "l", "h", "v", "z", "c", "s", "q", ]); static SVG_LETTER_REGEXP = /[a-zA-Z]/; // 在Renderer2D类中添加新的解析方法 static parseSVGPath(d) {