项目初始化

This commit is contained in:
jerry
2025-01-21 01:46:34 +08:00
parent 364021b042
commit 48153e7761
962 changed files with 172070 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
declare const my: WechatMiniprogram.Wx;
export function getMiniBridge (): WechatMiniprogram.Wx {
if (typeof wx === 'undefined' && typeof my !== 'undefined') {
return my;
}
return wx;
}

View File

@@ -0,0 +1,16 @@
export class BezierPath {
_d?: string;
_transform?: any;
_styles?: any;
_shape?: any;
constructor(
readonly d: string,
readonly transform: any,
readonly styles: any
) {
this._d = d;
this._transform = transform;
this._styles = styles;
}
}

View File

@@ -0,0 +1,20 @@
import { BezierPath } from "./bezier_path";
export class EllipsePath extends BezierPath {
_x;
_y;
_radiusX;
_radiusY;
_transform;
_styles;
constructor(x: number, y: number, radiusX: number, radiusY: number, transform: any, styles: any) {
super('', transform, styles);
this._x = x;
this._y = y;
this._radiusX = radiusX;
this._radiusY = radiusY;
this._transform = transform;
this._styles = styles;
}
}

View File

@@ -0,0 +1,183 @@
import { BezierPath } from "./bezier_path";
export class FrameEntity {
alpha = 0.0;
transform = {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
tx: 0.0,
ty: 0.0,
};
layout = {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
};
nx = 0.0;
ny = 0.0;
maskPath?: BezierPath;
shapes: any[] = [];
static lastShapes: any;
constructor(spec: any) {
this.alpha = parseFloat(spec.alpha) || 0.0;
if (spec.layout) {
this.layout.x = parseFloat(spec.layout.x) || 0.0;
this.layout.y = parseFloat(spec.layout.y) || 0.0;
this.layout.width = parseFloat(spec.layout.width) || 0.0;
this.layout.height = parseFloat(spec.layout.height) || 0.0;
}
if (spec.transform) {
this.transform.a = parseFloat(spec.transform.a) || 1.0;
this.transform.b = parseFloat(spec.transform.b) || 0.0;
this.transform.c = parseFloat(spec.transform.c) || 0.0;
this.transform.d = parseFloat(spec.transform.d) || 1.0;
this.transform.tx = parseFloat(spec.transform.tx) || 0.0;
this.transform.ty = parseFloat(spec.transform.ty) || 0.0;
}
if (spec.clipPath && spec.clipPath.length > 0) {
this.maskPath = new BezierPath(spec.clipPath, undefined, {
fill: "#000000",
});
}
if (spec.shapes) {
if (spec.shapes instanceof Array) {
spec.shapes.forEach((shape: any) => {
shape.pathArgs = shape.args;
switch (shape.type) {
case 0:
shape.type = "shape";
shape.pathArgs = shape.shape;
break;
case 1:
shape.type = "rect";
shape.pathArgs = shape.rect;
break;
case 2:
shape.type = "ellipse";
shape.pathArgs = shape.ellipse;
break;
case 3:
shape.type = "keep";
break;
}
if (shape.styles) {
if (shape.styles.fill) {
if (typeof shape.styles.fill["r"] === "number")
shape.styles.fill[0] = shape.styles.fill["r"];
if (typeof shape.styles.fill["g"] === "number")
shape.styles.fill[1] = shape.styles.fill["g"];
if (typeof shape.styles.fill["b"] === "number")
shape.styles.fill[2] = shape.styles.fill["b"];
if (typeof shape.styles.fill["a"] === "number")
shape.styles.fill[3] = shape.styles.fill["a"];
}
if (shape.styles.stroke) {
if (typeof shape.styles.stroke["r"] === "number")
shape.styles.stroke[0] = shape.styles.stroke["r"];
if (typeof shape.styles.stroke["g"] === "number")
shape.styles.stroke[1] = shape.styles.stroke["g"];
if (typeof shape.styles.stroke["b"] === "number")
shape.styles.stroke[2] = shape.styles.stroke["b"];
if (typeof shape.styles.stroke["a"] === "number")
shape.styles.stroke[3] = shape.styles.stroke["a"];
}
let lineDash = shape.styles.lineDash || [];
if (shape.styles.lineDashI > 0) {
lineDash.push(shape.styles.lineDashI);
}
if (shape.styles.lineDashII > 0) {
if (lineDash.length < 1) {
lineDash.push(0);
}
lineDash.push(shape.styles.lineDashII);
lineDash.push(0);
}
if (shape.styles.lineDashIII > 0) {
if (lineDash.length < 2) {
lineDash.push(0);
lineDash.push(0);
}
lineDash[2] = shape.styles.lineDashIII;
}
shape.styles.lineDash = lineDash;
switch (shape.styles.lineJoin) {
case 0:
shape.styles.lineJoin = "miter";
break;
case 1:
shape.styles.lineJoin = "round";
break;
case 2:
shape.styles.lineJoin = "bevel";
break;
}
switch (shape.styles.lineCap) {
case 0:
shape.styles.lineCap = "butt";
break;
case 1:
shape.styles.lineCap = "round";
break;
case 2:
shape.styles.lineCap = "square";
break;
}
}
});
}
if (spec.shapes[0] && spec.shapes[0].type === "keep") {
this.shapes = FrameEntity.lastShapes;
} else {
this.shapes = spec.shapes;
FrameEntity.lastShapes = spec.shapes;
}
}
let llx =
this.transform.a * this.layout.x +
this.transform.c * this.layout.y +
this.transform.tx;
let lrx =
this.transform.a * (this.layout.x + this.layout.width) +
this.transform.c * this.layout.y +
this.transform.tx;
let lbx =
this.transform.a * this.layout.x +
this.transform.c * (this.layout.y + this.layout.height) +
this.transform.tx;
let rbx =
this.transform.a * (this.layout.x + this.layout.width) +
this.transform.c * (this.layout.y + this.layout.height) +
this.transform.tx;
let lly =
this.transform.b * this.layout.x +
this.transform.d * this.layout.y +
this.transform.ty;
let lry =
this.transform.b * (this.layout.x + this.layout.width) +
this.transform.d * this.layout.y +
this.transform.ty;
let lby =
this.transform.b * this.layout.x +
this.transform.d * (this.layout.y + this.layout.height) +
this.transform.ty;
let rby =
this.transform.b * (this.layout.x + this.layout.width) +
this.transform.d * (this.layout.y + this.layout.height) +
this.transform.ty;
this.nx = Math.min(Math.min(lbx, rbx), Math.min(llx, lrx));
this.ny = Math.min(Math.min(lby, rby), Math.min(lly, lry));
}
}

