diff --git a/package.json b/package.json index cf4b046..ce9ad2c 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,15 @@ "compile": "tsc --project ./tsconfig.json --noEmit false", "eslint": "eslint ./src ./packages/*/src", "eslint-fix": "eslint ./src --fix" - }, "main": "./index", "dependencies": { + "clientjs": "^0.2.1", + "gifuct-js": "^2.1.2", "howler": "^2.2.4", "pako": "^2.1.0", "pixi-filters": "^6.1.5", - "pixi.js": "^8.15.0", - "clientjs": "^0.2.1" + "pixi.js": "^8.15.0" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/packages/room/src/object/visualization/furniture/FurnitureBadgeDisplayVisualization.ts b/packages/room/src/object/visualization/furniture/FurnitureBadgeDisplayVisualization.ts index 5c68a27..20d8c77 100644 --- a/packages/room/src/object/visualization/furniture/FurnitureBadgeDisplayVisualization.ts +++ b/packages/room/src/object/visualization/furniture/FurnitureBadgeDisplayVisualization.ts @@ -2,6 +2,7 @@ import { IGraphicAsset, IRoomObjectSprite, RoomObjectVariable } from '@nitrots/a import { GetConfiguration } from '@nitrots/configuration'; import { GetSessionDataManager } from '@nitrots/session'; import { Texture } from 'pixi.js'; +import { parseGIF, decompressFrames } from 'gifuct-js'; import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization'; export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisualization @@ -13,28 +14,67 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali private _badgeAssetNameNormalScale = ''; private _badgeAssetNameSmallScale = ''; 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 { return super.getTexture(scale, layerId, asset); } - - public get sprites(): IRoomObjectSprite[] -{ - const sprites = super.sprites; - - for(const sprite of sprites) + + public update(geometry: any, time: number, update: boolean, skipUpdate: boolean): void { - if(!sprite) continue; - - if(sprite.name === this._badgeAssetNameNormalScale || sprite.name === this._badgeAssetNameSmallScale) + super.update(geometry, time, update, skipUpdate); + + // 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; + } + + 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(); + } + } + } + } } } - - return sprites; -} protected updateModel(scale: number): boolean { @@ -49,6 +89,8 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali this._badgeAssetNameNormalScale = ''; this._badgeAssetNameSmallScale = ''; this._badgeVisibleInState = -1; + this._gifFrames = null; + this._frameDelays = null; return needsUpdate; } @@ -62,6 +104,11 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali const visibleInState = this.object.model.getValue(RoomObjectVariable.FURNITURE_BADGE_VISIBLE_IN_STATE); this._badgeVisibleInState = isNaN(visibleInState) ? -1 : visibleInState; + this._gifFrames = null; + this._frameDelays = null; + this._currentFrame = 0; + this._lastFrameTime = 0; + this.addBadgeToAssetCollection(badgeId); const layerId = FurnitureBadgeDisplayVisualization.BADGE_LAYER_ID; @@ -151,33 +198,102 @@ export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisuali return offset; } - private addBadgeToAssetCollection(badgeId: string): void - { - const sessionDataManager = GetSessionDataManager(); + private async addBadgeToAssetCollection(badgeId: string): Promise + { + const sessionDataManager = GetSessionDataManager(); - let tex = sessionDataManager.getBadgeImage(badgeId); - if (!tex) tex = sessionDataManager.getGroupBadgeImage(badgeId); + let tex = sessionDataManager.getBadgeImage(badgeId); + if (!tex) tex = sessionDataManager.getGroupBadgeImage(badgeId); - if (!tex || !this.asset) return; + if (!tex || !this.asset) return; - const canvas = (tex.source as any).resource as HTMLCanvasElement; - const ctx = canvas.getContext('2d'); - const imageData = ctx.getImageData(0, 0, 1, 1); - const isEmpty = imageData.data[3] === 0; + const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement; + const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true }); + const imageData = ctx.getImageData(0, 0, 1, 1); + const isEmpty = imageData.data[3] === 0; - if (isEmpty) { - const badgeUrl = GetConfiguration().getValue('badge.asset.url', '').replace('%badgename%', badgeId); - - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.onload = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - tex.source.update(); - }; - img.src = badgeUrl; - } + if (isEmpty || !this._gifFrames) + { + const badgeUrl = GetConfiguration().getValue('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]; - this.asset.addAsset(badgeId, tex, true, 0, 0, false, false); - } -} + 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(); + img.crossOrigin = 'anonymous'; + img.onload = () => + { + ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height); + ctx.drawImage(img, 0, 0, badgeCanvas.width, badgeCanvas.height); + tex.source.update(); + }; + img.src = badgeUrl; + } + } + + this.asset.addAsset(badgeId, tex, true, 0, 0, false, false); + } +} \ No newline at end of file diff --git a/packages/room/src/renderer/RoomSpriteCanvas.ts b/packages/room/src/renderer/RoomSpriteCanvas.ts index a5d1e9c..a37a758 100644 --- a/packages/room/src/renderer/RoomSpriteCanvas.ts +++ b/packages/room/src/renderer/RoomSpriteCanvas.ts @@ -455,14 +455,16 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas sortableSprite.x = (spriteX - this._screenOffsetX); sortableSprite.y = (spriteY - this._screenOffsetY); - sortableSprite.z = ((z + sprite.relativeDepth) + (3.7E-11 * count)); + + let calculatedZ = ((z + sprite.relativeDepth) + (3.7E-11 * count)); - // Ensure badge renders on top of furniture const isBadgeSprite = (sprite.tag === 'BADGE'); - - if(isBadgeSprite) { - sortableSprite.z = -999; - } + if(isBadgeSprite) + { + calculatedZ = calculatedZ + 0.001; + } + + sortableSprite.z = calculatedZ; spriteCount++; count++; diff --git a/packages/room/src/renderer/utils/SortableSprite.ts b/packages/room/src/renderer/utils/SortableSprite.ts index 4f6e2c3..f46c690 100644 --- a/packages/room/src/renderer/utils/SortableSprite.ts +++ b/packages/room/src/renderer/utils/SortableSprite.ts @@ -10,6 +10,8 @@ export class SortableSprite implements ISortableSprite private _x: number; private _y: number; private _z: number; + private _isBadge: boolean; + private _isAvatar: boolean; constructor() { @@ -19,12 +21,16 @@ export class SortableSprite implements ISortableSprite this._x = 0; this._y = 0; this._z = 0; + this._isBadge = false; + this._isAvatar = false; } public dispose(): void { this._z = -(SortableSprite.Z_INFINITY); this._sprite = null; + this._isBadge = false; + this._isAvatar = false; } public get name(): string @@ -76,4 +82,24 @@ export class SortableSprite implements ISortableSprite { 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; + } +} \ No newline at end of file