You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 15:06:20 +00:00
🆙 Added animated gif to badge_display
Display Container
├── Furniture Container (renders 1st - back)
│ └── All furniture sprites sorted by z-depth
├── Avatar Container (renders 2nd - middle)
│ └── All avatar sprites sorted by z-depth
└── Badge Container (renders 3rd - front)
└── All badge sprites sorted by z-depth
✅ Badges render on top of their furniture
✅ Avatars respect proper 3D depth (in front when in front, behind when behind)
✅ Animated GIFs work beautifully
✅ Clean, maintainable code with just a tiny z-offset
This commit is contained in:
+3
-3
@@ -24,15 +24,15 @@
|
|||||||
"compile": "tsc --project ./tsconfig.json --noEmit false",
|
"compile": "tsc --project ./tsconfig.json --noEmit false",
|
||||||
"eslint": "eslint ./src ./packages/*/src",
|
"eslint": "eslint ./src ./packages/*/src",
|
||||||
"eslint-fix": "eslint ./src --fix"
|
"eslint-fix": "eslint ./src --fix"
|
||||||
|
|
||||||
},
|
},
|
||||||
"main": "./index",
|
"main": "./index",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clientjs": "^0.2.1",
|
||||||
|
"gifuct-js": "^2.1.2",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"pixi-filters": "^6.1.5",
|
"pixi-filters": "^6.1.5",
|
||||||
"pixi.js": "^8.15.0",
|
"pixi.js": "^8.15.0"
|
||||||
"clientjs": "^0.2.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.13.0",
|
||||||
|
|||||||
+134
-18
@@ -2,6 +2,7 @@ import { IGraphicAsset, IRoomObjectSprite, RoomObjectVariable } from '@nitrots/a
|
|||||||
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 { Texture } from 'pixi.js';
|
||||||
|
import { parseGIF, decompressFrames } from 'gifuct-js';
|
||||||
import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization';
|
import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization';
|
||||||
|
|
||||||
export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisualization
|
export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisualization
|
||||||
@@ -13,28 +14,67 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
|
|||||||
private _badgeAssetNameNormalScale = '';
|
private _badgeAssetNameNormalScale = '';
|
||||||
private _badgeAssetNameSmallScale = '';
|
private _badgeAssetNameSmallScale = '';
|
||||||
private _badgeVisibleInState = -1;
|
private _badgeVisibleInState = -1;
|
||||||
|
private _gifFrames: ImageData[] = null;
|
||||||
|
private _frameDelays: number[] = null;
|
||||||
|
private _currentFrame = 0;
|
||||||
|
private _lastFrameTime = 0;
|
||||||
|
|
||||||
public getTexture(scale: number, layerId: number, asset: IGraphicAsset): Texture
|
public getTexture(scale: number, layerId: number, asset: IGraphicAsset): Texture
|
||||||
{
|
{
|
||||||
return super.getTexture(scale, layerId, asset);
|
return super.getTexture(scale, layerId, asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get sprites(): IRoomObjectSprite[]
|
public update(geometry: any, time: number, update: boolean, skipUpdate: boolean): void
|
||||||
{
|
|
||||||
const sprites = super.sprites;
|
|
||||||
|
|
||||||
for(const sprite of sprites)
|
|
||||||
{
|
{
|
||||||
if(!sprite) continue;
|
super.update(geometry, time, update, skipUpdate);
|
||||||
|
|
||||||
if(sprite.name === this._badgeAssetNameNormalScale || sprite.name === this._badgeAssetNameSmallScale)
|
// Update animated GIF
|
||||||
|
if(this._gifFrames && this._gifFrames.length > 1)
|
||||||
{
|
{
|
||||||
sprite.relativeDepth = 0.01;
|
const sessionDataManager = GetSessionDataManager();
|
||||||
}
|
let tex = sessionDataManager.getBadgeImage(this._badgeId);
|
||||||
|
if (!tex) tex = sessionDataManager.getGroupBadgeImage(this._badgeId);
|
||||||
|
|
||||||
|
if(!tex)
|
||||||
|
{
|
||||||
|
console.warn('⚠️ No texture found for badge:', this._badgeId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprites;
|
const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement;
|
||||||
}
|
|
||||||
|
const now = performance.now();
|
||||||
|
const elapsed = now - this._lastFrameTime;
|
||||||
|
|
||||||
|
const frameDelay = (this._frameDelays[this._currentFrame] || 10) * 10;
|
||||||
|
|
||||||
|
if(elapsed >= frameDelay)
|
||||||
|
{
|
||||||
|
this._lastFrameTime = now;
|
||||||
|
const oldFrame = this._currentFrame;
|
||||||
|
this._currentFrame = (this._currentFrame + 1) % this._gifFrames.length;
|
||||||
|
|
||||||
|
const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
const frame = this._gifFrames[this._currentFrame];
|
||||||
|
|
||||||
|
if(frame)
|
||||||
|
{
|
||||||
|
ctx.putImageData(frame, 0, 0);
|
||||||
|
tex.source.update();
|
||||||
|
|
||||||
|
const assetName = this._badgeAssetNameNormalScale;
|
||||||
|
if(this.asset && assetName)
|
||||||
|
{
|
||||||
|
const asset = this.getAsset(assetName, FurnitureBadgeDisplayVisualization.BADGE_LAYER_ID);
|
||||||
|
if(asset && asset.texture)
|
||||||
|
{
|
||||||
|
asset.texture.source.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected updateModel(scale: number): boolean
|
protected updateModel(scale: number): boolean
|
||||||
{
|
{
|
||||||
@@ -49,6 +89,8 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
|
|||||||
this._badgeAssetNameNormalScale = '';
|
this._badgeAssetNameNormalScale = '';
|
||||||
this._badgeAssetNameSmallScale = '';
|
this._badgeAssetNameSmallScale = '';
|
||||||
this._badgeVisibleInState = -1;
|
this._badgeVisibleInState = -1;
|
||||||
|
this._gifFrames = null;
|
||||||
|
this._frameDelays = null;
|
||||||
|
|
||||||
return needsUpdate;
|
return needsUpdate;
|
||||||
}
|
}
|
||||||
@@ -62,6 +104,11 @@ 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._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;
|
||||||
@@ -151,7 +198,7 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
|
|||||||
return offset;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addBadgeToAssetCollection(badgeId: string): void
|
private async addBadgeToAssetCollection(badgeId: string): Promise<void>
|
||||||
{
|
{
|
||||||
const sessionDataManager = GetSessionDataManager();
|
const sessionDataManager = GetSessionDataManager();
|
||||||
|
|
||||||
@@ -160,23 +207,92 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali
|
|||||||
|
|
||||||
if (!tex || !this.asset) return;
|
if (!tex || !this.asset) return;
|
||||||
|
|
||||||
const canvas = (tex.source as any).resource as HTMLCanvasElement;
|
const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement;
|
||||||
const ctx = canvas.getContext('2d');
|
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) {
|
if (isEmpty || !this._gifFrames)
|
||||||
|
{
|
||||||
const badgeUrl = GetConfiguration().getValue<string>('badge.asset.url', '').replace('%badgename%', badgeId);
|
const badgeUrl = GetConfiguration().getValue<string>('badge.asset.url', '').replace('%badgename%', badgeId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const response = await fetch(badgeUrl);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const gif = parseGIF(arrayBuffer);
|
||||||
|
const frames = decompressFrames(gif, true);
|
||||||
|
|
||||||
|
if(frames && frames.length > 0)
|
||||||
|
{
|
||||||
|
this._gifFrames = [];
|
||||||
|
this._frameDelays = [];
|
||||||
|
|
||||||
|
const accCanvas = document.createElement('canvas');
|
||||||
|
accCanvas.width = gif.lsd.width;
|
||||||
|
accCanvas.height = gif.lsd.height;
|
||||||
|
const accCtx = accCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
|
||||||
|
for(let i = 0; i < frames.length; i++)
|
||||||
|
{
|
||||||
|
const frame = frames[i];
|
||||||
|
|
||||||
|
if(i > 0)
|
||||||
|
{
|
||||||
|
const prevDisposal = frames[i - 1].disposalType;
|
||||||
|
if(prevDisposal === 2)
|
||||||
|
{
|
||||||
|
accCtx.clearRect(0, 0, gif.lsd.width, gif.lsd.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
tempCanvas.width = frame.dims.width;
|
||||||
|
tempCanvas.height = frame.dims.height;
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const patchData = new ImageData(
|
||||||
|
new Uint8ClampedArray(frame.patch),
|
||||||
|
frame.dims.width,
|
||||||
|
frame.dims.height
|
||||||
|
);
|
||||||
|
tempCtx.putImageData(patchData, 0, 0);
|
||||||
|
|
||||||
|
accCtx.drawImage(tempCanvas, frame.dims.left, frame.dims.top);
|
||||||
|
|
||||||
|
const fullFrame = accCtx.getImageData(0, 0, gif.lsd.width, gif.lsd.height);
|
||||||
|
this._gifFrames.push(fullFrame);
|
||||||
|
this._frameDelays.push(frame.delay || 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentFrame = 0;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(this._gifFrames[0], 0, 0);
|
||||||
|
tex.source.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(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 = () =>
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
{
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height);
|
||||||
|
ctx.drawImage(img, 0, 0, badgeCanvas.width, badgeCanvas.height);
|
||||||
tex.source.update();
|
tex.source.update();
|
||||||
};
|
};
|
||||||
img.src = badgeUrl;
|
img.src = badgeUrl;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.asset.addAsset(badgeId, tex, true, 0, 0, false, false);
|
this.asset.addAsset(badgeId, tex, true, 0, 0, false, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -455,15 +455,17 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas
|
|||||||
|
|
||||||
sortableSprite.x = (spriteX - this._screenOffsetX);
|
sortableSprite.x = (spriteX - this._screenOffsetX);
|
||||||
sortableSprite.y = (spriteY - this._screenOffsetY);
|
sortableSprite.y = (spriteY - this._screenOffsetY);
|
||||||
sortableSprite.z = ((z + sprite.relativeDepth) + (3.7E-11 * count));
|
|
||||||
|
|
||||||
// Ensure badge renders on top of furniture
|
let calculatedZ = ((z + sprite.relativeDepth) + (3.7E-11 * count));
|
||||||
|
|
||||||
const isBadgeSprite = (sprite.tag === 'BADGE');
|
const isBadgeSprite = (sprite.tag === 'BADGE');
|
||||||
|
if(isBadgeSprite)
|
||||||
if(isBadgeSprite) {
|
{
|
||||||
sortableSprite.z = -999;
|
calculatedZ = calculatedZ + 0.001;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortableSprite.z = calculatedZ;
|
||||||
|
|
||||||
spriteCount++;
|
spriteCount++;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export class SortableSprite implements ISortableSprite
|
|||||||
private _x: number;
|
private _x: number;
|
||||||
private _y: number;
|
private _y: number;
|
||||||
private _z: number;
|
private _z: number;
|
||||||
|
private _isBadge: boolean;
|
||||||
|
private _isAvatar: boolean;
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
@@ -19,12 +21,16 @@ export class SortableSprite implements ISortableSprite
|
|||||||
this._x = 0;
|
this._x = 0;
|
||||||
this._y = 0;
|
this._y = 0;
|
||||||
this._z = 0;
|
this._z = 0;
|
||||||
|
this._isBadge = false;
|
||||||
|
this._isAvatar = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void
|
public dispose(): void
|
||||||
{
|
{
|
||||||
this._z = -(SortableSprite.Z_INFINITY);
|
this._z = -(SortableSprite.Z_INFINITY);
|
||||||
this._sprite = null;
|
this._sprite = null;
|
||||||
|
this._isBadge = false;
|
||||||
|
this._isAvatar = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get name(): string
|
public get name(): string
|
||||||
@@ -76,4 +82,24 @@ export class SortableSprite implements ISortableSprite
|
|||||||
{
|
{
|
||||||
this._z = z;
|
this._z = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isBadge(): boolean
|
||||||
|
{
|
||||||
|
return this._isBadge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set isBadge(value: boolean)
|
||||||
|
{
|
||||||
|
this._isBadge = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isAvatar(): boolean
|
||||||
|
{
|
||||||
|
return this._isAvatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set isAvatar(value: boolean)
|
||||||
|
{
|
||||||
|
this._isAvatar = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user