embedded-react 0.1.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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +224 -0
- package/aot/compile.mjs +3066 -0
- package/aot/screenshot-smoke.mjs +110 -0
- package/aot/style-map.mjs +248 -0
- package/assets/bake-font.mjs +190 -0
- package/assets/bake-image.mjs +50 -0
- package/assets/build-builtin-font.mjs +51 -0
- package/assets/emit-c.mjs +187 -0
- package/assets/emit-container.mjs +121 -0
- package/assets/emit-pack.mjs +128 -0
- package/assets/index.mjs +72 -0
- package/assets/rasterize.mjs +169 -0
- package/build.mjs +136 -0
- package/pack-container.mjs +161 -0
- package/package.json +79 -0
- package/persist-transform.mjs +106 -0
- package/src/embedded-react/Animated.js +352 -0
- package/src/embedded-react/AppRegistry.js +49 -0
- package/src/embedded-react/Easing.js +39 -0
- package/src/embedded-react/LayoutAnimation.js +45 -0
- package/src/embedded-react/Platform.js +26 -0
- package/src/embedded-react/StyleSheet.js +36 -0
- package/src/embedded-react/components.js +44 -0
- package/src/embedded-react/imperative.js +68 -0
- package/src/embedded-react/index.js +52 -0
- package/src/embedded-react/layout-anim-config.js +91 -0
- package/src/embedded-react/split-style.js +58 -0
- package/src/embedded-react/svg-ops.js +564 -0
- package/src/embedded-react/usePersistentState.js +69 -0
- package/src/host-config.js +196 -0
- package/src/native-ui.js +24 -0
- package/src/props.js +183 -0
- package/src/renderer.js +57 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Emits the generated C for baked assets: one .c with the image pixel arrays + font BitmapFont data,
|
|
18
|
+
// and one .h declaring er_register_assets(). The .c depends only on the public engine header
|
|
19
|
+
// (er_scene.h, which exposes both er_image_load/er_font_register and the BitmapFont format), so it
|
|
20
|
+
// drops into any target's build the same way the bundle does.
|
|
21
|
+
|
|
22
|
+
/** Sanitizes an asset/family name into a C identifier fragment. */
|
|
23
|
+
function cIdent(name) {
|
|
24
|
+
let id = String(name).replace(/[^0-9a-zA-Z_]/g, '_');
|
|
25
|
+
if (id && /[0-9]/.test(id[0])) id = '_' + id;
|
|
26
|
+
return id || '_asset';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Formats an array of byte/word strings into wrapped C initializer lines. */
|
|
30
|
+
function rows(values, perLine, indent = ' ') {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = 0; i < values.length; i += perLine) {
|
|
33
|
+
out.push(indent + values.slice(i, i + perLine).join(', ') + ',');
|
|
34
|
+
}
|
|
35
|
+
return out.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Emits the GlyphInfo designated-initializer for one glyph. */
|
|
39
|
+
function glyphInit(g) {
|
|
40
|
+
return (
|
|
41
|
+
`{ .bitmap_offset = ${g.bitmapOffset}, .width = ${g.width}, .height = ${g.height}, ` +
|
|
42
|
+
`.x_offset = ${g.xOffset}, .y_offset = ${g.yOffset}, .advance = ${g.advance} }`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Builds the .c and .h text for a set of baked images and fonts.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} opts
|
|
50
|
+
* @param {string} opts.headerName Basename of the .h (for the .c's #include).
|
|
51
|
+
* @param {Array<object>} [opts.images] Results from bakeImage().
|
|
52
|
+
* @param {Array<object>} [opts.fonts] Results from bakeFont().
|
|
53
|
+
* @returns {{c:string, h:string}}
|
|
54
|
+
*/
|
|
55
|
+
export function emitAssetsC({ headerName, images = [], fonts = [] }) {
|
|
56
|
+
const guard = 'ER_' + cIdent(headerName).toUpperCase();
|
|
57
|
+
const h =
|
|
58
|
+
`/* Generated by bridges/quickjs/js/assets — DO NOT EDIT. */\n` +
|
|
59
|
+
`#ifndef ${guard}\n#define ${guard}\n\n` +
|
|
60
|
+
`#ifdef __cplusplus\nextern "C"\n{\n#endif\n\n` +
|
|
61
|
+
` /** @brief Registers every baked image and font with the engine (call once at boot, after\n` +
|
|
62
|
+
` * embedded_renderer_set_backend()). */\n` +
|
|
63
|
+
` void er_register_assets(void);\n\n` +
|
|
64
|
+
`#ifdef __cplusplus\n}\n#endif\n\n` +
|
|
65
|
+
`#endif\n`;
|
|
66
|
+
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(`/* Generated by bridges/quickjs/js/assets — DO NOT EDIT. */`);
|
|
69
|
+
lines.push(`/* Images: premultiplied ARGB8888, referenced by pointer. Fonts: baked BitmapFont per size. */`);
|
|
70
|
+
lines.push(``);
|
|
71
|
+
lines.push(`#include "${headerName}"`);
|
|
72
|
+
lines.push(`#include "er_scene.h" /* er_image_load, er_font_register, BitmapFont */`);
|
|
73
|
+
lines.push(`#include <stddef.h>`);
|
|
74
|
+
lines.push(`#include <stdint.h>`);
|
|
75
|
+
lines.push(``);
|
|
76
|
+
|
|
77
|
+
// --- Images ---------------------------------------------------------------------------------
|
|
78
|
+
for (const img of images) {
|
|
79
|
+
const id = cIdent(img.name);
|
|
80
|
+
const hex = Array.from(img.pixels, (p) => '0x' + p.toString(16).toUpperCase().padStart(8, '0') + 'u');
|
|
81
|
+
lines.push(`/* image "${img.name}" — ${img.width}x${img.height} */`);
|
|
82
|
+
lines.push(`static const uint32_t ${id}_px[] = {`);
|
|
83
|
+
lines.push(rows(hex, 8));
|
|
84
|
+
lines.push(`};`);
|
|
85
|
+
lines.push(``);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Fonts ----------------------------------------------------------------------------------
|
|
89
|
+
const fontRegistrations = [];
|
|
90
|
+
for (const font of fonts) {
|
|
91
|
+
const famId = cIdent(font.family);
|
|
92
|
+
lines.push(`/* font "${font.family}" — ${font.sizes.map((s) => `${s.pixelSize}px`).join(', ')} */`);
|
|
93
|
+
emitFontSizes(lines, font, famId, 'static const '); // file-local: only er_register_assets refers to these
|
|
94
|
+
for (const sz of font.sizes) {
|
|
95
|
+
fontRegistrations.push(` er_font_register(${JSON.stringify(font.family)}, &g_font_${famId}_${sz.pixelSize});`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Registration entry point ---------------------------------------------------------------
|
|
100
|
+
lines.push(`void er_register_assets(void)`);
|
|
101
|
+
lines.push(`{`);
|
|
102
|
+
for (const img of images) {
|
|
103
|
+
lines.push(` er_image_load(${JSON.stringify(img.name)}, ${cIdent(img.name)}_px, ${img.width}, ${img.height});`);
|
|
104
|
+
}
|
|
105
|
+
for (const reg of fontRegistrations) lines.push(reg);
|
|
106
|
+
if (images.length === 0 && fontRegistrations.length === 0) {
|
|
107
|
+
lines.push(` /* no baked assets */`);
|
|
108
|
+
}
|
|
109
|
+
lines.push(`}`);
|
|
110
|
+
lines.push(``);
|
|
111
|
+
|
|
112
|
+
return { c: lines.join('\n'), h };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Emits the per-size BitmapFont blocks (bitmap + glyphs + extras + struct) for one font. */
|
|
116
|
+
function emitFontSizes(lines, font, symbol, fontStorage) {
|
|
117
|
+
for (const sz of font.sizes) {
|
|
118
|
+
const prefix = `s_${symbol}_${sz.pixelSize}`;
|
|
119
|
+
const hex = sz.bitmap.map((b) => '0x' + b.toString(16).toUpperCase().padStart(2, '0'));
|
|
120
|
+
lines.push(`static const uint8_t ${prefix}_bitmap[${sz.bitmap.length || 1}] = {`);
|
|
121
|
+
lines.push(sz.bitmap.length ? rows(hex, 16) : ' 0x00,');
|
|
122
|
+
lines.push(`};`);
|
|
123
|
+
lines.push(``);
|
|
124
|
+
|
|
125
|
+
lines.push(`static const GlyphInfo ${prefix}_glyphs[${sz.dense.length}] = {`);
|
|
126
|
+
for (const g of sz.dense) lines.push(` ${glyphInit(g)},`);
|
|
127
|
+
lines.push(`};`);
|
|
128
|
+
lines.push(``);
|
|
129
|
+
|
|
130
|
+
if (sz.extras.length) {
|
|
131
|
+
lines.push(`static const ExtraGlyph ${prefix}_extras[${sz.extras.length}] = {`);
|
|
132
|
+
for (const e of sz.extras) {
|
|
133
|
+
lines.push(` { .codepoint = 0x${e.codepoint.toString(16).toUpperCase()}, .info = ${glyphInit(e.info)} },`);
|
|
134
|
+
}
|
|
135
|
+
lines.push(`};`);
|
|
136
|
+
lines.push(``);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push(`${fontStorage}BitmapFont g_font_${symbol}_${sz.pixelSize} = {`);
|
|
140
|
+
lines.push(` .bitmap = ${prefix}_bitmap,`);
|
|
141
|
+
lines.push(` .glyphs = ${prefix}_glyphs,`);
|
|
142
|
+
lines.push(` .extras = ${sz.extras.length ? `${prefix}_extras` : 'NULL'},`);
|
|
143
|
+
lines.push(` .extras_count = ${sz.extras.length},`);
|
|
144
|
+
lines.push(` .first = 0x${sz.first.toString(16).toUpperCase()},`);
|
|
145
|
+
lines.push(` .last = 0x${sz.last.toString(16).toUpperCase()},`);
|
|
146
|
+
lines.push(` .pixel_size = ${sz.pixelSize},`);
|
|
147
|
+
lines.push(` .line_height = ${sz.lineHeight},`);
|
|
148
|
+
lines.push(` .baseline = ${sz.baseline},`);
|
|
149
|
+
lines.push(` .format = ${sz.bpp},`);
|
|
150
|
+
lines.push(`};`);
|
|
151
|
+
lines.push(``);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Emits the engine's built-in font translation unit (font_data.c format): the per-size BitmapFont
|
|
157
|
+
* structs plus the `g_<symbol>_sizes[]` array + `_count` that font_registry.c falls back to. Depends
|
|
158
|
+
* only on font_bitmap.h. Symbol must be 'inter' to satisfy the extern decls in font_bitmap.h.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} opts
|
|
161
|
+
* @param {object} opts.font Result of bakeFont().
|
|
162
|
+
* @param {string} [opts.symbol] C symbol prefix (default 'inter').
|
|
163
|
+
* @param {string} [opts.sourceName] Source font filename, for the header comment.
|
|
164
|
+
* @returns {string} The .c source text.
|
|
165
|
+
*/
|
|
166
|
+
export function emitBuiltinFont({ font, symbol = 'inter', sourceName = '' }) {
|
|
167
|
+
const sizesStr = font.sizes.map((s) => s.pixelSize).join(',');
|
|
168
|
+
const bpp = font.sizes[0] ? font.sizes[0].bpp : 4;
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push(`/* AUTO-GENERATED by bridges/quickjs/js/assets/build-builtin-font.mjs — do not edit by hand.`);
|
|
171
|
+
lines.push(` * Source : ${sourceName}`);
|
|
172
|
+
lines.push(` * Sizes : ${sizesStr} (bpp ${bpp}, family "${font.family}")`);
|
|
173
|
+
lines.push(` * Regenerate: cd bridges/quickjs/js && npm run build:builtin-font`);
|
|
174
|
+
lines.push(` */`);
|
|
175
|
+
lines.push(`#include "font_bitmap.h"`);
|
|
176
|
+
lines.push(`#include <stddef.h>`);
|
|
177
|
+
lines.push(``);
|
|
178
|
+
|
|
179
|
+
emitFontSizes(lines, font, symbol, 'const '); // file-scope const: referenced by g_<symbol>_sizes
|
|
180
|
+
|
|
181
|
+
lines.push(`const BitmapFont *const g_${symbol}_sizes[] = {`);
|
|
182
|
+
for (const sz of font.sizes) lines.push(` &g_font_${symbol}_${sz.pixelSize},`);
|
|
183
|
+
lines.push(`};`);
|
|
184
|
+
lines.push(`const size_t g_${symbol}_sizes_count = sizeof(g_${symbol}_sizes) / sizeof(g_${symbol}_sizes[0]);`);
|
|
185
|
+
lines.push(``);
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ERCF — the embedded-react config container: one self-describing blob holding an app's compiled
|
|
18
|
+
// QuickJS bytecode plus its asset pack, with a version stamp and an integrity CRC. This is the
|
|
19
|
+
// universal "a config" unit the device-agnostic loader (er_runtime_load_container, in the bridge)
|
|
20
|
+
// verifies and runs — on the desktop demo, an ESP32-S3 partition, or an STM32H7 config region.
|
|
21
|
+
//
|
|
22
|
+
// Two CRCs are NOT the same thing: this internal CRC32 is embedded-react's own integrity check
|
|
23
|
+
// (catches a corrupt/partially-written config). A bootloader's transfer/flash CRC (e.g. SREC_CAT on
|
|
24
|
+
// the STM32H7) is a separate, project-specific step layered on top of this .erpkg by the firmware's
|
|
25
|
+
// upload toolchain.
|
|
26
|
+
//
|
|
27
|
+
// Little-endian (matches the LE hosts: x86 desktop, ESP32-S3, STM32H7). Format:
|
|
28
|
+
//
|
|
29
|
+
// magic "ERCF" (4 bytes)
|
|
30
|
+
// format_version u32 = 1
|
|
31
|
+
// crc32 u32 — CRC-32/IEEE over every byte AFTER this field (offset 12 to end)
|
|
32
|
+
// qjs_tag u16 len + bytes — QuickJS release the bytecode targets (loader rejects mismatch)
|
|
33
|
+
// section_count u32
|
|
34
|
+
// sections[section_count]: type u32, len u32, bytes
|
|
35
|
+
// type 1 = QuickJS bytecode (run last)
|
|
36
|
+
// type 2 = ERPK asset pack (registered before the app mounts)
|
|
37
|
+
|
|
38
|
+
const FORMAT_VERSION = 1;
|
|
39
|
+
|
|
40
|
+
export const SECTION_BYTECODE = 1;
|
|
41
|
+
export const SECTION_ASSET_PACK = 2;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* CRC-32/IEEE (zlib polynomial 0xEDB88320), computed without a lookup table to match the C loader's
|
|
45
|
+
* crc32_bytes() byte-for-byte.
|
|
46
|
+
*
|
|
47
|
+
* @param {Buffer|Uint8Array} buf Bytes to checksum.
|
|
48
|
+
* @returns {number} The CRC as an unsigned 32-bit integer.
|
|
49
|
+
*/
|
|
50
|
+
export function crc32(buf) {
|
|
51
|
+
let crc = 0xffffffff;
|
|
52
|
+
for (let i = 0; i < buf.length; i++) {
|
|
53
|
+
crc ^= buf[i];
|
|
54
|
+
for (let b = 0; b < 8; b++) {
|
|
55
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Accumulates little-endian binary chunks (mirrors emit-pack.mjs's Writer). */
|
|
62
|
+
class Writer {
|
|
63
|
+
constructor() {
|
|
64
|
+
this.chunks = [];
|
|
65
|
+
}
|
|
66
|
+
u16(v) {
|
|
67
|
+
const b = Buffer.alloc(2);
|
|
68
|
+
b.writeUInt16LE(v & 0xffff, 0);
|
|
69
|
+
this.chunks.push(b);
|
|
70
|
+
}
|
|
71
|
+
u32(v) {
|
|
72
|
+
const b = Buffer.alloc(4);
|
|
73
|
+
b.writeUInt32LE(v >>> 0, 0);
|
|
74
|
+
this.chunks.push(b);
|
|
75
|
+
}
|
|
76
|
+
str(s) {
|
|
77
|
+
const b = Buffer.from(s, 'utf8');
|
|
78
|
+
this.u16(b.length);
|
|
79
|
+
this.chunks.push(b);
|
|
80
|
+
}
|
|
81
|
+
bytes(buf) {
|
|
82
|
+
this.chunks.push(Buffer.from(buf));
|
|
83
|
+
}
|
|
84
|
+
done() {
|
|
85
|
+
return Buffer.concat(this.chunks);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Serializes a compiled app + assets into an ERCF container.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} opts
|
|
93
|
+
* @param {Buffer|Uint8Array} opts.bytecode QuickJS bytecode blob (.qbc) — required.
|
|
94
|
+
* @param {Buffer|Uint8Array} [opts.assetPack] ERPK asset pack bytes; omitted/empty → no asset section.
|
|
95
|
+
* @param {string} opts.qjsTag QuickJS release tag the bytecode targets (e.g. "v0.15.0").
|
|
96
|
+
* @returns {Buffer} The container bytes.
|
|
97
|
+
*/
|
|
98
|
+
export function emitContainer({ bytecode, assetPack, qjsTag }) {
|
|
99
|
+
if (!bytecode || !bytecode.length) throw new Error('emitContainer: bytecode is required');
|
|
100
|
+
if (!qjsTag) throw new Error('emitContainer: qjsTag is required');
|
|
101
|
+
|
|
102
|
+
// Build the body (everything after the crc32 field) first, then prepend magic+version+crc.
|
|
103
|
+
const body = new Writer();
|
|
104
|
+
body.str(qjsTag);
|
|
105
|
+
const sections = [];
|
|
106
|
+
if (assetPack && assetPack.length) sections.push([SECTION_ASSET_PACK, assetPack]);
|
|
107
|
+
sections.push([SECTION_BYTECODE, bytecode]);
|
|
108
|
+
body.u32(sections.length);
|
|
109
|
+
for (const [type, data] of sections) {
|
|
110
|
+
body.u32(type);
|
|
111
|
+
body.u32(data.length);
|
|
112
|
+
body.bytes(data);
|
|
113
|
+
}
|
|
114
|
+
const bodyBytes = body.done();
|
|
115
|
+
|
|
116
|
+
const head = new Writer();
|
|
117
|
+
head.bytes(Buffer.from('ERCF', 'ascii'));
|
|
118
|
+
head.u32(FORMAT_VERSION);
|
|
119
|
+
head.u32(crc32(bodyBytes));
|
|
120
|
+
return Buffer.concat([head.done(), bodyBytes]);
|
|
121
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ERPK — a binary asset pack for the simulator's runtime loader (see /SIMULATOR.md, Phase 2b).
|
|
18
|
+
//
|
|
19
|
+
// Produced from the SAME JS bakers (bake-image.mjs / bake-font.mjs) as the device's baked C, so the
|
|
20
|
+
// simulator renders pixel-identical images and fonts. The sim host (tools/simulator/asset_pack.c)
|
|
21
|
+
// loads this at runtime and calls er_image_load / er_font_register, so assets hot-reload without a
|
|
22
|
+
// sim rebuild. Little-endian (the simulator is x86). Format:
|
|
23
|
+
//
|
|
24
|
+
// magic "ERPK" (4 bytes), version u32=1, n_images u32, n_font_sizes u32
|
|
25
|
+
// images[n_images]: name(u16 len + bytes), width u32, height u32, pixels (w*h * u32 ARGB)
|
|
26
|
+
// fonts[n_font_sizes]: family(u16 len + bytes), pixel_size/line_height/baseline/format (4*u8),
|
|
27
|
+
// first u16, last u16, glyph_count u16, extras_count u16, bitmap_len u32,
|
|
28
|
+
// glyphs[glyph_count] (9B: off u32, w/h u8, xo/yo i8, adv u8),
|
|
29
|
+
// extras[extras_count] (13B: codepoint u32 + the 9B glyph),
|
|
30
|
+
// bitmap (bitmap_len bytes)
|
|
31
|
+
|
|
32
|
+
const VERSION = 1;
|
|
33
|
+
|
|
34
|
+
/** Accumulates little-endian binary chunks. */
|
|
35
|
+
class Writer {
|
|
36
|
+
constructor() {
|
|
37
|
+
this.chunks = [];
|
|
38
|
+
}
|
|
39
|
+
u8(v) {
|
|
40
|
+
const b = Buffer.alloc(1);
|
|
41
|
+
b.writeUInt8(v & 0xff, 0);
|
|
42
|
+
this.chunks.push(b);
|
|
43
|
+
}
|
|
44
|
+
i8(v) {
|
|
45
|
+
const b = Buffer.alloc(1);
|
|
46
|
+
b.writeInt8(Math.max(-128, Math.min(127, v | 0)), 0);
|
|
47
|
+
this.chunks.push(b);
|
|
48
|
+
}
|
|
49
|
+
u16(v) {
|
|
50
|
+
const b = Buffer.alloc(2);
|
|
51
|
+
b.writeUInt16LE(v & 0xffff, 0);
|
|
52
|
+
this.chunks.push(b);
|
|
53
|
+
}
|
|
54
|
+
u32(v) {
|
|
55
|
+
const b = Buffer.alloc(4);
|
|
56
|
+
b.writeUInt32LE(v >>> 0, 0);
|
|
57
|
+
this.chunks.push(b);
|
|
58
|
+
}
|
|
59
|
+
str(s) {
|
|
60
|
+
const b = Buffer.from(s, 'utf8');
|
|
61
|
+
this.u16(b.length);
|
|
62
|
+
this.chunks.push(b);
|
|
63
|
+
}
|
|
64
|
+
bytes(buf) {
|
|
65
|
+
this.chunks.push(Buffer.from(buf));
|
|
66
|
+
}
|
|
67
|
+
done() {
|
|
68
|
+
return Buffer.concat(this.chunks);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Writes one glyph record (9 bytes). */
|
|
73
|
+
function writeGlyph(w, g) {
|
|
74
|
+
w.u32(g.bitmapOffset);
|
|
75
|
+
w.u8(g.width);
|
|
76
|
+
w.u8(g.height);
|
|
77
|
+
w.i8(g.xOffset);
|
|
78
|
+
w.i8(g.yOffset);
|
|
79
|
+
w.u8(g.advance);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Serializes baked images + fonts into an ERPK pack.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} opts
|
|
86
|
+
* @param {Array<object>} [opts.images] Results of bakeImage().
|
|
87
|
+
* @param {Array<object>} [opts.fonts] Results of bakeFont().
|
|
88
|
+
* @returns {Buffer} The pack bytes.
|
|
89
|
+
*/
|
|
90
|
+
export function emitAssetPack({ images = [], fonts = [] }) {
|
|
91
|
+
const w = new Writer();
|
|
92
|
+
w.bytes(Buffer.from('ERPK', 'ascii'));
|
|
93
|
+
w.u32(VERSION);
|
|
94
|
+
w.u32(images.length);
|
|
95
|
+
const fontSizes = fonts.reduce((n, f) => n + f.sizes.length, 0);
|
|
96
|
+
w.u32(fontSizes);
|
|
97
|
+
|
|
98
|
+
for (const img of images) {
|
|
99
|
+
w.str(img.name);
|
|
100
|
+
w.u32(img.width);
|
|
101
|
+
w.u32(img.height);
|
|
102
|
+
// pixels is a Uint32Array (premultiplied ARGB words); copy its LE bytes verbatim.
|
|
103
|
+
w.bytes(Buffer.from(img.pixels.buffer, img.pixels.byteOffset, img.pixels.byteLength));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const font of fonts) {
|
|
107
|
+
for (const sz of font.sizes) {
|
|
108
|
+
w.str(font.family);
|
|
109
|
+
w.u8(sz.pixelSize);
|
|
110
|
+
w.u8(sz.lineHeight);
|
|
111
|
+
w.u8(sz.baseline);
|
|
112
|
+
w.u8(sz.bpp);
|
|
113
|
+
w.u16(sz.first);
|
|
114
|
+
w.u16(sz.last);
|
|
115
|
+
w.u16(sz.dense.length);
|
|
116
|
+
w.u16(sz.extras.length);
|
|
117
|
+
w.u32(sz.bitmap.length);
|
|
118
|
+
for (const g of sz.dense) writeGlyph(w, g);
|
|
119
|
+
for (const e of sz.extras) {
|
|
120
|
+
w.u32(e.codepoint);
|
|
121
|
+
writeGlyph(w, e.info);
|
|
122
|
+
}
|
|
123
|
+
w.bytes(Buffer.from(sz.bitmap));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return w.done();
|
|
128
|
+
}
|
package/assets/index.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Asset baking orchestrator: turns the images and fonts an app imports into a single generated C
|
|
18
|
+
// translation unit (assets.generated.c + .h) exposing er_register_assets(). Invoked by build.mjs
|
|
19
|
+
// after bundling; the example firmware compiles the .c and calls er_register_assets() at boot.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { bakeImage } from './bake-image.mjs';
|
|
23
|
+
import { bakeFont } from './bake-font.mjs';
|
|
24
|
+
import { emitAssetsC } from './emit-c.mjs';
|
|
25
|
+
import { emitAssetPack } from './emit-pack.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bakes the given assets and writes assets.generated.{c,h} into outDir.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} opts
|
|
31
|
+
* @param {Array<{path:string,name:string}>} [opts.images] Discovered image imports.
|
|
32
|
+
* @param {Array<{path:string,family:string,sizes:number[],bpp:number,glyphs:any}>} [opts.fonts]
|
|
33
|
+
* Discovered font imports with their resolved size/bpp/glyph config.
|
|
34
|
+
* @param {string} opts.outDir Directory to write the generated files into.
|
|
35
|
+
* @returns {{cPath:string, hPath:string, images:number, fonts:number}}
|
|
36
|
+
*/
|
|
37
|
+
export function bakeAssets({ images = [], fonts = [], outDir }) {
|
|
38
|
+
const bakedImages = images.map((i) => bakeImage(i));
|
|
39
|
+
const bakedFonts = fonts.map((f) => bakeFont(f));
|
|
40
|
+
|
|
41
|
+
const headerName = 'assets.generated.h';
|
|
42
|
+
const { c, h } = emitAssetsC({ headerName, images: bakedImages, fonts: bakedFonts });
|
|
43
|
+
|
|
44
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
45
|
+
const cPath = path.join(outDir, 'assets.generated.c');
|
|
46
|
+
const hPath = path.join(outDir, headerName);
|
|
47
|
+
fs.writeFileSync(cPath, c);
|
|
48
|
+
fs.writeFileSync(hPath, h);
|
|
49
|
+
|
|
50
|
+
const fontSizes = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
|
|
51
|
+
return { cPath, hPath, images: bakedImages.length, fonts: fontSizes };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Bakes the given assets into a binary ERPK pack the simulator loads at runtime (hot-reloadable),
|
|
56
|
+
* using the same bakers as bakeAssets so the result is pixel-identical.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} opts
|
|
59
|
+
* @param {Array<{path:string,name:string}>} [opts.images]
|
|
60
|
+
* @param {Array<{path:string,family:string,sizes:number[],bpp:number,glyphs:any}>} [opts.fonts]
|
|
61
|
+
* @param {string} opts.outPath Path to write the .pack file.
|
|
62
|
+
* @returns {{path:string, bytes:number, images:number, fonts:number}}
|
|
63
|
+
*/
|
|
64
|
+
export function bakeAssetPack({ images = [], fonts = [], outPath }) {
|
|
65
|
+
const bakedImages = images.map((i) => bakeImage(i));
|
|
66
|
+
const bakedFonts = fonts.map((f) => bakeFont(f));
|
|
67
|
+
const pack = emitAssetPack({ images: bakedImages, fonts: bakedFonts });
|
|
68
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
69
|
+
fs.writeFileSync(outPath, pack);
|
|
70
|
+
const fontSizes = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
|
|
71
|
+
return { path: outPath, bytes: pack.length, images: bakedImages.length, fonts: fontSizes };
|
|
72
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Pure-JS glyph rasterizer — fills an opentype.js Path into a grayscale coverage bitmap with no
|
|
18
|
+
// native dependencies. Curves are flattened to polylines, then each pixel's coverage is computed by
|
|
19
|
+
// scanline-filling at SS× supersampling using the nonzero winding rule (TrueType's convention).
|
|
20
|
+
//
|
|
21
|
+
// The output convention matches what the engine's text renderer expects (see draw_glyph /
|
|
22
|
+
// draw_glyph_aa in engine/text/text_renderer.c): coordinates come from glyph.getPath(0, baseline,
|
|
23
|
+
// pixelSize), so xOffset is the glyph's left bearing from the pen and yOffset is the distance from
|
|
24
|
+
// the line box top (y = 0) down to the top of the glyph ink.
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Flattens opentype path commands into closed contours of {x, y} points.
|
|
28
|
+
*
|
|
29
|
+
* @param {Array} commands opentype.js Path commands (M/L/Q/C/Z).
|
|
30
|
+
* @param {number} steps Subdivision steps per Bézier curve.
|
|
31
|
+
* @returns {Array<Array<{x:number,y:number}>>} One point array per contour.
|
|
32
|
+
*/
|
|
33
|
+
function flattenCommands(commands, steps) {
|
|
34
|
+
const contours = [];
|
|
35
|
+
let cur = null;
|
|
36
|
+
let cx = 0;
|
|
37
|
+
let cy = 0; // current point
|
|
38
|
+
let sx = 0;
|
|
39
|
+
let sy = 0; // contour start
|
|
40
|
+
for (const c of commands) {
|
|
41
|
+
if (c.type === 'M') {
|
|
42
|
+
if (cur && cur.length > 1) contours.push(cur);
|
|
43
|
+
cur = [{ x: c.x, y: c.y }];
|
|
44
|
+
cx = sx = c.x;
|
|
45
|
+
cy = sy = c.y;
|
|
46
|
+
} else if (c.type === 'L') {
|
|
47
|
+
cur.push({ x: c.x, y: c.y });
|
|
48
|
+
cx = c.x;
|
|
49
|
+
cy = c.y;
|
|
50
|
+
} else if (c.type === 'Q') {
|
|
51
|
+
for (let i = 1; i <= steps; i++) {
|
|
52
|
+
const t = i / steps;
|
|
53
|
+
const mt = 1 - t;
|
|
54
|
+
cur.push({
|
|
55
|
+
x: mt * mt * cx + 2 * mt * t * c.x1 + t * t * c.x,
|
|
56
|
+
y: mt * mt * cy + 2 * mt * t * c.y1 + t * t * c.y,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
cx = c.x;
|
|
60
|
+
cy = c.y;
|
|
61
|
+
} else if (c.type === 'C') {
|
|
62
|
+
for (let i = 1; i <= steps; i++) {
|
|
63
|
+
const t = i / steps;
|
|
64
|
+
const mt = 1 - t;
|
|
65
|
+
cur.push({
|
|
66
|
+
x: mt * mt * mt * cx + 3 * mt * mt * t * c.x1 + 3 * mt * t * t * c.x2 + t * t * t * c.x,
|
|
67
|
+
y: mt * mt * mt * cy + 3 * mt * mt * t * c.y1 + 3 * mt * t * t * c.y2 + t * t * t * c.y,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
cx = c.x;
|
|
71
|
+
cy = c.y;
|
|
72
|
+
} else if (c.type === 'Z') {
|
|
73
|
+
if (cur) {
|
|
74
|
+
cur.push({ x: sx, y: sy }); // close the contour
|
|
75
|
+
if (cur.length > 1) contours.push(cur);
|
|
76
|
+
cur = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (cur && cur.length > 1) contours.push(cur);
|
|
81
|
+
return contours;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rasterizes a glyph path to an 8-bit coverage bitmap (0-255 per pixel).
|
|
86
|
+
*
|
|
87
|
+
* @param {object} path opentype.js Path (from glyph.getPath(0, baseline, pixelSize)).
|
|
88
|
+
* @param {object} [opts]
|
|
89
|
+
* @param {number} [opts.ss] Supersample factor per axis (coverage levels = ss*ss). Default 4.
|
|
90
|
+
* @param {number} [opts.steps] Bézier subdivision steps. Default 12.
|
|
91
|
+
* @returns {{width:number,height:number,xOffset:number,yOffset:number,coverage:Uint8Array}}
|
|
92
|
+
* An empty (width 0, height 0) result for whitespace / glyphs with no ink.
|
|
93
|
+
*/
|
|
94
|
+
export function rasterize(path, opts = {}) {
|
|
95
|
+
const ss = opts.ss || 4;
|
|
96
|
+
const steps = opts.steps || 12;
|
|
97
|
+
const contours = flattenCommands(path.commands, steps);
|
|
98
|
+
|
|
99
|
+
let minX = Infinity;
|
|
100
|
+
let minY = Infinity;
|
|
101
|
+
let maxX = -Infinity;
|
|
102
|
+
let maxY = -Infinity;
|
|
103
|
+
for (const ct of contours) {
|
|
104
|
+
for (const p of ct) {
|
|
105
|
+
if (p.x < minX) minX = p.x;
|
|
106
|
+
if (p.x > maxX) maxX = p.x;
|
|
107
|
+
if (p.y < minY) minY = p.y;
|
|
108
|
+
if (p.y > maxY) maxY = p.y;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isFinite(minX)) {
|
|
112
|
+
return { width: 0, height: 0, xOffset: 0, yOffset: 0, coverage: new Uint8Array(0) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const x0 = Math.floor(minX);
|
|
116
|
+
const y0 = Math.floor(minY);
|
|
117
|
+
const width = Math.ceil(maxX) - x0;
|
|
118
|
+
const height = Math.ceil(maxY) - y0;
|
|
119
|
+
if (width <= 0 || height <= 0) {
|
|
120
|
+
return { width: 0, height: 0, xOffset: 0, yOffset: 0, coverage: new Uint8Array(0) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Non-horizontal edges, normalized so ylo < yhi; dir is the winding contribution.
|
|
124
|
+
const edges = [];
|
|
125
|
+
for (const ct of contours) {
|
|
126
|
+
for (let i = 0; i + 1 < ct.length; i++) {
|
|
127
|
+
const a = ct[i];
|
|
128
|
+
const b = ct[i + 1];
|
|
129
|
+
if (a.y === b.y) continue;
|
|
130
|
+
if (a.y < b.y) edges.push({ ylo: a.y, yhi: b.y, x: a.x, dxdy: (b.x - a.x) / (b.y - a.y), dir: 1 });
|
|
131
|
+
else edges.push({ ylo: b.y, yhi: a.y, x: b.x, dxdy: (a.x - b.x) / (a.y - b.y), dir: -1 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const counts = new Uint16Array(width * height); // covered subsamples per output pixel
|
|
136
|
+
const subW = width * ss;
|
|
137
|
+
for (let sy = 0; sy < height * ss; sy++) {
|
|
138
|
+
const sampleY = y0 + (sy + 0.5) / ss;
|
|
139
|
+
const xs = [];
|
|
140
|
+
for (const e of edges) {
|
|
141
|
+
if (sampleY >= e.ylo && sampleY < e.yhi) {
|
|
142
|
+
xs.push({ x: e.x + (sampleY - e.ylo) * e.dxdy, dir: e.dir });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (xs.length < 2) continue;
|
|
146
|
+
xs.sort((p, q) => p.x - q.x);
|
|
147
|
+
const outRow = (sy / ss) | 0;
|
|
148
|
+
let wind = 0;
|
|
149
|
+
for (let i = 0; i + 1 < xs.length; i++) {
|
|
150
|
+
wind += xs[i].dir;
|
|
151
|
+
if (wind === 0) continue;
|
|
152
|
+
// Interior span [xs[i].x, xs[i+1].x): tally the subsample columns whose centre falls inside.
|
|
153
|
+
let sxa = Math.ceil((xs[i].x - x0) * ss - 0.5);
|
|
154
|
+
let sxb = Math.ceil((xs[i + 1].x - x0) * ss - 0.5);
|
|
155
|
+
if (sxa < 0) sxa = 0;
|
|
156
|
+
if (sxb > subW) sxb = subW;
|
|
157
|
+
for (let sx = sxa; sx < sxb; sx++) {
|
|
158
|
+
counts[outRow * width + ((sx / ss) | 0)]++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const maxCov = ss * ss;
|
|
164
|
+
const coverage = new Uint8Array(width * height);
|
|
165
|
+
for (let i = 0; i < coverage.length; i++) {
|
|
166
|
+
coverage[i] = Math.round((counts[i] * 255) / maxCov);
|
|
167
|
+
}
|
|
168
|
+
return { width, height, xOffset: x0, yOffset: y0, coverage };
|
|
169
|
+
}
|