2021-12-11 17:37:08 +01:00
|
|
|
|
/*
|
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
2022-09-13 14:27:28 +02:00
|
|
|
|
import type {AnimationItemGroup, AnimationItemWrapper} from '../../components/animationIntersector';
|
2023-01-25 15:21:38 +01:00
|
|
|
|
import type {Middleware} from '../../helpers/middleware';
|
2023-03-01 11:20:49 +01:00
|
|
|
|
import type {LiteModeKey} from '../../helpers/liteMode';
|
2022-08-04 08:49:54 +02:00
|
|
|
|
import CAN_USE_TRANSFERABLES from '../../environment/canUseTransferables';
|
|
|
|
|
import IS_APPLE_MX from '../../environment/appleMx';
|
|
|
|
|
import {IS_ANDROID, IS_APPLE_MOBILE, IS_APPLE, IS_SAFARI} from '../../environment/userAgent';
|
|
|
|
|
import EventListenerBase from '../../helpers/eventListenerBase';
|
|
|
|
|
import mediaSizes from '../../helpers/mediaSizes';
|
|
|
|
|
import clamp from '../../helpers/number/clamp';
|
|
|
|
|
import QueryableWorker from './queryableWorker';
|
2022-08-31 06:22:16 +02:00
|
|
|
|
import IS_IMAGE_BITMAP_SUPPORTED from '../../environment/imageBitmapSupport';
|
2022-09-13 14:27:28 +02:00
|
|
|
|
import framesCache, {FramesCache, FramesCacheItem} from '../../helpers/framesCache';
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
export type RLottieOptions = {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
container: HTMLElement | HTMLElement[],
|
2023-01-25 15:21:38 +01:00
|
|
|
|
middleware?: Middleware,
|
2022-08-04 08:49:54 +02:00
|
|
|
|
canvas?: HTMLCanvasElement,
|
|
|
|
|
autoplay?: boolean,
|
|
|
|
|
animationData: Blob,
|
2022-11-01 18:39:23 +01:00
|
|
|
|
loop?: RLottiePlayer['loop'],
|
2021-12-11 17:37:08 +01:00
|
|
|
|
width?: number,
|
|
|
|
|
height?: number,
|
2022-08-13 14:14:06 +02:00
|
|
|
|
group?: AnimationItemGroup,
|
2022-01-16 03:45:41 +01:00
|
|
|
|
noCache?: boolean,
|
|
|
|
|
needUpscale?: boolean,
|
2021-12-11 17:37:08 +01:00
|
|
|
|
skipRatio?: number,
|
|
|
|
|
initFrame?: number, // index
|
|
|
|
|
color?: RLottieColor,
|
|
|
|
|
inverseColor?: RLottieColor,
|
|
|
|
|
name?: string,
|
|
|
|
|
skipFirstFrameRendering?: boolean,
|
2022-08-31 06:22:16 +02:00
|
|
|
|
toneIndex?: number,
|
2023-03-01 11:20:49 +01:00
|
|
|
|
sync?: boolean,
|
|
|
|
|
liteModeKey?: LiteModeKey
|
2021-12-11 17:37:08 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type RLottieColor = [number, number, number];
|
|
|
|
|
|
2022-09-03 19:04:48 +02:00
|
|
|
|
export function getLottiePixelRatio(width: number, height: number, needUpscale?: boolean) {
|
|
|
|
|
let pixelRatio = clamp(window.devicePixelRatio, 1, 2);
|
|
|
|
|
if(pixelRatio > 1 && !needUpscale) {
|
|
|
|
|
if(width > 90 && height > 90) {
|
|
|
|
|
if(!IS_APPLE && mediaSizes.isMobile) {
|
|
|
|
|
pixelRatio = 1;
|
|
|
|
|
}
|
2022-11-01 18:39:23 +01:00
|
|
|
|
} else if((width > 60 && height > 60) || IS_ANDROID) {
|
2022-09-03 19:04:48 +02:00
|
|
|
|
pixelRatio = Math.max(1.5, pixelRatio - 1.5);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pixelRatio;
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
export default class RLottiePlayer extends EventListenerBase<{
|
|
|
|
|
enterFrame: (frameNo: number) => void,
|
|
|
|
|
ready: () => void,
|
|
|
|
|
firstFrame: () => void,
|
2022-08-13 14:14:06 +02:00
|
|
|
|
cached: () => void,
|
|
|
|
|
destroy: () => void
|
2022-09-13 14:27:28 +02:00
|
|
|
|
}> implements AnimationItemWrapper {
|
|
|
|
|
public static CACHE = framesCache;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
private static reqId = 0;
|
|
|
|
|
|
|
|
|
|
public reqId = 0;
|
|
|
|
|
public curFrame: number;
|
|
|
|
|
private frameCount: number;
|
|
|
|
|
private fps: number;
|
|
|
|
|
private skipDelta: number;
|
2022-08-31 06:22:16 +02:00
|
|
|
|
public name: string;
|
|
|
|
|
public cacheName: string;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
private toneIndex: number;
|
|
|
|
|
|
|
|
|
|
private worker: QueryableWorker;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
private width = 0;
|
|
|
|
|
private height = 0;
|
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
public el: HTMLElement[];
|
|
|
|
|
public canvas: HTMLCanvasElement[];
|
|
|
|
|
private contexts: CanvasRenderingContext2D[];
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
public paused = true;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// public paused = false;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
public direction = 1;
|
|
|
|
|
private speed = 1;
|
|
|
|
|
public autoplay = true;
|
|
|
|
|
public _autoplay: boolean; // ! will be used to store original value for settings.stickers.loop
|
2022-11-01 18:39:23 +01:00
|
|
|
|
public loop: number | boolean = true;
|
2023-01-28 12:52:24 +01:00
|
|
|
|
public _loop: RLottiePlayer['loop']; // ! will be used to store original value for settings.stickers.loop
|
2022-09-13 14:27:28 +02:00
|
|
|
|
public group: AnimationItemGroup = '';
|
2023-03-01 11:20:49 +01:00
|
|
|
|
public liteModeKey: LiteModeKey;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
private frInterval: number;
|
|
|
|
|
private frThen: number;
|
|
|
|
|
private rafId: number;
|
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// private caching = false;
|
|
|
|
|
// private removed = false;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-09-13 14:27:28 +02:00
|
|
|
|
private cache: FramesCacheItem;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
private imageData: ImageData;
|
|
|
|
|
public clamped: Uint8ClampedArray;
|
|
|
|
|
private cachingDelta = 0;
|
|
|
|
|
|
|
|
|
|
private initFrame: number;
|
|
|
|
|
private color: RLottieColor;
|
|
|
|
|
private inverseColor: RLottieColor;
|
|
|
|
|
|
2022-01-23 20:45:34 +01:00
|
|
|
|
public minFrame: number;
|
|
|
|
|
public maxFrame: number;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-11-01 18:39:23 +01:00
|
|
|
|
private playedTimes = 0;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
private currentMethod: RLottiePlayer['mainLoopForwards'] | RLottiePlayer['mainLoopBackwards'];
|
|
|
|
|
private frameListener: (currentFrame: number) => void;
|
|
|
|
|
private skipFirstFrameRendering: boolean;
|
2022-01-08 13:52:14 +01:00
|
|
|
|
private playToFrameOnFrameCallback: (frameNo: number) => void;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
public overrideRender: (frame: ImageData | HTMLCanvasElement | ImageBitmap) => void;
|
|
|
|
|
private renderedFirstFrame: boolean;
|
|
|
|
|
|
2022-09-25 19:49:33 +02:00
|
|
|
|
private raw: boolean;
|
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
constructor({el, worker, options}: {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
el: RLottiePlayer['el'],
|
2021-12-11 17:37:08 +01:00
|
|
|
|
worker: QueryableWorker,
|
|
|
|
|
options: RLottieOptions
|
|
|
|
|
}) {
|
|
|
|
|
super(true);
|
|
|
|
|
|
|
|
|
|
this.reqId = ++RLottiePlayer['reqId'];
|
|
|
|
|
this.el = el;
|
|
|
|
|
this.worker = worker;
|
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
for(const i in options) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
if(this.hasOwnProperty(i)) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this[i] = options[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._loop = this.loop;
|
|
|
|
|
this._autoplay = this.autoplay;
|
|
|
|
|
|
|
|
|
|
// ! :(
|
|
|
|
|
this.initFrame = options.initFrame;
|
|
|
|
|
this.color = options.color;
|
|
|
|
|
this.inverseColor = options.inverseColor;
|
|
|
|
|
this.name = options.name;
|
|
|
|
|
this.skipFirstFrameRendering = options.skipFirstFrameRendering;
|
|
|
|
|
this.toneIndex = options.toneIndex;
|
2022-09-25 19:49:33 +02:00
|
|
|
|
this.raw = this.color !== undefined;
|
2023-03-01 11:20:49 +01:00
|
|
|
|
this.liteModeKey = options.liteModeKey;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
if(this.name) {
|
2022-09-13 14:27:28 +02:00
|
|
|
|
this.cacheName = RLottiePlayer.CACHE.generateName(this.name, this.width, this.height, this.color, this.toneIndex);
|
2022-08-31 06:22:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
// * Skip ratio (30fps)
|
|
|
|
|
let skipRatio: number;
|
|
|
|
|
if(options.skipRatio !== undefined) skipRatio = options.skipRatio;
|
2022-06-27 02:43:36 +02:00
|
|
|
|
else if((IS_ANDROID || IS_APPLE_MOBILE || (IS_APPLE && !IS_SAFARI && !IS_APPLE_MX)) && this.width < 100 && this.height < 100 && !options.needUpscale) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
skipRatio = 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.skipDelta = skipRatio !== undefined ? 1 / skipRatio | 0 : 1;
|
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// options.needUpscale = true;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
// * Pixel ratio
|
2022-09-03 19:04:48 +02:00
|
|
|
|
const pixelRatio = getLottiePixelRatio(this.width, this.height, options.needUpscale);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.width = Math.round(this.width * pixelRatio);
|
|
|
|
|
this.height = Math.round(this.height * pixelRatio);
|
2022-06-07 14:28:50 +02:00
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// options.noCache = true;
|
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
// * Cache frames params
|
|
|
|
|
if(!options.noCache/* && false */) {
|
|
|
|
|
// проверка на размер уже после скейлинга, сделано для попапа и сайдбара, где стикеры 80х80 и 68х68, туда нужно 75%
|
|
|
|
|
if(IS_APPLE && this.width > 100 && this.height > 100) {
|
2022-08-04 08:49:54 +02:00
|
|
|
|
this.cachingDelta = 2; // 2 // 50%
|
2021-12-11 17:37:08 +01:00
|
|
|
|
} else if(this.width < 100 && this.height < 100) {
|
|
|
|
|
this.cachingDelta = Infinity; // 100%
|
|
|
|
|
} else {
|
|
|
|
|
this.cachingDelta = 4; // 75%
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
// this.cachingDelta = Infinity;
|
2022-08-31 06:22:16 +02:00
|
|
|
|
// this.cachingDelta = 0;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
// if(isApple) {
|
|
|
|
|
// this.cachingDelta = 0; //2 // 50%
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
if(!this.canvas) {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.canvas = this.el.map(() => {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
canvas.classList.add('rlottie');
|
|
|
|
|
canvas.width = this.width;
|
|
|
|
|
canvas.height = this.height;
|
|
|
|
|
canvas.dpr = pixelRatio;
|
|
|
|
|
return canvas;
|
|
|
|
|
});
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.contexts = this.canvas.map((canvas) => canvas.getContext('2d'));
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-09-25 19:49:33 +02:00
|
|
|
|
if(!IS_IMAGE_BITMAP_SUPPORTED || this.raw) {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.imageData = new ImageData(this.width, this.height);
|
2022-02-10 20:58:26 +01:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
if(CAN_USE_TRANSFERABLES) {
|
|
|
|
|
this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
if(this.name) {
|
2022-09-13 14:27:28 +02:00
|
|
|
|
this.cache = RLottiePlayer.CACHE.getCache(this.cacheName);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
} else {
|
2022-09-13 14:27:28 +02:00
|
|
|
|
this.cache = FramesCache.createCache();
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public clearCache() {
|
2022-02-10 20:58:26 +01:00
|
|
|
|
if(this.cachingDelta === Infinity) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
if(this.cacheName && this.cache.counter > 1) { // skip clearing because same sticker can be still visible
|
2021-12-11 17:37:08 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.cache.clearCache();
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-09-25 19:49:33 +02:00
|
|
|
|
public sendQuery(args: any[], transfer?: Transferable[]) {
|
|
|
|
|
this.worker.sendQuery([args.shift(), this.reqId, ...args], transfer);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-05 20:20:38 +01:00
|
|
|
|
public loadFromData(data: RLottieOptions['animationData']) {
|
2022-09-25 19:49:33 +02:00
|
|
|
|
this.sendQuery(['loadFromData', data, this.width, this.height, this.toneIndex, this.color !== undefined/* , this.canvas.transferControlToOffscreen() */]);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public play() {
|
|
|
|
|
if(!this.paused) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.paused = false;
|
|
|
|
|
this.setMainLoop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public pause(clearPendingRAF = true) {
|
|
|
|
|
if(this.paused) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.paused = true;
|
|
|
|
|
if(clearPendingRAF) {
|
|
|
|
|
clearTimeout(this.rafId);
|
2022-11-01 18:39:23 +01:00
|
|
|
|
this.rafId = undefined;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// window.cancelAnimationFrame(this.rafId);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private resetCurrentFrame() {
|
2022-01-23 20:45:34 +01:00
|
|
|
|
return this.curFrame = this.initFrame ?? (this.direction === 1 ? this.minFrame : this.maxFrame);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public stop(renderFirstFrame = true) {
|
|
|
|
|
this.pause();
|
|
|
|
|
|
|
|
|
|
const curFrame = this.resetCurrentFrame();
|
|
|
|
|
if(renderFirstFrame) {
|
|
|
|
|
this.requestFrame(curFrame);
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// this.sendQuery('renderFrame', this.curFrame);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public restart() {
|
|
|
|
|
this.stop(false);
|
|
|
|
|
this.play();
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 11:20:49 +01:00
|
|
|
|
public playOrRestart() {
|
|
|
|
|
if(!this.paused) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(this.curFrame === this.maxFrame) {
|
|
|
|
|
this.restart();
|
|
|
|
|
} else {
|
|
|
|
|
this.play();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
public setSpeed(speed: number) {
|
|
|
|
|
if(this.speed === speed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.speed = speed;
|
|
|
|
|
|
|
|
|
|
if(!this.paused) {
|
|
|
|
|
this.setMainLoop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setDirection(direction: number) {
|
|
|
|
|
if(this.direction === direction) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.direction = direction;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
if(!this.paused) {
|
|
|
|
|
this.setMainLoop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public remove() {
|
|
|
|
|
this.pause();
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.sendQuery(['destroy']);
|
2022-09-13 14:27:28 +02:00
|
|
|
|
if(this.cacheName) RLottiePlayer.CACHE.releaseCache(this.cacheName);
|
2022-08-13 14:14:06 +02:00
|
|
|
|
this.dispatchEvent('destroy');
|
|
|
|
|
this.cleanup();
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private applyColor(frame: Uint8ClampedArray) {
|
|
|
|
|
const [r, g, b] = this.color;
|
|
|
|
|
for(let i = 0, length = frame.length; i < length; i += 4) {
|
|
|
|
|
if(frame[i + 3] !== 0) {
|
|
|
|
|
frame[i] = r;
|
|
|
|
|
frame[i + 1] = g;
|
|
|
|
|
frame[i + 2] = b;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private applyInversing(frame: Uint8ClampedArray) {
|
|
|
|
|
const [r, g, b] = this.inverseColor;
|
|
|
|
|
for(let i = 0, length = frame.length; i < length; i += 4) {
|
|
|
|
|
if(frame[i + 3] === 0) {
|
|
|
|
|
frame[i] = r;
|
|
|
|
|
frame[i + 1] = g;
|
|
|
|
|
frame[i + 2] = b;
|
|
|
|
|
frame[i + 3] = 255;
|
|
|
|
|
} else {
|
|
|
|
|
frame[i + 3] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
public renderFrame2(frame: Uint8ClampedArray | HTMLCanvasElement | ImageBitmap, frameNo: number) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
/* this.setListenerResult('enterFrame', frameNo);
|
|
|
|
|
return; */
|
|
|
|
|
|
|
|
|
|
try {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
if(frame instanceof Uint8ClampedArray) {
|
|
|
|
|
if(this.color) {
|
|
|
|
|
this.applyColor(frame);
|
|
|
|
|
}
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
if(this.inverseColor) {
|
|
|
|
|
this.applyInversing(frame);
|
|
|
|
|
}
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.imageData.data.set(frame);
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
|
|
|
|
// this.context.putImageData(new ImageData(frame, this.width, this.height), 0, 0);
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.contexts.forEach((context, idx) => {
|
|
|
|
|
let cachedSource: HTMLCanvasElement | ImageBitmap = this.cache.framesNew.get(frameNo);
|
|
|
|
|
if(!(frame instanceof Uint8ClampedArray)) {
|
|
|
|
|
cachedSource = frame;
|
|
|
|
|
} else if(idx > 0) {
|
|
|
|
|
cachedSource = this.canvas[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!cachedSource) {
|
|
|
|
|
// console.log('drawing from data');
|
|
|
|
|
const c = document.createElement('canvas');
|
|
|
|
|
c.width = context.canvas.width;
|
|
|
|
|
c.height = context.canvas.height;
|
|
|
|
|
c.getContext('2d').putImageData(this.imageData, 0, 0);
|
|
|
|
|
this.cache.framesNew.set(frameNo, c);
|
|
|
|
|
cachedSource = c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(this.overrideRender && this.renderedFirstFrame) {
|
|
|
|
|
this.overrideRender(cachedSource || this.imageData);
|
|
|
|
|
} else if(cachedSource) {
|
|
|
|
|
// console.log('drawing from canvas');
|
|
|
|
|
context.clearRect(0, 0, cachedSource.width, cachedSource.height);
|
|
|
|
|
context.drawImage(cachedSource, 0, 0);
|
|
|
|
|
} else {
|
|
|
|
|
context.putImageData(this.imageData, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!this.renderedFirstFrame) {
|
|
|
|
|
this.renderedFirstFrame = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.dispatchEvent('enterFrame', frameNo);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
} catch(err) {
|
|
|
|
|
console.error('RLottiePlayer renderFrame error:', err/* , frame */, this.width, this.height);
|
|
|
|
|
this.autoplay = false;
|
|
|
|
|
this.pause();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
public renderFrame(frame: Parameters<RLottiePlayer['renderFrame2']>[0], frameNo: number) {
|
|
|
|
|
const canCacheFrame = this.cachingDelta && (frameNo % this.cachingDelta || !frameNo);
|
|
|
|
|
if(canCacheFrame) {
|
|
|
|
|
if(frame instanceof Uint8ClampedArray && !this.cache.frames.has(frameNo)) {
|
|
|
|
|
this.cache.frames.set(frameNo, new Uint8ClampedArray(frame));// frame;
|
|
|
|
|
} else if(IS_IMAGE_BITMAP_SUPPORTED && frame instanceof ImageBitmap && !this.cache.framesNew.has(frameNo)) {
|
|
|
|
|
this.cache.framesNew.set(frameNo, frame);
|
|
|
|
|
}
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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;
|
|
|
|
|
|
|
|
|
|
if(delta < 0) {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
const timeout = this.frInterval > -delta ? -delta % this.frInterval : this.frInterval;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
if(this.rafId) clearTimeout(this.rafId);
|
2022-08-31 06:22:16 +02:00
|
|
|
|
this.rafId = window.setTimeout(() => {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.renderFrame2(frame, frameNo);
|
2022-08-31 06:22:16 +02:00
|
|
|
|
}, timeout);
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// await new Promise((resolve) => setTimeout(resolve, -delta % this.frInterval));
|
2022-08-31 06:22:16 +02:00
|
|
|
|
return;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.renderFrame2(frame, frameNo);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public requestFrame(frameNo: number) {
|
2022-08-31 06:22:16 +02:00
|
|
|
|
const frame = this.cache.frames.get(frameNo);
|
|
|
|
|
const frameNew = this.cache.framesNew.get(frameNo);
|
|
|
|
|
if(frameNew) {
|
|
|
|
|
this.renderFrame(frameNew, frameNo);
|
|
|
|
|
} else if(frame) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.renderFrame(frame, frameNo);
|
|
|
|
|
} else {
|
2022-02-10 20:58:26 +01:00
|
|
|
|
if(this.clamped && !this.clamped.length) { // fix detached
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.clamped = new Uint8ClampedArray(this.width * this.height * 4);
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2022-09-25 19:49:33 +02:00
|
|
|
|
this.sendQuery(['renderFrame', frameNo], this.clamped ? [this.clamped.buffer] : undefined);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-08 13:52:14 +01:00
|
|
|
|
private onLap() {
|
2022-11-01 18:39:23 +01:00
|
|
|
|
if(++this.playedTimes === this.loop) {
|
|
|
|
|
this.loop = false;
|
|
|
|
|
}
|
2022-01-08 13:52:14 +01:00
|
|
|
|
|
|
|
|
|
if(!this.loop) {
|
|
|
|
|
this.pause(false);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2022-02-10 20:58:26 +01:00
|
|
|
|
|
|
|
|
|
return true;
|
2022-01-08 13:52:14 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
private mainLoopForwards() {
|
|
|
|
|
const {skipDelta, maxFrame} = this;
|
2022-01-08 13:52:14 +01:00
|
|
|
|
const frame = (this.curFrame + skipDelta) > maxFrame ? this.curFrame = (this.loop ? this.minFrame : this.maxFrame) : this.curFrame += skipDelta;
|
|
|
|
|
// console.log('mainLoopForwards', this.curFrame, skipDelta, frame);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
this.requestFrame(frame);
|
|
|
|
|
if((frame + skipDelta) > maxFrame) {
|
2022-02-10 20:58:26 +01:00
|
|
|
|
return this.onLap();
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
private mainLoopBackwards() {
|
|
|
|
|
const {skipDelta, minFrame} = this;
|
2022-01-08 13:52:14 +01:00
|
|
|
|
const frame = (this.curFrame - skipDelta) < minFrame ? this.curFrame = (this.loop ? this.maxFrame : this.minFrame) : this.curFrame -= skipDelta;
|
|
|
|
|
// console.log('mainLoopBackwards', this.curFrame, skipDelta, frame);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
this.requestFrame(frame);
|
|
|
|
|
if((frame - skipDelta) < minFrame) {
|
2022-02-10 20:58:26 +01:00
|
|
|
|
return this.onLap();
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setMainLoop() {
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// window.cancelAnimationFrame(this.rafId);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
clearTimeout(this.rafId);
|
2022-11-01 18:39:23 +01:00
|
|
|
|
this.rafId = undefined;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
this.frInterval = 1000 / this.fps / this.speed * this.skipDelta;
|
|
|
|
|
this.frThen = Date.now() - this.frInterval;
|
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// console.trace('setMainLoop', this.frInterval, this.direction, this, JSON.stringify(this.listenerResults), this.listenerResults);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
const method = (this.direction === 1 ? this.mainLoopForwards : this.mainLoopBackwards).bind(this);
|
|
|
|
|
this.currentMethod = method;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// this.frameListener && this.removeListener('enterFrame', this.frameListener);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// setTimeout(() => {
|
|
|
|
|
// this.addListener('enterFrame', this.frameListener);
|
|
|
|
|
// }, 0);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
if(this.frameListener) {
|
|
|
|
|
const lastResult = this.listenerResults.enterFrame;
|
|
|
|
|
if(lastResult !== undefined) {
|
|
|
|
|
this.frameListener(this.curFrame);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
|
|
|
|
// this.mainLoop(method);
|
|
|
|
|
// this.r(method);
|
|
|
|
|
// method();
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public playPart(options: {
|
2022-08-04 08:49:54 +02:00
|
|
|
|
from: number,
|
|
|
|
|
to: number,
|
2021-12-11 17:37:08 +01:00
|
|
|
|
callback?: () => void
|
|
|
|
|
}) {
|
|
|
|
|
this.pause();
|
|
|
|
|
|
|
|
|
|
const {from, to, callback} = options;
|
|
|
|
|
this.curFrame = from - 1;
|
|
|
|
|
|
|
|
|
|
return this.playToFrame({
|
|
|
|
|
frame: to,
|
|
|
|
|
direction: to > from ? 1 : -1,
|
|
|
|
|
callback
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public playToFrame(options: {
|
2022-08-04 08:49:54 +02:00
|
|
|
|
frame: number,
|
|
|
|
|
speed?: number,
|
2021-12-11 17:37:08 +01:00
|
|
|
|
direction?: number,
|
|
|
|
|
callback?: () => void
|
|
|
|
|
}) {
|
|
|
|
|
this.pause();
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
const {frame, speed, callback, direction} = options;
|
|
|
|
|
this.setDirection(direction === undefined ? this.curFrame > frame ? -1 : 1 : direction);
|
|
|
|
|
speed !== undefined && this.setSpeed(speed);
|
|
|
|
|
|
|
|
|
|
const bounds = [this.curFrame, frame];
|
|
|
|
|
if(this.direction === -1) bounds.reverse();
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.loop = false;
|
|
|
|
|
this.setMinMax(bounds[0], bounds[1]);
|
2022-01-08 13:52:14 +01:00
|
|
|
|
|
|
|
|
|
if(this.playToFrameOnFrameCallback) {
|
|
|
|
|
this.removeEventListener('enterFrame', this.playToFrameOnFrameCallback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(callback) {
|
|
|
|
|
this.playToFrameOnFrameCallback = (frameNo: number) => {
|
|
|
|
|
if(frameNo === frame) {
|
|
|
|
|
this.removeEventListener('enterFrame', this.playToFrameOnFrameCallback);
|
|
|
|
|
callback();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.addEventListener('enterFrame', this.playToFrameOnFrameCallback);
|
|
|
|
|
}
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
this.play();
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-08 13:52:14 +01:00
|
|
|
|
public setColor(color: RLottieColor, renderIfPaused: boolean) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.color = color;
|
|
|
|
|
|
2022-01-08 13:52:14 +01:00
|
|
|
|
if(renderIfPaused && this.paused) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.renderFrame2(this.imageData.data, this.curFrame);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public setInverseColor(color: RLottieColor) {
|
|
|
|
|
this.inverseColor = color;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setMinMax(minFrame = 0, maxFrame = this.frameCount - 1) {
|
|
|
|
|
this.minFrame = minFrame;
|
|
|
|
|
this.maxFrame = maxFrame;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async onLoad(frameCount: number, fps: number) {
|
|
|
|
|
this.frameCount = frameCount;
|
|
|
|
|
this.fps = fps;
|
|
|
|
|
this.setMinMax();
|
|
|
|
|
if(this.initFrame !== undefined) {
|
|
|
|
|
this.initFrame = clamp(this.initFrame, this.minFrame, this.maxFrame);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const curFrame = this.resetCurrentFrame();
|
|
|
|
|
|
|
|
|
|
// * 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;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// this.sendQuery('renderFrame', 0);
|
|
|
|
|
|
|
|
|
|
// Кешировать сразу не получится, рендер стикера (тайгер) занимает 519мс,
|
|
|
|
|
// если рендерить 75% с получением каждого кадра из воркера, будет 475мс, т.е. при 100% было бы 593мс, потеря на передаче 84мс.
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
/* console.time('cache' + this.reqId);
|
|
|
|
|
for(let i = 0; i < frameCount; ++i) {
|
|
|
|
|
//if(this.removed) return;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
if(i % 4) {
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
delete this.listenerResults.enterFrame;
|
|
|
|
|
this.addListener('enterFrame', resolve, true);
|
|
|
|
|
this.requestFrame(i);
|
2022-08-04 08:49:54 +02:00
|
|
|
|
});
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
|
console.timeEnd('cache' + this.reqId); */
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// console.log('cached');
|
2021-12-11 17:37:08 +01:00
|
|
|
|
/* this.el.innerHTML = '';
|
|
|
|
|
this.el.append(this.canvas);
|
|
|
|
|
return; */
|
|
|
|
|
|
|
|
|
|
!this.skipFirstFrameRendering && this.requestFrame(curFrame);
|
|
|
|
|
this.dispatchEvent('ready');
|
|
|
|
|
this.addEventListener('enterFrame', () => {
|
|
|
|
|
this.dispatchEvent('firstFrame');
|
|
|
|
|
|
2022-08-31 06:22:16 +02:00
|
|
|
|
if(!this.canvas[0].parentNode && this.el && !this.overrideRender) {
|
|
|
|
|
this.el.forEach((container, idx) => container.append(this.canvas[idx]));
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// console.log('enterFrame firstFrame');
|
|
|
|
|
|
|
|
|
|
// let lastTime = this.frThen;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
this.frameListener = () => {
|
2022-09-13 14:27:28 +02:00
|
|
|
|
if(this.paused || !this.currentMethod) {
|
2021-12-11 17:37:08 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const time = Date.now();
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// console.log(`enterFrame handle${this.reqId}`, time, (time - lastTime), this.frInterval);
|
2021-12-11 17:37:08 +01:00
|
|
|
|
/* if(Math.round(time - lastTime + this.frInterval * 0.25) < Math.round(this.frInterval)) {
|
|
|
|
|
return;
|
|
|
|
|
} */
|
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
|
// lastTime = time;
|
2021-12-11 17:37:08 +01:00
|
|
|
|
|
|
|
|
|
this.frThen = time + this.frInterval;
|
|
|
|
|
const canContinue = this.currentMethod();
|
|
|
|
|
if(!canContinue && !this.loop && this.autoplay) {
|
|
|
|
|
this.autoplay = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.addEventListener('enterFrame', this.frameListener);
|
2022-08-31 06:22:16 +02:00
|
|
|
|
// setInterval(this.frameListener, this.frInterval);
|
2022-01-23 20:45:34 +01:00
|
|
|
|
|
2022-09-13 14:27:28 +02:00
|
|
|
|
// ! fix autoplaying since there will be no animationIntersector for it
|
2022-01-23 20:45:34 +01:00
|
|
|
|
if(this.group === 'none' && this.autoplay) {
|
|
|
|
|
this.play();
|
|
|
|
|
}
|
2021-12-11 17:37:08 +01:00
|
|
|
|
}, {once: true});
|
|
|
|
|
}
|
|
|
|
|
}
|