View File

@@ -0,0 +1,5 @@
import { Parser as MParser } from "./parser";
import { Player as MPlayer } from "./player";
export const Parser = MParser;
export const Player = MPlayer;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
import { VideoEntity } from "./video_entity";
import { getMiniBridge } from "./adaptor";
const { inflate } = require("./pako");
const { ProtoMovieEntity } = require("./proto");
const wx = getMiniBridge();
export class Parser {
load(url: string): Promise<VideoEntity> {
return new Promise((resolver, rejector) => {
if (url.indexOf("http://") === 0 || url.indexOf("https://") === 0) {
wx.request({
url: url,
// @ts-ignore
dataType: "arraybuffer",
responseType: "arraybuffer",
success: (res) => {
try {
const inflatedData = inflate(res.data as any);
const movieData = ProtoMovieEntity.decode(inflatedData);
resolver(new VideoEntity(movieData));
} catch (error) {
rejector(error);
}
},
fail: (error) => {
rejector(error);
},
});
} else {
wx.getFileSystemManager().readFile({
filePath: url,
success: (res) => {
try {
const inflatedData = inflate(res.data as any);
const movieData = ProtoMovieEntity.decode(inflatedData);
resolver(new VideoEntity(movieData));
} catch (error) {
rejector(error);
}
},
fail: (error) => {
rejector(error);
},
});
}
});
}
}

View File

