msagent.js: fixed bitmap drawing
Now everything is top down, correctly colored, as you'd expect! Woohoo!
This commit is contained in:
parent
9f211ec36a
commit
76541dd31d
3 changed files with 94 additions and 47 deletions
|
@ -1,45 +1,86 @@
|
||||||
import { AcsData } from "./character.js";
|
import { BufferStream, SeekDir } from './buffer.js';
|
||||||
import { AcsAnimationFrameInfo } from "./structs/animation.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 {
|
export class Agent {
|
||||||
private data: AcsData;
|
private data: AcsData;
|
||||||
private cnv: HTMLCanvasElement;
|
private cnv: HTMLCanvasElement;
|
||||||
private ctx: CanvasRenderingContext2D;
|
private ctx: CanvasRenderingContext2D;
|
||||||
constructor(data: AcsData) {
|
constructor(data: AcsData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.cnv = document.createElement("canvas");
|
this.cnv = document.createElement('canvas');
|
||||||
this.ctx = this.cnv.getContext("2d")!;
|
this.ctx = this.cnv.getContext('2d')!;
|
||||||
this.cnv.width = data.characterInfo.charWidth;
|
this.cnv.width = data.characterInfo.charWidth;
|
||||||
this.cnv.height = data.characterInfo.charHeight;
|
this.cnv.height = data.characterInfo.charHeight;
|
||||||
this.cnv.style.position = "fixed";
|
this.cnv.style.position = 'fixed';
|
||||||
this.hide();
|
this.hide();
|
||||||
this.renderFrame(this.data.animInfo[0].animationData.frameInfo[0]);
|
this.renderFrame(this.data.animInfo[0].animationData.frameInfo[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderFrame(frame: AcsAnimationFrameInfo) {
|
private renderFrame(frame: AcsAnimationFrameInfo) {
|
||||||
for (const mimg of frame.images) {
|
this.ctx.clearRect(0, 0, this.cnv.width, this.cnv.height);
|
||||||
const img = this.data.images[mimg.imageIndex];
|
for (const mimg of frame.images) {
|
||||||
let data = this.ctx.createImageData(img.image.width, img.image.height);
|
this.drawImage(this.data.images[mimg.imageIndex], mimg.xOffset, mimg.yOffset);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToDom(parent: HTMLElement = document.body) {
|
// Draw a single image from the agent's image table.
|
||||||
parent.appendChild(this.cnv);
|
drawImage(imageEntry: AcsImageEntry, xOffset: number, yOffset: number) {
|
||||||
}
|
let rgbaBuffer = new Uint32Array(imageEntry.image.width * imageEntry.image.height);
|
||||||
|
|
||||||
show() {
|
let buffer = imageEntry.image.data;
|
||||||
this.cnv.style.display = "block";
|
let bufStream = new BufferStream(buffer);
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
let rows = new Array<Uint8Array>(imageEntry.image.height - 1);
|
||||||
this.cnv.style.display = "none";
|
|
||||||
}
|
// 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';
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -91,9 +91,9 @@ export class RGBAColor {
|
||||||
b = 0;
|
b = 0;
|
||||||
a = 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 {
|
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) {
|
static from_gdi_rgbquad(val: number) {
|
||||||
|
@ -102,11 +102,18 @@ export class RGBAColor {
|
||||||
// Extract individual RGB values from the RGBQUAD
|
// Extract individual RGB values from the RGBQUAD
|
||||||
// We ignore the last 8 bits because it is always left
|
// We ignore the last 8 bits because it is always left
|
||||||
// as 0x00 or if uncleared, just random garbage.
|
// 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;
|
return quad;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,12 @@ import * as msagent from "@msagent-chat/msagent.js";
|
||||||
let input = document.getElementById("testbed-input") as HTMLInputElement;
|
let input = document.getElementById("testbed-input") as HTMLInputElement;
|
||||||
|
|
||||||
input.addEventListener("change", async () => {
|
input.addEventListener("change", async () => {
|
||||||
|
|
||||||
|
|
||||||
let buffer = await input.files![0].arrayBuffer();
|
let buffer = await input.files![0].arrayBuffer();
|
||||||
|
|
||||||
console.log("About to parse character");
|
console.log("About to parse character");
|
||||||
let agent = msagent.agentParseCharacterTestbed(new Uint8Array(buffer));
|
let agent = msagent.agentParseCharacterTestbed(new Uint8Array(buffer));
|
||||||
agent.addToDom(document.body);
|
agent.addToDom(document.body);
|
||||||
|
|
||||||
agent.show();
|
agent.show();
|
||||||
console.log("parsed character");
|
console.log("parsed character");
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue