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; } }