@@ -0,0 +1,331 @@
"use strict";
import { Renderer } from "./renderer";
import { ValueAnimator } from "./value_animator";
import { VideoEntity } from "./video_entity";
import { getMiniBridge } from "./adaptor";
const wx = getMiniBridge();
interface Range {
location: number;
length: number;
}
interface DynamicText {
text: string;
size: number;
family: string;
color: string;
offset: { x: number; y: number };
}
export class Player {
canvas?: WechatMiniprogram.Canvas;
ctx?: WechatMiniprogram.CanvasContext;
async setCanvas(
selector: string,
component?: WechatMiniprogram.Component.TrivialInstance
): Promise<any> {
return new Promise((resolver, rej) => {
let query = wx.createSelectorQuery();
if (component) {
query = query.in(component);
}
query
.select(selector)
.fields({ node: true, size: true })
.exec((res) => {
this.canvas = res?.[0]?.node;
if (!this.canvas) {
rej("canvas not found.");
return;
}
this.ctx = this.canvas!.getContext("2d");
if (!this.ctx) {
rej("canvas context not found.");
return;
}
const dpr = wx.getSystemInfoSync().pixelRatio;
this.canvas!.width = res[0].width * dpr;
this.canvas!.height = res[0].height * dpr;
resolver(undefined);
});
});
}
loops = 0;
clearsAfterStop = true;
fillMode = "Forward";
_videoItem?: VideoEntity;
async setVideoItem(videoItem?: VideoEntity): Promise<any> {
this._currentFrame = 0;
this._videoItem = videoItem;
if (videoItem) {
const keyedImages = await Promise.all(
Object.keys(videoItem.spec.images).map(async (it) => {
try {
const data = await this.loadWXImage(videoItem.spec.images[it]);
return { key: it, value: data };
} catch (error) {
return { key: it, value: undefined };
}
})
);
let decodedImages: { [key: string]: any } = {};
keyedImages.forEach((it) => {
decodedImages[it.key] = it.value;
});
videoItem.decodedImages = decodedImages;
this._renderer = new Renderer(this._videoItem!, this.ctx!, this.canvas!);
} else {
this._renderer = undefined;
}
this.clear();
this._update();
}
loadWXImage(data: Uint8Array | string): Promise<any> {
if (!this.canvas) throw "no canvas";
return new Promise((res, rej) => {
const img: WechatMiniprogram.Image = this.canvas!.createImage();
img.onload = () => {
res(img);
};
img.onerror = (error) => {
console.log(error);
rej("image decoded fail.");
};
if (typeof data === "string") {
img.src = data;
} else {
img.src = "data:image/png;base64," + wx.arrayBufferToBase64(data);
}
});
}
_contentMode = "AspectFit";
setContentMode(contentMode: string) {
this._contentMode = contentMode;
this._update();
}
startAnimation(reverse: boolean = false) {
this.stopAnimation(false);
this._doStart(undefined, reverse, undefined);
}
startAnimationWithRange(range: Range, reverse: boolean = false) {
this.stopAnimation(false);
this._doStart(range, reverse, undefined);
}
pauseAnimation() {
this.stopAnimation(false);
}
stopAnimation(clear?: boolean) {
this._forwardAnimating = false;
if (this._animator !== undefined) {
this._animator.stop();
}
if (clear === undefined) {
clear = this.clearsAfterStop;
}
if (clear) {
this.clear();
}
}
clear() {
this._renderer?.clear();
// this._renderer?.clearAudios();
}
stepToFrame(frame: number, andPlay: boolean = false) {
const videoItem = this._videoItem;
if (!videoItem) return;
if (frame >= videoItem.frames || frame < 0) {
return;
}
this.pauseAnimation();
this._currentFrame = frame;
this._update();
if (andPlay) {
this._doStart(undefined, false, this._currentFrame);
}
}
stepToPercentage(percentage: number, andPlay: boolean = false) {
const videoItem = this._videoItem;
if (!videoItem) return;
let frame = percentage * videoItem.frames;
if (frame >= videoItem.frames && frame > 0) {
frame = videoItem.frames - 1;
}
this.stepToFrame(frame, andPlay);
}
async setImage(src: Uint8Array | string, forKey: string): Promise<any> {
const img = await this.loadWXImage(src);
this._dynamicImage[forKey] = img;
}
setText(dynamicText: DynamicText, forKey: string) {
this._dynamicText[forKey] = dynamicText;
}
clearDynamicObjects() {
this._dynamicImage = {};
this._dynamicText = {};
}
onFinished(callback: () => void) {
this._onFinished = callback;
}
onFrame(callback: (frame: number) => void) {
this._onFrame = callback;
}
onPercentage(callback: (percentage: number) => void) {
this._onPercentage = callback;
}
/**
* Private methods & properties
*/
_renderer?: Renderer;
_animator: ValueAnimator = new ValueAnimator();
_forwardAnimating = false;
_currentFrame = 0;
_dynamicImage: { [key: string]: any } = {};
_dynamicText: { [key: string]: DynamicText } = {};
_onFinished?: () => void;
_onFrame?: (frame: number) => void;
_onPercentage?: (percentage: number) => void;
_doStart(range?: Range, reverse: boolean = false, fromFrame: number = 0) {
const videoItem = this._videoItem;
if (!videoItem) return;
this._animator = new ValueAnimator();
this._animator.canvas = this.canvas;
if (range !== undefined) {
this._animator.startValue = Math.max(0, range.location);
this._animator.endValue = Math.min(
videoItem.frames - 1,
range.location + range.length
);
this._animator.duration =
(this._animator.endValue - this._animator.startValue + 1) *
(1.0 / videoItem.FPS) *
1000;
} else {
this._animator.startValue = 0;
this._animator.endValue = videoItem.frames - 1;
this._animator.duration = videoItem.frames * (1.0 / videoItem.FPS) * 1000;
}
this._animator.loops = this.loops <= 0 ? Infinity : this.loops;
this._animator.fillRule = this.fillMode === "Backward" ? 1 : 0;
this._animator.onUpdate = (value) => {
if (this._currentFrame === Math.floor(value)) {
return;
}
if (this._forwardAnimating && this._currentFrame > Math.floor(value)) {
// this._renderer.clearAudios();
}
this._currentFrame = Math.floor(value);
this._update();
if (typeof this._onFrame === "function") {
this._onFrame(this._currentFrame);
}
if (typeof this._onPercentage === "function") {
this._onPercentage((this._currentFrame + 1) / videoItem.frames);
}
};
this._animator.onEnd = () => {
this._forwardAnimating = false;
if (this.clearsAfterStop === true) {
this.clear();
}
if (typeof this._onFinished === "function") {
this._onFinished();
}
};
if (reverse === true) {
this._animator.reverse(fromFrame);
this._forwardAnimating = false;
} else {
this._animator.start(fromFrame);
this._forwardAnimating = true;
}
this._currentFrame = this._animator.startValue;
this._update();
}
_resize() {
const ctx = this.ctx;
const videoItem = this._videoItem;
if (!ctx) return;
if (!videoItem) return;
let scaleX = 1.0;
let scaleY = 1.0;
let translateX = 0.0;
let translateY = 0.0;
let targetSize = {
width: this.canvas!.width,
height: this.canvas!.height,
};
let imageSize = videoItem.videoSize;
if (this._contentMode === "Fill") {
scaleX = targetSize.width / imageSize.width;
scaleY = targetSize.height / imageSize.height;
} else if (
this._contentMode === "AspectFit" ||
this._contentMode === "AspectFill"
) {
const imageRatio = imageSize.width / imageSize.height;
const viewRatio = targetSize.width / targetSize.height;
if (
(imageRatio >= viewRatio && this._contentMode === "AspectFit") ||
(imageRatio <= viewRatio && this._contentMode === "AspectFill")
) {
scaleX = scaleY = targetSize.width / imageSize.width;
translateY = (targetSize.height - imageSize.height * scaleY) / 2.0;
} else if (
(imageRatio < viewRatio && this._contentMode === "AspectFit") ||
(imageRatio > viewRatio && this._contentMode === "AspectFill")
) {
scaleX = scaleY = targetSize.height / imageSize.height;
translateX = (targetSize.width - imageSize.width * scaleX) / 2.0;
}
}
if (this._renderer) {
this._renderer.globalTransform = {
a: scaleX,
b: 0.0,
c: 0.0,
d: scaleY,
tx: translateX,
ty: translateY,
};
}
}
_update() {
this._resize();
if (this._renderer) {
this._renderer._dynamicImage = this._dynamicImage;
this._renderer._dynamicText = this._dynamicText;
this._renderer.drawFrame(this._currentFrame);
this._renderer.playAudio(this._currentFrame);
}
}
}

