msagent.js: add wordballoon code

also initalize msagent.js inside of the webapp
This commit is contained in:
Lily Tsuru 2024-07-02 22:30:52 -04:00
parent 0c7fece5b6
commit c3c0d33e5b
11 changed files with 334 additions and 7 deletions

20
.prettierrc.json Normal file
View file

@ -0,0 +1,20 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": true,
"printWidth": 200,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true,
"vueIndentScriptAndStyle": false
}

View file

@ -1,12 +1,17 @@
{
"name": "@msagent-chat/msagent.js",
"version": "0.1.0",
"packageManager": "yarn@4.2.2",
"devDependencies": {
"@parcel/core": "^2.12.0",
"parcel": "^2.12.0",
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsc"
"build": "parcel build"
},
"main": "./dist/index.js",
"type": "module",
"source": "src/index.ts",
"module": "./dist/index.js",
"types": "./dist/index.d.ts"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 B

View file

@ -0,0 +1,11 @@
import { wordballoonInit } from "./wordballoon.js";
export * from "./types.js";
export * from "./sprite.js";
export * from "./wordballoon.js";
// Convinence function which initalizes all of msagent.js.
export async function agentInit() {
await wordballoonInit();
}

40
msagent.js/src/sprite.ts Normal file
View file

@ -0,0 +1,40 @@
// Sprite utilities
import { Rect } from './types';
// Load a image asynchronously
export async function spriteLoadImage(uri: string): Promise<HTMLImageElement> {
return new Promise((res, rej) => {
let image = new Image();
image.onload = () => res(image);
image.onerror = (err) => rej(new Error('Error loading image'));
image.crossOrigin = 'anonymous';
image.src = uri;
});
}
export async function spriteCutSpriteFromSpriteSheet(spriteSheet: HTMLImageElement, rect: Rect) {
let tmp_canvas = document.createElement('canvas');
let ctx = tmp_canvas.getContext('2d')!;
tmp_canvas.width = rect.w;
tmp_canvas.height = rect.h;
// draw the piece here!
ctx.drawImage(spriteSheet, rect.x, rect.y, rect.w, rect.h, 0, 0, rect.w, rect.h);
return spriteLoadImage(tmp_canvas.toDataURL());
}
export function spriteDraw(ctx: CanvasRenderingContext2D, sprite: HTMLImageElement, x: number, y: number) {
ctx.drawImage(sprite, x, y);
}
export function spriteDrawRotated(ctx: CanvasRenderingContext2D, sprite: HTMLImageElement, deg: number, x: number, y: number) {
ctx.save();
ctx.translate(x + sprite.width / 2, y + sprite.width / 2);
ctx.rotate((deg * Math.PI) / 180);
ctx.translate(-(x + sprite.width / 2), -(y + sprite.width / 2));
spriteDraw(ctx, sprite, x, y);
ctx.restore();
}

16
msagent.js/src/types.ts Normal file
View file

@ -0,0 +1,16 @@
export type Rect = {
x: number,
y: number,
w: number,
h: number
};
export type Size = {
w: number,
h: number
};
export type Point = {
x: number,
y: number
};

View file

@ -0,0 +1,172 @@
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;
}
}

View file

@ -6,5 +6,10 @@
"protocol",
"msagent.js"
],
"packageManager": "yarn@4.2.2"
"packageManager": "yarn@4.2.2",
"devDependencies": {
"@parcel/packager-ts": "2.12.0",
"@parcel/transformer-typescript-types": "2.12.0",
"typescript": ">=3.0.0"
}
}

View file

