223 lines
6.7 KiB
TypeScript
223 lines
6.7 KiB
TypeScript
/*
|
||
* https://github.com/morethanwords/tweb
|
||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||
*/
|
||
|
||
importScripts('rlottie-wasm.js');
|
||
//import Module, { allocate, intArrayFromString } from './rlottie-wasm';
|
||
|
||
const _Module = (self as any).Module as any;
|
||
|
||
const DEFAULT_FPS = 60;
|
||
|
||
export class RLottieItem {
|
||
private stringOnWasmHeap: any = null;
|
||
private handle: any = null;
|
||
private frameCount = 0;
|
||
|
||
private dead = false;
|
||
//private context: OffscreenCanvasRenderingContext2D;
|
||
|
||
constructor(private reqId: number, jsString: string, private width: number, private height: number, private fps: number/* , private canvas: OffscreenCanvas */) {
|
||
this.fps = Math.max(1, Math.min(60, fps || DEFAULT_FPS));
|
||
|
||
//this.context = canvas.getContext('2d');
|
||
|
||
this.init(jsString);
|
||
|
||
reply('loaded', this.reqId, this.frameCount, this.fps);
|
||
|
||
/* let frame = 0;
|
||
setInterval(() => {
|
||
if(frame >= this.frameCount) frame = 0;
|
||
let _frame = frame++;
|
||
this.render(_frame, null);
|
||
}, 1000 / this.fps); */
|
||
}
|
||
|
||
private init(jsString: string) {
|
||
try {
|
||
this.handle = worker.Api.init();
|
||
|
||
// @ts-ignore
|
||
this.stringOnWasmHeap = allocate(intArrayFromString(jsString), 'i8', 0);
|
||
|
||
this.frameCount = worker.Api.loadFromData(this.handle, this.stringOnWasmHeap);
|
||
|
||
worker.Api.resize(this.handle, this.width, this.height);
|
||
} catch(e) {
|
||
console.error('init RLottieItem error:', e);
|
||
reply('error', this.reqId, e);
|
||
}
|
||
}
|
||
|
||
public render(frameNo: number, clamped: Uint8ClampedArray) {
|
||
if(this.dead) return;
|
||
//return;
|
||
|
||
if(this.frameCount < frameNo || frameNo < 0) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
worker.Api.render(this.handle, frameNo);
|
||
|
||
var bufferPointer = worker.Api.buffer(this.handle);
|
||
|
||
var data = _Module.HEAPU8.subarray(bufferPointer, bufferPointer + (this.width * this.height * 4));
|
||
|
||
if(!clamped) {
|
||
clamped = new Uint8ClampedArray(data);
|
||
} else {
|
||
clamped.set(data);
|
||
}
|
||
|
||
//this.context.putImageData(new ImageData(clamped, this.width, this.height), 0, 0);
|
||
|
||
reply('frame', this.reqId, frameNo, clamped);
|
||
} catch(e) {
|
||
console.error('Render error:', e);
|
||
this.dead = true;
|
||
reply('error', this.reqId, e);
|
||
}
|
||
}
|
||
|
||
public destroy() {
|
||
this.dead = true;
|
||
|
||
worker.Api.destroy(this.handle);
|
||
}
|
||
}
|
||
|
||
class RLottieWorker {
|
||
public Api: any = {};
|
||
|
||
public initApi() {
|
||
this.Api = {
|
||
init: _Module.cwrap('lottie_init', '', []),
|
||
destroy: _Module.cwrap('lottie_destroy', '', ['number']),
|
||
resize: _Module.cwrap('lottie_resize', '', ['number', 'number', 'number']),
|
||
buffer: _Module.cwrap('lottie_buffer', 'number', ['number']),
|
||
render: _Module.cwrap('lottie_render', '', ['number', 'number']),
|
||
loadFromData: _Module.cwrap('lottie_load_from_data', 'number', ['number', 'number']),
|
||
};
|
||
}
|
||
|
||
public init() {
|
||
this.initApi();
|
||
reply('ready');
|
||
}
|
||
}
|
||
|
||
const worker = new RLottieWorker();
|
||
|
||
_Module.onRuntimeInitialized = function() {
|
||
worker.init();
|
||
};
|
||
|
||
const items: {[reqId: string]: RLottieItem} = {};
|
||
const queryableFunctions = {
|
||
loadFromData: function(reqId: number, jsString: string, width: number, height: number/* , canvas: OffscreenCanvas */) {
|
||
try {
|
||
// ! WARNING, с этой проверкой не все стикеры работают, например - ДУРКА
|
||
/* if(!/"tgs":\s*?1./.test(jsString)) {
|
||
throw new Error('Invalid file');
|
||
} */
|
||
|
||
/* let perf = performance.now();
|
||
let json = JSON.parse(jsString);
|
||
console.log('sticker decode:', performance.now() - perf); */
|
||
|
||
const match = jsString.match(/"fr":\s*?(\d+?),/);
|
||
const frameRate = +match?.[1] || DEFAULT_FPS;
|
||
|
||
//console.log('Rendering sticker:', reqId, frameRate, 'now rendered:', Object.keys(items).length);
|
||
|
||
items[reqId] = new RLottieItem(reqId, jsString, width, height, frameRate/* , canvas */);
|
||
} catch(e) {
|
||
console.error('Invalid file for sticker:', jsString);
|
||
reply('error', reqId, e);
|
||
}
|
||
},
|
||
destroy: function(reqId: number) {
|
||
if(!items.hasOwnProperty(reqId)) {
|
||
return;
|
||
}
|
||
|
||
items[reqId].destroy();
|
||
delete items[reqId];
|
||
},
|
||
renderFrame: function(reqId: number, frameNo: number, clamped: Uint8ClampedArray) {
|
||
//console.log('worker renderFrame', reqId, frameNo, clamped);
|
||
items[reqId].render(frameNo, clamped);
|
||
}
|
||
};
|
||
|
||
function defaultReply(message: any) {
|
||
// your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly
|
||
// do something
|
||
}
|
||
|
||
/**
|
||
* Returns true when run in WebKit derived browsers.
|
||
* This is used as a workaround for a memory leak in Safari caused by using Transferable objects to
|
||
* transfer data between WebWorkers and the main thread.
|
||
* https://github.com/mapbox/mapbox-gl-js/issues/8771
|
||
*
|
||
* This should be removed once the underlying Safari issue is fixed.
|
||
*
|
||
* @private
|
||
* @param scope {WindowOrWorkerGlobalScope} Since this function is used both on the main thread and WebWorker context,
|
||
* let the calling scope pass in the global scope object.
|
||
* @returns {boolean}
|
||
*/
|
||
let _isSafari: boolean = null;
|
||
function isSafari(scope: any) {
|
||
if(_isSafari === null) {
|
||
const userAgent = scope.navigator ? scope.navigator.userAgent : null;
|
||
_isSafari = !!scope.safari ||
|
||
!!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome'))));
|
||
}
|
||
return _isSafari;
|
||
}
|
||
|
||
function reply(...args: any[]) {
|
||
if(arguments.length < 1) {
|
||
throw new TypeError('reply - not enough arguments');
|
||
}
|
||
|
||
//if(arguments[0] === 'frame') return;
|
||
|
||
var args = Array.prototype.slice.call(arguments, 1);
|
||
if(isSafari(self)) {
|
||
postMessage({ 'queryMethodListener': arguments[0], 'queryMethodArguments': args });
|
||
} else {
|
||
var transfer = [];
|
||
for(var i = 0; i < args.length; i++) {
|
||
if(args[i] instanceof ArrayBuffer) {
|
||
transfer.push(args[i]);
|
||
}
|
||
|
||
if(args[i].buffer && args[i].buffer instanceof ArrayBuffer) {
|
||
transfer.push(args[i].buffer);
|
||
//args[i] = args[i].buffer;
|
||
}
|
||
}
|
||
|
||
postMessage({ 'queryMethodListener': arguments[0], 'queryMethodArguments': args }, transfer);
|
||
}
|
||
|
||
//postMessage({ 'queryMethodListener': arguments[0], 'queryMethodArguments': Array.prototype.slice.call(arguments, 1) });
|
||
//console.error(transfer, args);
|
||
}
|
||
|
||
onmessage = function(oEvent) {
|
||
if(oEvent.data instanceof Object && oEvent.data.hasOwnProperty('queryMethod') && oEvent.data.hasOwnProperty('queryMethodArguments')) {
|
||
// @ts-ignore
|
||
queryableFunctions[oEvent.data.queryMethod].apply(self, oEvent.data.queryMethodArguments);
|
||
} else {
|
||
defaultReply(oEvent.data);
|
||
}
|
||
};
|