View File

@@ -0,0 +1,5 @@
const protobuf = require("../protobuf_mp/protobuf")
const svgaDescriptor = JSON.parse(`{"nested":{"com":{"nested":{"opensource":{"nested":{"svga":{"options":{"objc_class_prefix":"SVGAProto","java_package":"com.opensource.svgaplayer.proto"},"nested":{"MovieParams":{"fields":{"viewBoxWidth":{"type":"float","id":1},"viewBoxHeight":{"type":"float","id":2},"fps":{"type":"int32","id":3},"frames":{"type":"int32","id":4}}},"SpriteEntity":{"fields":{"imageKey":{"type":"string","id":1},"frames":{"rule":"repeated","type":"FrameEntity","id":2},"matteKey":{"type":"string","id":3}}},"AudioEntity":{"fields":{"audioKey":{"type":"string","id":1},"startFrame":{"type":"int32","id":2},"endFrame":{"type":"int32","id":3},"startTime":{"type":"int32","id":4},"totalTime":{"type":"int32","id":5}}},"Layout":{"fields":{"x":{"type":"float","id":1},"y":{"type":"float","id":2},"width":{"type":"float","id":3},"height":{"type":"float","id":4}}},"Transform":{"fields":{"a":{"type":"float","id":1},"b":{"type":"float","id":2},"c":{"type":"float","id":3},"d":{"type":"float","id":4},"tx":{"type":"float","id":5},"ty":{"type":"float","id":6}}},"ShapeEntity":{"oneofs":{"args":{"oneof":["shape","rect","ellipse"]}},"fields":{"type":{"type":"ShapeType","id":1},"shape":{"type":"ShapeArgs","id":2},"rect":{"type":"RectArgs","id":3},"ellipse":{"type":"EllipseArgs","id":4},"styles":{"type":"ShapeStyle","id":10},"transform":{"type":"Transform","id":11}},"nested":{"ShapeType":{"values":{"SHAPE":0,"RECT":1,"ELLIPSE":2,"KEEP":3}},"ShapeArgs":{"fields":{"d":{"type":"string","id":1}}},"RectArgs":{"fields":{"x":{"type":"float","id":1},"y":{"type":"float","id":2},"width":{"type":"float","id":3},"height":{"type":"float","id":4},"cornerRadius":{"type":"float","id":5}}},"EllipseArgs":{"fields":{"x":{"type":"float","id":1},"y":{"type":"float","id":2},"radiusX":{"type":"float","id":3},"radiusY":{"type":"float","id":4}}},"ShapeStyle":{"fields":{"fill":{"type":"RGBAColor","id":1},"stroke":{"type":"RGBAColor","id":2},"strokeWidth":{"type":"float","id":3},"lineCap":{"type":"LineCap","id":4},"lineJoin":{"type":"LineJoin","id":5},"miterLimit":{"type":"float","id":6},"lineDashI":{"type":"float","id":7},"lineDashII":{"type":"float","id":8},"lineDashIII":{"type":"float","id":9}},"nested":{"RGBAColor":{"fields":{"r":{"type":"float","id":1},"g":{"type":"float","id":2},"b":{"type":"float","id":3},"a":{"type":"float","id":4}}},"LineCap":{"values":{"LineCap_BUTT":0,"LineCap_ROUND":1,"LineCap_SQUARE":2}},"LineJoin":{"values":{"LineJoin_MITER":0,"LineJoin_ROUND":1,"LineJoin_BEVEL":2}}}}}},"FrameEntity":{"fields":{"alpha":{"type":"float","id":1},"layout":{"type":"Layout","id":2},"transform":{"type":"Transform","id":3},"clipPath":{"type":"string","id":4},"shapes":{"rule":"repeated","type":"ShapeEntity","id":5}}},"MovieEntity":{"fields":{"version":{"type":"string","id":1},"params":{"type":"MovieParams","id":2},"images":{"keyType":"string","type":"bytes","id":3},"sprites":{"rule":"repeated","type":"SpriteEntity","id":4},"audios":{"rule":"repeated","type":"AudioEntity","id":5}}}}}}}}}}}`)
export const proto = protobuf.Root.fromJSON(svgaDescriptor);
export const ProtoMovieEntity = proto.lookupType("com.opensource.svga.MovieEntity");

