ede6056f64
Improve invokeAfter Something more
811 lines
23 KiB
TypeScript
811 lines
23 KiB
TypeScript
/*
|
||
* https://github.com/morethanwords/tweb
|
||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||
*/
|
||
|
||
import RLottieWorker from 'worker-loader!./rlottie/rlottie.worker';
|
||
import animationIntersector from "../components/animationIntersector";
|
||
import { MOUNT_CLASS_TO } from '../config/debug';
|
||
import EventListenerBase from "../helpers/eventListenerBase";
|
||
import mediaSizes from "../helpers/mediaSizes";
|
||
import { clamp } from '../helpers/number';
|
||
import { pause } from '../helpers/schedulers';
|
||
import { isAndroid, isApple, isAppleMobile, isSafari } from "../helpers/userAgent";
|
||
import { logger, LogTypes } from "./logger";
|
||
import apiManager from "./mtproto/mtprotoworker";
|
||
|
||
let convert = (value: number) => {
|
||
return Math.round(Math.min(Math.max(value, 0), 1) * 255);
|
||
};
|
||
|
||
type RLottiePlayerListeners = 'enterFrame' | 'ready' | 'firstFrame' | 'cached';
|
||
type RLottieOptions = {
|
||
container: HTMLElement,
|
||
autoplay?: boolean,
|
||
animationData: string,
|
||
loop?: boolean,
|
||
width?: number,
|
||
height?: number,
|
||
group?: string,
|
||
noCache?: true,
|
||
needUpscale?: true,
|
||
skipRatio?: number
|
||
};
|
||
|
||
export class RLottiePlayer extends EventListenerBase<{
|
||
enterFrame: (frameNo: number) => void,
|
||
ready: () => void,
|
||
firstFrame: () => void,
|
||
cached: () => void
|
||
}> {
|
||
public static reqId = 0;
|
||
|
||
public reqId = 0;
|
||
public curFrame: number;
|
||
public frameCount: number;
|
||
public fps: number;
|
||
public skipDelta: number;
|
||
|
||
public worker: QueryableWorker;
|
||
|
||
public width = 0;
|
||
public height = 0;
|
||
|
||
public el: HTMLElement;
|
||
public canvas: HTMLCanvasElement;
|
||
public context: CanvasRenderingContext2D;
|
||
|
||
public paused = true;
|
||
//public paused = false;
|
||
public direction = 1;
|
||
public speed = 1;
|
||
public autoplay = true;
|
||
public _autoplay: boolean; // ! will be used to store original value for settings.stickers.loop
|
||
public loop = true;
|
||
public _loop: boolean; // ! will be used to store original value for settings.stickers.loop
|
||
public group = '';
|
||
|
||
private frInterval: number;
|
||
private frThen: number;
|
||
private rafId: number;
|
||
|
||
//private caching = false;
|
||
//private removed = false;
|
||
|
||
private frames: {[frameNo: string]: Uint8ClampedArray} = {};
|
||
public imageData: ImageData;
|
||
public clamped: Uint8ClampedArray;
|
||
public cachingDelta = 0;
|
||
|
||
//private playedTimes = 0;
|
||
|
||
private currentMethod: RLottiePlayer['mainLoopForwards'] | RLottiePlayer['mainLoopBackwards'];
|
||
private frameListener: () => void;
|
||
|
||
constructor({el, worker, options}: {
|
||
el: HTMLElement,
|
||
worker: QueryableWorker,
|
||
options: RLottieOptions
|
||
}) {
|
||
super(true);
|
||
|
||
this.reqId = ++RLottiePlayer['reqId'];
|
||
this.el = el;
|
||
this.worker = worker;
|
||
|
||
for(let i in options) {
|
||
if(this.hasOwnProperty(i)) {
|
||
// @ts-ignore
|
||
this[i] = options[i];
|
||
}
|
||
}
|
||
|
||
this._loop = this.loop;
|
||
this._autoplay = this.autoplay;
|
||
|
||
// * Skip ratio (30fps)
|
||
let skipRatio: number;
|
||
if(options.skipRatio !== undefined) skipRatio = options.skipRatio;
|
||
else if((isAndroid || isAppleMobile || (isApple && !isSafari)) && this.width < 100 && this.height < 100) {
|
||
skipRatio = 0.5;
|
||
}
|
||
|
||
this.skipDelta = skipRatio !== undefined ? 1 / skipRatio | 0 : 1;
|
||
|
||
//options.needUpscale = true;
|
||
|
||
// * Pixel ratio
|
||
//const pixelRatio = window.devicePixelRatio;
|
||
const pixelRatio = clamp(window.devicePixelRatio, 1, 2);
|
||
if(pixelRatio > 1) {
|
||
//this.cachingEnabled = true;//this.width < 100 && this.height < 100;
|
||
if(options.needUpscale) {
|
||
this.width = Math.round(this.width * pixelRatio);
|
||
this.height = Math.round(this.height * pixelRatio);
|
||
} else if(pixelRatio > 1) {
|
||
if(this.width > 100 && this.height > 100) {
|
||
if(isApple || !mediaSizes.isMobile) {
|
||
/* this.width = Math.round(this.width * (pixelRatio - 1));
|
||
this.height = Math.round(this.height * (pixelRatio - 1)); */
|
||
this.width = Math.round(this.width * pixelRatio);
|
||
this.height = Math.round(this.height * pixelRatio);
|
||
} else if(pixelRatio > 2.5) {
|
||
this.width = Math.round(this.width * (pixelRatio - 1.5));
|
||
this.height = Math.round(this.height * (pixelRatio - 1.5));
|
||
}
|
||
} else {
|
||
this.width = Math.round(this.width * Math.max(1.5, pixelRatio - 1.5));
|
||
this.height = Math.round(this.height * Math.max(1.5, pixelRatio - 1.5));
|
||
}
|
||
}
|
||
}
|
||
|
||
//options.noCache = true;
|
||
|
||
// * Cache frames params
|
||
if(!options.noCache/* && false */) {
|
||
// проверка на размер уже после скейлинга, сделано для попапа и сайдбара, где стикеры 80х80 и 68х68, туда нужно 75%
|
||
if(isApple && this.width > 100 && this.height > 100) {
|
||
this.cachingDelta = 2; //2 // 50%
|
||
} else if(this.width < 100 && this.height < 100) {
|
||
this.cachingDelta = Infinity; // 100%
|
||
} else {
|
||
this.cachingDelta = 4; // 75%
|
||
}
|
||
}
|
||
|
||
// this.cachingDelta = Infinity;
|
||
// if(isApple) {
|
||
// this.cachingDelta = 0; //2 // 50%
|
||
// }
|
||
|
||
/* this.width *= 0.8;
|
||
this.height *= 0.8; */
|
||
|
||
//console.log("RLottiePlayer width:", this.width, this.height, options);
|
||
this.canvas = document.createElement('canvas');
|
||
this.canvas.classList.add('rlottie');
|
||
this.canvas.width = this.width;
|
||
this.canvas.height = this.height;
|
||
this.context = this.canvas.getContext('2d');
|
||
|
||
this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
|
||
this.imageData = new ImageData(this.width, this.height);
|
||
}
|
||
|
||
public clearCache() {
|
||
this.frames = {};
|
||
}
|
||
|
||
public sendQuery(methodName: string, ...args: any[]) {
|
||
//console.trace('RLottie sendQuery:', methodName);
|
||
this.worker.sendQuery(methodName, this.reqId, ...args);
|
||
}
|
||
|
||
public loadFromData(jsonString: string) {
|
||
this.sendQuery('loadFromData', jsonString, this.width, this.height/* , this.canvas.transferControlToOffscreen() */);
|
||
}
|
||
|
||
public play() {
|
||
if(!this.paused) return;
|
||
|
||
//return;
|
||
|
||
//console.log('RLOTTIE PLAY' + this.reqId);
|
||
|
||
this.paused = false;
|
||
this.setMainLoop();
|
||
}
|
||
|
||
public pause(clearPendingRAF = true) {
|
||
if(this.paused) return;
|
||
|
||
this.paused = true;
|
||
if(clearPendingRAF) {
|
||
clearTimeout(this.rafId);
|
||
}
|
||
//window.cancelAnimationFrame(this.rafId);
|
||
}
|
||
|
||
public stop(renderFirstFrame = true) {
|
||
this.pause();
|
||
|
||
this.curFrame = this.direction === 1 ? 0 : this.frameCount;
|
||
if(renderFirstFrame) {
|
||
this.requestFrame(this.curFrame);
|
||
//this.sendQuery('renderFrame', this.curFrame);
|
||
}
|
||
}
|
||
|
||
public restart() {
|
||
this.stop(false);
|
||
this.play();
|
||
}
|
||
|
||
public setSpeed(speed: number) {
|
||
this.speed = speed;
|
||
|
||
if(!this.paused) {
|
||
this.setMainLoop();
|
||
}
|
||
}
|
||
|
||
public setDirection(direction: number) {
|
||
this.direction = direction;
|
||
|
||
if(!this.paused) {
|
||
this.setMainLoop();
|
||
}
|
||
}
|
||
|
||
public remove() {
|
||
//alert('remove');
|
||
lottieLoader.onDestroy(this.reqId);
|
||
this.pause();
|
||
this.sendQuery('destroy');
|
||
//this.removed = true;
|
||
}
|
||
|
||
public renderFrame2(frame: Uint8ClampedArray, frameNo: number) {
|
||
/* this.setListenerResult('enterFrame', frameNo);
|
||
return; */
|
||
|
||
try {
|
||
this.imageData.data.set(frame);
|
||
|
||
//this.context.putImageData(new ImageData(frame, this.width, this.height), 0, 0);
|
||
//let perf = performance.now();
|
||
this.context.putImageData(this.imageData, 0, 0);
|
||
//console.log('renderFrame2 perf:', performance.now() - perf);
|
||
} catch(err) {
|
||
console.error('RLottiePlayer renderFrame error:', err/* , frame */, this.width, this.height);
|
||
this.autoplay = false;
|
||
this.pause();
|
||
return;
|
||
}
|
||
|
||
//console.log('set result enterFrame', frameNo);
|
||
this.dispatchEvent('enterFrame', frameNo);
|
||
}
|
||
|
||
public renderFrame(frame: Uint8ClampedArray, frameNo: number) {
|
||
//console.log('renderFrame', frameNo, this);
|
||
if(this.cachingDelta && (frameNo % this.cachingDelta || !frameNo) && !this.frames[frameNo]) {
|
||
this.frames[frameNo] = new Uint8ClampedArray(frame);//frame;
|
||
}
|
||
|
||
/* if(!this.listenerResults.hasOwnProperty('cached')) {
|
||
this.setListenerResult('enterFrame', frameNo);
|
||
if(frameNo === (this.frameCount - 1)) {
|
||
this.setListenerResult('cached');
|
||
}
|
||
|
||
return;
|
||
} */
|
||
|
||
if(this.frInterval) {
|
||
const now = Date.now(), delta = now - this.frThen;
|
||
//console.log(`renderFrame delta${this.reqId}:`, this, delta, this.frInterval);
|
||
|
||
if(delta < 0) {
|
||
if(this.rafId) clearTimeout(this.rafId);
|
||
return this.rafId = window.setTimeout(() => {
|
||
this.renderFrame2(frame, frameNo);
|
||
}, this.frInterval > -delta ? -delta % this.frInterval : this.frInterval);
|
||
//await new Promise((resolve) => setTimeout(resolve, -delta % this.frInterval));
|
||
}
|
||
}
|
||
|
||
this.renderFrame2(frame, frameNo);
|
||
}
|
||
|
||
public requestFrame(frameNo: number) {
|
||
if(this.frames[frameNo]) {
|
||
this.renderFrame(this.frames[frameNo], frameNo);
|
||
} else if(isSafari) {
|
||
this.sendQuery('renderFrame', frameNo);
|
||
} else {
|
||
if(!this.clamped.length) { // fix detached
|
||
this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
|
||
}
|
||
|
||
this.sendQuery('renderFrame', frameNo, this.clamped);
|
||
}
|
||
}
|
||
|
||
private mainLoopForwards() {
|
||
const frame = (this.curFrame + this.skipDelta) >= this.frameCount ? this.curFrame = 0 : this.curFrame += this.skipDelta;
|
||
//console.log('mainLoopForwards', this.curFrame, this.skipDelta, frame);
|
||
|
||
this.requestFrame(frame);
|
||
if((frame + this.skipDelta) >= this.frameCount) {
|
||
//this.playedTimes++;
|
||
|
||
if(!this.loop) {
|
||
this.pause(false);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private mainLoopBackwards() {
|
||
const frame = (this.curFrame - this.skipDelta) < 0 ? this.curFrame = this.frameCount - 1 : this.curFrame -= this.skipDelta;
|
||
//console.log('mainLoopBackwards', this.curFrame, this.skipDelta, frame);
|
||
|
||
this.requestFrame(frame);
|
||
if((frame - this.skipDelta) < 0) {
|
||
//this.playedTimes++;
|
||
|
||
if(!this.loop) {
|
||
this.pause(false);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public setMainLoop() {
|
||
//window.cancelAnimationFrame(this.rafId);
|
||
clearTimeout(this.rafId);
|
||
|
||
this.frInterval = 1000 / this.fps / this.speed * this.skipDelta;
|
||
this.frThen = Date.now() - this.frInterval;
|
||
|
||
//console.trace('setMainLoop', this.frInterval, this.direction, this, JSON.stringify(this.listenerResults), this.listenerResults);
|
||
|
||
const method = (this.direction === 1 ? this.mainLoopForwards : this.mainLoopBackwards).bind(this);
|
||
this.currentMethod = method;
|
||
//this.frameListener && this.removeListener('enterFrame', this.frameListener);
|
||
|
||
//setTimeout(() => {
|
||
//this.addListener('enterFrame', this.frameListener);
|
||
//}, 0);
|
||
|
||
if(this.frameListener && this.listenerResults.hasOwnProperty('enterFrame')) {
|
||
this.frameListener();
|
||
}
|
||
|
||
//this.mainLoop(method);
|
||
//this.r(method);
|
||
//method();
|
||
}
|
||
|
||
public async onLoad(frameCount: number, fps: number) {
|
||
this.curFrame = this.direction === 1 ? 0 : frameCount - 1;
|
||
this.frameCount = frameCount;
|
||
this.fps = fps;
|
||
|
||
// * Handle 30fps stickers if 30fps set
|
||
if(this.fps < 60 && this.skipDelta !== 1) {
|
||
const diff = 60 / fps;
|
||
this.skipDelta = this.skipDelta / diff | 0;
|
||
}
|
||
|
||
this.frInterval = 1000 / this.fps / this.speed * this.skipDelta;
|
||
this.frThen = Date.now() - this.frInterval;
|
||
//this.sendQuery('renderFrame', 0);
|
||
|
||
// Кешировать сразу не получится, рендер стикера (тайгер) занимает 519мс,
|
||
// если рендерить 75% с получением каждого кадра из воркера, будет 475мс, т.е. при 100% было бы 593мс, потеря на передаче 84мс.
|
||
|
||
/* console.time('cache' + this.reqId);
|
||
for(let i = 0; i < frameCount; ++i) {
|
||
//if(this.removed) return;
|
||
|
||
if(i % 4) {
|
||
await new Promise((resolve) => {
|
||
delete this.listenerResults.enterFrame;
|
||
this.addListener('enterFrame', resolve, true);
|
||
this.requestFrame(i);
|
||
});
|
||
}
|
||
}
|
||
|
||
console.timeEnd('cache' + this.reqId); */
|
||
//console.log('cached');
|
||
/* this.el.innerHTML = '';
|
||
this.el.append(this.canvas);
|
||
return; */
|
||
|
||
this.requestFrame(0);
|
||
this.dispatchEvent('ready');
|
||
this.addEventListener('enterFrame', () => {
|
||
this.dispatchEvent('firstFrame');
|
||
|
||
this.el.appendChild(this.canvas);
|
||
|
||
//console.log('enterFrame firstFrame');
|
||
|
||
//let lastTime = this.frThen;
|
||
this.frameListener = () => {
|
||
if(this.paused) {
|
||
return;
|
||
}
|
||
|
||
const time = Date.now();
|
||
//console.log(`enterFrame handle${this.reqId}`, time, (time - lastTime), this.frInterval);
|
||
/* if(Math.round(time - lastTime + this.frInterval * 0.25) < Math.round(this.frInterval)) {
|
||
return;
|
||
} */
|
||
|
||
//lastTime = time;
|
||
|
||
this.frThen = time + this.frInterval;
|
||
const canContinue = this.currentMethod();
|
||
if(!canContinue && !this.loop && this.autoplay) {
|
||
this.autoplay = false;
|
||
}
|
||
};
|
||
|
||
this.addEventListener('enterFrame', this.frameListener);
|
||
}, {once: true});
|
||
}
|
||
}
|
||
|
||
class QueryableWorker extends EventListenerBase<any> {
|
||
constructor(private worker: Worker, private defaultListener: (data: any) => void = () => {}, onError?: (error: any) => void) {
|
||
super();
|
||
|
||
if(onError) {
|
||
this.worker.onerror = onError;
|
||
}
|
||
|
||
this.worker.onmessage = (event) => {
|
||
//return;
|
||
//console.log('worker onmessage', event.data);
|
||
if(event.data instanceof Object &&
|
||
event.data.hasOwnProperty('queryMethodListener') &&
|
||
event.data.hasOwnProperty('queryMethodArguments')) {
|
||
/* if(event.data.queryMethodListener === 'frame') {
|
||
return;
|
||
} */
|
||
|
||
this.dispatchEvent(event.data.queryMethodListener, ...event.data.queryMethodArguments);
|
||
} else {
|
||
this.defaultListener.call(this, event.data);
|
||
}
|
||
};
|
||
}
|
||
|
||
public postMessage(message: any) {
|
||
this.worker.postMessage(message);
|
||
}
|
||
|
||
public terminate() {
|
||
this.worker.terminate();
|
||
}
|
||
|
||
public sendQuery(queryMethod: string, ...args: any[]) {
|
||
if(isSafari) {
|
||
this.worker.postMessage({
|
||
'queryMethod': queryMethod,
|
||
'queryMethodArguments': args
|
||
});
|
||
} else {
|
||
//const transfer: (ArrayBuffer | OffscreenCanvas)[] = [];
|
||
const transfer: ArrayBuffer[] = [];
|
||
args.forEach(arg => {
|
||
if(arg instanceof ArrayBuffer) {
|
||
transfer.push(arg);
|
||
}
|
||
|
||
if(arg.buffer && arg.buffer instanceof ArrayBuffer) {
|
||
transfer.push(arg.buffer);
|
||
}
|
||
});
|
||
|
||
//console.log('transfer', transfer);
|
||
this.worker.postMessage({
|
||
'queryMethod': queryMethod,
|
||
'queryMethodArguments': args
|
||
}, transfer as PostMessageOptions);
|
||
}
|
||
}
|
||
}
|
||
|
||
type LottieShape = {
|
||
c: {
|
||
k: number[]
|
||
},
|
||
ty: 'st' | 'fl',
|
||
it?: LottieShape[]
|
||
};
|
||
class LottieLoader {
|
||
public isWebAssemblySupported = typeof(WebAssembly) !== 'undefined';
|
||
public loadPromise: Promise<void> = !this.isWebAssemblySupported ? Promise.reject() : undefined;
|
||
public loaded = false;
|
||
|
||
// https://github.com/telegramdesktop/tdesktop/blob/97d8ee75d51874fcb74a9bfadc79f835c82be54a/Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp#L46
|
||
private static COLORREPLACEMENTS = [
|
||
[
|
||
[0xf77e41, 0xcb7b55],
|
||
[0xffb139, 0xf6b689],
|
||
[0xffd140, 0xffcda7],
|
||
[0xffdf79, 0xffdfc5],
|
||
],
|
||
|
||
[
|
||
[0xf77e41, 0xa45a38],
|
||
[0xffb139, 0xdf986b],
|
||
[0xffd140, 0xedb183],
|
||
[0xffdf79, 0xf4c3a0],
|
||
],
|
||
|
||
[
|
||
[0xf77e41, 0x703a17],
|
||
[0xffb139, 0xab673d],
|
||
[0xffd140, 0xc37f4e],
|
||
[0xffdf79, 0xd89667],
|
||
],
|
||
|
||
[
|
||
[0xf77e41, 0x4a2409],
|
||
[0xffb139, 0x7d3e0e],
|
||
[0xffd140, 0x965529],
|
||
[0xffdf79, 0xa96337],
|
||
],
|
||
|
||
[
|
||
[0xf77e41, 0x200f0a],
|
||
[0xffb139, 0x412924],
|
||
[0xffd140, 0x593d37],
|
||
[0xffdf79, 0x63453f],
|
||
]
|
||
];
|
||
|
||
private workersLimit = 4;
|
||
private players: {[reqId: number]: RLottiePlayer} = {};
|
||
|
||
private workers: QueryableWorker[] = [];
|
||
private curWorkerNum = 0;
|
||
|
||
private log = logger('LOTTIE', LogTypes.Error);
|
||
|
||
public getAnimation(element: HTMLElement) {
|
||
for(const i in this.players) {
|
||
if(this.players[i].el === element) {
|
||
return this.players[i];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public setLoop(loop: boolean) {
|
||
for(const i in this.players) {
|
||
const player = this.players[i];
|
||
player.loop = loop;
|
||
player.autoplay = player._autoplay;
|
||
}
|
||
}
|
||
|
||
public loadLottieWorkers() {
|
||
if(this.loadPromise) {
|
||
return this.loadPromise;
|
||
}
|
||
|
||
return this.loadPromise = new Promise((resolve, reject) => {
|
||
let remain = this.workersLimit;
|
||
for(let i = 0; i < this.workersLimit; ++i) {
|
||
const worker = this.workers[i] = new QueryableWorker(new RLottieWorker());
|
||
|
||
worker.addEventListener('ready', () => {
|
||
this.log('worker #' + i + ' ready');
|
||
|
||
worker.addEventListener('frame', this.onFrame);
|
||
worker.addEventListener('loaded', this.onPlayerLoaded);
|
||
worker.addEventListener('error', this.onPlayerError);
|
||
|
||
--remain;
|
||
if(!remain) {
|
||
this.log('workers ready');
|
||
resolve();
|
||
this.loaded = true;
|
||
}
|
||
}, {once: true});
|
||
}
|
||
});
|
||
}
|
||
|
||
private applyReplacements(object: {
|
||
layers: Array<{shapes: LottieShape[]}>
|
||
}, toneIndex: number) {
|
||
const replacements = LottieLoader.COLORREPLACEMENTS[Math.max(toneIndex - 1, 0)];
|
||
|
||
const applyTo = (smth: LottieShape) => {
|
||
const k = smth.c.k;
|
||
const color = convert(k[2]) | (convert(k[1]) << 8) | (convert(k[0]) << 16);
|
||
|
||
const foundReplacement = replacements.find(p => p[0] === color);
|
||
if(foundReplacement) {
|
||
k[0] = ((foundReplacement[1] >> 16) & 255) / 255;
|
||
k[1] = ((foundReplacement[1] >> 8) & 255) / 255;
|
||
k[2] = (foundReplacement[1] & 255) / 255;
|
||
}
|
||
|
||
//console.log('foundReplacement!', foundReplacement, color.toString(16), k);
|
||
};
|
||
|
||
const checkSmth = (smth: LottieShape) => {
|
||
switch(smth.ty) {
|
||
case 'st':
|
||
case 'fl':
|
||
applyTo(smth);
|
||
break;
|
||
}
|
||
|
||
if(smth.hasOwnProperty('it')) {
|
||
iterateIt(smth.it);
|
||
}
|
||
};
|
||
|
||
const iterateIt = (it: LottieShape['it']) => {
|
||
for(const smth of it) {
|
||
checkSmth(smth);
|
||
}
|
||
};
|
||
|
||
try {
|
||
for(const layer of object.layers) {
|
||
if(!layer.shapes) continue;
|
||
|
||
for(const shape of layer.shapes) {
|
||
if(!shape.it) {
|
||
checkSmth(shape);
|
||
continue;
|
||
}
|
||
|
||
iterateIt(shape.it);
|
||
}
|
||
}
|
||
} catch(err) {
|
||
this.log.warn('cant apply replacements', err, object, toneIndex);
|
||
}
|
||
}
|
||
|
||
public loadAnimationFromURL(params: Omit<RLottieOptions, 'animationData'>, url: string): Promise<RLottiePlayer> {
|
||
if(!this.isWebAssemblySupported) {
|
||
return this.loadPromise as any;
|
||
}
|
||
|
||
if(!this.loaded) {
|
||
this.loadLottieWorkers();
|
||
}
|
||
|
||
return fetch(url)
|
||
.then(res => res.arrayBuffer())
|
||
.then(data => apiManager.invokeCrypto('gzipUncompress', data, true))
|
||
/* .then(str => {
|
||
return new Promise<string>((resolve) => setTimeout(() => resolve(str), 2e3));
|
||
}) */
|
||
.then(str => {
|
||
return this.loadAnimationWorker(Object.assign(params, {animationData: str as string/* JSON.parse(str) */, needUpscale: true}));
|
||
});
|
||
}
|
||
|
||
public waitForFirstFrame(player: RLottiePlayer): Promise<void> {
|
||
return Promise.race([
|
||
/* new Promise<void>((resolve) => {
|
||
player.addEventListener('firstFrame', () => {
|
||
setTimeout(() => resolve(), 1500);
|
||
}, true);
|
||
}) */
|
||
new Promise<void>((resolve) => {
|
||
player.addEventListener('firstFrame', resolve, {once: true});
|
||
}),
|
||
pause(2500)
|
||
]);
|
||
}
|
||
|
||
public async loadAnimationWorker(params: RLottieOptions, group = '', toneIndex = -1): Promise<RLottiePlayer> {
|
||
if(!this.isWebAssemblySupported) {
|
||
return this.loadPromise as any;
|
||
}
|
||
//params.autoplay = true;
|
||
|
||
if(toneIndex >= 1 && toneIndex <= 5) {
|
||
/* params.animationData = copy(params.animationData);
|
||
this.applyReplacements(params.animationData, toneIndex); */
|
||
|
||
const newAnimationData = JSON.parse(params.animationData);
|
||
this.applyReplacements(newAnimationData, toneIndex);
|
||
params.animationData = JSON.stringify(newAnimationData);
|
||
}
|
||
|
||
if(!this.loaded) {
|
||
await this.loadLottieWorkers();
|
||
}
|
||
|
||
if(!params.width || !params.height) {
|
||
params.width = parseInt(params.container.style.width);
|
||
params.height = parseInt(params.container.style.height);
|
||
}
|
||
|
||
if(!params.width || !params.height) {
|
||
throw new Error('No size for sticker!');
|
||
}
|
||
|
||
params.group = group;
|
||
|
||
const player = this.initPlayer(params.container, params);
|
||
animationIntersector.addAnimation(player, group);
|
||
|
||
return player;
|
||
}
|
||
|
||
private onPlayerLoaded = (reqId: number, frameCount: number, fps: number) => {
|
||
const rlPlayer = this.players[reqId];
|
||
if(!rlPlayer) {
|
||
this.log.warn('onPlayerLoaded on destroyed player:', reqId, frameCount);
|
||
return;
|
||
}
|
||
|
||
this.log.debug('onPlayerLoaded');
|
||
rlPlayer.onLoad(frameCount, fps);
|
||
//rlPlayer.addListener('firstFrame', () => {
|
||
//animationIntersector.addAnimation(player, group);
|
||
//}, true);
|
||
};
|
||
|
||
private onFrame = (reqId: number, frameNo: number, frame: Uint8ClampedArray) => {
|
||
const rlPlayer = this.players[reqId];
|
||
if(!rlPlayer) {
|
||
this.log.warn('onFrame on destroyed player:', reqId, frameNo);
|
||
return;
|
||
}
|
||
|
||
rlPlayer.clamped = frame;
|
||
rlPlayer.renderFrame(frame, frameNo);
|
||
};
|
||
|
||
private onPlayerError = (reqId: number, error: Error) => {
|
||
const rlPlayer = this.players[reqId];
|
||
if(rlPlayer) {
|
||
// ! will need refactoring later, this is not the best way to remove the animation
|
||
const animations = animationIntersector.getAnimations(rlPlayer.el);
|
||
animations.forEach(animation => {
|
||
animationIntersector.checkAnimation(animation, true, true);
|
||
});
|
||
}
|
||
};
|
||
|
||
public onDestroy(reqId: number) {
|
||
delete this.players[reqId];
|
||
}
|
||
|
||
public destroyWorkers() {
|
||
this.workers.forEach((worker, idx) => {
|
||
worker.terminate();
|
||
this.log('worker #' + idx + ' terminated');
|
||
});
|
||
|
||
this.log('workers destroyed');
|
||
this.workers.length = 0;
|
||
}
|
||
|
||
private initPlayer(el: HTMLElement, options: RLottieOptions) {
|
||
const rlPlayer = new RLottiePlayer({
|
||
el,
|
||
worker: this.workers[this.curWorkerNum++],
|
||
options
|
||
});
|
||
|
||
this.players[rlPlayer.reqId] = rlPlayer;
|
||
if(this.curWorkerNum >= this.workers.length) {
|
||
this.curWorkerNum = 0;
|
||
}
|
||
|
||
rlPlayer.loadFromData(options.animationData);
|
||
|
||
return rlPlayer;
|
||
}
|
||
}
|
||
|
||
const lottieLoader = new LottieLoader();
|
||
MOUNT_CLASS_TO.lottieLoader = lottieLoader;
|
||
export default lottieLoader;
|