diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..d41a549 --- /dev/null +++ b/.prettierrc.json @@ -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 +} diff --git a/msagent.js/package.json b/msagent.js/package.json index 1ed8e58..8d7317b 100644 --- a/msagent.js/package.json +++ b/msagent.js/package.json @@ -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" } diff --git a/msagent.js/res/wordballoon.png b/msagent.js/res/wordballoon.png new file mode 100644 index 0000000..878c579 Binary files /dev/null and b/msagent.js/res/wordballoon.png differ diff --git a/msagent.js/src/index.ts b/msagent.js/src/index.ts index e69de29..2d0e502 100644 --- a/msagent.js/src/index.ts +++ b/msagent.js/src/index.ts @@ -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(); +} diff --git a/msagent.js/src/sprite.ts b/msagent.js/src/sprite.ts new file mode 100644 index 0000000..7aeafe8 --- /dev/null +++ b/msagent.js/src/sprite.ts @@ -0,0 +1,40 @@ +// Sprite utilities + +import { Rect } from './types'; + +// Load a image asynchronously +export async function spriteLoadImage(uri: string): Promise { + 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(); +} diff --git a/msagent.js/src/types.ts b/msagent.js/src/types.ts new file mode 100644 index 0000000..ffceaa4 --- /dev/null +++ b/msagent.js/src/types.ts @@ -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 +}; diff --git a/msagent.js/src/wordballoon.ts b/msagent.js/src/wordballoon.ts new file mode 100644 index 0000000..6bae8eb --- /dev/null +++ b/msagent.js/src/wordballoon.ts @@ -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; + } +} diff --git a/package.json b/package.json index 4957662..2999197 100644 --- a/package.json +++ b/package.json @@ -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" + } } diff --git a/webapp/package.json b/webapp/package.json index a3bb783..0b2a61d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -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", diff --git a/webapp/src/ts/main.ts b/webapp/src/ts/main.ts index b021cff..cea3361 100644 --- a/webapp/src/ts/main.ts +++ b/webapp/src/ts/main.ts @@ -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"; -}); \ No newline at end of file +}); + +document.addEventListener('DOMContentLoaded', async () => { + await agentInit(); +}); diff --git a/yarn.lock b/yarn.lock index e72121b..d6e7142 100644 --- a/yarn.lock +++ b/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": +"typescript@patch:typescript@npm%3A>=3.0.0#optional!builtin, typescript@patch:typescript@npm%3A^5.5.3#optional!builtin": version: 5.5.3 resolution: "typescript@patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=b45daf" bin: