sci2-ts 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/lib/991.sc +44 -30
- package/lib/993.sc +40 -6
- package/lib/996.sc +8 -3
- package/package.json +1 -1
- package/packages/content/src/room.js +46 -3
- package/packages/content/src/room.ts +41 -3
- package/packages/content/src/target.d.ts +9 -0
- package/packages/content/src/target.ts +5 -0
- package/packages/sci/src/index.d.ts +1 -0
- package/packages/sci/src/index.js +1 -0
- package/packages/sci/src/index.ts +1 -0
- package/packages/sci/src/text/font.d.ts +19 -2
- package/packages/sci/src/text/font.js +62 -14
- package/packages/sci/src/text/font.ts +64 -15
- package/packages/sci/src/text/styled.d.ts +40 -0
- package/packages/sci/src/text/styled.js +92 -0
- package/packages/sci/src/text/styled.ts +110 -0
- package/packages/sci/src/vm/kernels/text.js +40 -29
- package/packages/sci/src/vm/kernels/text.ts +41 -28
- package/tools/art/build.d.ts +9 -3
- package/tools/art/build.js +33 -2
- package/tools/art/build.ts +27 -4
- package/tools/art/font.d.ts +20 -0
- package/tools/art/font.js +35 -0
- package/tools/art/font.ts +41 -0
- package/tools/game/build.d.ts +3 -1
- package/tools/game/build.js +13 -1
- package/tools/game/build.ts +17 -3
|
@@ -3,8 +3,19 @@ export interface Glyph {
|
|
|
3
3
|
height: number;
|
|
4
4
|
/** 1 byte per pixel, 1 = ink. */
|
|
5
5
|
pixels: Uint8Array;
|
|
6
|
+
/** Where the bitmap starts from the pen (default 0); it can be left of it, an italic j's tail. */
|
|
7
|
+
bearingX?: number;
|
|
8
|
+
/** How far the pen moves on (default the width); ink can reach past it. */
|
|
9
|
+
advance?: number;
|
|
6
10
|
}
|
|
7
11
|
|
|
12
|
+
/** How far the pen moves on after a glyph. */
|
|
13
|
+
export const advanceOf = (g: Glyph) => g.advance ?? g.width;
|
|
14
|
+
|
|
15
|
+
/** The extension at the end of a font resource with bearings and advances: see writeFont. */
|
|
16
|
+
const METRICS_TAG = [0x58, 0x4d, 0x54, 0x31]; // "XMT1"
|
|
17
|
+
const hasOwnMetrics = (g: Glyph | undefined) => !!g && ((g.bearingX ?? 0) !== 0 || (g.advance ?? g.width) !== g.width);
|
|
18
|
+
|
|
8
19
|
export interface Font {
|
|
9
20
|
/** Line height in pixels. */
|
|
10
21
|
height: number;
|
|
@@ -22,6 +33,11 @@ export interface Font {
|
|
|
22
33
|
*
|
|
23
34
|
* Each glyph: u8 width, u8 height, then rows of 1bpp pixels, MSB first,
|
|
24
35
|
* each row padded to a whole byte.
|
|
36
|
+
*
|
|
37
|
+
* A font can also carry each glyph's bearing and advance (writeFont adds them when any glyph
|
|
38
|
+
* has its own): after the glyphs, i8 bearing and u8 advance per character, then "XMT1". SCI
|
|
39
|
+
* reads glyphs by their offsets and never gets there; without it, bearings are 0 and
|
|
40
|
+
* advances the widths, as fonts always were.
|
|
25
41
|
*/
|
|
26
42
|
export function parseFont(data: Uint8Array): Font {
|
|
27
43
|
const v = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -43,6 +59,16 @@ export function parseFont(data: Uint8Array): Font {
|
|
|
43
59
|
}
|
|
44
60
|
glyphs[c] = { width, height: h, pixels };
|
|
45
61
|
}
|
|
62
|
+
const tagAt = data.length - 4;
|
|
63
|
+
if (tagAt - count * 2 >= 6 + count * 2 && METRICS_TAG.every((b, i) => data[tagAt + i] === b)) {
|
|
64
|
+
const at = tagAt - count * 2;
|
|
65
|
+
glyphs.forEach((g, c) => {
|
|
66
|
+
if (!g) return;
|
|
67
|
+
const bearing = (data[at + c * 2]! << 24) >> 24, advance = data[at + c * 2 + 1]!;
|
|
68
|
+
if (bearing) g.bearingX = bearing;
|
|
69
|
+
if (advance !== g.width) g.advance = advance;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
46
72
|
return { height, glyphs };
|
|
47
73
|
}
|
|
48
74
|
|
|
@@ -61,7 +87,9 @@ export function writeFont(font: Font): Uint8Array {
|
|
|
61
87
|
}
|
|
62
88
|
return out;
|
|
63
89
|
});
|
|
64
|
-
const
|
|
90
|
+
const metrics = font.glyphs.some(hasOwnMetrics);
|
|
91
|
+
const bodySize = bodies.reduce((n, b) => n + b.length, 0);
|
|
92
|
+
const out = new Uint8Array(6 + count * 2 + bodySize + (metrics ? count * 2 + 4 : 0));
|
|
65
93
|
const v = new DataView(out.buffer);
|
|
66
94
|
v.setUint16(2, count, true);
|
|
67
95
|
v.setUint16(4, font.height, true);
|
|
@@ -72,11 +100,20 @@ export function writeFont(font: Font): Uint8Array {
|
|
|
72
100
|
out.set(b, at);
|
|
73
101
|
at += b.length;
|
|
74
102
|
});
|
|
103
|
+
if (metrics) {
|
|
104
|
+
font.glyphs.forEach((g, c) => {
|
|
105
|
+
const bearing = g?.bearingX ?? 0, advance = g ? advanceOf(g) : 0;
|
|
106
|
+
if (bearing < -128 || bearing > 127 || advance < 0 || advance > 255) throw new Error(`glyph ${c}: bearing ${bearing}, advance ${advance}`);
|
|
107
|
+
out[at + c * 2] = bearing & 0xff;
|
|
108
|
+
out[at + c * 2 + 1] = advance;
|
|
109
|
+
});
|
|
110
|
+
out.set(METRICS_TAG, at + count * 2);
|
|
111
|
+
}
|
|
75
112
|
return out;
|
|
76
113
|
}
|
|
77
114
|
|
|
78
115
|
export const textWidth = (font: Font, text: string): number =>
|
|
79
|
-
[...text].reduce((w, ch) =>
|
|
116
|
+
[...text].reduce((w, ch) => { const g = font.glyphs[ch.charCodeAt(0)]; return w + (g ? advanceOf(g) : 0); }, 0);
|
|
80
117
|
|
|
81
118
|
/** Splits text into lines no wider than `maxWidth`, breaking at spaces and honouring CR/LF. */
|
|
82
119
|
export function wrapText(font: Font, text: string, maxWidth: number): string[] {
|
|
@@ -102,24 +139,36 @@ export interface TextBitmap {
|
|
|
102
139
|
pixels: Uint8Array;
|
|
103
140
|
}
|
|
104
141
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Renders already-wrapped lines, each aligned within the line width. Ink that starts left of
|
|
144
|
+
* the pen or runs past the last advance (an italic's first and last letters) is kept: the
|
|
145
|
+
* bitmap grows to hold it, and `originX` says where the pen started.
|
|
146
|
+
*/
|
|
147
|
+
export function renderText(font: Font, lines: string[], align: "left" | "center" = "left"): TextBitmap & { originX: number } {
|
|
148
|
+
const lineWidth = Math.max(1, ...lines.map((l) => textWidth(font, l)));
|
|
149
|
+
const pens = lines.map((line) => {
|
|
150
|
+
let x = align === "center" ? Math.floor((lineWidth - textWidth(font, line)) / 2) : 0;
|
|
151
|
+
return [...line].flatMap((ch) => {
|
|
152
|
+
const g = font.glyphs[ch.charCodeAt(0)];
|
|
153
|
+
if (!g) return [];
|
|
154
|
+
const at = { g, x: x + (g.bearingX ?? 0) };
|
|
155
|
+
x += advanceOf(g);
|
|
156
|
+
return [at];
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
const inks = pens.flat();
|
|
160
|
+
const left = Math.min(0, ...inks.map((p) => p.x)), right = Math.max(lineWidth, ...inks.map((p) => p.x + p.g.width));
|
|
161
|
+
const width = right - left;
|
|
108
162
|
const height = Math.max(1, lines.length * font.height);
|
|
109
163
|
const pixels = new Uint8Array(width * height);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for (const ch of line) {
|
|
114
|
-
const g = font.glyphs[ch.charCodeAt(0)];
|
|
115
|
-
if (!g) continue;
|
|
116
|
-
for (let gy = 0; gy < g.height; gy++) {
|
|
164
|
+
pens.forEach((row, r) => {
|
|
165
|
+
for (const { g, x } of row) {
|
|
166
|
+
for (let gy = 0; gy < g.height && r * font.height + gy < height; gy++) {
|
|
117
167
|
for (let gx = 0; gx < g.width; gx++) {
|
|
118
|
-
if (g.pixels[gy * g.width + gx]) pixels[(
|
|
168
|
+
if (g.pixels[gy * g.width + gx]) pixels[(r * font.height + gy) * width + x - left + gx] = 1;
|
|
119
169
|
}
|
|
120
170
|
}
|
|
121
|
-
x += g.width;
|
|
122
171
|
}
|
|
123
172
|
});
|
|
124
|
-
return { width, height, pixels };
|
|
173
|
+
return { width, height, pixels, originX: -left };
|
|
125
174
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Font, type Glyph } from "./font.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Text with styles in it, as SCI32 writes them: `|f3|` switches to font 3 and `|f|` back to
|
|
4
|
+
* the text's own; `|c5|` and `|c|` do the same for the colour. A word whose style changes
|
|
5
|
+
* part-way through is still one word, and the codes take no room.
|
|
6
|
+
*/
|
|
7
|
+
export interface StyledChar {
|
|
8
|
+
ch: string;
|
|
9
|
+
font: number;
|
|
10
|
+
/** A colour of its own, or undefined for the text's. */
|
|
11
|
+
color?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseStyled(text: string, font: number): StyledChar[];
|
|
14
|
+
/** The text without its codes. */
|
|
15
|
+
export declare const plainOf: (text: string) => string;
|
|
16
|
+
type FontOf = (n: number) => Font;
|
|
17
|
+
/** The pen's travel over some characters: the sum of their advances. */
|
|
18
|
+
export declare function styledWidth(fontOf: FontOf, chars: StyledChar[]): number;
|
|
19
|
+
export interface StyledLine {
|
|
20
|
+
chars: StyledChar[];
|
|
21
|
+
/** The pen's travel: what wrapping and alignment go by. */
|
|
22
|
+
width: number;
|
|
23
|
+
/** The tallest font in it (the text's own if it's empty). */
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lines no wider than maxWidth, breaking after spaces and at CR/LF, each character measured
|
|
28
|
+
* in its own font. A word too long for a line gets a line of its own.
|
|
29
|
+
*/
|
|
30
|
+
export declare function wrapStyled(fontOf: FontOf, chars: StyledChar[], font: number, maxWidth: number): StyledLine[];
|
|
31
|
+
/**
|
|
32
|
+
* Where each character's ink goes: the pen starts at x (moved by alignment), and each glyph
|
|
33
|
+
* is drawn at the pen plus its bearing, then the pen moves on by its advance.
|
|
34
|
+
*/
|
|
35
|
+
export declare function placeLine(fontOf: FontOf, line: StyledLine, x: number): {
|
|
36
|
+
glyph: Glyph;
|
|
37
|
+
x: number;
|
|
38
|
+
color?: number;
|
|
39
|
+
}[];
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { advanceOf } from "./font.js";
|
|
2
|
+
const CODE = /\|([fc])(\d*)\|/y;
|
|
3
|
+
export function parseStyled(text, font) {
|
|
4
|
+
const out = [];
|
|
5
|
+
let f = font, color;
|
|
6
|
+
for (let i = 0; i < text.length;) {
|
|
7
|
+
CODE.lastIndex = i;
|
|
8
|
+
const m = text[i] === "|" ? CODE.exec(text) : null;
|
|
9
|
+
if (m) {
|
|
10
|
+
if (m[1] === "f")
|
|
11
|
+
f = m[2] ? Number(m[2]) : font;
|
|
12
|
+
else
|
|
13
|
+
color = m[2] ? Number(m[2]) : undefined;
|
|
14
|
+
i = CODE.lastIndex;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
out.push({ ch: text[i], font: f, ...(color === undefined ? {} : { color }) });
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** The text without its codes. */
|
|
23
|
+
export const plainOf = (text) => text.replace(/\|[fc]\d*\|/g, "");
|
|
24
|
+
const glyphOf = (fontOf, c) => fontOf(c.font).glyphs[c.ch.charCodeAt(0)];
|
|
25
|
+
/** The pen's travel over some characters: the sum of their advances. */
|
|
26
|
+
export function styledWidth(fontOf, chars) {
|
|
27
|
+
return chars.reduce((w, c) => { const g = glyphOf(fontOf, c); return w + (g ? advanceOf(g) : 0); }, 0);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Lines no wider than maxWidth, breaking after spaces and at CR/LF, each character measured
|
|
31
|
+
* in its own font. A word too long for a line gets a line of its own.
|
|
32
|
+
*/
|
|
33
|
+
export function wrapStyled(fontOf, chars, font, maxWidth) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
const finish = (line) => {
|
|
36
|
+
while (line.length && line[line.length - 1].ch === " ")
|
|
37
|
+
line.pop();
|
|
38
|
+
lines.push({ chars: line, width: styledWidth(fontOf, line), height: Math.max(fontOf(font).height, ...line.map((c) => fontOf(c.font).height)) });
|
|
39
|
+
};
|
|
40
|
+
// Paragraphs, then words: a word is its letters and the spaces after them.
|
|
41
|
+
const paragraphs = [[]];
|
|
42
|
+
for (let i = 0; i < chars.length; i++) {
|
|
43
|
+
const c = chars[i];
|
|
44
|
+
if (c.ch === "\r" || c.ch === "\n") {
|
|
45
|
+
if (c.ch === "\r" && chars[i + 1]?.ch === "\n")
|
|
46
|
+
i++;
|
|
47
|
+
paragraphs.push([]);
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
paragraphs[paragraphs.length - 1].push(c);
|
|
51
|
+
}
|
|
52
|
+
for (const paragraph of paragraphs) {
|
|
53
|
+
const words = [];
|
|
54
|
+
let word = [];
|
|
55
|
+
paragraph.forEach((c, i) => {
|
|
56
|
+
word.push(c);
|
|
57
|
+
if (c.ch === " " && paragraph[i + 1]?.ch !== " ")
|
|
58
|
+
(words.push(word), (word = []));
|
|
59
|
+
});
|
|
60
|
+
if (word.length)
|
|
61
|
+
words.push(word);
|
|
62
|
+
let line = [];
|
|
63
|
+
for (const w of words) {
|
|
64
|
+
const trimmed = [...line, ...w];
|
|
65
|
+
while (trimmed.length && trimmed[trimmed.length - 1].ch === " ")
|
|
66
|
+
trimmed.pop();
|
|
67
|
+
if (line.length && styledWidth(fontOf, trimmed) > maxWidth) {
|
|
68
|
+
finish(line);
|
|
69
|
+
line = [];
|
|
70
|
+
}
|
|
71
|
+
line.push(...w);
|
|
72
|
+
}
|
|
73
|
+
finish(line);
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Where each character's ink goes: the pen starts at x (moved by alignment), and each glyph
|
|
79
|
+
* is drawn at the pen plus its bearing, then the pen moves on by its advance.
|
|
80
|
+
*/
|
|
81
|
+
export function placeLine(fontOf, line, x) {
|
|
82
|
+
const out = [];
|
|
83
|
+
let pen = x;
|
|
84
|
+
for (const c of line.chars) {
|
|
85
|
+
const glyph = glyphOf(fontOf, c);
|
|
86
|
+
if (!glyph)
|
|
87
|
+
continue;
|
|
88
|
+
out.push({ glyph, x: pen + (glyph.bearingX ?? 0), ...(c.color === undefined ? {} : { color: c.color }) });
|
|
89
|
+
pen += advanceOf(glyph);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { advanceOf, type Font, type Glyph } from "./font.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Text with styles in it, as SCI32 writes them: `|f3|` switches to font 3 and `|f|` back to
|
|
5
|
+
* the text's own; `|c5|` and `|c|` do the same for the colour. A word whose style changes
|
|
6
|
+
* part-way through is still one word, and the codes take no room.
|
|
7
|
+
*/
|
|
8
|
+
export interface StyledChar {
|
|
9
|
+
ch: string;
|
|
10
|
+
font: number;
|
|
11
|
+
/** A colour of its own, or undefined for the text's. */
|
|
12
|
+
color?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CODE = /\|([fc])(\d*)\|/y;
|
|
16
|
+
|
|
17
|
+
export function parseStyled(text: string, font: number): StyledChar[] {
|
|
18
|
+
const out: StyledChar[] = [];
|
|
19
|
+
let f = font, color: number | undefined;
|
|
20
|
+
for (let i = 0; i < text.length; ) {
|
|
21
|
+
CODE.lastIndex = i;
|
|
22
|
+
const m = text[i] === "|" ? CODE.exec(text) : null;
|
|
23
|
+
if (m) {
|
|
24
|
+
if (m[1] === "f") f = m[2] ? Number(m[2]) : font;
|
|
25
|
+
else color = m[2] ? Number(m[2]) : undefined;
|
|
26
|
+
i = CODE.lastIndex;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
out.push({ ch: text[i]!, font: f, ...(color === undefined ? {} : { color }) });
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The text without its codes. */
|
|
36
|
+
export const plainOf = (text: string) => text.replace(/\|[fc]\d*\|/g, "");
|
|
37
|
+
|
|
38
|
+
type FontOf = (n: number) => Font;
|
|
39
|
+
const glyphOf = (fontOf: FontOf, c: StyledChar): Glyph | undefined => fontOf(c.font).glyphs[c.ch.charCodeAt(0)];
|
|
40
|
+
|
|
41
|
+
/** The pen's travel over some characters: the sum of their advances. */
|
|
42
|
+
export function styledWidth(fontOf: FontOf, chars: StyledChar[]): number {
|
|
43
|
+
return chars.reduce((w, c) => { const g = glyphOf(fontOf, c); return w + (g ? advanceOf(g) : 0); }, 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StyledLine {
|
|
47
|
+
chars: StyledChar[];
|
|
48
|
+
/** The pen's travel: what wrapping and alignment go by. */
|
|
49
|
+
width: number;
|
|
50
|
+
/** The tallest font in it (the text's own if it's empty). */
|
|
51
|
+
height: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lines no wider than maxWidth, breaking after spaces and at CR/LF, each character measured
|
|
56
|
+
* in its own font. A word too long for a line gets a line of its own.
|
|
57
|
+
*/
|
|
58
|
+
export function wrapStyled(fontOf: FontOf, chars: StyledChar[], font: number, maxWidth: number): StyledLine[] {
|
|
59
|
+
const lines: StyledLine[] = [];
|
|
60
|
+
const finish = (line: StyledChar[]) => {
|
|
61
|
+
while (line.length && line[line.length - 1]!.ch === " ") line.pop();
|
|
62
|
+
lines.push({ chars: line, width: styledWidth(fontOf, line), height: Math.max(fontOf(font).height, ...line.map((c) => fontOf(c.font).height)) });
|
|
63
|
+
};
|
|
64
|
+
// Paragraphs, then words: a word is its letters and the spaces after them.
|
|
65
|
+
const paragraphs: StyledChar[][] = [[]];
|
|
66
|
+
for (let i = 0; i < chars.length; i++) {
|
|
67
|
+
const c = chars[i]!;
|
|
68
|
+
if (c.ch === "\r" || c.ch === "\n") {
|
|
69
|
+
if (c.ch === "\r" && chars[i + 1]?.ch === "\n") i++;
|
|
70
|
+
paragraphs.push([]);
|
|
71
|
+
} else paragraphs[paragraphs.length - 1]!.push(c);
|
|
72
|
+
}
|
|
73
|
+
for (const paragraph of paragraphs) {
|
|
74
|
+
const words: StyledChar[][] = [];
|
|
75
|
+
let word: StyledChar[] = [];
|
|
76
|
+
paragraph.forEach((c, i) => {
|
|
77
|
+
word.push(c);
|
|
78
|
+
if (c.ch === " " && paragraph[i + 1]?.ch !== " ") (words.push(word), (word = []));
|
|
79
|
+
});
|
|
80
|
+
if (word.length) words.push(word);
|
|
81
|
+
let line: StyledChar[] = [];
|
|
82
|
+
for (const w of words) {
|
|
83
|
+
const trimmed = [...line, ...w];
|
|
84
|
+
while (trimmed.length && trimmed[trimmed.length - 1]!.ch === " ") trimmed.pop();
|
|
85
|
+
if (line.length && styledWidth(fontOf, trimmed) > maxWidth) {
|
|
86
|
+
finish(line);
|
|
87
|
+
line = [];
|
|
88
|
+
}
|
|
89
|
+
line.push(...w);
|
|
90
|
+
}
|
|
91
|
+
finish(line);
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Where each character's ink goes: the pen starts at x (moved by alignment), and each glyph
|
|
98
|
+
* is drawn at the pen plus its bearing, then the pen moves on by its advance.
|
|
99
|
+
*/
|
|
100
|
+
export function placeLine(fontOf: FontOf, line: StyledLine, x: number): { glyph: Glyph; x: number; color?: number }[] {
|
|
101
|
+
const out: { glyph: Glyph; x: number; color?: number }[] = [];
|
|
102
|
+
let pen = x;
|
|
103
|
+
for (const c of line.chars) {
|
|
104
|
+
const glyph = glyphOf(fontOf, c);
|
|
105
|
+
if (!glyph) continue;
|
|
106
|
+
out.push({ glyph, x: pen + (glyph.bearingX ?? 0), ...(c.color === undefined ? {} : { color: c.color }) });
|
|
107
|
+
pen += advanceOf(glyph);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ResourceType } from "../../resource/types.js";
|
|
2
|
-
import { parseFont
|
|
2
|
+
import { parseFont } from "../../text/font.js";
|
|
3
|
+
import { parseStyled, placeLine, styledWidth, wrapStyled } from "../../text/styled.js";
|
|
3
4
|
import { hasRef, parseMessages } from "../../text/message.js";
|
|
4
5
|
import { NULL, fromInt, segmentOf, toSigned } from "../value.js";
|
|
5
6
|
import { stringHelpers } from "./arrays.js";
|
|
@@ -95,31 +96,38 @@ const DEFAULT_MAX_WIDTH = (320 * 3) / 5;
|
|
|
95
96
|
* arrow glyphs on dialog scroll buttons.
|
|
96
97
|
*/
|
|
97
98
|
export const unescapeText = (s) => s.replace(/\\([0-9a-fA-F]{2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
98
|
-
/**
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Draws wrapped text into a bitmap, inside `rect`, with alignment. The text can change font
|
|
101
|
+
* and colour (`|f3|`, `|c5|`). Ink can start left of the pen or run past it (an italic's
|
|
102
|
+
* tail) into the box's margin; only the bitmap's edges stop it.
|
|
103
|
+
*/
|
|
104
|
+
function drawText(vm, bmp, fontNo, str, rect, color, align) {
|
|
105
|
+
const fontOf = (n) => text(vm).font(n);
|
|
100
106
|
const width = rect.right - rect.left;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
for (const ch of line) {
|
|
107
|
-
const g = font.glyphs[ch.charCodeAt(0)];
|
|
108
|
-
if (!g)
|
|
109
|
-
continue;
|
|
107
|
+
let y0 = rect.top;
|
|
108
|
+
for (const line of wrapStyled(fontOf, parseStyled(str, fontNo), fontNo, width)) {
|
|
109
|
+
const x = rect.left + (align === TextAlign.Center ? Math.floor((width - line.width) / 2) : align === TextAlign.Right ? width - line.width : 0);
|
|
110
|
+
for (const { glyph: g, x: gx0, color: c } of placeLine(fontOf, line, x)) {
|
|
111
|
+
const ink = c ?? color;
|
|
110
112
|
for (let gy = 0; gy < g.height; gy++) {
|
|
111
113
|
const y = y0 + gy;
|
|
112
114
|
if (y < rect.top || y >= rect.bottom || y >= bmp.height)
|
|
113
115
|
continue;
|
|
114
116
|
for (let gx = 0; gx < g.width; gx++) {
|
|
115
|
-
const px =
|
|
117
|
+
const px = gx0 + gx;
|
|
116
118
|
if (g.pixels[gy * g.width + gx] && px >= 0 && px < bmp.width)
|
|
117
|
-
bmp.pixels[y * bmp.width + px] =
|
|
119
|
+
bmp.pixels[y * bmp.width + px] = ink;
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
|
-
x += g.width;
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
y0 += line.height;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** The size text takes in a font, wrapped to maxWidth: the widest line's advance, and the lines' heights. */
|
|
127
|
+
function measure(vm, fontNo, str, maxWidth) {
|
|
128
|
+
const fontOf = (n) => text(vm).font(n);
|
|
129
|
+
const lines = wrapStyled(fontOf, parseStyled(str, fontNo), fontNo, maxWidth);
|
|
130
|
+
return { width: Math.max(0, ...lines.map((l) => l.width)), height: lines.reduce((h, l) => h + l.height, 0) };
|
|
123
131
|
}
|
|
124
132
|
function drawBorder(bmp, color) {
|
|
125
133
|
for (let x = 0; x < bmp.width; x++)
|
|
@@ -186,33 +194,36 @@ export const textKernels = {
|
|
|
186
194
|
},
|
|
187
195
|
// TextSize(rectArray, text, font, maxWidth): writes [left, top, right, bottom] (inclusive).
|
|
188
196
|
TextSize: (vm, [rect = 0, str = 0, fontNo = 0, maxWidth = 0]) => {
|
|
189
|
-
const font = text(vm).font(fontNo);
|
|
190
197
|
const s = stringHelpers.str(vm, str);
|
|
198
|
+
const fontOf = (n) => text(vm).font(n);
|
|
199
|
+
const oneLine = () => {
|
|
200
|
+
const chars = parseStyled(s, fontNo);
|
|
201
|
+
return { width: styledWidth(fontOf, chars), height: Math.max(fontOf(fontNo).height, ...chars.map((c) => fontOf(c.font).height)) };
|
|
202
|
+
};
|
|
191
203
|
let w, h;
|
|
192
204
|
const max = toSigned(maxWidth);
|
|
193
205
|
if (max >= 0) {
|
|
194
206
|
const limit = max || DEFAULT_MAX_WIDTH;
|
|
195
|
-
|
|
207
|
+
const one = oneLine();
|
|
208
|
+
if (!/[\r\n]/.test(s) && one.width <= limit) {
|
|
196
209
|
// Fits on one line: measured as-is, spaces included (a lone " " has a width).
|
|
197
|
-
w =
|
|
198
|
-
h = font.height;
|
|
210
|
+
({ width: w, height: h } = one);
|
|
199
211
|
}
|
|
200
212
|
else {
|
|
201
|
-
const
|
|
202
|
-
w = Math.min(limit,
|
|
203
|
-
h =
|
|
213
|
+
const m = measure(vm, fontNo, s, limit);
|
|
214
|
+
w = Math.min(limit, m.width);
|
|
215
|
+
h = m.height;
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
else {
|
|
207
|
-
w =
|
|
208
|
-
h = font.height;
|
|
219
|
+
({ width: w, height: h } = oneLine());
|
|
209
220
|
}
|
|
210
221
|
const a = stringHelpers.resolve(vm, rect);
|
|
211
222
|
if (a)
|
|
212
223
|
a.data.splice(0, 4, 0, 0, fromInt(w - 1), fromInt(h - 1));
|
|
213
224
|
return 0;
|
|
214
225
|
},
|
|
215
|
-
TextWidth: (vm, [str = 0, fontNo = 0]) =>
|
|
226
|
+
TextWidth: (vm, [str = 0, fontNo = 0]) => styledWidth((n) => text(vm).font(n), parseStyled(stringHelpers.str(vm, str), fontNo)),
|
|
216
227
|
PointSize: (vm, [fontNo = 0]) => text(vm).font(fontNo).height,
|
|
217
228
|
TextFonts: () => 0,
|
|
218
229
|
TextColors: () => 0,
|
|
@@ -225,7 +236,7 @@ export const textKernels = {
|
|
|
225
236
|
return NULL;
|
|
226
237
|
const w = Math.max(1, toSigned(width)), h = Math.max(1, toSigned(height));
|
|
227
238
|
const str = unescapeText(stringHelpers.str(vm, vm.getProp(obj, "text") ?? 0));
|
|
228
|
-
const
|
|
239
|
+
const fontNo = prop(vm, obj, "font");
|
|
229
240
|
const fore = prop(vm, obj, "fore") & 0xff;
|
|
230
241
|
const align = prop(vm, obj, "mode");
|
|
231
242
|
const rect = {
|
|
@@ -255,7 +266,7 @@ export const textKernels = {
|
|
|
255
266
|
if (found)
|
|
256
267
|
vm.memory.bitmap(ref).pixels.set(found.cel.pixels);
|
|
257
268
|
}
|
|
258
|
-
drawText(vm.memory.bitmap(ref),
|
|
269
|
+
drawText(vm, vm.memory.bitmap(ref), fontNo, str, rect, fore, align);
|
|
259
270
|
return ref;
|
|
260
271
|
},
|
|
261
272
|
/**
|
|
@@ -304,7 +315,7 @@ export const textKernels = {
|
|
|
304
315
|
if (border >= 0)
|
|
305
316
|
drawBorder(bmp, border & 0xff);
|
|
306
317
|
const cursor = (frame >> 4) & 1 ? "" : "_"; // blink
|
|
307
|
-
drawText(bmp, font, text + cursor, rect, fore, TextAlign.Left);
|
|
318
|
+
drawText(vm, bmp, prop(vm, obj, "font"), text + cursor, rect, fore, TextAlign.Left);
|
|
308
319
|
vm.setProp(obj, "bitmap", bitmap);
|
|
309
320
|
graphicsKernels.FrameOut(vm, []);
|
|
310
321
|
yield { frame: true };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ResourceType } from "../../resource/types.ts";
|
|
2
|
-
import { parseFont,
|
|
2
|
+
import { parseFont, type Font } from "../../text/font.ts";
|
|
3
|
+
import { parseStyled, placeLine, styledWidth, wrapStyled } from "../../text/styled.ts";
|
|
3
4
|
import { hasRef, parseMessages, type MessageFile, type MessageTuple } from "../../text/message.ts";
|
|
4
5
|
import type { SciBitmap } from "../memory.ts";
|
|
5
6
|
import type { FrameRequest, KernelFn, SendRequest, Vm } from "../vm.ts";
|
|
@@ -106,28 +107,37 @@ const DEFAULT_MAX_WIDTH = (320 * 3) / 5;
|
|
|
106
107
|
*/
|
|
107
108
|
export const unescapeText = (s: string) => s.replace(/\\([0-9a-fA-F]{2})/g, (_m, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
|
108
109
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Draws wrapped text into a bitmap, inside `rect`, with alignment. The text can change font
|
|
112
|
+
* and colour (`|f3|`, `|c5|`). Ink can start left of the pen or run past it (an italic's
|
|
113
|
+
* tail) into the box's margin; only the bitmap's edges stop it.
|
|
114
|
+
*/
|
|
115
|
+
function drawText(vm: Vm, bmp: SciBitmap, fontNo: number, str: string, rect: { left: number; top: number; right: number; bottom: number }, color: number, align: number) {
|
|
116
|
+
const fontOf = (n: number) => text(vm).font(n);
|
|
111
117
|
const width = rect.right - rect.left;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const ch of line) {
|
|
118
|
-
const g = font.glyphs[ch.charCodeAt(0)];
|
|
119
|
-
if (!g) continue;
|
|
118
|
+
let y0 = rect.top;
|
|
119
|
+
for (const line of wrapStyled(fontOf, parseStyled(str, fontNo), fontNo, width)) {
|
|
120
|
+
const x = rect.left + (align === TextAlign.Center ? Math.floor((width - line.width) / 2) : align === TextAlign.Right ? width - line.width : 0);
|
|
121
|
+
for (const { glyph: g, x: gx0, color: c } of placeLine(fontOf, line, x)) {
|
|
122
|
+
const ink = c ?? color;
|
|
120
123
|
for (let gy = 0; gy < g.height; gy++) {
|
|
121
124
|
const y = y0 + gy;
|
|
122
125
|
if (y < rect.top || y >= rect.bottom || y >= bmp.height) continue;
|
|
123
126
|
for (let gx = 0; gx < g.width; gx++) {
|
|
124
|
-
const px =
|
|
125
|
-
if (g.pixels[gy * g.width + gx] && px >= 0 && px < bmp.width) bmp.pixels[y * bmp.width + px] =
|
|
127
|
+
const px = gx0 + gx;
|
|
128
|
+
if (g.pixels[gy * g.width + gx] && px >= 0 && px < bmp.width) bmp.pixels[y * bmp.width + px] = ink;
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
|
-
x += g.width;
|
|
129
131
|
}
|
|
130
|
-
|
|
132
|
+
y0 += line.height;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The size text takes in a font, wrapped to maxWidth: the widest line's advance, and the lines' heights. */
|
|
137
|
+
function measure(vm: Vm, fontNo: number, str: string, maxWidth: number): { width: number; height: number } {
|
|
138
|
+
const fontOf = (n: number) => text(vm).font(n);
|
|
139
|
+
const lines = wrapStyled(fontOf, parseStyled(str, fontNo), fontNo, maxWidth);
|
|
140
|
+
return { width: Math.max(0, ...lines.map((l) => l.width)), height: lines.reduce((h, l) => h + l.height, 0) };
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
function drawBorder(bmp: SciBitmap, color: number) {
|
|
@@ -185,30 +195,33 @@ export const textKernels: Record<string, KernelFn> = {
|
|
|
185
195
|
|
|
186
196
|
// TextSize(rectArray, text, font, maxWidth): writes [left, top, right, bottom] (inclusive).
|
|
187
197
|
TextSize: (vm, [rect = 0, str = 0, fontNo = 0, maxWidth = 0]) => {
|
|
188
|
-
const font = text(vm).font(fontNo);
|
|
189
198
|
const s = stringHelpers.str(vm, str);
|
|
199
|
+
const fontOf = (n: number) => text(vm).font(n);
|
|
200
|
+
const oneLine = () => {
|
|
201
|
+
const chars = parseStyled(s, fontNo);
|
|
202
|
+
return { width: styledWidth(fontOf, chars), height: Math.max(fontOf(fontNo).height, ...chars.map((c) => fontOf(c.font).height)) };
|
|
203
|
+
};
|
|
190
204
|
let w: number, h: number;
|
|
191
205
|
const max = toSigned(maxWidth);
|
|
192
206
|
if (max >= 0) {
|
|
193
207
|
const limit = max || DEFAULT_MAX_WIDTH;
|
|
194
|
-
|
|
208
|
+
const one = oneLine();
|
|
209
|
+
if (!/[\r\n]/.test(s) && one.width <= limit) {
|
|
195
210
|
// Fits on one line: measured as-is, spaces included (a lone " " has a width).
|
|
196
|
-
w =
|
|
197
|
-
h = font.height;
|
|
211
|
+
({ width: w, height: h } = one);
|
|
198
212
|
} else {
|
|
199
|
-
const
|
|
200
|
-
w = Math.min(limit,
|
|
201
|
-
h =
|
|
213
|
+
const m = measure(vm, fontNo, s, limit);
|
|
214
|
+
w = Math.min(limit, m.width);
|
|
215
|
+
h = m.height;
|
|
202
216
|
}
|
|
203
217
|
} else {
|
|
204
|
-
w =
|
|
205
|
-
h = font.height;
|
|
218
|
+
({ width: w, height: h } = oneLine());
|
|
206
219
|
}
|
|
207
220
|
const a = stringHelpers.resolve(vm, rect);
|
|
208
221
|
if (a) a.data.splice(0, 4, 0, 0, fromInt(w - 1), fromInt(h - 1));
|
|
209
222
|
return 0;
|
|
210
223
|
},
|
|
211
|
-
TextWidth: (vm, [str = 0, fontNo = 0]) =>
|
|
224
|
+
TextWidth: (vm, [str = 0, fontNo = 0]) => styledWidth((n) => text(vm).font(n), parseStyled(stringHelpers.str(vm, str), fontNo)),
|
|
212
225
|
PointSize: (vm, [fontNo = 0]) => text(vm).font(fontNo).height,
|
|
213
226
|
TextFonts: () => 0,
|
|
214
227
|
TextColors: () => 0,
|
|
@@ -221,7 +234,7 @@ export const textKernels: Record<string, KernelFn> = {
|
|
|
221
234
|
if (!vm.memory.object(obj)) return NULL;
|
|
222
235
|
const w = Math.max(1, toSigned(width)), h = Math.max(1, toSigned(height));
|
|
223
236
|
const str = unescapeText(stringHelpers.str(vm, vm.getProp(obj, "text") ?? 0));
|
|
224
|
-
const
|
|
237
|
+
const fontNo = prop(vm, obj, "font");
|
|
225
238
|
const fore = prop(vm, obj, "fore") & 0xff;
|
|
226
239
|
const align = prop(vm, obj, "mode");
|
|
227
240
|
const rect = {
|
|
@@ -247,7 +260,7 @@ export const textKernels: Record<string, KernelFn> = {
|
|
|
247
260
|
ref = vm.memory.newBitmap(cw, ch, found?.cel.skipColor ?? 0, found?.cel.skipColor ?? 0);
|
|
248
261
|
if (found) vm.memory.bitmap(ref)!.pixels.set(found.cel.pixels);
|
|
249
262
|
}
|
|
250
|
-
drawText(vm.memory.bitmap(ref)!,
|
|
263
|
+
drawText(vm, vm.memory.bitmap(ref)!, fontNo, str, rect, fore, align);
|
|
251
264
|
return ref;
|
|
252
265
|
},
|
|
253
266
|
|
|
@@ -295,7 +308,7 @@ export const textKernels: Record<string, KernelFn> = {
|
|
|
295
308
|
const bmp = vm.memory.bitmap(bitmap)!;
|
|
296
309
|
if (border >= 0) drawBorder(bmp, border & 0xff);
|
|
297
310
|
const cursor = (frame >> 4) & 1 ? "" : "_"; // blink
|
|
298
|
-
drawText(bmp, font, text + cursor, rect, fore, TextAlign.Left);
|
|
311
|
+
drawText(vm, bmp, prop(vm, obj, "font"), text + cursor, rect, fore, TextAlign.Left);
|
|
299
312
|
vm.setProp(obj, "bitmap", bitmap);
|
|
300
313
|
graphicsKernels.FrameOut!(vm, []);
|
|
301
314
|
yield { frame: true };
|
package/tools/art/build.d.ts
CHANGED
|
@@ -24,8 +24,11 @@ export interface ArtManifest {
|
|
|
24
24
|
mirror: boolean;
|
|
25
25
|
})[];
|
|
26
26
|
}[];
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Fonts (tools/art/font.ts): drawn as sheets of glyphs, or a bitmap font with its own
|
|
29
|
+
* metrics, a JSON file (bearings, advances, its atlas) exported from the font's source.
|
|
30
|
+
*/
|
|
31
|
+
fonts?: ({
|
|
29
32
|
number: number;
|
|
30
33
|
png: string;
|
|
31
34
|
cell: [number, number];
|
|
@@ -33,7 +36,10 @@ export interface ArtManifest {
|
|
|
33
36
|
spacing?: number;
|
|
34
37
|
space?: number;
|
|
35
38
|
lineHeight?: number;
|
|
36
|
-
}
|
|
39
|
+
} | {
|
|
40
|
+
number: number;
|
|
41
|
+
metrics: string;
|
|
42
|
+
})[];
|
|
37
43
|
}
|
|
38
44
|
export declare class ArtError extends Error {
|
|
39
45
|
}
|