@ -9,6 +9,7 @@
},
"packageManager": "yarn@4.2.2",
"devDependencies": {
"@msagent-chat/msagent.js": "*",
"@parcel/core": "^2.12.0",
"parcel": "^2.12.0",
"run-script-os": "^1.1.6",

View file

@ -1,4 +1,6 @@
import { MSWindow, MSWindowStartPosition } from "./MSWindow.js"
import { MSWindow, MSWindowStartPosition } from "./MSWindow.js";
import { agentInit } from "@msagent-chat/msagent.js";
const elements = {
logonView: document.getElementById("logonView") as HTMLDivElement,
@ -24,3 +26,7 @@ elements.logonForm.addEventListener('submit', e => {
elements.logonView.style.display = "none";
elements.chatView.style.display = "block";
});
document.addEventListener('DOMContentLoaded', async () => {
await agentInit();
});

View file

@ -164,6 +164,16 @@ __metadata:
languageName: node
linkType: hard
"@msagent-chat/msagent.js@npm:*, @msagent-chat/msagent.js@workspace:msagent.js":
version: 0.0.0-use.local
resolution: "@msagent-chat/msagent.js@workspace:msagent.js"
dependencies:
"@parcel/core": "npm:^2.12.0"
parcel: "npm:^2.12.0"
typescript: "npm:^5.5.3"
languageName: unknown
linkType: soft
"@msagent-chat/protocol@workspace:protocol":
version: 0.0.0-use.local
resolution: "@msagent-chat/protocol@workspace:protocol"
@ -189,6 +199,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@msagent-chat/webapp@workspace:webapp"
dependencies:
"@msagent-chat/msagent.js": "npm:*"
"@parcel/core": "npm:^2.12.0"
parcel: "npm:^2.12.0"
run-script-os: "npm:^1.1.6"
@ -618,6 +629,15 @@ __metadata:
languageName: node
linkType: hard
"@parcel/packager-ts@npm:2.12.0":
version: 2.12.0
resolution: "@parcel/packager-ts@npm:2.12.0"
dependencies:
"@parcel/plugin": "npm:2.12.0"
checksum: 10c0/244f94c0d33cfb76e429196bf826e73f508ee4e4af7aa736b02ff3de65ace8ea12a0d7bf8d7d1e6c2ee4d0d5f1db8564db01d58379b08cf8bd4c90d0476f053b
languageName: node
linkType: hard
"@parcel/packager-wasm@npm:2.12.0":
version: 2.12.0
resolution: "@parcel/packager-wasm@npm:2.12.0"
@ -913,6 +933,33 @@ __metadata:
languageName: node
linkType: hard
"@parcel/transformer-typescript-types@npm:2.12.0":
version: 2.12.0
resolution: "@parcel/transformer-typescript-types@npm:2.12.0"
dependencies:
"@parcel/diagnostic": "npm:2.12.0"
"@parcel/plugin": "npm:2.12.0"
"@parcel/source-map": "npm:^2.1.1"
"@parcel/ts-utils": "npm:2.12.0"
"@parcel/utils": "npm:2.12.0"
nullthrows: "npm:^1.1.1"
peerDependencies:
typescript: ">=3.0.0"
checksum: 10c0/6b0e89b64f2cfc5e56b972b02a6a1b97c50e60b6ab935389ebf9479d05d164fe4ff9dc297b0d78ed3d3578f80049425b056512546d45877f549a6713d54efd03
languageName: node
linkType: hard
"@parcel/ts-utils@npm:2.12.0":
version: 2.12.0
resolution: "@parcel/ts-utils@npm:2.12.0"
dependencies:
nullthrows: "npm:^1.1.1"
peerDependencies:
typescript: ">=3.0.0"
checksum: 10c0/be53613cb3d950f5a6f0690af4bdda1ec9aaf0e97a035c66271230bd322ae29568d10831e61ded899188ca36d80add8ccbfcb494ebd4b0d9795cb51ccb7425cc
languageName: node
linkType: hard
"@parcel/types@npm:2.12.0":
version: 2.12.0
resolution: "@parcel/types@npm:2.12.0"
@ -2688,6 +2735,10 @@ __metadata:
"msagent-chat@workspace:.":
version: 0.0.0-use.local
resolution: "msagent-chat@workspace:."
dependencies:
"@parcel/packager-ts": "npm:2.12.0"
"@parcel/transformer-typescript-types": "npm:2.12.0"
typescript: "npm:>=3.0.0"
languageName: unknown
linkType: soft
@ -3517,7 +3568,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.5.3":
"typescript@npm:>=3.0.0, typescript@npm:^5.5.3":
version: 5.5.3
resolution: "typescript@npm:5.5.3"
bin:
@ -3527,7 +3578,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.5.3#optional!builtin<compat/typescript>":
"typescript@patch:typescript@npm%3A>=3.0.0#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.5.3#optional!builtin<compat/typescript>":
version: 5.5.3
resolution: "typescript@patch:typescript@npm%3A5.5.3#optional!builtin<compat/typescript>::version=5.5.3&hash=b45daf"
bin: