octopus-svga
Version:
高性能SVGA动效播放器,支持Web/微信小程序/支付宝小程序/抖音小程序,设计目标是 解析速度更快、体积更小、性能更高、兼容性更高、功能更丰富。
1,721 lines (1,700 loc) • 172 kB
JavaScript
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) {