🆙 Update Furniture Badge

- Better performance - PixiJS handles animation
- GPU acceleration - Textures are uploaded to GPU instead of CPU canvas operations
This commit is contained in:
DuckieTM
2026-01-31 20:54:42 +01:00
parent 4f2c2f904c
commit 1fe48bcda7
@@ -1,7 +1,7 @@
import { IGraphicAsset, IRoomObjectSprite, RoomObjectVariable } from '@nitrots/api'; import { IGraphicAsset, IRoomObjectSprite, RoomObjectVariable } from '@nitrots/api';
import { GetConfiguration } from '@nitrots/configuration'; import { GetConfiguration } from '@nitrots/configuration';
import { GetSessionDataManager } from '@nitrots/session'; import { GetSessionDataManager } from '@nitrots/session';
import { Texture } from 'pixi.js'; import { AnimatedSprite, Texture } from 'pixi.js';
import { parseGIF, decompressFrames } from 'gifuct-js'; import { parseGIF, decompressFrames } from 'gifuct-js';
import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization'; import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization';
@@ -14,10 +14,36 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
private _badgeAssetNameNormalScale = ''; private _badgeAssetNameNormalScale = '';
private _badgeAssetNameSmallScale = ''; private _badgeAssetNameSmallScale = '';
private _badgeVisibleInState = -1; private _badgeVisibleInState = -1;
private _gifFrames: ImageData[] = null; private _frameTextures: Texture[] = null;
private _frameDelays: number[] = null; private _animatedSprite: AnimatedSprite = null;
private _currentFrame = 0; private _lastFrameIndex = -1;
private _lastFrameTime = 0;
public dispose(): void
{
this.disposeAnimatedSprite();
super.dispose();
}
private disposeAnimatedSprite(): void
{
if(this._animatedSprite)
{
this._animatedSprite.stop();
this._animatedSprite.destroy();
this._animatedSprite = null;
}
if(this._frameTextures)
{
for(const texture of this._frameTextures)
{
texture.destroy(true);
}
this._frameTextures = null;
}
this._lastFrameIndex = -1;
}
public getTexture(scale: number, layerId: number, asset: IGraphicAsset): Texture public getTexture(scale: number, layerId: number, asset: IGraphicAsset): Texture
{ {
@@ -28,40 +54,40 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
{ {
super.update(geometry, time, update, skipUpdate); super.update(geometry, time, update, skipUpdate);
// Update animated GIF // GIF using AnimatedSprite's
if(this._gifFrames && this._gifFrames.length > 1) if(this._animatedSprite && this._frameTextures && this._frameTextures.length > 1)
{ {
const sessionDataManager = GetSessionDataManager(); const currentFrameIndex = this._animatedSprite.currentFrame;
let tex = sessionDataManager.getBadgeImage(this._badgeId);
if (!tex) tex = sessionDataManager.getGroupBadgeImage(this._badgeId);
if(currentFrameIndex !== this._lastFrameIndex)
if(!tex)
{ {
console.warn('⚠️ No texture found for badge:', this._badgeId); this._lastFrameIndex = currentFrameIndex;
return;
} const sessionDataManager = GetSessionDataManager();
let tex = sessionDataManager.getBadgeImage(this._badgeId);
const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement; if(!tex) tex = sessionDataManager.getGroupBadgeImage(this._badgeId);
const now = performance.now(); if(!tex) return;
const elapsed = now - this._lastFrameTime;
const currentFrameTexture = this._frameTextures[currentFrameIndex];
const frameDelay = (this._frameDelays[this._currentFrame] || 10) * 10; if(!currentFrameTexture) return;
if(elapsed >= frameDelay) const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement;
{
this._lastFrameTime = now;
const oldFrame = this._currentFrame;
this._currentFrame = (this._currentFrame + 1) % this._gifFrames.length;
const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true }); const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true });
const frame = this._gifFrames[this._currentFrame];
const frameCanvas = document.createElement('canvas');
if(frame) frameCanvas.width = currentFrameTexture.width;
frameCanvas.height = currentFrameTexture.height;
const frameCtx = frameCanvas.getContext('2d');
const frameSource = (currentFrameTexture.source as any).resource;
if(frameSource instanceof HTMLCanvasElement || frameSource instanceof HTMLImageElement)
{ {
ctx.putImageData(frame, 0, 0); ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height);
ctx.drawImage(frameSource, 0, 0);
tex.source.update(); tex.source.update();
const assetName = this._badgeAssetNameNormalScale; const assetName = this._badgeAssetNameNormalScale;
if(this.asset && assetName) if(this.asset && assetName)
{ {
@@ -83,19 +109,18 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
const badgeStatus = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BADGE_IMAGE_STATUS); const badgeStatus = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BADGE_IMAGE_STATUS);
const badgeId = this.object.model.getValue<string>(RoomObjectVariable.FURNITURE_BADGE_ASSET_NAME); const badgeId = this.object.model.getValue<string>(RoomObjectVariable.FURNITURE_BADGE_ASSET_NAME);
if (badgeStatus === -1) if(badgeStatus === -1)
{ {
this._badgeId = ''; this._badgeId = '';
this._badgeAssetNameNormalScale = ''; this._badgeAssetNameNormalScale = '';
this._badgeAssetNameSmallScale = ''; this._badgeAssetNameSmallScale = '';
this._badgeVisibleInState = -1; this._badgeVisibleInState = -1;
this._gifFrames = null; this.disposeAnimatedSprite();
this._frameDelays = null;
return needsUpdate; return needsUpdate;
} }
if ((badgeStatus === 1) && badgeId && (badgeId !== this._badgeId)) if((badgeStatus === 1) && badgeId && (badgeId !== this._badgeId))
{ {
this._badgeId = badgeId; this._badgeId = badgeId;
this._badgeAssetNameNormalScale = badgeId; this._badgeAssetNameNormalScale = badgeId;
@@ -104,11 +129,7 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
const visibleInState = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BADGE_VISIBLE_IN_STATE); const visibleInState = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BADGE_VISIBLE_IN_STATE);
this._badgeVisibleInState = isNaN(visibleInState) ? -1 : visibleInState; this._badgeVisibleInState = isNaN(visibleInState) ? -1 : visibleInState;
this._gifFrames = null; this.disposeAnimatedSprite();
this._frameDelays = null;
this._currentFrame = 0;
this._lastFrameTime = 0;
this.addBadgeToAssetCollection(badgeId); this.addBadgeToAssetCollection(badgeId);
const layerId = FurnitureBadgeDisplayVisualization.BADGE_LAYER_ID; const layerId = FurnitureBadgeDisplayVisualization.BADGE_LAYER_ID;
@@ -127,21 +148,21 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
{ {
const tag = this.getLayerTag(scale, this.direction, layerId); const tag = this.getLayerTag(scale, this.direction, layerId);
if (tag !== FurnitureBadgeDisplayVisualization.BADGE_TAG) if(tag !== FurnitureBadgeDisplayVisualization.BADGE_TAG)
{ {
return super.getSpriteAssetName(scale, layerId); return super.getSpriteAssetName(scale, layerId);
} }
if ((this._badgeVisibleInState !== -1) && (this.object.getState(0) !== this._badgeVisibleInState)) if((this._badgeVisibleInState !== -1) && (this.object.getState(0) !== this._badgeVisibleInState))
{ {
return super.getSpriteAssetName(scale, layerId); return super.getSpriteAssetName(scale, layerId);
} }
const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale; const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale;
if (!assetName) return super.getSpriteAssetName(scale, layerId); if(!assetName) return super.getSpriteAssetName(scale, layerId);
const a = this.getAsset(assetName, layerId); const a = this.getAsset(assetName, layerId);
if (!a || !a.texture) return super.getSpriteAssetName(scale, layerId); if(!a || !a.texture) return super.getSpriteAssetName(scale, layerId);
return assetName; return assetName;
} }
@@ -152,7 +173,7 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
const tag = this.getLayerTag(scale, this.direction, layerId); const tag = this.getLayerTag(scale, this.direction, layerId);
if (tag === FurnitureBadgeDisplayVisualization.BADGE_TAG) if(tag === FurnitureBadgeDisplayVisualization.BADGE_TAG)
{ {
sprite.visible = true; sprite.visible = true;
sprite.alpha = 255; sprite.alpha = 255;
@@ -164,13 +185,13 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
{ {
let offset = super.getLayerXOffset(scale, direction, layerId); let offset = super.getLayerXOffset(scale, direction, layerId);
if (this.getLayerTag(scale, direction, layerId) === FurnitureBadgeDisplayVisualization.BADGE_TAG) if(this.getLayerTag(scale, direction, layerId) === FurnitureBadgeDisplayVisualization.BADGE_TAG)
{ {
const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale; const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale;
if (!assetName) return offset; if(!assetName) return offset;
const a = this.getAsset(assetName, layerId); const a = this.getAsset(assetName, layerId);
if (!a) return offset; if(!a) return offset;
const targetW = (scale === 64) ? 40 : 20; const targetW = (scale === 64) ? 40 : 20;
offset += ((targetW - a.width) / 2); offset += ((targetW - a.width) / 2);
@@ -183,13 +204,13 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
{ {
let offset = super.getLayerYOffset(scale, direction, layerId); let offset = super.getLayerYOffset(scale, direction, layerId);
if (this.getLayerTag(scale, direction, layerId) === FurnitureBadgeDisplayVisualization.BADGE_TAG) if(this.getLayerTag(scale, direction, layerId) === FurnitureBadgeDisplayVisualization.BADGE_TAG)
{ {
const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale; const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale;
if (!assetName) return offset; if(!assetName) return offset;
const a = this.getAsset(assetName, layerId); const a = this.getAsset(assetName, layerId);
if (!a) return offset; if(!a) return offset;
const targetH = (scale === 64) ? 40 : 20; const targetH = (scale === 64) ? 40 : 20;
offset += ((targetH - a.height) / 2); offset += ((targetH - a.height) / 2);
@@ -203,36 +224,36 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
const sessionDataManager = GetSessionDataManager(); const sessionDataManager = GetSessionDataManager();
let tex = sessionDataManager.getBadgeImage(badgeId); let tex = sessionDataManager.getBadgeImage(badgeId);
if (!tex) tex = sessionDataManager.getGroupBadgeImage(badgeId); if(!tex) tex = sessionDataManager.getGroupBadgeImage(badgeId);
if (!tex || !this.asset) return; if(!tex || !this.asset) return;
const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement; const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement;
const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true }); const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true });
const imageData = ctx.getImageData(0, 0, 1, 1); const imageData = ctx.getImageData(0, 0, 1, 1);
const isEmpty = imageData.data[3] === 0; const isEmpty = imageData.data[3] === 0;
if (isEmpty || !this._gifFrames) if(isEmpty || !this._frameTextures)
{ {
const badgeUrl = GetConfiguration().getValue<string>('badge.asset.url', '').replace('%badgename%', badgeId); const badgeUrl = GetConfiguration().getValue<string>('badge.asset.url', '').replace('%badgename%', badgeId);
try try
{ {
const response = await fetch(badgeUrl); const response = await fetch(badgeUrl);
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
const gif = parseGIF(arrayBuffer); const gif = parseGIF(arrayBuffer);
const frames = decompressFrames(gif, true); const frames = decompressFrames(gif, true);
if(frames && frames.length > 0) if(frames && frames.length > 0)
{ {
this._gifFrames = []; this._frameTextures = [];
this._frameDelays = []; const frameDelays: number[] = [];
const accCanvas = document.createElement('canvas'); const accCanvas = document.createElement('canvas');
accCanvas.width = gif.lsd.width; accCanvas.width = gif.lsd.width;
accCanvas.height = gif.lsd.height; accCanvas.height = gif.lsd.height;
const accCtx = accCanvas.getContext('2d', { willReadFrequently: true }); const accCtx = accCanvas.getContext('2d', { willReadFrequently: true });
for(let i = 0; i < frames.length; i++) for(let i = 0; i < frames.length; i++)
{ {
const frame = frames[i]; const frame = frames[i];
@@ -245,43 +266,60 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
accCtx.clearRect(0, 0, gif.lsd.width, gif.lsd.height); accCtx.clearRect(0, 0, gif.lsd.width, gif.lsd.height);
} }
} }
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
tempCanvas.width = frame.dims.width; tempCanvas.width = frame.dims.width;
tempCanvas.height = frame.dims.height; tempCanvas.height = frame.dims.height;
const tempCtx = tempCanvas.getContext('2d'); const tempCtx = tempCanvas.getContext('2d');
const patchData = new ImageData( const patchData = new ImageData(
new Uint8ClampedArray(frame.patch), new Uint8ClampedArray(frame.patch),
frame.dims.width, frame.dims.width,
frame.dims.height frame.dims.height
); );
tempCtx.putImageData(patchData, 0, 0); tempCtx.putImageData(patchData, 0, 0);
accCtx.drawImage(tempCanvas, frame.dims.left, frame.dims.top); accCtx.drawImage(tempCanvas, frame.dims.left, frame.dims.top);
const fullFrame = accCtx.getImageData(0, 0, gif.lsd.width, gif.lsd.height); const frameCanvas = document.createElement('canvas');
this._gifFrames.push(fullFrame); frameCanvas.width = gif.lsd.width;
this._frameDelays.push(frame.delay || 10); frameCanvas.height = gif.lsd.height;
const frameCtx = frameCanvas.getContext('2d');
frameCtx.drawImage(accCanvas, 0, 0);
const frameTexture = Texture.from(frameCanvas);
this._frameTextures.push(frameTexture);
frameDelays.push(frame.delay || 10);
} }
this._currentFrame = 0; if(this._frameTextures.length > 1)
this._lastFrameTime = performance.now();
const avgDelay = this._frameDelays.reduce((a, b) => a + b, 0) / this._frameDelays.length;
if(avgDelay > 50)
{ {
this._frameDelays = this._frameDelays.map(() => 10); this._animatedSprite = new AnimatedSprite(this._frameTextures);
const avgDelay = frameDelays.reduce((a, b) => a + b, 0) / frameDelays.length;
const delayMs = (avgDelay > 50 ? 10 : avgDelay) * 10;
const framesPerTick = 16.67 / delayMs;
this._animatedSprite.animationSpeed = framesPerTick;
this._animatedSprite.loop = true;
this._animatedSprite.play();
this._lastFrameIndex = -1;
}
const firstFrameSource = (this._frameTextures[0].source as any).resource;
if(firstFrameSource instanceof HTMLCanvasElement)
{
ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height);
ctx.drawImage(firstFrameSource, 0, 0);
tex.source.update();
} }
ctx.putImageData(this._gifFrames[0], 0, 0);
tex.source.update();
} }
} }
catch(err) catch(err)
{ {
console.error('Failed to parse GIF, using static image:', err); console.error('Failed to parse GIF, using static image:', err);
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous'; img.crossOrigin = 'anonymous';
img.onload = () => img.onload = () =>
@@ -296,4 +334,4 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
this.asset.addAsset(badgeId, tex, true, 0, 0, false, false); this.asset.addAsset(badgeId, tex, true, 0, 0, false, false);
} }
} }