msagent.js: add wordballoon code
also initalize msagent.js inside of the webapp
This commit is contained in:
parent
0c7fece5b6
commit
c3c0d33e5b
11 changed files with 334 additions and 7 deletions
20
.prettierrc.json
Normal file
20
.prettierrc.json
Normal 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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
BIN
msagent.js/res/wordballoon.png
Normal file
BIN
msagent.js/res/wordballoon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 218 B |
|
@ -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
40
msagent.js/src/sprite.ts
Normal 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
16
msagent.js/src/types.ts
Normal 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
|
||||
};
|
172
msagent.js/src/wordballoon.ts
Normal file
172
msagent.js/src/wordballoon.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
@ -23,4 +25,8 @@ elements.logonForm.addEventListener('submit', e => {
|
|||
logonWindow.hide();
|
||||
elements.logonView.style.display = "none";
|
||||
elements.chatView.style.display = "block";
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await agentInit();
|
||||
});
|
||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue