msagent.js: fixed bitmap drawing

Now everything is top down, correctly colored, as you'd expect! Woohoo!
This commit is contained in:
Lily Tsuru 2024-07-09 22:42:12 -04:00
parent 9f211ec36a
commit 76541dd31d
3 changed files with 94 additions and 47 deletions

View file

@ -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";
}
let rows = new Array<Uint8Array>(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';
}
}

View file

@ -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;
}

View file

@ -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");
})