View File

@@ -0,0 +1,30 @@
import { BezierPath } from "./bezier_path";
export class RectPath extends BezierPath {
_x;
_y;
_width;
_height;
_cornerRadius;
_transform;
_styles;
constructor(
x: number,
y: number,
width: number,
height: number,
cornerRadius: number,
transform: any,
styles: any
) {
super("", transform, styles);
this._x = x;
this._y = y;
this._width = width;
this._height = height;
this._cornerRadius = cornerRadius;
this._transform = transform;
this._styles = styles;
}
}

View File

@@ -0,0 +1,558 @@
import { BezierPath } from "./bezier_path";
import { EllipsePath } from "./ellipse_path";
import { RectPath } from "./rect_path";
import { SpriteEntity } from "./sprite_entity";
import { VideoEntity } from "./video_entity";
interface Point {
x: number;
y: number;
x1: number;
y1: number;
x2: number;
y2: number;
}
interface DynamicText {
text: string;
size: number;
family: string;
color: string;
offset: { x: number; y: number };
}
const validMethods = "MLHVCSQRZmlhvcsqrz";
export class Renderer {
constructor(
readonly videoItem: VideoEntity,
readonly ctx: WechatMiniprogram.CanvasContext,
readonly canvas: WechatMiniprogram.Canvas
) {}
globalTransform?: {
a: number;
b: number;
c: number;
d: number;
tx: number;
ty: number;
};
_dynamicImage: { [key: string]: any } = {};
_dynamicText: { [key: string]: DynamicText } = {};
clear() {
const ctx = this.ctx;
const areaFrame = {
x: 0.0,
y: 0.0,
width: this.canvas.width,
height: this.canvas.height,
};
ctx.clearRect(areaFrame.x, areaFrame.y, areaFrame.width, areaFrame.height);
}
drawFrame(frame: number) {
const ctx = this.ctx;
const areaFrame = {
x: 0.0,
y: 0.0,
width: this.canvas.width,
height: this.canvas.height,
};
ctx.clearRect(areaFrame.x, areaFrame.y, areaFrame.width, areaFrame.height);
var matteSprites: any = {};
var isMatteing = false;
var sprites = this.videoItem.sprites;
sprites.forEach((sprite, index) => {
if (sprites[0].imageKey?.indexOf(".matte") == -1) {
this.drawSprite(sprite, frame);
return;
}
if (sprite.imageKey?.indexOf(".matte") != -1) {
matteSprite[sprite.imageKey!] = sprite;
return;
}
var lastSprite = sprites[index - 1];
if (
isMatteing &&
(!sprite.matteKey ||
sprite.matteKey.length == 0 ||
sprite.matteKey != lastSprite.matteKey)
) {
isMatteing = false;
var matteSprite = matteSprites[sprite.matteKey!];
ctx.globalCompositeOperation = "destination-in";
this.drawSprite(matteSprite, frame);
ctx.globalCompositeOperation = "source-over";
ctx.restore();
}
if (
sprite.matteKey != null &&
(lastSprite.matteKey == null ||
lastSprite.matteKey.length == 0 ||
lastSprite.matteKey != sprite.matteKey)
) {
isMatteing = true;
ctx.save();
}
this.drawSprite(sprite, frame);
if (isMatteing && index == sprites.length - 1) {
var matteSprite = matteSprites.get(sprite.matteKey);
ctx.globalCompositeOperation = "destination-in";
this.drawSprite(matteSprite, frame);
ctx.globalCompositeOperation = "source-over";
ctx.restore();
}
});
}
drawSprite(sprite: SpriteEntity, frameIndex: number) {
let frameItem = sprite.frames[frameIndex];
if (frameItem.alpha < 0.05) {
return;
}
const ctx = this.ctx;
ctx.save();
if (this.globalTransform) {
ctx.transform(
this.globalTransform.a,
this.globalTransform.b,
this.globalTransform.c,
this.globalTransform.d,
this.globalTransform.tx,
this.globalTransform.ty
);
}
ctx.globalAlpha = frameItem.alpha;
ctx.transform(
frameItem.transform.a,
frameItem.transform.b,
frameItem.transform.c,
frameItem.transform.d,
frameItem.transform.tx,
frameItem.transform.ty
);
let bitmapKey = sprite.imageKey?.replace(".matte", "");
if (!bitmapKey) return;
let img =
this._dynamicImage[bitmapKey] ?? this.videoItem.decodedImages[bitmapKey];
if (frameItem.maskPath !== undefined && frameItem.maskPath !== null) {
frameItem.maskPath._styles = undefined;
this.drawBezier(frameItem.maskPath);
ctx.clip();
}
if (img) {
ctx.drawImage(
img,
0,
0,
img.width,
img.height,
0,
0,
frameItem.layout.width,
frameItem.layout.height
);
}
frameItem.shapes &&
frameItem.shapes.forEach((shape: any) => {
if (shape.type === "shape" && shape.pathArgs && shape.pathArgs.d) {
this.drawBezier(
new BezierPath(shape.pathArgs.d, shape.transform, shape.styles)
);
}
if (shape.type === "ellipse" && shape.pathArgs) {
this.drawEllipse(
new EllipsePath(
parseFloat(shape.pathArgs.x) || 0.0,
parseFloat(shape.pathArgs.y) || 0.0,
parseFloat(shape.pathArgs.radiusX) || 0.0,
parseFloat(shape.pathArgs.radiusY) || 0.0,
shape.transform,
shape.styles
)
);
}
if (shape.type === "rect" && shape.pathArgs) {
this.drawRect(
new RectPath(
parseFloat(shape.pathArgs.x) || 0.0,
parseFloat(shape.pathArgs.y) || 0.0,
parseFloat(shape.pathArgs.width) || 0.0,
parseFloat(shape.pathArgs.height) || 0.0,
parseFloat(shape.pathArgs.cornerRadius) || 0.0,
shape.transform,
shape.styles
)
);
}
});
let dynamicText = this._dynamicText[bitmapKey];
if (dynamicText !== undefined) {
ctx.font = `${dynamicText.size}px ${dynamicText.family ?? "Arial"}`;
let textWidth = ctx.measureText(dynamicText.text).width;
ctx.fillStyle = dynamicText.color;
let offsetX =
dynamicText.offset !== undefined && dynamicText.offset.x !== undefined
? isNaN(dynamicText.offset.x)
? 0
: dynamicText.offset.x
: 0;
let offsetY =
dynamicText.offset !== undefined && dynamicText.offset.y !== undefined
? isNaN(dynamicText.offset.y)
? 0
: dynamicText.offset.y
: 0;
ctx.fillText(
dynamicText.text,
(frameItem.layout.width - textWidth) / 2 + offsetX,
frameItem.layout.height / 2 + offsetY
);
}
ctx.restore();
}
playAudio(frame: number) {}
resetShapeStyles(obj: any) {
const ctx = this.ctx;
const styles = obj._styles;
if (!styles) {
return;
}
if (styles && styles.stroke) {
ctx.strokeStyle = `rgba(${(styles.stroke[0] * 255).toFixed(0)}, ${(
styles.stroke[1] * 255
).toFixed(0)}, ${(styles.stroke[2] * 255).toFixed(0)}, ${
styles.stroke[3]
})`;
} else {
ctx.strokeStyle = "transparent";
}
if (styles) {
ctx.lineWidth = styles.strokeWidth || undefined;
ctx.lineCap = styles.lineCap || undefined;
ctx.lineJoin = styles.lineJoin || undefined;
ctx.miterLimit = styles.miterLimit || undefined;
}
if (styles && styles.fill) {
ctx.fillStyle = `rgba(${(styles.fill[0] * 255).toFixed(0)}, ${(
styles.fill[1] * 255
).toFixed(0)}, ${(styles.fill[2] * 255).toFixed(0)}, ${styles.fill[3]})`;
} else {
ctx.fillStyle = "transparent";
}
if (styles && styles.lineDash) {
ctx.setLineDash(
[styles.lineDash[0], styles.lineDash[1]],
styles.lineDash[2]
);
}
}
drawBezier(obj: any) {
const ctx = this.ctx;
ctx.save();
this.resetShapeStyles(obj);
if (obj._transform !== undefined && obj._transform !== null) {
ctx.transform(
obj._transform.a,
obj._transform.b,
obj._transform.c,
obj._transform.d,
obj._transform.tx,
obj._transform.ty
);
}
let currentPoint: Point = { x: 0, y: 0, x1: 0, y1: 0, x2: 0, y2: 0 };
ctx.beginPath();
const d = obj._d.replace(/([a-zA-Z])/g, "|||$1 ").replace(/,/g, " ");
d.split("|||").forEach((segment: string) => {
if (segment.length == 0) {
return;
}
const firstLetter = segment.substr(0, 1);
if (validMethods.indexOf(firstLetter) >= 0) {
const args = segment.substr(1).trim().split(" ");
this.drawBezierElement(currentPoint, firstLetter, args);
}
});
if (obj._styles && obj._styles.fill) {
ctx.fill();
}
if (obj._styles && obj._styles.stroke) {
ctx.stroke();
}
ctx.restore();
}
drawBezierElement(currentPoint: Point, method: string, args: any) {
const ctx = this.ctx;
switch (method) {
case "M":
currentPoint.x = Number(args[0]);
currentPoint.y = Number(args[1]);
ctx.moveTo(currentPoint.x, currentPoint.y);
break;
case "m":
currentPoint.x += Number(args[0]);
currentPoint.y += Number(args[1]);
ctx.moveTo(currentPoint.x, currentPoint.y);
break;
case "L":
currentPoint.x = Number(args[0]);
currentPoint.y = Number(args[1]);
ctx.lineTo(currentPoint.x, currentPoint.y);
break;
case "l":
currentPoint.x += Number(args[0]);
currentPoint.y += Number(args[1]);
ctx.lineTo(currentPoint.x, currentPoint.y);
break;
case "H":
currentPoint.x = Number(args[0]);
ctx.lineTo(currentPoint.x, currentPoint.y);
break;
case "h":
currentPoint.x += Number(args[0]);
ctx.lineTo(currentPoint.x, currentPoint.y);
break;
case "V":
currentPoint.y = Number(args[0]);
ctx.lineTo(currentPoint.x, currentPoint.y);
break;
case "v":
currentPoint.y += Number(args[0]);
ctx.lineTo(currentPoint.x, currentPoint.y);
break;
case "C":
currentPoint.x1 = Number(args[0]);
currentPoint.y1 = Number(args[1]);
currentPoint.x2 = Number(args[2]);
currentPoint.y2 = Number(args[3]);
currentPoint.x = Number(args[4]);
currentPoint.y = Number(args[5]);
ctx.bezierCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x2,
currentPoint.y2,
currentPoint.x,
currentPoint.y
);
break;
case "c":
currentPoint.x1 = currentPoint.x + Number(args[0]);
currentPoint.y1 = currentPoint.y + Number(args[1]);
currentPoint.x2 = currentPoint.x + Number(args[2]);
currentPoint.y2 = currentPoint.y + Number(args[3]);
currentPoint.x += Number(args[4]);
currentPoint.y += Number(args[5]);
ctx.bezierCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x2,
currentPoint.y2,
currentPoint.x,
currentPoint.y
);
break;
case "S":
if (
currentPoint.x1 &&
currentPoint.y1 &&
currentPoint.x2 &&
currentPoint.y2
) {
currentPoint.x1 = currentPoint.x - currentPoint.x2 + currentPoint.x;
currentPoint.y1 = currentPoint.y - currentPoint.y2 + currentPoint.y;
currentPoint.x2 = Number(args[0]);
currentPoint.y2 = Number(args[1]);
currentPoint.x = Number(args[2]);
currentPoint.y = Number(args[3]);
ctx.bezierCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x2,
currentPoint.y2,
currentPoint.x,
currentPoint.y
);
} else {
currentPoint.x1 = Number(args[0]);
currentPoint.y1 = Number(args[1]);
currentPoint.x = Number(args[2]);
currentPoint.y = Number(args[3]);
ctx.quadraticCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x,
currentPoint.y
);
}
break;
case "s":
if (
currentPoint.x1 &&
currentPoint.y1 &&
currentPoint.x2 &&
currentPoint.y2
) {
currentPoint.x1 = currentPoint.x - currentPoint.x2 + currentPoint.x;
currentPoint.y1 = currentPoint.y - currentPoint.y2 + currentPoint.y;
currentPoint.x2 = currentPoint.x + Number(args[0]);
currentPoint.y2 = currentPoint.y + Number(args[1]);
currentPoint.x += Number(args[2]);
currentPoint.y += Number(args[3]);
ctx.bezierCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x2,
currentPoint.y2,
currentPoint.x,
currentPoint.y
);
} else {
currentPoint.x1 = currentPoint.x + Number(args[0]);
currentPoint.y1 = currentPoint.y + Number(args[1]);
currentPoint.x += Number(args[2]);
currentPoint.y += Number(args[3]);
ctx.quadraticCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x,
currentPoint.y
);
}
break;
case "Q":
currentPoint.x1 = Number(args[0]);
currentPoint.y1 = Number(args[1]);
currentPoint.x = Number(args[2]);
currentPoint.y = Number(args[3]);
ctx.quadraticCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x,
currentPoint.y
);
break;
case "q":
currentPoint.x1 = currentPoint.x + Number(args[0]);
currentPoint.y1 = currentPoint.y + Number(args[1]);
currentPoint.x += Number(args[2]);
currentPoint.y += Number(args[3]);
ctx.quadraticCurveTo(
currentPoint.x1,
currentPoint.y1,
currentPoint.x,
currentPoint.y
);
break;
case "A":
break;
case "a":
break;
case "Z":
case "z":
ctx.closePath();
break;
default:
break;
}
}
drawEllipse(obj: any) {
const ctx = this.ctx;
ctx.save();
this.resetShapeStyles(obj);
if (obj._transform !== undefined && obj._transform !== null) {
ctx.transform(
obj._transform.a,
obj._transform.b,
obj._transform.c,
obj._transform.d,
obj._transform.tx,
obj._transform.ty
);
}
let x = obj._x - obj._radiusX;
let y = obj._y - obj._radiusY;
let w = obj._radiusX * 2;
let h = obj._radiusY * 2;
var kappa = 0.5522848,
ox = (w / 2) * kappa,
oy = (h / 2) * kappa,
xe = x + w,
ye = y + h,
xm = x + w / 2,
ym = y + h / 2;
ctx.beginPath();
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
if (obj._styles && obj._styles.fill) {
ctx.fill();
}
if (obj._styles && obj._styles.stroke) {
ctx.stroke();
}
ctx.restore();
}
drawRect(obj: any) {
const ctx = this.ctx;
ctx.save();
this.resetShapeStyles(obj);
if (obj._transform !== undefined && obj._transform !== null) {
ctx.transform(
obj._transform.a,
obj._transform.b,
obj._transform.c,
obj._transform.d,
obj._transform.tx,
obj._transform.ty
);
}
let x = obj._x;
let y = obj._y;
let width = obj._width;
let height = obj._height;
let radius = obj._cornerRadius;
if (width < 2 * radius) {
radius = width / 2;
}
if (height < 2 * radius) {
radius = height / 2;
}
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
if (obj._styles && obj._styles.fill) {
ctx.fill();
}
if (obj._styles && obj._styles.stroke) {
ctx.stroke();
}
ctx.restore();
}
}

