embedded-react 0.3.0 → 0.4.1
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/aot/compile.mjs +2407 -697
- package/aot/screenshot-smoke.mjs +34 -17
- package/aot/style-map.mjs +156 -80
- package/assets/bake-font.mjs +45 -21
- package/assets/bake-image.mjs +7 -5
- package/assets/bake-svg.mjs +563 -0
- package/assets/build-builtin-font.mjs +25 -12
- package/assets/emit-c.mjs +52 -20
- package/assets/emit-container.mjs +5 -3
- package/assets/emit-pack.mjs +8 -2
- package/assets/index.mjs +25 -16
- package/assets/rasterize.mjs +45 -11
- package/assets/svg-loader.mjs +81 -0
- package/build.mjs +43 -20
- package/cli.mjs +134 -52
- package/pack-container.mjs +84 -35
- package/package.json +8 -3
- package/persist-transform.mjs +23 -9
- package/qjsc-wasm.mjs +19 -8
- package/sim/embedded-react.wasm +0 -0
- package/sim-server.mjs +160 -48
- package/src/embedded-react/Animated.js +51 -36
- package/src/embedded-react/AppRegistry.js +4 -4
- package/src/embedded-react/Easing.js +1 -1
- package/src/embedded-react/LayoutAnimation.js +13 -6
- package/src/embedded-react/StyleSheet.js +1 -1
- package/src/embedded-react/imperative.js +19 -7
- package/src/embedded-react/index.js +8 -8
- package/src/embedded-react/layout-anim-config.js +13 -9
- package/src/embedded-react/split-style.js +6 -6
- package/src/embedded-react/svg-ops.js +369 -41
- package/src/embedded-react/usePersistentState.js +3 -3
- package/src/host-config.js +137 -18
- package/src/native-ui.js +3 -1
- package/src/props.js +22 -10
- package/src/renderer.js +3 -3
package/assets/emit-c.mjs
CHANGED
|
@@ -52,7 +52,7 @@ function glyphInit(g) {
|
|
|
52
52
|
* @param {Array<object>} [opts.fonts] Results from bakeFont().
|
|
53
53
|
* @returns {{c:string, h:string}}
|
|
54
54
|
*/
|
|
55
|
-
export function emitAssetsC({
|
|
55
|
+
export function emitAssetsC({headerName, images = [], fonts = []}) {
|
|
56
56
|
const guard = 'ER_' + cIdent(headerName).toUpperCase();
|
|
57
57
|
const h =
|
|
58
58
|
`/* Generated by bridges/quickjs/js/assets — DO NOT EDIT. */\n` +
|
|
@@ -66,10 +66,14 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
|
|
|
66
66
|
|
|
67
67
|
const lines = [];
|
|
68
68
|
lines.push(`/* Generated by bridges/quickjs/js/assets — DO NOT EDIT. */`);
|
|
69
|
-
lines.push(
|
|
69
|
+
lines.push(
|
|
70
|
+
`/* Images: premultiplied ARGB8888, referenced by pointer. Fonts: baked BitmapFont per size. */`,
|
|
71
|
+
);
|
|
70
72
|
lines.push(``);
|
|
71
73
|
lines.push(`#include "${headerName}"`);
|
|
72
|
-
lines.push(
|
|
74
|
+
lines.push(
|
|
75
|
+
`#include "er_scene.h" /* er_image_load, er_font_register, BitmapFont */`,
|
|
76
|
+
);
|
|
73
77
|
lines.push(`#include <stddef.h>`);
|
|
74
78
|
lines.push(`#include <stdint.h>`);
|
|
75
79
|
lines.push(``);
|
|
@@ -77,7 +81,10 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
|
|
|
77
81
|
// --- Images ---------------------------------------------------------------------------------
|
|
78
82
|
for (const img of images) {
|
|
79
83
|
const id = cIdent(img.name);
|
|
80
|
-
const hex = Array.from(
|
|
84
|
+
const hex = Array.from(
|
|
85
|
+
img.pixels,
|
|
86
|
+
p => '0x' + p.toString(16).toUpperCase().padStart(8, '0') + 'u',
|
|
87
|
+
);
|
|
81
88
|
lines.push(`/* image "${img.name}" — ${img.width}x${img.height} */`);
|
|
82
89
|
lines.push(`static const uint32_t ${id}_px[] = {`);
|
|
83
90
|
lines.push(rows(hex, 8));
|
|
@@ -89,10 +96,14 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
|
|
|
89
96
|
const fontRegistrations = [];
|
|
90
97
|
for (const font of fonts) {
|
|
91
98
|
const famId = cIdent(font.family);
|
|
92
|
-
lines.push(
|
|
99
|
+
lines.push(
|
|
100
|
+
`/* font "${font.family}" — ${font.sizes.map(s => `${s.pixelSize}px`).join(', ')} */`,
|
|
101
|
+
);
|
|
93
102
|
emitFontSizes(lines, font, famId, 'static const '); // file-local: only er_register_assets refers to these
|
|
94
103
|
for (const sz of font.sizes) {
|
|
95
|
-
fontRegistrations.push(
|
|
104
|
+
fontRegistrations.push(
|
|
105
|
+
` er_font_register(${JSON.stringify(font.family)}, &g_font_${famId}_${sz.pixelSize});`,
|
|
106
|
+
);
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
|
|
@@ -100,7 +111,9 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
|
|
|
100
111
|
lines.push(`void er_register_assets(void)`);
|
|
101
112
|
lines.push(`{`);
|
|
102
113
|
for (const img of images) {
|
|
103
|
-
lines.push(
|
|
114
|
+
lines.push(
|
|
115
|
+
` er_image_load(${JSON.stringify(img.name)}, ${cIdent(img.name)}_px, ${img.width}, ${img.height});`,
|
|
116
|
+
);
|
|
104
117
|
}
|
|
105
118
|
for (const reg of fontRegistrations) lines.push(reg);
|
|
106
119
|
if (images.length === 0 && fontRegistrations.length === 0) {
|
|
@@ -109,28 +122,38 @@ export function emitAssetsC({ headerName, images = [], fonts = [] }) {
|
|
|
109
122
|
lines.push(`}`);
|
|
110
123
|
lines.push(``);
|
|
111
124
|
|
|
112
|
-
return {
|
|
125
|
+
return {c: lines.join('\n'), h};
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
/** Emits the per-size BitmapFont blocks (bitmap + glyphs + extras + struct) for one font. */
|
|
116
129
|
function emitFontSizes(lines, font, symbol, fontStorage) {
|
|
117
130
|
for (const sz of font.sizes) {
|
|
118
131
|
const prefix = `s_${symbol}_${sz.pixelSize}`;
|
|
119
|
-
const hex = sz.bitmap.map(
|
|
120
|
-
|
|
132
|
+
const hex = sz.bitmap.map(
|
|
133
|
+
b => '0x' + b.toString(16).toUpperCase().padStart(2, '0'),
|
|
134
|
+
);
|
|
135
|
+
lines.push(
|
|
136
|
+
`static const uint8_t ${prefix}_bitmap[${sz.bitmap.length || 1}] = {`,
|
|
137
|
+
);
|
|
121
138
|
lines.push(sz.bitmap.length ? rows(hex, 16) : ' 0x00,');
|
|
122
139
|
lines.push(`};`);
|
|
123
140
|
lines.push(``);
|
|
124
141
|
|
|
125
|
-
lines.push(
|
|
142
|
+
lines.push(
|
|
143
|
+
`static const GlyphInfo ${prefix}_glyphs[${sz.dense.length}] = {`,
|
|
144
|
+
);
|
|
126
145
|
for (const g of sz.dense) lines.push(` ${glyphInit(g)},`);
|
|
127
146
|
lines.push(`};`);
|
|
128
147
|
lines.push(``);
|
|
129
148
|
|
|
130
149
|
if (sz.extras.length) {
|
|
131
|
-
lines.push(
|
|
150
|
+
lines.push(
|
|
151
|
+
`static const ExtraGlyph ${prefix}_extras[${sz.extras.length}] = {`,
|
|
152
|
+
);
|
|
132
153
|
for (const e of sz.extras) {
|
|
133
|
-
lines.push(
|
|
154
|
+
lines.push(
|
|
155
|
+
` { .codepoint = 0x${e.codepoint.toString(16).toUpperCase()}, .info = ${glyphInit(e.info)} },`,
|
|
156
|
+
);
|
|
134
157
|
}
|
|
135
158
|
lines.push(`};`);
|
|
136
159
|
lines.push(``);
|
|
@@ -139,7 +162,9 @@ function emitFontSizes(lines, font, symbol, fontStorage) {
|
|
|
139
162
|
lines.push(`${fontStorage}BitmapFont g_font_${symbol}_${sz.pixelSize} = {`);
|
|
140
163
|
lines.push(` .bitmap = ${prefix}_bitmap,`);
|
|
141
164
|
lines.push(` .glyphs = ${prefix}_glyphs,`);
|
|
142
|
-
lines.push(
|
|
165
|
+
lines.push(
|
|
166
|
+
` .extras = ${sz.extras.length ? `${prefix}_extras` : 'NULL'},`,
|
|
167
|
+
);
|
|
143
168
|
lines.push(` .extras_count = ${sz.extras.length},`);
|
|
144
169
|
lines.push(` .first = 0x${sz.first.toString(16).toUpperCase()},`);
|
|
145
170
|
lines.push(` .last = 0x${sz.last.toString(16).toUpperCase()},`);
|
|
@@ -163,14 +188,18 @@ function emitFontSizes(lines, font, symbol, fontStorage) {
|
|
|
163
188
|
* @param {string} [opts.sourceName] Source font filename, for the header comment.
|
|
164
189
|
* @returns {string} The .c source text.
|
|
165
190
|
*/
|
|
166
|
-
export function emitBuiltinFont({
|
|
167
|
-
const sizesStr = font.sizes.map(
|
|
191
|
+
export function emitBuiltinFont({font, symbol = 'inter', sourceName = ''}) {
|
|
192
|
+
const sizesStr = font.sizes.map(s => s.pixelSize).join(',');
|
|
168
193
|
const bpp = font.sizes[0] ? font.sizes[0].bpp : 4;
|
|
169
194
|
const lines = [];
|
|
170
|
-
lines.push(
|
|
195
|
+
lines.push(
|
|
196
|
+
`/* AUTO-GENERATED by bridges/quickjs/js/assets/build-builtin-font.mjs — do not edit by hand.`,
|
|
197
|
+
);
|
|
171
198
|
lines.push(` * Source : ${sourceName}`);
|
|
172
199
|
lines.push(` * Sizes : ${sizesStr} (bpp ${bpp}, family "${font.family}")`);
|
|
173
|
-
lines.push(
|
|
200
|
+
lines.push(
|
|
201
|
+
` * Regenerate: cd bridges/quickjs/js && npm run build:builtin-font`,
|
|
202
|
+
);
|
|
174
203
|
lines.push(` */`);
|
|
175
204
|
lines.push(`#include "font_bitmap.h"`);
|
|
176
205
|
lines.push(`#include <stddef.h>`);
|
|
@@ -179,9 +208,12 @@ export function emitBuiltinFont({ font, symbol = 'inter', sourceName = '' }) {
|
|
|
179
208
|
emitFontSizes(lines, font, symbol, 'const '); // file-scope const: referenced by g_<symbol>_sizes
|
|
180
209
|
|
|
181
210
|
lines.push(`const BitmapFont *const g_${symbol}_sizes[] = {`);
|
|
182
|
-
for (const sz of font.sizes)
|
|
211
|
+
for (const sz of font.sizes)
|
|
212
|
+
lines.push(` &g_font_${symbol}_${sz.pixelSize},`);
|
|
183
213
|
lines.push(`};`);
|
|
184
|
-
lines.push(
|
|
214
|
+
lines.push(
|
|
215
|
+
`const size_t g_${symbol}_sizes_count = sizeof(g_${symbol}_sizes) / sizeof(g_${symbol}_sizes[0]);`,
|
|
216
|
+
);
|
|
185
217
|
lines.push(``);
|
|
186
218
|
return lines.join('\n');
|
|
187
219
|
}
|
|
@@ -95,15 +95,17 @@ class Writer {
|
|
|
95
95
|
* @param {string} opts.qjsTag QuickJS release tag the bytecode targets (e.g. "v0.15.0").
|
|
96
96
|
* @returns {Buffer} The container bytes.
|
|
97
97
|
*/
|
|
98
|
-
export function emitContainer({
|
|
99
|
-
if (!bytecode || !bytecode.length)
|
|
98
|
+
export function emitContainer({bytecode, assetPack, qjsTag}) {
|
|
99
|
+
if (!bytecode || !bytecode.length)
|
|
100
|
+
throw new Error('emitContainer: bytecode is required');
|
|
100
101
|
if (!qjsTag) throw new Error('emitContainer: qjsTag is required');
|
|
101
102
|
|
|
102
103
|
// Build the body (everything after the crc32 field) first, then prepend magic+version+crc.
|
|
103
104
|
const body = new Writer();
|
|
104
105
|
body.str(qjsTag);
|
|
105
106
|
const sections = [];
|
|
106
|
-
if (assetPack && assetPack.length)
|
|
107
|
+
if (assetPack && assetPack.length)
|
|
108
|
+
sections.push([SECTION_ASSET_PACK, assetPack]);
|
|
107
109
|
sections.push([SECTION_BYTECODE, bytecode]);
|
|
108
110
|
body.u32(sections.length);
|
|
109
111
|
for (const [type, data] of sections) {
|
package/assets/emit-pack.mjs
CHANGED
|
@@ -87,7 +87,7 @@ function writeGlyph(w, g) {
|
|
|
87
87
|
* @param {Array<object>} [opts.fonts] Results of bakeFont().
|
|
88
88
|
* @returns {Buffer} The pack bytes.
|
|
89
89
|
*/
|
|
90
|
-
export function emitAssetPack({
|
|
90
|
+
export function emitAssetPack({images = [], fonts = []}) {
|
|
91
91
|
const w = new Writer();
|
|
92
92
|
w.bytes(Buffer.from('ERPK', 'ascii'));
|
|
93
93
|
w.u32(VERSION);
|
|
@@ -100,7 +100,13 @@ export function emitAssetPack({ images = [], fonts = [] }) {
|
|
|
100
100
|
w.u32(img.width);
|
|
101
101
|
w.u32(img.height);
|
|
102
102
|
// pixels is a Uint32Array (premultiplied ARGB words); copy its LE bytes verbatim.
|
|
103
|
-
w.bytes(
|
|
103
|
+
w.bytes(
|
|
104
|
+
Buffer.from(
|
|
105
|
+
img.pixels.buffer,
|
|
106
|
+
img.pixels.byteOffset,
|
|
107
|
+
img.pixels.byteLength,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
for (const font of fonts) {
|
package/assets/index.mjs
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
// after bundling; the example firmware compiles the .c and calls er_register_assets() at boot.
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
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
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Bakes the given assets and writes assets.generated.{c,h} into outDir.
|
|
@@ -34,21 +34,25 @@ import { emitAssetPack } from './emit-pack.mjs';
|
|
|
34
34
|
* @param {string} opts.outDir Directory to write the generated files into.
|
|
35
35
|
* @returns {{cPath:string, hPath:string, images:number, fonts:number}}
|
|
36
36
|
*/
|
|
37
|
-
export function bakeAssets({
|
|
38
|
-
const bakedImages = images.map(
|
|
39
|
-
const bakedFonts = fonts.map(
|
|
37
|
+
export function bakeAssets({images = [], fonts = [], outDir}) {
|
|
38
|
+
const bakedImages = images.map(i => bakeImage(i));
|
|
39
|
+
const bakedFonts = fonts.map(f => bakeFont(f));
|
|
40
40
|
|
|
41
41
|
const headerName = 'assets.generated.h';
|
|
42
|
-
const {
|
|
42
|
+
const {c, h} = emitAssetsC({
|
|
43
|
+
headerName,
|
|
44
|
+
images: bakedImages,
|
|
45
|
+
fonts: bakedFonts,
|
|
46
|
+
});
|
|
43
47
|
|
|
44
|
-
fs.mkdirSync(outDir, {
|
|
48
|
+
fs.mkdirSync(outDir, {recursive: true});
|
|
45
49
|
const cPath = path.join(outDir, 'assets.generated.c');
|
|
46
50
|
const hPath = path.join(outDir, headerName);
|
|
47
51
|
fs.writeFileSync(cPath, c);
|
|
48
52
|
fs.writeFileSync(hPath, h);
|
|
49
53
|
|
|
50
54
|
const fontSizes = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
|
|
51
|
-
return {
|
|
55
|
+
return {cPath, hPath, images: bakedImages.length, fonts: fontSizes};
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
@@ -61,12 +65,17 @@ export function bakeAssets({ images = [], fonts = [], outDir }) {
|
|
|
61
65
|
* @param {string} opts.outPath Path to write the .pack file.
|
|
62
66
|
* @returns {{path:string, bytes:number, images:number, fonts:number}}
|
|
63
67
|
*/
|
|
64
|
-
export function bakeAssetPack({
|
|
65
|
-
const bakedImages = images.map(
|
|
66
|
-
const bakedFonts = fonts.map(
|
|
67
|
-
const pack = emitAssetPack({
|
|
68
|
-
fs.mkdirSync(path.dirname(outPath), {
|
|
68
|
+
export function bakeAssetPack({images = [], fonts = [], outPath}) {
|
|
69
|
+
const bakedImages = images.map(i => bakeImage(i));
|
|
70
|
+
const bakedFonts = fonts.map(f => bakeFont(f));
|
|
71
|
+
const pack = emitAssetPack({images: bakedImages, fonts: bakedFonts});
|
|
72
|
+
fs.mkdirSync(path.dirname(outPath), {recursive: true});
|
|
69
73
|
fs.writeFileSync(outPath, pack);
|
|
70
74
|
const fontSizes = bakedFonts.reduce((n, f) => n + f.sizes.length, 0);
|
|
71
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
path: outPath,
|
|
77
|
+
bytes: pack.length,
|
|
78
|
+
images: bakedImages.length,
|
|
79
|
+
fonts: fontSizes,
|
|
80
|
+
};
|
|
72
81
|
}
|
package/assets/rasterize.mjs
CHANGED
|
@@ -40,11 +40,11 @@ function flattenCommands(commands, steps) {
|
|
|
40
40
|
for (const c of commands) {
|
|
41
41
|
if (c.type === 'M') {
|
|
42
42
|
if (cur && cur.length > 1) contours.push(cur);
|
|
43
|
-
cur = [{
|
|
43
|
+
cur = [{x: c.x, y: c.y}];
|
|
44
44
|
cx = sx = c.x;
|
|
45
45
|
cy = sy = c.y;
|
|
46
46
|
} else if (c.type === 'L') {
|
|
47
|
-
cur.push({
|
|
47
|
+
cur.push({x: c.x, y: c.y});
|
|
48
48
|
cx = c.x;
|
|
49
49
|
cy = c.y;
|
|
50
50
|
} else if (c.type === 'Q') {
|
|
@@ -63,15 +63,23 @@ function flattenCommands(commands, steps) {
|
|
|
63
63
|
const t = i / steps;
|
|
64
64
|
const mt = 1 - t;
|
|
65
65
|
cur.push({
|
|
66
|
-
x:
|
|
67
|
-
|
|
66
|
+
x:
|
|
67
|
+
mt * mt * mt * cx +
|
|
68
|
+
3 * mt * mt * t * c.x1 +
|
|
69
|
+
3 * mt * t * t * c.x2 +
|
|
70
|
+
t * t * t * c.x,
|
|
71
|
+
y:
|
|
72
|
+
mt * mt * mt * cy +
|
|
73
|
+
3 * mt * mt * t * c.y1 +
|
|
74
|
+
3 * mt * t * t * c.y2 +
|
|
75
|
+
t * t * t * c.y,
|
|
68
76
|
});
|
|
69
77
|
}
|
|
70
78
|
cx = c.x;
|
|
71
79
|
cy = c.y;
|
|
72
80
|
} else if (c.type === 'Z') {
|
|
73
81
|
if (cur) {
|
|
74
|
-
cur.push({
|
|
82
|
+
cur.push({x: sx, y: sy}); // close the contour
|
|
75
83
|
if (cur.length > 1) contours.push(cur);
|
|
76
84
|
cur = null;
|
|
77
85
|
}
|
|
@@ -109,7 +117,13 @@ export function rasterize(path, opts = {}) {
|
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
if (!Number.isFinite(minX)) {
|
|
112
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
width: 0,
|
|
122
|
+
height: 0,
|
|
123
|
+
xOffset: 0,
|
|
124
|
+
yOffset: 0,
|
|
125
|
+
coverage: new Uint8Array(0),
|
|
126
|
+
};
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
const x0 = Math.floor(minX);
|
|
@@ -117,7 +131,13 @@ export function rasterize(path, opts = {}) {
|
|
|
117
131
|
const width = Math.ceil(maxX) - x0;
|
|
118
132
|
const height = Math.ceil(maxY) - y0;
|
|
119
133
|
if (width <= 0 || height <= 0) {
|
|
120
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
width: 0,
|
|
136
|
+
height: 0,
|
|
137
|
+
xOffset: 0,
|
|
138
|
+
yOffset: 0,
|
|
139
|
+
coverage: new Uint8Array(0),
|
|
140
|
+
};
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
// Non-horizontal edges, normalized so ylo < yhi; dir is the winding contribution.
|
|
@@ -127,8 +147,22 @@ export function rasterize(path, opts = {}) {
|
|
|
127
147
|
const a = ct[i];
|
|
128
148
|
const b = ct[i + 1];
|
|
129
149
|
if (a.y === b.y) continue;
|
|
130
|
-
if (a.y < b.y)
|
|
131
|
-
|
|
150
|
+
if (a.y < b.y)
|
|
151
|
+
edges.push({
|
|
152
|
+
ylo: a.y,
|
|
153
|
+
yhi: b.y,
|
|
154
|
+
x: a.x,
|
|
155
|
+
dxdy: (b.x - a.x) / (b.y - a.y),
|
|
156
|
+
dir: 1,
|
|
157
|
+
});
|
|
158
|
+
else
|
|
159
|
+
edges.push({
|
|
160
|
+
ylo: b.y,
|
|
161
|
+
yhi: a.y,
|
|
162
|
+
x: b.x,
|
|
163
|
+
dxdy: (a.x - b.x) / (a.y - b.y),
|
|
164
|
+
dir: -1,
|
|
165
|
+
});
|
|
132
166
|
}
|
|
133
167
|
}
|
|
134
168
|
|
|
@@ -139,7 +173,7 @@ export function rasterize(path, opts = {}) {
|
|
|
139
173
|
const xs = [];
|
|
140
174
|
for (const e of edges) {
|
|
141
175
|
if (sampleY >= e.ylo && sampleY < e.yhi) {
|
|
142
|
-
xs.push({
|
|
176
|
+
xs.push({x: e.x + (sampleY - e.ylo) * e.dxdy, dir: e.dir});
|
|
143
177
|
}
|
|
144
178
|
}
|
|
145
179
|
if (xs.length < 2) continue;
|
|
@@ -165,5 +199,5 @@ export function rasterize(path, opts = {}) {
|
|
|
165
199
|
for (let i = 0; i < coverage.length; i++) {
|
|
166
200
|
coverage[i] = Math.round((counts[i] * 255) / maxCov);
|
|
167
201
|
}
|
|
168
|
-
return {
|
|
202
|
+
return {width, height, xOffset: x0, yOffset: y0, coverage};
|
|
169
203
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
// esbuild loader for `import logo from './logo.svg'`. Two outcomes, decided at bake time:
|
|
18
|
+
// - VECTOR: the SVG is fully representable as the engine's op-tape — bake it and INLINE a
|
|
19
|
+
// {kind:'vector', ops, paints, gradients, width, height} artifact (small numeric data) into the bundle.
|
|
20
|
+
// - RASTER: the SVG uses features the vector baker can't represent (text, mask, filter, use,
|
|
21
|
+
// pattern, …). We warn, rasterize the whole SVG via resvg to a PNG, register that PNG through the
|
|
22
|
+
// bundler's normal image pipeline (a small `addRasterAsset(name, pngPath)` callback), and inline a
|
|
23
|
+
// {kind:'raster', name, width, height} artifact. A <Svg source> renders kind:'vector' as a vector node
|
|
24
|
+
// and kind:'raster' as an image node (host-config), so the fallback is transparent to the app.
|
|
25
|
+
// Shared by every Flow A bundler so the .svg handling lives in one place.
|
|
26
|
+
import {readFileSync} from 'node:fs';
|
|
27
|
+
import {basename} from 'node:path';
|
|
28
|
+
import {svgToVector, svgToRaster, writeRasterPng} from './bake-svg.mjs';
|
|
29
|
+
|
|
30
|
+
const assetName = p => basename(p).replace(/\.[^.]+$/, '');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers a `.svg` onLoad on an esbuild build.
|
|
34
|
+
*
|
|
35
|
+
* @param {import('esbuild').PluginBuild} build The esbuild build passed to a plugin's setup().
|
|
36
|
+
* @param {(name:string, pngPath:string)=>void} [addRasterAsset] Registers a rasterized-SVG PNG into the
|
|
37
|
+
* bundler's image collection (same mechanism as `import x from './x.png'`). When omitted, an SVG that
|
|
38
|
+
* needs raster fallback is baked as a (lossy) vector and a warning notes the dropped content.
|
|
39
|
+
*/
|
|
40
|
+
export function registerSvgVectorLoader(build, addRasterAsset) {
|
|
41
|
+
build.onLoad({filter: /\.svg$/i}, async args => {
|
|
42
|
+
try {
|
|
43
|
+
const svg = readFileSync(args.path, 'utf8');
|
|
44
|
+
const {dropped = [], ...vec} = await svgToVector(svg);
|
|
45
|
+
if (dropped.length) {
|
|
46
|
+
const feats = dropped.join(', ');
|
|
47
|
+
if (addRasterAsset) {
|
|
48
|
+
console.warn(
|
|
49
|
+
`embedded-react: ${basename(args.path)} uses unsupported SVG feature(s) [${feats}] — rasterizing ` +
|
|
50
|
+
`it as a fallback image. (Raster loses scalability and costs RAM; simplify the SVG to keep it a ` +
|
|
51
|
+
`live vector.)`,
|
|
52
|
+
);
|
|
53
|
+
const {width, height, png} = await svgToRaster(svg);
|
|
54
|
+
const name = assetName(args.path);
|
|
55
|
+
addRasterAsset(name, writeRasterPng(name, png));
|
|
56
|
+
return {
|
|
57
|
+
contents: `module.exports = ${JSON.stringify({kind: 'raster', name, width, height})};`,
|
|
58
|
+
loader: 'js',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
console.warn(
|
|
62
|
+
`embedded-react: ${basename(args.path)} uses unsupported SVG feature(s) [${feats}] and this build has ` +
|
|
63
|
+
`no raster fallback — that content will NOT render. Simplify the SVG, or import it via a bundler that ` +
|
|
64
|
+
`supports the image pipeline.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
contents: `module.exports = ${JSON.stringify({kind: 'vector', ...vec})};`,
|
|
69
|
+
loader: 'js',
|
|
70
|
+
};
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return {
|
|
73
|
+
errors: [
|
|
74
|
+
{
|
|
75
|
+
text: `embedded-react: failed to bake SVG ${args.path}: ${e.message}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
package/build.mjs
CHANGED
|
@@ -26,11 +26,12 @@
|
|
|
26
26
|
// npm run build -- marine-dash # a specific demo by folder name
|
|
27
27
|
// Outputs are always dist/app.bundle.js + dist/assets.generated.{c,h} — the single "active" app the
|
|
28
28
|
// example hosts pick up.
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
29
|
+
import {build} from 'esbuild';
|
|
30
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
31
|
+
import {dirname, resolve, basename} from 'node:path';
|
|
32
|
+
import {existsSync, readdirSync, readFileSync} from 'node:fs';
|
|
33
|
+
import {bakeAssets} from './assets/index.mjs';
|
|
34
|
+
import {registerSvgVectorLoader} from './assets/svg-loader.mjs';
|
|
34
35
|
|
|
35
36
|
const here = dirname(fileURLToPath(import.meta.url)); // bridges/quickjs/js
|
|
36
37
|
const repoRoot = resolve(here, '../../..');
|
|
@@ -46,9 +47,9 @@ const entry = resolve(demoDir, 'index.jsx');
|
|
|
46
47
|
|
|
47
48
|
if (!existsSync(entry)) {
|
|
48
49
|
const available = existsSync(demosDir)
|
|
49
|
-
? readdirSync(demosDir, {
|
|
50
|
-
.filter(
|
|
51
|
-
.map(
|
|
50
|
+
? readdirSync(demosDir, {withFileTypes: true})
|
|
51
|
+
.filter(d => d.isDirectory())
|
|
52
|
+
.map(d => d.name)
|
|
52
53
|
: [];
|
|
53
54
|
console.error(`Demo "${demo}" not found (expected ${entry}).`);
|
|
54
55
|
console.error(`Available demos: ${available.join(', ') || '(none)'}`);
|
|
@@ -64,16 +65,23 @@ const fonts = new Map(); // family -> path
|
|
|
64
65
|
const assetPlugin = {
|
|
65
66
|
name: 'embedded-react-assets',
|
|
66
67
|
setup(build) {
|
|
67
|
-
build.onLoad({
|
|
68
|
+
build.onLoad({filter: /\.(png|jpe?g|webp|gif|bmp)$/i}, args => {
|
|
68
69
|
const name = basename(args.path).replace(/\.[^.]+$/, '');
|
|
69
70
|
images.set(name, args.path);
|
|
70
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
contents: `module.exports = ${JSON.stringify(name)};`,
|
|
73
|
+
loader: 'js',
|
|
74
|
+
};
|
|
71
75
|
});
|
|
72
|
-
build.onLoad({
|
|
76
|
+
build.onLoad({filter: /\.(ttf|otf)$/i}, args => {
|
|
73
77
|
const family = basename(args.path).replace(/\.[^.]+$/, '');
|
|
74
78
|
fonts.set(family, args.path);
|
|
75
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
contents: `module.exports = ${JSON.stringify(family)};`,
|
|
81
|
+
loader: 'js',
|
|
82
|
+
};
|
|
76
83
|
});
|
|
84
|
+
registerSvgVectorLoader(build, (name, p) => images.set(name, p));
|
|
77
85
|
},
|
|
78
86
|
};
|
|
79
87
|
|
|
@@ -90,11 +98,11 @@ await build({
|
|
|
90
98
|
// them: map the bare `embedded-react` import to the library source and let the demo's bare deps
|
|
91
99
|
// (react, react-reconciler) resolve from this package's node_modules. (The library's own internal
|
|
92
100
|
// imports still resolve relatively / from node_modules as before.)
|
|
93
|
-
alias: {
|
|
101
|
+
alias: {'embedded-react': libEntry},
|
|
94
102
|
nodePaths: [nodeModules],
|
|
95
103
|
plugins: [assetPlugin],
|
|
96
104
|
// Production React: smaller and avoids dev-only warning machinery that needs more shims.
|
|
97
|
-
define: {
|
|
105
|
+
define: {'process.env.NODE_ENV': '"production"'},
|
|
98
106
|
legalComments: 'none',
|
|
99
107
|
logLevel: 'info',
|
|
100
108
|
});
|
|
@@ -107,7 +115,11 @@ console.log(`Bundled demo "${demo}" -> dist/app.bundle.js`);
|
|
|
107
115
|
// and will snap to the nearest baked size at runtime; pin them via assets.config.js if needed.
|
|
108
116
|
const bundleSrc = readFileSync(bundlePath, 'utf8');
|
|
109
117
|
const discoveredSizes = [
|
|
110
|
-
...new Set(
|
|
118
|
+
...new Set(
|
|
119
|
+
[...bundleSrc.matchAll(/\bfontSize\s*:\s*(\d+(?:\.\d+)?)/g)].map(m =>
|
|
120
|
+
Math.round(Number(m[1])),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
111
123
|
].sort((a, b) => a - b);
|
|
112
124
|
|
|
113
125
|
// Optional per-demo overrides: demos/<demo>/assets.config.js
|
|
@@ -121,14 +133,25 @@ const fontConfig = config.fonts || {};
|
|
|
121
133
|
|
|
122
134
|
const fontJobs = [...fonts.entries()].map(([family, path]) => {
|
|
123
135
|
const fc = fontConfig[family] || {};
|
|
124
|
-
const sizes =
|
|
125
|
-
|
|
136
|
+
const sizes =
|
|
137
|
+
fc.sizes && fc.sizes.length
|
|
138
|
+
? fc.sizes
|
|
139
|
+
: discoveredSizes.length
|
|
140
|
+
? discoveredSizes
|
|
141
|
+
: [16];
|
|
142
|
+
return {path, family, sizes, bpp: fc.bpp ?? 4, glyphs: fc.glyphs ?? 'ascii'};
|
|
126
143
|
});
|
|
127
|
-
const imageJobs = [...images.entries()].map(([name, path]) => ({
|
|
144
|
+
const imageJobs = [...images.entries()].map(([name, path]) => ({path, name}));
|
|
128
145
|
|
|
129
|
-
const summary = bakeAssets({
|
|
146
|
+
const summary = bakeAssets({
|
|
147
|
+
images: imageJobs,
|
|
148
|
+
fonts: fontJobs,
|
|
149
|
+
outDir: distDir,
|
|
150
|
+
});
|
|
130
151
|
const fontDesc = fontJobs.length
|
|
131
|
-
? fontJobs
|
|
152
|
+
? fontJobs
|
|
153
|
+
.map(f => `${f.family}@[${f.sizes.join(',')}]x${f.bpp}bpp`)
|
|
154
|
+
.join(', ')
|
|
132
155
|
: 'none';
|
|
133
156
|
console.log(
|
|
134
157
|
`Baked ${summary.images} image(s), ${summary.fonts} font size(s) -> dist/assets.generated.c\n` +
|