[Perf] RLottie: Add GPU-acceleration with ImageBitmap
This commit is contained in:
parent
a5cda0f209
commit
572fb78daa
@ -16,8 +16,8 @@ interface Params {
|
||||
coords?: { x: number; y: number };
|
||||
}
|
||||
|
||||
type Frames = ArrayBuffer[];
|
||||
type Chunks = (Frames | undefined)[];
|
||||
type Frame = ImageBitmap;
|
||||
type Chunks = (Frame[] | undefined)[];
|
||||
|
||||
// TODO Consider removing chunks
|
||||
const CHUNK_SIZE = 1;
|
||||
@ -49,6 +49,8 @@ class RLottie {
|
||||
|
||||
private imgSize!: number;
|
||||
|
||||
private imageData!: ImageData;
|
||||
|
||||
private msPerFrame = 1000 / 60;
|
||||
|
||||
private reduceFactor = 1;
|
||||
@ -263,6 +265,7 @@ class RLottie {
|
||||
|
||||
if (!this.imgSize) {
|
||||
this.imgSize = imgSize;
|
||||
this.imageData = new ImageData(imgSize, imgSize);
|
||||
}
|
||||
|
||||
if (this.isRendererInited) {
|
||||
@ -299,6 +302,7 @@ class RLottie {
|
||||
this.tgsUrl,
|
||||
this.imgSize,
|
||||
this.params.isLowPriority,
|
||||
this.customColor,
|
||||
this.onRendererInit.bind(this),
|
||||
],
|
||||
});
|
||||
@ -407,26 +411,14 @@ class RLottie {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arr = new Uint8ClampedArray(frame);
|
||||
if (this.customColor) {
|
||||
for (let i = 0; i < arr.length; i += 4) {
|
||||
/* eslint-disable prefer-destructuring */
|
||||
arr[i] = this.customColor[0];
|
||||
arr[i + 1] = this.customColor[1];
|
||||
arr[i + 2] = this.customColor[2];
|
||||
/* eslint-enable prefer-destructuring */
|
||||
}
|
||||
}
|
||||
|
||||
const imageData = new ImageData(arr, this.imgSize, this.imgSize);
|
||||
|
||||
this.containers.forEach((containerData) => {
|
||||
const {
|
||||
ctx, isLoaded, isPaused, coords: { x, y } = {}, onLoad,
|
||||
} = containerData;
|
||||
|
||||
if (!isLoaded || !isPaused) {
|
||||
ctx.putImageData(imageData, x || 0, y || 0);
|
||||
ctx.clearRect(x || 0, y || 0, this.imgSize, this.imgSize);
|
||||
ctx.drawImage(frame, x || 0, y || 0);
|
||||
}
|
||||
|
||||
if (!isLoaded) {
|
||||
@ -511,6 +503,17 @@ class RLottie {
|
||||
return chunk[indexInChunk];
|
||||
}
|
||||
|
||||
private setFrame(frameIndex: number, frame: Frame) {
|
||||
const chunkIndex = this.getChunkIndex(frameIndex);
|
||||
const indexInChunk = this.getFrameIndexInChunk(frameIndex);
|
||||
const chunk = this.chunks[chunkIndex];
|
||||
if (!chunk) {
|
||||
return;
|
||||
}
|
||||
|
||||
chunk[indexInChunk] = frame;
|
||||
}
|
||||
|
||||
private getFrameIndexInChunk(frameIndex: number) {
|
||||
const chunkIndex = this.getChunkIndex(frameIndex);
|
||||
return frameIndex - chunkIndex * this.chunkSize;
|
||||
@ -557,7 +560,7 @@ class RLottie {
|
||||
}
|
||||
}
|
||||
|
||||
private onFrameLoad(frameIndex: number, arrayBuffer: ArrayBuffer) {
|
||||
private onFrameLoad(frameIndex: number, imageBitmap: ImageBitmap) {
|
||||
const chunkIndex = this.getChunkIndex(frameIndex);
|
||||
const chunk = this.chunks[chunkIndex];
|
||||
// Frame can be skipped and chunk can be already cleaned up
|
||||
@ -565,7 +568,7 @@ class RLottie {
|
||||
return;
|
||||
}
|
||||
|
||||
chunk[this.getFrameIndexInChunk(frameIndex)] = arrayBuffer;
|
||||
chunk[this.getFrameIndexInChunk(frameIndex)] = imageBitmap;
|
||||
|
||||
if (this.isWaiting) {
|
||||
this.doPlay();
|
||||
|
||||
@ -33,6 +33,8 @@ const renderers = new Map<string, {
|
||||
imgSize: number;
|
||||
reduceFactor: number;
|
||||
handle: any;
|
||||
imageData: ImageData;
|
||||
customColor?: [number, number, number];
|
||||
}>();
|
||||
|
||||
async function init(
|
||||
@ -40,6 +42,7 @@ async function init(
|
||||
tgsUrl: string,
|
||||
imgSize: number,
|
||||
isLowPriority: boolean,
|
||||
customColor: [number, number, number] | undefined,
|
||||
onInit: CancellableCallback,
|
||||
) {
|
||||
if (!rLottieApi) {
|
||||
@ -52,9 +55,14 @@ async function init(
|
||||
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
|
||||
rLottieApi.resize(handle, imgSize, imgSize);
|
||||
|
||||
const imageData = new ImageData(imgSize, imgSize);
|
||||
|
||||
const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount);
|
||||
|
||||
renderers.set(key, { imgSize, reduceFactor, handle });
|
||||
renderers.set(key, {
|
||||
imgSize, reduceFactor, handle, imageData, customColor,
|
||||
});
|
||||
|
||||
onInit(reduceFactor, msPerFrame, reducedFramesCount);
|
||||
}
|
||||
|
||||
@ -74,6 +82,7 @@ async function changeData(
|
||||
const framesCount = rLottieApi.loadFromData(handle, stringOnWasmHeap);
|
||||
|
||||
const { reduceFactor, msPerFrame, reducedFramesCount } = calcParams(json, isLowPriority, framesCount);
|
||||
|
||||
onInit(reduceFactor, msPerFrame, reducedFramesCount);
|
||||
}
|
||||
|
||||
@ -110,7 +119,9 @@ async function renderFrames(
|
||||
await rLottieApiPromise;
|
||||
}
|
||||
|
||||
const { imgSize, reduceFactor, handle } = renderers.get(key)!;
|
||||
const {
|
||||
imgSize, reduceFactor, handle, imageData, customColor,
|
||||
} = renderers.get(key)!;
|
||||
|
||||
for (let i = fromIndex; i <= toIndex; i++) {
|
||||
const realIndex = i * reduceFactor;
|
||||
@ -118,8 +129,26 @@ async function renderFrames(
|
||||
rLottieApi.render(handle, realIndex);
|
||||
const bufferPointer = rLottieApi.buffer(handle);
|
||||
const data = Module.HEAPU8.subarray(bufferPointer, bufferPointer + (imgSize * imgSize * 4));
|
||||
const arrayBuffer = new Uint8ClampedArray(data).buffer;
|
||||
onProgress(i, arrayBuffer);
|
||||
|
||||
if (customColor) {
|
||||
const arr = new Uint8ClampedArray(data);
|
||||
applyColor(arr, customColor);
|
||||
imageData.data.set(arr);
|
||||
} else {
|
||||
imageData.data.set(data);
|
||||
}
|
||||
|
||||
const imageBitmap = await createImageBitmap(imageData);
|
||||
|
||||
onProgress(i, imageBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
function applyColor(arr: Uint8ClampedArray, color: [number, number, number]) {
|
||||
for (let i = 0; i < arr.length; i += 4) {
|
||||
arr[i] = color[0];
|
||||
arr[i + 1] = color[1];
|
||||
arr[i + 2] = color[2];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export default function createInterface(api: Record<string, Function>) {
|
||||
type: 'methodCallback',
|
||||
messageId,
|
||||
callbackArgs,
|
||||
}, lastArg instanceof ArrayBuffer ? [lastArg] : undefined);
|
||||
}, isTransferable(lastArg) ? [lastArg] : undefined);
|
||||
};
|
||||
|
||||
callbackState.set(messageId, callback);
|
||||
@ -78,6 +78,10 @@ export default function createInterface(api: Record<string, Function>) {
|
||||
};
|
||||
}
|
||||
|
||||
function isTransferable(obj: any) {
|
||||
return obj instanceof ArrayBuffer || obj instanceof ImageBitmap;
|
||||
}
|
||||
|
||||
function handleErrors() {
|
||||
self.onerror = (e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -92,9 +96,9 @@ function handleErrors() {
|
||||
});
|
||||
}
|
||||
|
||||
function sendToOrigin(data: WorkerMessageData, arrayBuffers?: ArrayBuffer[]) {
|
||||
if (arrayBuffers) {
|
||||
postMessage(data, arrayBuffers);
|
||||
function sendToOrigin(data: WorkerMessageData, transferables?: Transferable[]) {
|
||||
if (transferables) {
|
||||
postMessage(data, transferables);
|
||||
} else {
|
||||
postMessage(data);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user