View File

@@ -0,0 +1,18 @@
import { FrameEntity } from "./frame_entity";
export class SpriteEntity {
matteKey?: string;
imageKey?: string;
frames: any[];
constructor(spec: any) {
this.matteKey = spec.matteKey;
this.imageKey = spec.imageKey;
this.frames =
spec.frames?.map((obj: any) => {
return new FrameEntity(obj);
}) ?? [];
}
}

View File

@@ -0,0 +1,99 @@
export class ValueAnimator {
static currentTimeMillsecond = () => {
if (typeof performance === "undefined") {
return new Date().getTime();
}
return performance.now();
};
canvas?: WechatMiniprogram.Canvas;
startValue = 0;
endValue = 0;
duration = 0;
loops = 1;
fillRule = 0;
mRunning = false;
mStartTime = 0;
mCurrentFrication = 0.0;
mReverse = false;
onStart = () => {};
onUpdate = (value: number) => {};
onEnd = () => {};
start(currentValue?: number) {
this.doStart(false, currentValue);
}
reverse(currentValue?: number) {
this.doStart(true, currentValue);
}
stop() {
this.doStop();
}
animatedValue() {
return (
(this.endValue - this.startValue) * this.mCurrentFrication +
this.startValue
);
}
doStart(reverse: boolean, currentValue: number | undefined = undefined) {
this.mReverse = reverse;
this.mRunning = true;
this.mStartTime = ValueAnimator.currentTimeMillsecond();
if (currentValue) {
if (reverse) {
this.mStartTime -=
(1.0 - currentValue / (this.endValue - this.startValue)) *
this.duration;
} else {
this.mStartTime -=
(currentValue / (this.endValue - this.startValue)) * this.duration;
}
}
this.mCurrentFrication = 0.0;
this.onStart();
this.doFrame();
}
doStop() {
this.mRunning = false;
}
doFrame() {
const renderLoop = () => {
if (!this.mRunning) return;
this.doDeltaTime(ValueAnimator.currentTimeMillsecond() - this.mStartTime);
this.canvas?.requestAnimationFrame(renderLoop);
};
this.canvas?.requestAnimationFrame(renderLoop);
// if (this.mRunning) {
// this.doDeltaTime(ValueAnimator.currentTimeMillsecond() - this.mStartTime)
// if (this.mRunning && this.canvas !== undefined) {
// this.canvas.requestAnimationFrame(this.doFrame.bind(this))
// }
// }
}
doDeltaTime(deltaTime: number) {
let ended = false;
if (deltaTime >= this.duration * this.loops) {
this.mCurrentFrication = this.fillRule === 1 ? 0.0 : 1.0;
this.mRunning = false;
ended = true;
} else {
this.mCurrentFrication = (deltaTime % this.duration) / this.duration;
if (this.mReverse) {
this.mCurrentFrication = 1.0 - this.mCurrentFrication;
}
}
this.onUpdate(this.animatedValue());
if (this.mRunning === false && ended) {
this.onEnd();
}
}
}

View File

@@ -0,0 +1,37 @@
import { SpriteEntity } from "./sprite_entity";
export class VideoEntity {
version = "2.0.0";
videoSize: {
width: number;
height: number;
};
FPS: number;
frames: number;
sprites: SpriteEntity[];
audios: any[];
decodedImages: { [key: string]: any } = {};
constructor(
readonly spec: any
) {
this.version = spec.ver;
this.videoSize = {
width: spec.params.viewBoxWidth || 0.0,
height: spec.params.viewBoxHeight || 0.0,
};
this.FPS = spec.params.fps || 20;
this.frames = spec.params.frames || 0;
this.sprites =
spec.sprites?.map((obj: any) => {
return new SpriteEntity(obj);
}) ?? [];
this.audios = [];
}
}