From f57dae4cf77f3fa67543aa1701400ad1fd41fa6b Mon Sep 17 00:00:00 2001 From: LostMyCode <63048878+LostMyCode@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:45:30 +0000 Subject: [PATCH 1/3] Refactor sprite rendering to use direct pixel buffer manipulation and PIXI.Texture.fromBuffer - RS_Sprite, WrappedAnim: Remove CanvasManager dependency and drawPixel/drawBlendPixel indirection. Instead, generate frame pixel data in a Uint8ClampedArray and produce PIXI.Textures via `PIXI.Texture.fromBuffer`. - Texture.js: Replace old CanvasManager/canvas-based workflow with direct pixel buffer generation and rendering. Store/calculate pixel data for each frame and use it to create textures. - Update shadow and outline alpha values to 0-255 scale for both RS_Sprite and WrappedAnim. - All pixel writing/drawing and blending is now handled manually in buffer, improving performance and reliability and enabling easier integration with PIXI. --- engine/RS_Sprite.js | 112 +++++++++------ engine/WrappedAnim.js | 94 +++++++------ game/models/Texture.js | 307 ++++++++++++++++++++++------------------- 3 files changed, 286 insertions(+), 227 deletions(-) diff --git a/engine/RS_Sprite.js b/engine/RS_Sprite.js index d13a8c5..69cc8a9 100644 --- a/engine/RS_Sprite.js +++ b/engine/RS_Sprite.js @@ -4,10 +4,10 @@ import BufferReader, { TYPE_DEF } from "../utils/BufferReader"; import { PUT_NORMAL, SPRITE_BPP16, SPRITE_BPP8 } from "./DrawH"; import { REGSDHEADER, SDHEADER } from "./ImageH"; import Rect from "./Rect"; -import CanvasManager from "../utils/CanvasManager"; import { getRGBA15bit } from "../utils/RedStoneRandom"; -const canvasManager = new CanvasManager; +const SHADOW_PIXEL_DATA = [7, 7, 7, 128]; // Alpha changed to 0-255 scale +const OUTLINE_PIXEL_DATA = [1, 1, 1, 255]; export default class RS_Sprite { constructor() { @@ -55,16 +55,6 @@ export default class RS_Sprite { this.height = 0; } - // getMaxSize() { - // this.maxSpriteWidth = 0; - // this.maxSpriteHeight = 0; - - // this.maxShadowWidth = 0; - // this.maxShadowHeight = 0; - - - // } - _load(buffer, loadPalette) { const reader = new BufferReader(buffer); @@ -211,8 +201,7 @@ export default class RS_Sprite { if (!this._8Sprite && !this._16Sprite) return; - const canvas = this.getCanvas(index); - const texture = PIXI.Texture.from(canvas); + const texture = this.getPixiTexture(index); const sprite = new PIXI.Sprite(texture); sprite.position.set(x, y); @@ -228,8 +217,7 @@ export default class RS_Sprite { if (!this._8Sprite && !this._16Sprite) return; - const canvas = this.getCanvas(index); - const texture = PIXI.Texture.from(canvas); + const texture = this.getPixiTexture(index); const sprite = pixiSprite; sprite.texture = texture; @@ -357,21 +345,74 @@ export default class RS_Sprite { return { width, height, left, top, dataOffset: offset }; } - getCanvas(frame, type = "body") { - return this.frameCache[type][frame] || this.createFrameCanvas(frame, type); + getPixiTexture(frame, type = "body") { + return this.frameCache[type][frame] || this.createFrameTexture(frame, type); } - createFrameCanvas(frame, type = "body") { + createFrameTexture(frame, type = "body") { + const { pixelDataBuffer, width, height } = this.createFramePixelData(frame, type); + const texture = PIXI.Texture.fromBuffer(pixelDataBuffer, width, height); + + this.frameCache[type][frame] = texture; + + return texture; + } + + createFramePixelData(frame, type = "body") { const { dataOffset, width, height } = this.getFrame(frame, type); const reader = this.getSpriteData(type); - const _drawPixel = canvasManager.drawPixel; - const _getRGB = getRGBA15bit; const isUseOpacity = false; // tmp; let unityCount, unityWidth, w, h, colorReference, colorData1, colorData2; reader.offset = dataOffset + 8; - canvasManager.resize(width, height); + const pixelDataBuffer = new Uint8ClampedArray(width * height * 4); + + const drawPixel = (x, y, rgba) => { + if (x < 0 || x >= width || y < 0 || y >= height) return; + const idx = (y * width + x) * 4; + pixelDataBuffer[idx] = rgba[0]; + pixelDataBuffer[idx + 1] = rgba[1]; + pixelDataBuffer[idx + 2] = rgba[2]; + pixelDataBuffer[idx + 3] = rgba[3]; + }; + + const drawBlendPixel = (x, y, rgba) => { + if (x < 0 || x >= width || y < 0 || y >= height) return; + const idx = (y * width + x) * 4; + const src_r = rgba[0]; + const src_g = rgba[1]; + const src_b = rgba[2]; + const src_a = rgba[3] / 255; + + if (src_a === 1) { + drawPixel(x, y, rgba); + return; + } + + const dst_r = pixelDataBuffer[idx]; + const dst_g = pixelDataBuffer[idx + 1]; + const dst_b = pixelDataBuffer[idx + 2]; + const dst_a = pixelDataBuffer[idx + 3] / 255; + + const out_a = src_a + dst_a * (1 - src_a); + if (out_a === 0) { + pixelDataBuffer[idx] = 0; + pixelDataBuffer[idx + 1] = 0; + pixelDataBuffer[idx + 2] = 0; + pixelDataBuffer[idx + 3] = 0; + return; + } + + const out_r = (src_r * src_a + dst_r * dst_a * (1 - src_a)) / out_a; + const out_g = (src_g * src_a + dst_g * dst_a * (1 - src_a)) / out_a; + const out_b = (src_b * src_a + dst_b * dst_a * (1 - src_a)) / out_a; + + pixelDataBuffer[idx] = out_r; + pixelDataBuffer[idx + 1] = out_g; + pixelDataBuffer[idx + 2] = out_b; + pixelDataBuffer[idx + 3] = out_a * 255; + }; if (type === "body") { if (this.bpp === SPRITE_BPP16) { @@ -387,12 +428,7 @@ export default class RS_Sprite { colorData2 = reader.readUInt8(); colorData1 = reader.readUInt8(); - _drawPixel.call( - canvasManager, - w, - h, - _getRGB.call(this, colorData1, colorData2, isUseOpacity) - ) + drawPixel(w, h, getRGBA15bit(colorData1, colorData2, isUseOpacity)); w++; } @@ -412,12 +448,7 @@ export default class RS_Sprite { colorData1 = this.plt[colorReference * 2 + 1]; colorData2 = this.plt[colorReference * 2]; - _drawPixel.call( - canvasManager, - w, - h, - _getRGB.call(this, colorData1, colorData2, isUseOpacity) - ) + drawPixel(w, h, getRGBA15bit(colorData1, colorData2, isUseOpacity)); w++; } @@ -437,22 +468,13 @@ export default class RS_Sprite { unityWidth = reader.readUInt8(); while (unityWidth--) { - canvasManager.drawBlendPixel(w, h, pixelData); - + drawBlendPixel(w, h, pixelData); w++; } } } } - canvasManager.update(); - - const canvas = canvasManager.canvas; - - canvasManager.reset(); - - this.frameCache[type][frame] = canvas; - - return canvas; + return { pixelDataBuffer, width, height }; } } \ No newline at end of file diff --git a/engine/WrappedAnim.js b/engine/WrappedAnim.js index afbc3d4..e6827b0 100644 --- a/engine/WrappedAnim.js +++ b/engine/WrappedAnim.js @@ -2,17 +2,15 @@ import * as PIXI from "pixi.js"; import Texture from "../game/models/Texture"; import BufferReader from "../utils/BufferReader"; -import CanvasManager from "../utils/CanvasManager"; import { getRGBA15bit } from "../utils/RedStoneRandom"; import Anim from "./Anim"; -const SHADOW_PIXEL_DATA = [7, 7, 7, 0x80]; -const OUTLINE_PIXEL_DATA = [1, 1, 1, 0xff]; +const SHADOW_PIXEL_DATA = [7, 7, 7, 128]; +const OUTLINE_PIXEL_DATA = [1, 1, 1, 255]; export default class WrappedAnim extends Anim { constructor() { super(); - this.canvasManager = new CanvasManager; this.frameCache = { body: {}, shadow: {} @@ -62,25 +60,61 @@ export default class WrappedAnim extends Anim { return { width, height, left, top, dataOffset: offset }; } - getCanvas(frame, type = "body") { - if (this.frameCache[type][frame]?.canvas) return this.frameCache[type][frame].canvas; - - const canvas = this.drawCanvas(frame, type); - - return canvas; - } - - drawCanvas(frame, type = "body") { + createFramePixelData(frame, type = "body") { const { dataOffset, width, height } = this.getFrame(frame, type); const reader = this.getSpriteData(type); - const _drawPixel = this.canvasManager.drawPixel; - const _getRGB = getRGBA15bit; const isUseOpacity = false; // tmp; let unityCount, unityWidth, w, h, colorReference, colorData1, colorData2; reader.offset = dataOffset + 8; - this.canvasManager.resize(width, height); + const pixelDataBuffer = new Uint8ClampedArray(width * height * 4); + + const drawPixel = (x, y, rgba) => { + if (x < 0 || x >= width || y < 0 || y >= height) return; + const idx = (y * width + x) * 4; + pixelDataBuffer[idx] = rgba[0]; + pixelDataBuffer[idx + 1] = rgba[1]; + pixelDataBuffer[idx + 2] = rgba[2]; + pixelDataBuffer[idx + 3] = rgba[3]; + }; + + const drawBlendPixel = (x, y, rgba) => { + if (x < 0 || x >= width || y < 0 || y >= height) return; + const idx = (y * width + x) * 4; + const src_r = rgba[0]; + const src_g = rgba[1]; + const src_b = rgba[2]; + const src_a = rgba[3] / 255; + + if (src_a === 1) { + drawPixel(x, y, rgba); + return; + } + + const dst_r = pixelDataBuffer[idx]; + const dst_g = pixelDataBuffer[idx + 1]; + const dst_b = pixelDataBuffer[idx + 2]; + const dst_a = pixelDataBuffer[idx + 3] / 255; + + const out_a = src_a + dst_a * (1 - src_a); + if (out_a === 0) { + pixelDataBuffer[idx] = 0; + pixelDataBuffer[idx + 1] = 0; + pixelDataBuffer[idx + 2] = 0; + pixelDataBuffer[idx + 3] = 0; + return; + } + + const out_r = (src_r * src_a + dst_r * dst_a * (1 - src_a)) / out_a; + const out_g = (src_g * src_a + dst_g * dst_a * (1 - src_a)) / out_a; + const out_b = (src_b * src_a + dst_b * dst_a * (1 - src_a)) / out_a; + + pixelDataBuffer[idx] = out_r; + pixelDataBuffer[idx + 1] = out_g; + pixelDataBuffer[idx + 2] = out_b; + pixelDataBuffer[idx + 3] = out_a * 255; + }; if (type === "body") { for (h = 0; h < height; h++) { @@ -96,12 +130,7 @@ export default class WrappedAnim extends Anim { colorData1 = this.sprite.plt[colorReference * 2 + 1]; colorData2 = this.sprite.plt[colorReference * 2]; - _drawPixel.call( - this.canvasManager, - w, - h, - _getRGB.call(this, colorData1, colorData2, isUseOpacity) - ) + drawPixel(w, h, getRGBA15bit(colorData1, colorData2, isUseOpacity)); w++; } @@ -119,34 +148,21 @@ export default class WrappedAnim extends Anim { unityWidth = reader.readUInt8(); while (unityWidth--) { - this.canvasManager.drawBlendPixel(w, h, pixelData); - + drawBlendPixel(w, h, pixelData); w++; } } } } - this.canvasManager.update(); - - const canvas = this.canvasManager.canvas; - - this.canvasManager.reset(); - - if (this.frameCache[type][frame]) { - this.frameCache[type][frame].canvas = canvas; - } else { - this.frameCache[type][frame] = { canvas }; - } - - return canvas; + return { pixelDataBuffer, width, height }; } getPixiTexture(frame, type = "body") { if (this.frameCache[type][frame]?.texture) return this.frameCache[type][frame].texture; - const canvas = this.getCanvas(frame, type); - const texture = PIXI.Texture.from(canvas); + const { pixelDataBuffer, width, height } = this.createFramePixelData(frame, type); + const texture = PIXI.Texture.fromBuffer(pixelDataBuffer, width, height); if (this.frameCache[type][frame]) { this.frameCache[type][frame].texture = texture; diff --git a/game/models/Texture.js b/game/models/Texture.js index 03e2de8..575005d 100644 --- a/game/models/Texture.js +++ b/game/models/Texture.js @@ -756,62 +756,109 @@ class Texture { ); } - createTextureCanvases(type = "body") { - for (let i = 0; i < this.frameCount; i++) { - this.draw(i, type); + getCanvas(frameIndex, type = "body") { + const shape = this.shape[type]; + + if (shape.canvas[frameIndex]) { + return shape.canvas[frameIndex]; } - return this.shape[type].canvas; - } - createTextureCanvas(frameIndex, type) { - this.draw(frameIndex, type); - return this.shape[type].canvas[frameIndex]; - } + const width = this.getIsUseMargin() ? this.maxSizeInfo.outerWidth : shape.width[frameIndex]; + const height = this.getIsUseMargin() ? this.maxSizeInfo.outerHeight : shape.height[frameIndex]; - getCanvas(frameIndex, type = "body") { - if (!this.isAnalyzed) this.analyze(); - if (!this.shape[type].canvas[frameIndex]) this.createTextureCanvas(frameIndex, type); - return this.shape[type].canvas[frameIndex].canvas; + const app = new PIXI.Application({ + width, + height, + backgroundColor: 0x1099bb, + antialias: true + }); + + const texture = this.getPixiTexture(frameIndex, type); + const sprite = new PIXI.Sprite(texture); + const renderTexture = PIXI.RenderTexture.create({ + width: texture.width, + height: texture.height, + resolution: texture.resolution || 1 + }); + + app.renderer.render(sprite, { renderTexture }); + + const canvas = document.createElement('canvas'); + canvas.width = texture.width; + canvas.height = texture.height; + const ctx = canvas.getContext('2d'); + + const pixels = app.renderer.extract.pixels(renderTexture); + const imageData = ctx.createImageData(texture.width, texture.height); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + + renderTexture.destroy(); + sprite.destroy(); + + shape.canvas[frameIndex] = canvas; + + return canvas; } getPixiTexture(frameIndex, type = "body") { if (!this.isAnalyzed) this.analyze(); - if (!this.shape[type].canvas[frameIndex]) this.createTextureCanvas(frameIndex, type); - const texture = PIXI.Texture.from(this.shape[type].canvas[frameIndex].canvas); - return texture; - } - draw(frameIndex, type = "body") { - this.drawFrame = frameIndex; - this.drawShapeType = type; - const canvasManager = this.shape[type].canvas[frameIndex] || new CanvasManager(); - this.shape[type].canvas[frameIndex] = canvasManager; + const shape = this.shape[type]; + + // Return from cache if available + if (shape.textures[frameIndex]) { + return shape.textures[frameIndex]; + } - this.resizeCanvas(); + // Generate pixel data if not available + if (!shape.pixelData[frameIndex]) { + this.draw(frameIndex, type); + } + + const pixelData = shape.pixelData[frameIndex]; + if (!pixelData) { + // Return empty texture for empty frames + return PIXI.Texture.EMPTY; + } - type === "body" && this.drawBody(); - type === "shadow" && this.drawShadow(); - type === "outline" && this.drawOutline(); + const width = this.getIsUseMargin() ? this.maxSizeInfo.outerWidth : shape.width[frameIndex]; + const height = this.getIsUseMargin() ? this.maxSizeInfo.outerHeight : shape.height[frameIndex]; - // update - canvasManager.update(); - } + const texture = PIXI.Texture.fromBuffer(pixelData, width, height); + shape.textures[frameIndex] = texture; // Cache the texture - redraw() { - this.draw(this.drawFrame); + return texture; } - resizeCanvas() { - const canvasManager = this.shape[this.drawShapeType].canvas[this.drawFrame]; + draw(frameIndex, type = "body") { + const shape = this.shape[type]; + const width = this.getIsUseMargin() ? this.maxSizeInfo.outerWidth : shape.width[frameIndex]; + const height = this.getIsUseMargin() ? this.maxSizeInfo.outerHeight : shape.height[frameIndex]; - if (false/* this.getIsUseMargin() */) { - canvasManager.resize(this.maxSizeInfo.outerWidth, this.maxSizeInfo.outerHeight); - } else { - canvasManager.resize(this.shape[this.drawShapeType].width[this.drawFrame], this.shape[this.drawShapeType].height[this.drawFrame]) + if (width === 0 || height === 0) { + shape.pixelData[frameIndex] = null; + return; + } + + const pixelData = new Uint8ClampedArray(width * height * 4); + + switch (type) { + case "body": + this.drawBody(frameIndex, pixelData, width, height); + break; + case "shadow": + this.drawShadow(frameIndex, pixelData, width, height); + break; + case "outline": + this.drawOutline(frameIndex, pixelData, width, height); + break; } + + shape.pixelData[frameIndex] = pixelData; } - drawBody() { + drawBody(frameIndex, pixelData, canvasWidth, canvasHeight) { if (!this.getIsShowBody()) { return; } @@ -819,38 +866,29 @@ class Texture { switch (this.fileExtension) { case "smi": case "mpr": - this.drawBodySmi(); + this.drawBodySmi(frameIndex, pixelData, canvasWidth, canvasHeight); break; default: if (this.isHighColor) { - this.drawBodyHighColor(); + this.drawBodyHighColor(frameIndex, pixelData, canvasWidth, canvasHeight); } else { - this.drawBodyLowColor(); + this.drawBodyLowColor(frameIndex, pixelData, canvasWidth, canvasHeight); } } } - drawBodySmi() { + drawBodySmi(frameIndex, pixelData, canvasWidth, canvasHeight) { const reader = this.reader; - const canvasManager = this.shape.body.canvas[this.drawFrame]; - - var isUseOpacity = this.getIsUseOpacity(); - var isUseMargin = this.getIsUseMargin(); - var isUseBackground = this.getIsUseBackground(); + const isUseOpacity = this.getIsUseOpacity(); + const isUseMargin = this.getIsUseMargin(); - var startOffset = this.shape.body.startOffset[this.drawFrame]; - var width = this.shape.body.width[this.drawFrame]; - var height = this.shape.body.height[this.drawFrame]; - var left = isUseMargin ? this.maxSizeInfo.left - this.shape.body.left[this.drawFrame] : 0; - var top = isUseMargin ? this.maxSizeInfo.top - this.shape.body.top[this.drawFrame] : 0; + const startOffset = this.shape.body.startOffset[frameIndex]; + const width = this.shape.body.width[frameIndex]; + const height = this.shape.body.height[frameIndex]; + const left = isUseMargin ? this.maxSizeInfo.left - this.shape.body.left[frameIndex] : 0; + const top = isUseMargin ? this.maxSizeInfo.top - this.shape.body.top[frameIndex] : 0; - var w, h, colorData1, colorData2, _drawPixel; - - if (isUseBackground && isUseOpacity) { - _drawPixel = canvasManager.drawBlendPixel; - } else { - _drawPixel = canvasManager.drawPixel; - } + let w, h, colorData1, colorData2; reader.offset = startOffset; @@ -863,37 +901,28 @@ class Texture { colorData2 = reader.readUInt8(); colorData1 = reader.readUInt8(); - _drawPixel.call( - canvasManager, - left + w, - top + h, - getRGBA15bit(colorData1, colorData2, isUseOpacity) - ) + const [r, g, b, a] = getRGBA15bit(colorData1, colorData2, isUseOpacity); + const pixelIndex = ((top + h) * canvasWidth + (left + w)) * 4; + + pixelData[pixelIndex] = r; + pixelData[pixelIndex + 1] = g; + pixelData[pixelIndex + 2] = b; + pixelData[pixelIndex + 3] = a; } } } - drawBodyHighColor() { + drawBodyHighColor(frameIndex, pixelData, canvasWidth, canvasHeight) { const reader = this.reader; - const canvasManager = this.shape.body.canvas[this.drawFrame]; - - var isUseOpacity = this.getIsUseOpacity(); - var isUseMargin = this.getIsUseMargin(); - var isUseBackground = this.getIsUseBackground(); + const isUseOpacity = this.getIsUseOpacity(); + const isUseMargin = this.getIsUseMargin(); - var startOffset = this.shape.body.startOffset[this.drawFrame]; - var width = this.shape.body.width[this.drawFrame]; - var height = this.shape.body.height[this.drawFrame]; - var left = isUseMargin ? this.maxSizeInfo.left - this.shape.body.left[this.drawFrame] : 0; - var top = isUseMargin ? this.maxSizeInfo.top - this.shape.body.top[this.drawFrame] : 0; + const startOffset = this.shape.body.startOffset[frameIndex]; + const height = this.shape.body.height[frameIndex]; + const left = isUseMargin ? this.maxSizeInfo.left - this.shape.body.left[frameIndex] : 0; + const top = isUseMargin ? this.maxSizeInfo.top - this.shape.body.top[frameIndex] : 0; - var _drawPixel, w, h, unityCount, unityWidth, colorData1, colorData2; - - if (isUseBackground && isUseOpacity) { - _drawPixel = canvasManager.drawBlendPixel; - } else { - _drawPixel = canvasManager.drawPixel; - } + let w, h, unityCount, unityWidth, colorData1, colorData2; reader.offset = startOffset + 8; // Skip shape info data @@ -909,45 +938,36 @@ class Texture { colorData2 = reader.readUInt8(); colorData1 = reader.readUInt8(); - _drawPixel.call( - canvasManager, - left + w, - top + h, - getRGBA15bit(colorData1, colorData2, isUseOpacity) - ) + const [r, g, b, a] = getRGBA15bit(colorData1, colorData2, isUseOpacity); + const pixelIndex = ((top + h) * canvasWidth + (left + w)) * 4; + + pixelData[pixelIndex] = r; + pixelData[pixelIndex + 1] = g; + pixelData[pixelIndex + 2] = b; + pixelData[pixelIndex + 3] = a; w++; } } } } - drawBodyLowColor() { + drawBodyLowColor(frameIndex, pixelData, canvasWidth, canvasHeight) { const reader = this.reader; - const canvasManager = this.shape.body.canvas[this.drawFrame]; + const isUseOpacity = this.getIsUseOpacity(); + const isUseMargin = this.getIsUseMargin(); + const isUsePalette = this.getIsUsePalette(); - var isUseOpacity = this.getIsUseOpacity(); - var isUseMargin = this.getIsUseMargin(); - var isUseBackground = this.getIsUseBackground(); - var isUsePalette = this.getIsUsePalette(); - - var startOffset = this.shape.body.startOffset[this.drawFrame]; - var width = this.shape.body.width[this.drawFrame]; - var height = this.shape.body.height[this.drawFrame]; - var left = isUseMargin ? this.maxSizeInfo.left - this.shape.body.left[this.drawFrame] : 0; - var top = isUseMargin ? this.maxSizeInfo.top - this.shape.body.top[this.drawFrame] : 0; + const startOffset = this.shape.body.startOffset[frameIndex]; + const height = this.shape.body.height[frameIndex]; + const left = isUseMargin ? this.maxSizeInfo.left - this.shape.body.left[frameIndex] : 0; + const top = isUseMargin ? this.maxSizeInfo.top - this.shape.body.top[frameIndex] : 0; // var paletteFile = _App.fileManager.paletteFile; var paletteFile = null; var paletteNumber = this.selectedPaletteNumber; var paletteData = (isUsePalette && paletteFile) ? paletteFile.paletteData[paletteNumber] : this.paletteData; - var _drawPixel, _getRGB, w, h, unityCount, unityWidth, colorReference, colorData1, colorData2; - - if (isUseBackground && isUseOpacity) { - _drawPixel = canvasManager.drawBlendPixel; - } else { - _drawPixel = canvasManager.drawPixel; - } + var _getRGB, w, h, unityCount, unityWidth, colorReference, colorData1, colorData2; if (isUsePalette && paletteFile && paletteFile.is16bitColor) { _getRGB = getRGBA16bit; @@ -970,33 +990,31 @@ class Texture { colorData1 = paletteData[colorReference * 2 + 1]; colorData2 = paletteData[colorReference * 2]; - _drawPixel.call( - canvasManager, - left + w, - top + h, - _getRGB.call(this, colorData1, colorData2, isUseOpacity) - ) + const [r, g, b, a] = _getRGB.call(this, colorData1, colorData2, isUseOpacity); + const pixelIndex = ((top + h) * canvasWidth + (left + w)) * 4; + + pixelData[pixelIndex] = r; + pixelData[pixelIndex + 1] = g; + pixelData[pixelIndex + 2] = b; + pixelData[pixelIndex + 3] = a; w++; } } } } - drawShadow() { + drawShadow(frameIndex, pixelData, canvasWidth, canvasHeight) { if (!this.isExistShadow) { return; } const reader = this.reader; - const canvasManager = this.shape.shadow.canvas[this.drawFrame]; - - var startOffset = this.shape.shadow.startOffset[this.drawFrame]; - var width = this.shape.shadow.width[this.drawFrame]; - var height = this.shape.shadow.height[this.drawFrame]; - var left = 0; - var top = 0; + const startOffset = this.shape.shadow.startOffset[frameIndex]; + const height = this.shape.shadow.height[frameIndex]; + const left = 0; + const top = 0; - var w, h, unityCount, unityWidth; + let w, h, unityCount, unityWidth; reader.offset = startOffset + 8; // Skip shape info data @@ -1009,18 +1027,20 @@ class Texture { unityWidth = reader.readUInt8(); while (unityWidth--) { - canvasManager.drawBlendPixel( - left + w, - top + h, - SHADOW_PIXEL_DATA - ); + const [r, g, b, a] = SHADOW_PIXEL_DATA; + const pixelIndex = ((top + h) * canvasWidth + (left + w)) * 4; + + pixelData[pixelIndex] = r; + pixelData[pixelIndex + 1] = g; + pixelData[pixelIndex + 2] = b; + pixelData[pixelIndex + 3] = a; w++; } } } } - drawOutline() { + drawOutline(frameIndex, pixelData, canvasWidth, canvasHeight) { if (!this.isExistOutline || !this.getIsShowOutline()) { return; } @@ -1030,15 +1050,12 @@ class Texture { } const reader = this.reader; - const canvasManager = this.shape.outline.canvas[this.drawFrame]; - - var startOffset = this.shape.outline.startOffset[this.drawFrame]; - var width = this.shape.outline.width[this.drawFrame]; - var height = this.shape.outline.height[this.drawFrame]; - var left = this.maxSizeInfo.left - this.shape.outline.left[this.drawFrame]; - var top = this.maxSizeInfo.top - this.shape.outline.top[this.drawFrame]; + const startOffset = this.shape.outline.startOffset[frameIndex]; + const height = this.shape.outline.height[frameIndex]; + const left = this.maxSizeInfo.left - this.shape.outline.left[frameIndex]; + const top = this.maxSizeInfo.top - this.shape.outline.top[frameIndex]; - var w, h, unityCount, unityWidth; + let w, h, unityCount, unityWidth; this.stream.seek(startOffset + 8); reader.offset = startOffset + 8; // Skip shape info data @@ -1052,11 +1069,13 @@ class Texture { unityWidth = reader.readUInt8(); while (unityWidth--) { - canvasManager.drawBlendPixel( - left + w, - top + h, - OUTLINE_PIXEL_DATA - ); + const [r, g, b, a] = OUTLINE_PIXEL_DATA; + const pixelIndex = ((top + h) * canvasWidth + (left + w)) * 4; + + pixelData[pixelIndex] = r; + pixelData[pixelIndex + 1] = g; + pixelData[pixelIndex + 2] = b; + pixelData[pixelIndex + 3] = a; w++; } } @@ -1096,7 +1115,9 @@ class EffectShape { this.height = []; this.left = []; this.top = []; - this.canvas = []; + this.pixelData = []; // For storing raw pixel data + this.textures = []; // For caching PIXI.Texture + this.canvas = []; // For caching HTMLCanvasElement } } From ad00a4e55d1e02c6ef4f52eb4e3742ac46b59770 Mon Sep 17 00:00:00 2001 From: LostMyCode <63048878+LostMyCode@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:21:10 +0000 Subject: [PATCH 2/3] Fix memory leak by destroying PIXI.Application after rendering to canvas in Texture.getCanvas --- game/models/Texture.js | 1 + 1 file changed, 1 insertion(+) diff --git a/game/models/Texture.js b/game/models/Texture.js index 575005d..e40bc94 100644 --- a/game/models/Texture.js +++ b/game/models/Texture.js @@ -795,6 +795,7 @@ class Texture { renderTexture.destroy(); sprite.destroy(); + app.destroy(); shape.canvas[frameIndex] = canvas; From 03045904e9815e399e9525e06f04a02491fe7db1 Mon Sep 17 00:00:00 2001 From: LostMyCode <63048878+LostMyCode@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:18:55 +0000 Subject: [PATCH 3/3] Delete CanvasManager.js --- game/models/Texture.js | 1 - utils/CanvasManager.js | 111 ----------------------------------------- 2 files changed, 112 deletions(-) delete mode 100644 utils/CanvasManager.js diff --git a/game/models/Texture.js b/game/models/Texture.js index e40bc94..b74d7c6 100644 --- a/game/models/Texture.js +++ b/game/models/Texture.js @@ -1,6 +1,5 @@ import * as PIXI from "pixi.js"; import BufferReader from "../../utils/BufferReader"; -import CanvasManager from "../../utils/CanvasManager"; import { getRGBA15bit, getRGBA16bit, logger } from "../../utils/RedStoneRandom"; // Special Thanks: 今日のこぅくん diff --git a/utils/CanvasManager.js b/utils/CanvasManager.js deleted file mode 100644 index f6e07c7..0000000 --- a/utils/CanvasManager.js +++ /dev/null @@ -1,111 +0,0 @@ -import { logger } from "./RedStoneRandom"; - -const CANVAS_DEFAULT_WIDTH = 150; -const CANVAS_DEFAULT_HEIGHT = 150; - -class CanvasManager { - constructor() { - this.canvas = document.createElement("canvas"); - this.context = this.canvas.getContext("2d"); - - this.width = 0; - this.height = 0; - this.imageData = null; - this.backgroundColor = [255, 255, 255, 0xff]; // rgba - this.isErrorOccurred = false; - } - - reset() { - this.canvas = document.createElement("canvas"); - this.context = this.canvas.getContext("2d"); - - this.width = 0; - this.height = 0; - this.imageData = null; - - this.isErrorOccurred = false; - } - - initialize() { - this.resize(CANVAS_DEFAULT_WIDTH, CANVAS_DEFAULT_HEIGHT); - } - - getDataURL() { - return this.canvas.toDataURL("image/png"); - } - - resize(width, height) { - width = width === 0 ? 1 : width; - height = height === 0 ? 1 : height; - - if (!width || !height || width > 0xffff || height > 0xffff) { - return; - } - - if (width === this.width && height === this.height) { - return; - } - - this.width = width; - this.height = height; - this.canvas.width = width; - this.canvas.height = height; - this.imageData = this.context.createImageData(this.width, this.height); - } - - drawPixel(x, y, rgba) { - if (x < 0 || this.width <= x || y < 0 || this.height <= y) { - this.isErrorOccurred = true; - return - } - - const pixelData = this.imageData.data; - const index = (x + y * this.width) * 4; - - pixelData[index + 0] = rgba[0]; - pixelData[index + 1] = rgba[1]; - pixelData[index + 2] = rgba[2]; - pixelData[index + 3] = rgba[3]; - } - - drawBlendPixel(x, y, rgba) { - if (x < 0 || this.width <= x || y < 0 || this.height <= y) { - this.isErrorOccurred = true; - return - } - - var pixelData = this.imageData.data; - var index = (x + y * this.width) * 4; - var oldR = pixelData[index + 0]; - var oldG = pixelData[index + 1]; - var oldB = pixelData[index + 2]; - var oldA = pixelData[index + 3]; - var opacity = rgba[3] / 255; - - pixelData[index + 0] = oldR * (1 - opacity) + (rgba[0] * opacity); - pixelData[index + 1] = oldG * (1 - opacity) + (rgba[1] * opacity); - pixelData[index + 2] = oldB * (1 - opacity) + (rgba[2] * opacity); - pixelData[index + 3] = Math.min(0xff, oldA + rgba[3]); - } - - update() { - this.context.putImageData(this.imageData, 0, 0); - - if (this.isErrorOccurred) { - logger.error("error_draw") - this.isErrorOccurred = false; - } - } - - clear() { - if (!this.imageData) return; - var pixelData = this.imageData.data; - var i = pixelData.length; - - while (i--) { - pixelData[i] = 0; - } - } -} - -export default CanvasManager; \ No newline at end of file