MSAgent-Chat/msagent.js/src/wordballoon.ts
Lily Tsuru c3c0d33e5b msagent.js: add wordballoon code
also initalize msagent.js inside of the webapp
2024-07-02 23:04:45 -04:00

173 lines
4.8 KiB
TypeScript

import { spriteCutSpriteFromSpriteSheet, spriteDraw, spriteDrawRotated, spriteLoadImage } from './sprite';
import { Point, Rect, Size } from './types';
let corner_sprite: HTMLImageElement;
let straight_sprite: HTMLImageElement;
let tip_sprite: HTMLImageElement;
// Call *once* to initalize the wordballoon drawing system.
// Do not call other wordballoon* functions WITHOUT doing so.
export async function wordballoonInit() {
// Load the spritesheet
let sheet = await spriteLoadImage(new URL('../res/wordballoon.png', import.meta.url).toString());
// Cut out the various sprites we need to draw from the sheet.
corner_sprite = await spriteCutSpriteFromSpriteSheet(sheet, {
x: 0,
y: 0,
w: 12,
h: 13
});
straight_sprite = await spriteCutSpriteFromSpriteSheet(sheet, {
x: 12,
y: 0,
w: 12,
h: 13
});
tip_sprite = await spriteCutSpriteFromSpriteSheet(sheet, {
x: 24,
y: 0,
w: 10,
h: 18
});
}
// This function returns a rect which is the usable inner contents of the box.
export function wordballoonDraw(ctx: CanvasRenderingContext2D, at: Point, size: Size): Rect {
// Snap the size to a clean 12x12 system,
// so we stay (as close to) pixel perfect as possible.
// This is "lazy" but oh well. It works!
size.w -= size.w % 12;
size.h -= size.h % 12;
ctx.save();
// TODO: When we get the custom 2bpp gzip sprite stuff to work, we should set this up
// so it fills the *agent's* configured background color.
// This is just because our image is this color anyways, so it makes no sense to make it customizable *right now*.
ctx.fillStyle = '#ffffe7';
// Fill the inner portion of the balloon.
ctx.fillRect(at.x + 12, at.y + 13, size.w, size.h);
// draw the left side corner
spriteDraw(ctx, corner_sprite, at.x, at.y);
// draw the straight part of the balloon
let i = 1;
for (; i < size.w / 12 + 1; ++i) {
spriteDraw(ctx, straight_sprite, at.x + 12 * i, at.y);
}
// draw the right side corner
spriteDrawRotated(ctx, corner_sprite, 90, at.x + 12 * i, at.y);
// Draw both the left and right sides of the box. We can do this in one pass
// so we do that for simplicity.
let j = 1;
for (; j < size.h / 12; ++j) {
spriteDrawRotated(ctx, straight_sprite, 270, at.x, at.y + 12 * j);
spriteDrawRotated(ctx, straight_sprite, 90, at.x + 12 * i, at.y + 12 * j);
}
// Draw the bottom left corner
spriteDrawRotated(ctx, corner_sprite, 270, at.x, at.y + 12 * j);
// Draw the bottom of the box
i = 1;
for (; i < size.w / 12 + 1; ++i) {
spriteDrawRotated(ctx, straight_sprite, 180, at.x + 12 * i, at.y + 12 * j);
}
// Draw the bottom right corner
spriteDrawRotated(ctx, corner_sprite, 180, at.x + 12 * i, at.y + 12 * j);
// TODO: a tip point should be provided. We will pick the best corner to stick it on,
// and the best y coordinate on that corner to stick it on.
//
// For now, we always simply use the center of the bottom..
// Draw the tip.
spriteDraw(ctx, tip_sprite, at.x + size.w / 2, at.y + 12 * (j + 1) - 1);
ctx.restore();
return {
x: at.x + 12 * 2,
y: at.y + 13 * 2,
w: size.w - 12 * 2,
h: size.h - 13 * 2
};
}
function wordWrapToStringList(text: string, maxLength: number) {
// this was stolen off stackoverflow, it sucks but it (kind of) works
// it should probably be replaced at some point.
var result = [],
line: string[] = [];
var length = 0;
text.split(' ').forEach(function (word) {
if (length + word.length >= maxLength) {
result.push(line.join(' '));
line = [];
length = 0;
}
length += word.length + 1;
line.push(word);
});
if (line.length > 0) {
result.push(line.join(' '));
}
return result;
}
// This draws a wordballoon with text. This function respects the current context's font settings and does *not* modify them.
export function wordballoonDrawText(ctx: CanvasRenderingContext2D, at: Point, text: string, maxLen: number = 20) {
let lines = wordWrapToStringList(text, maxLen);
// Create metrics for each line
let metrics = lines.map((line) => {
return ctx.measureText(line);
});
let size = {
w: 0,
h: 26
};
// Work out the size of the wordballoon based on the metrics
for (let metric of metrics) {
let width = Math.abs(metric.actualBoundingBoxLeft) + Math.abs(metric.actualBoundingBoxRight);
let height = metric.actualBoundingBoxAscent + metric.actualBoundingBoxDescent;
// The largest line determines the total width of the balloon
if (width > size.w) {
size.w = width;
}
size.h += height * 1.25;
}
size.w = Math.floor(size.w + 12);
size.h = Math.floor(size.h);
// Draw the word balloon and get the inner rect
let rectInner = wordballoonDraw(ctx, at, size);
// Draw all the lines of text
let y = 0;
for (let i in lines) {
let metric = metrics[i];
let height = metric.actualBoundingBoxAscent + metric.actualBoundingBoxDescent;
ctx.fillText(lines[i], rectInner.x - 12, rectInner.y + y);
y += height * 1.25;
}
}