From 76541dd31dc8daf489c70b69c2402cb2e94b4e5e Mon Sep 17 00:00:00 2001 From: modeco80 Date: Tue, 9 Jul 2024 22:42:12 -0400 Subject: [PATCH] msagent.js: fixed bitmap drawing Now everything is top down, correctly colored, as you'd expect! Woohoo! --- msagent.js/src/agent.ts | 119 ++++++++++++++++++++++----------- msagent.js/src/structs/core.ts | 19 ++++-- webapp/src/ts/testbed.ts | 3 +- 3 files changed, 94 insertions(+), 47 deletions(-) diff --git a/msagent.js/src/agent.ts b/msagent.js/src/agent.ts index a128a2d..e96ca54 100644 --- a/msagent.js/src/agent.ts +++ b/msagent.js/src/agent.ts @@ -1,45 +1,86 @@ -import { AcsData } from "./character.js"; -import { AcsAnimationFrameInfo } from "./structs/animation.js"; +import { BufferStream, SeekDir } from './buffer.js'; +import { AcsData } from './character.js'; +import { AcsAnimationFrameInfo } from './structs/animation.js'; +import { AcsImageEntry } from './structs/image.js'; + +// probably should be in a utility module +function dwAlign(off: number): number { + let ul = off >>> 0; + + ul += 3; + ul >>= 2; + ul <<= 2; + return ul; +} export class Agent { - private data: AcsData; - private cnv: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - constructor(data: AcsData) { - this.data = data; - this.cnv = document.createElement("canvas"); - this.ctx = this.cnv.getContext("2d")!; - this.cnv.width = data.characterInfo.charWidth; - this.cnv.height = data.characterInfo.charHeight; - this.cnv.style.position = "fixed"; - this.hide(); - this.renderFrame(this.data.animInfo[0].animationData.frameInfo[0]); - } + private data: AcsData; + private cnv: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + constructor(data: AcsData) { + this.data = data; + this.cnv = document.createElement('canvas'); + this.ctx = this.cnv.getContext('2d')!; + this.cnv.width = data.characterInfo.charWidth; + this.cnv.height = data.characterInfo.charHeight; + this.cnv.style.position = 'fixed'; + this.hide(); + this.renderFrame(this.data.animInfo[0].animationData.frameInfo[0]); + } - private renderFrame(frame: AcsAnimationFrameInfo) { - for (const mimg of frame.images) { - const img = this.data.images[mimg.imageIndex]; - let data = this.ctx.createImageData(img.image.width, img.image.height); - for (let i = 0; i < img.image.data.length; i++) { - let px = this.data.characterInfo.palette[img.image.data[i]]; - data.data[(i * 4)] = px.r; - data.data[(i * 4) + 1] = px.g; - data.data[(i * 4) + 2] = px.b; - data.data[(i * 4) + 3] = px.a; - } - this.ctx.putImageData(data, mimg.xOffset, mimg.yOffset); - } - } + private renderFrame(frame: AcsAnimationFrameInfo) { + this.ctx.clearRect(0, 0, this.cnv.width, this.cnv.height); + for (const mimg of frame.images) { + this.drawImage(this.data.images[mimg.imageIndex], mimg.xOffset, mimg.yOffset); + } + } - addToDom(parent: HTMLElement = document.body) { - parent.appendChild(this.cnv); - } + // Draw a single image from the agent's image table. + drawImage(imageEntry: AcsImageEntry, xOffset: number, yOffset: number) { + let rgbaBuffer = new Uint32Array(imageEntry.image.width * imageEntry.image.height); - show() { - this.cnv.style.display = "block"; - } + let buffer = imageEntry.image.data; + let bufStream = new BufferStream(buffer); - hide() { - this.cnv.style.display = "none"; - } -} \ No newline at end of file + let rows = new Array(imageEntry.image.height - 1); + + // Read all the rows bottom-up first. This idiosyncracy is due to the fact + // that the bitmap data is actually formatted to be used as a GDI DIB + // (device-independent bitmap), so it inherits all the strange baggage from that. + for (let y = imageEntry.image.height - 1; y >= 0; --y) { + let row = bufStream.subBuffer(imageEntry.image.width).raw(); + let rowResized = row.slice(0, imageEntry.image.width); + rows[y] = rowResized; + + // Seek to the next DWORD aligned spot to get to the next row. + // For most images this may mean not seeking at all. + bufStream.seek(dwAlign(bufStream.tell()), SeekDir.BEG); + } + + // Next, draw the rows converted to RGBA, top down (so it's drawn as you'd expect) + for (let y = 0; y < imageEntry.image.height - 1; ++y) { + let row = rows[y]; + for (let x = 0; x < imageEntry.image.width; ++x) { + rgbaBuffer[y * imageEntry.image.width + x] = this.data.characterInfo.palette[row[x]].to_rgba(); + } + } + + let data = new ImageData(new Uint8ClampedArray(rgbaBuffer.buffer), imageEntry.image.width, imageEntry.image.height); + this.ctx.putImageData(data, xOffset, yOffset); + } + + addToDom(parent: HTMLElement = document.body) { + parent.appendChild(this.cnv); + } + + show() { + this.cnv.style.display = 'block'; + // TODO: play the show animation + } + + hide() { + // TODO: play the hide animation (then clear the canvas) + // (if not constructing. We can probably just duplicate this one line and put it in the constructor tbh) + this.cnv.style.display = 'none'; + } +} diff --git a/msagent.js/src/structs/core.ts b/msagent.js/src/structs/core.ts index 2d474bc..e69cb50 100644 --- a/msagent.js/src/structs/core.ts +++ b/msagent.js/src/structs/core.ts @@ -91,9 +91,9 @@ export class RGBAColor { b = 0; a = 0; - // Does what it says on the tin, converts to RGBA + // Does what it says on the tin, converts to a packed 32-bit RGBA value to_rgba(): number { - return (this.r << 24) | (this.g << 16) | (this.b << 8) | this.a; + return (this.a << 24) | (this.r << 16) | (this.g << 8) | this.b; } static from_gdi_rgbquad(val: number) { @@ -102,11 +102,18 @@ export class RGBAColor { // Extract individual RGB values from the RGBQUAD // We ignore the last 8 bits because it is always left // as 0x00 or if uncleared, just random garbage. - quad.r = (val & 0xff000000) >> 24; - quad.g = (val & 0x00ff0000) >> 16; - quad.b = (val & 0x0000ff00) >> 8; - quad.a = 255; + // this is wrong + //quad.r = (val & 0xff000000) >> 24; + //quad.g = (val & 0x00ff0000) >> 16; + //quad.b = (val & 0x0000ff00) >> 8; + + quad.r = (val & 0x000000ff); + quad.g = (val & 0x0000ff00) >> 8; + quad.b = (val & 0x00ff0000) >> 16; + + quad.a = 255; + console.log(val, quad); return quad; } diff --git a/webapp/src/ts/testbed.ts b/webapp/src/ts/testbed.ts index d752ab8..f671129 100644 --- a/webapp/src/ts/testbed.ts +++ b/webapp/src/ts/testbed.ts @@ -6,13 +6,12 @@ import * as msagent from "@msagent-chat/msagent.js"; let input = document.getElementById("testbed-input") as HTMLInputElement; input.addEventListener("change", async () => { - - let buffer = await input.files![0].arrayBuffer(); console.log("About to parse character"); let agent = msagent.agentParseCharacterTestbed(new Uint8Array(buffer)); agent.addToDom(document.body); + agent.show(); console.log("parsed character"); })