@glyph-engine/web 0.0.0-stage → 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/README.md +19 -2
- package/dist/canvasrenderer.d.ts +8 -0
- package/dist/canvassize.d.ts +2 -0
- package/dist/cellspan.d.ts +6 -0
- package/dist/glyphatlas.d.ts +28 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1821 -0
- package/dist/modules/camera.d.ts +13 -0
- package/dist/modules/canvas.d.ts +65 -0
- package/dist/modules/exportframe.d.ts +11 -0
- package/dist/modules/filedownload.d.ts +12 -0
- package/dist/modules/image.d.ts +22 -0
- package/dist/modules/load.d.ts +37 -0
- package/dist/modules/sort.d.ts +11 -0
- package/dist/renderer.d.ts +7 -0
- package/dist/run.d.ts +17 -0
- package/dist/storage.d.ts +10 -0
- package/dist/textrenderer.d.ts +8 -0
- package/dist/webglrenderer.d.ts +8 -0
- package/dist/webgpurenderer.d.ts +16 -0
- package/package.json +41 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,1821 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __returnValue = (v) => v;
|
|
3
|
+
function __exportSetter(name, newValue) {
|
|
4
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
5
|
+
}
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
set: __exportSetter.bind(all, name)
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/modules/camera.ts
|
|
17
|
+
var exports_camera = {};
|
|
18
|
+
__export(exports_camera, {
|
|
19
|
+
default: () => camera_default
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// src/modules/canvas.ts
|
|
23
|
+
var exports_canvas = {};
|
|
24
|
+
__export(exports_canvas, {
|
|
25
|
+
MODE_CENTER: () => MODE_CENTER,
|
|
26
|
+
MODE_COVER: () => MODE_COVER,
|
|
27
|
+
MODE_FIT: () => MODE_FIT,
|
|
28
|
+
default: () => Canvas
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// src/modules/exportframe.ts
|
|
32
|
+
var exports_exportframe = {};
|
|
33
|
+
__export(exports_exportframe, {
|
|
34
|
+
exportFrame: () => exportFrame
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// src/modules/filedownload.ts
|
|
38
|
+
var exports_filedownload = {};
|
|
39
|
+
__export(exports_filedownload, {
|
|
40
|
+
saveBlobAsFile: () => saveBlobAsFile,
|
|
41
|
+
saveSourceAsFile: () => saveSourceAsFile
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// src/modules/image.ts
|
|
45
|
+
var exports_image = {};
|
|
46
|
+
__export(exports_image, {
|
|
47
|
+
default: () => image_default
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/modules/load.ts
|
|
51
|
+
var exports_load = {};
|
|
52
|
+
__export(exports_load, {
|
|
53
|
+
default: () => load_default
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// src/modules/sort.ts
|
|
57
|
+
var exports_sort = {};
|
|
58
|
+
__export(exports_sort, {
|
|
59
|
+
sort: () => sort
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// src/index.ts
|
|
63
|
+
export * from "@glyph-engine/core";
|
|
64
|
+
|
|
65
|
+
// src/run.ts
|
|
66
|
+
import { startLoop } from "@glyph-engine/core";
|
|
67
|
+
|
|
68
|
+
// src/canvasrenderer.ts
|
|
69
|
+
import { colorString, cssColor, getChar, weightString } from "@glyph-engine/core";
|
|
70
|
+
|
|
71
|
+
// src/canvassize.ts
|
|
72
|
+
function fitCanvas(canvas, context, scale) {
|
|
73
|
+
const size = context.settings.canvasSize;
|
|
74
|
+
const w = Math.trunc((size ? size.width : context.width) * scale);
|
|
75
|
+
const h = Math.trunc((size ? size.height : context.height) * scale);
|
|
76
|
+
if (canvas.width !== w)
|
|
77
|
+
canvas.width = w;
|
|
78
|
+
if (canvas.height !== h)
|
|
79
|
+
canvas.height = h;
|
|
80
|
+
if (size) {
|
|
81
|
+
canvas.style.width = size.width + "px";
|
|
82
|
+
canvas.style.height = size.height + "px";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/cellspan.ts
|
|
87
|
+
var cache = new Map;
|
|
88
|
+
function cellSpan(ctx, font, char, cellWidth) {
|
|
89
|
+
if (char.length < 2 && !(char.charCodeAt(0) >= 4352))
|
|
90
|
+
return 1;
|
|
91
|
+
const key = font + "|" + cellWidth + "|" + char;
|
|
92
|
+
let span = cache.get(key);
|
|
93
|
+
if (span === undefined) {
|
|
94
|
+
span = ctx.measureText(char).width > cellWidth * 1.5 ? 2 : 1;
|
|
95
|
+
if (cache.size > 65536)
|
|
96
|
+
cache.clear();
|
|
97
|
+
cache.set(key, span);
|
|
98
|
+
}
|
|
99
|
+
return span;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/canvasrenderer.ts
|
|
103
|
+
var canvasrenderer_default = {
|
|
104
|
+
preferredElementNodeName: "CANVAS",
|
|
105
|
+
render
|
|
106
|
+
};
|
|
107
|
+
function render(context, frame) {
|
|
108
|
+
const canvas = context.settings.element;
|
|
109
|
+
const scale = devicePixelRatio;
|
|
110
|
+
const c = context.cols;
|
|
111
|
+
const r = context.rows;
|
|
112
|
+
const m = context.metrics;
|
|
113
|
+
const cw = m.cellWidth;
|
|
114
|
+
const ch = Math.round(m.lineHeight);
|
|
115
|
+
const settings = context.settings;
|
|
116
|
+
fitCanvas(canvas, context, scale);
|
|
117
|
+
const ff = " " + m.fontSize + "px " + m.fontFamily;
|
|
118
|
+
const bg = settings && settings.backgroundColor ? settings.backgroundColor : "white";
|
|
119
|
+
const fg = settings && settings.color ? settings.color : "black";
|
|
120
|
+
const fontWeight = settings && settings.fontWeight ? settings.fontWeight : "400";
|
|
121
|
+
const ctx = canvas.getContext("2d");
|
|
122
|
+
if (!ctx)
|
|
123
|
+
throw new Error("Canvas not supported");
|
|
124
|
+
ctx.fillStyle = bg;
|
|
125
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
126
|
+
ctx.save();
|
|
127
|
+
ctx.scale(scale, scale);
|
|
128
|
+
ctx.fillStyle = fg;
|
|
129
|
+
ctx.textBaseline = "top";
|
|
130
|
+
if (settings.canvasOffset) {
|
|
131
|
+
const offs = settings.canvasOffset;
|
|
132
|
+
const ox = Math.round(offs.x == "auto" ? (canvas.width / scale - c * cw) / 2 : offs.x);
|
|
133
|
+
const oy = Math.round(offs.y == "auto" ? (canvas.height / scale - r * ch) / 2 : offs.y);
|
|
134
|
+
ctx.translate(ox, oy);
|
|
135
|
+
}
|
|
136
|
+
const { chars, fg: fgs, bg: bgs, weight } = frame;
|
|
137
|
+
const bgPacked = cssColor(bg);
|
|
138
|
+
const fonts = [];
|
|
139
|
+
const fontOf = (w) => fonts[w] ??= (w ? weightString(w) : fontWeight) + ff;
|
|
140
|
+
if (settings.textAlign == "center") {
|
|
141
|
+
const widths = [];
|
|
142
|
+
for (let j = 0;j < r; j++) {
|
|
143
|
+
const offs = j * c;
|
|
144
|
+
let totalWidth = 0;
|
|
145
|
+
for (let i = 0;i < c; i++) {
|
|
146
|
+
ctx.font = fontOf(weight[offs + i]);
|
|
147
|
+
const w = ctx.measureText(getChar(frame, offs + i)).width;
|
|
148
|
+
totalWidth += w;
|
|
149
|
+
widths[i] = w;
|
|
150
|
+
}
|
|
151
|
+
let ox = (canvas.width / scale - totalWidth) * 0.5;
|
|
152
|
+
const y = j * ch;
|
|
153
|
+
for (let i = 0;i < c; i++) {
|
|
154
|
+
const k = offs + i;
|
|
155
|
+
const x = ox;
|
|
156
|
+
if (bgs[k] && bgs[k] !== bgPacked) {
|
|
157
|
+
ctx.fillStyle = colorString(bgs[k]);
|
|
158
|
+
ctx.fillRect(Math.round(x), y, Math.ceil(widths[i]), ch);
|
|
159
|
+
}
|
|
160
|
+
ctx.font = fontOf(weight[k]);
|
|
161
|
+
ctx.fillStyle = fgs[k] ? colorString(fgs[k]) : fg;
|
|
162
|
+
ctx.fillText(getChar(frame, k), ox, y);
|
|
163
|
+
ox += widths[i];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
let font = "";
|
|
168
|
+
for (let j = 0;j < r; j++) {
|
|
169
|
+
let col = 0;
|
|
170
|
+
for (let i = 0;i < c && col < c; i++) {
|
|
171
|
+
const k = j * c + i;
|
|
172
|
+
const x = col * cw;
|
|
173
|
+
const y = j * ch;
|
|
174
|
+
const cp = chars[k];
|
|
175
|
+
const empty = cp === 32 || cp === 0;
|
|
176
|
+
let char = "";
|
|
177
|
+
let span = 1;
|
|
178
|
+
if (!empty) {
|
|
179
|
+
const next = fontOf(weight[k]);
|
|
180
|
+
if (next !== font)
|
|
181
|
+
ctx.font = font = next;
|
|
182
|
+
char = getChar(frame, k);
|
|
183
|
+
if (cp >= 4352)
|
|
184
|
+
span = cellSpan(ctx, font, char, cw);
|
|
185
|
+
}
|
|
186
|
+
col += span;
|
|
187
|
+
if (bgs[k] && bgs[k] !== bgPacked) {
|
|
188
|
+
ctx.fillStyle = colorString(bgs[k]);
|
|
189
|
+
ctx.fillRect(Math.round(x), y, Math.ceil(cw * span), ch);
|
|
190
|
+
}
|
|
191
|
+
if (empty)
|
|
192
|
+
continue;
|
|
193
|
+
ctx.fillStyle = fgs[k] ? colorString(fgs[k]) : fg;
|
|
194
|
+
ctx.fillText(char, x, y);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
ctx.restore();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/storage.ts
|
|
202
|
+
var storage = {
|
|
203
|
+
store: function(key, obj) {
|
|
204
|
+
try {
|
|
205
|
+
localStorage.setItem(key, JSON.stringify(obj));
|
|
206
|
+
return true;
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
restore: function(key, target) {
|
|
212
|
+
const obj = JSON.parse(localStorage.getItem(key) || "{}");
|
|
213
|
+
Object.assign(target, obj);
|
|
214
|
+
return target;
|
|
215
|
+
},
|
|
216
|
+
clear: function(key) {
|
|
217
|
+
localStorage.removeItem(key);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var storage_default = storage;
|
|
221
|
+
|
|
222
|
+
// src/textrenderer.ts
|
|
223
|
+
import { colorString as colorString2, createFrame, getChar as getChar2, weightString as weightString2 } from "@glyph-engine/core";
|
|
224
|
+
var textrenderer_default = {
|
|
225
|
+
preferredElementNodeName: "PRE",
|
|
226
|
+
render: render2
|
|
227
|
+
};
|
|
228
|
+
var states = new WeakMap;
|
|
229
|
+
function render2(context, frame) {
|
|
230
|
+
const element = context.settings.element;
|
|
231
|
+
let state = states.get(element);
|
|
232
|
+
if (!state)
|
|
233
|
+
states.set(element, state = { back: null });
|
|
234
|
+
const { cols, rows } = frame;
|
|
235
|
+
let back = state.back;
|
|
236
|
+
if (!back || back.cols !== cols || back.rows !== rows) {
|
|
237
|
+
back = state.back = createFrame(cols, rows);
|
|
238
|
+
back.chars.fill(4294967295);
|
|
239
|
+
}
|
|
240
|
+
while (element.childElementCount < rows) {
|
|
241
|
+
const span = document.createElement("span");
|
|
242
|
+
span.style.display = "block";
|
|
243
|
+
element.appendChild(span);
|
|
244
|
+
}
|
|
245
|
+
while (element.childElementCount > rows) {
|
|
246
|
+
element.removeChild(element.lastChild);
|
|
247
|
+
}
|
|
248
|
+
const { chars, fg, bg, weight, html } = frame;
|
|
249
|
+
const fontWeight = context.settings.fontWeight;
|
|
250
|
+
for (let j = 0;j < rows; j++) {
|
|
251
|
+
const start = j * cols;
|
|
252
|
+
const end = start + cols;
|
|
253
|
+
let changed = html.size > 0;
|
|
254
|
+
for (let k = start;k < end && !changed; k++) {
|
|
255
|
+
changed = chars[k] !== back.chars[k] || fg[k] !== back.fg[k] || bg[k] !== back.bg[k] || weight[k] !== back.weight[k];
|
|
256
|
+
}
|
|
257
|
+
if (!changed)
|
|
258
|
+
continue;
|
|
259
|
+
back.chars.set(chars.subarray(start, end), start);
|
|
260
|
+
back.fg.set(fg.subarray(start, end), start);
|
|
261
|
+
back.bg.set(bg.subarray(start, end), start);
|
|
262
|
+
back.weight.set(weight.subarray(start, end), start);
|
|
263
|
+
let markup = "";
|
|
264
|
+
let tagIsOpen = false;
|
|
265
|
+
let pf = 0, pb = 0, pw = 0;
|
|
266
|
+
for (let k = start;k < end; k++) {
|
|
267
|
+
const extra = html.size > 0 ? html.get(k) : undefined;
|
|
268
|
+
if (extra?.begin) {
|
|
269
|
+
if (tagIsOpen) {
|
|
270
|
+
markup += "</span>";
|
|
271
|
+
pf = pb = pw = 0;
|
|
272
|
+
tagIsOpen = false;
|
|
273
|
+
}
|
|
274
|
+
markup += extra.begin;
|
|
275
|
+
}
|
|
276
|
+
const f = fg[k], b = bg[k], w = weight[k];
|
|
277
|
+
if (f !== pf || b !== pb || w !== pw) {
|
|
278
|
+
if (tagIsOpen)
|
|
279
|
+
markup += "</span>";
|
|
280
|
+
let css = "";
|
|
281
|
+
if (f)
|
|
282
|
+
css += "color:" + colorString2(f) + ";";
|
|
283
|
+
if (b)
|
|
284
|
+
css += "background:" + colorString2(b) + ";";
|
|
285
|
+
if (w && weightString2(w) !== fontWeight)
|
|
286
|
+
css += "font-weight:" + weightString2(w) + ";";
|
|
287
|
+
if (css)
|
|
288
|
+
css = ' style="' + css + '"';
|
|
289
|
+
markup += "<span" + css + ">";
|
|
290
|
+
tagIsOpen = true;
|
|
291
|
+
pf = f;
|
|
292
|
+
pb = b;
|
|
293
|
+
pw = w;
|
|
294
|
+
}
|
|
295
|
+
markup += getChar2(frame, k);
|
|
296
|
+
if (extra?.end) {
|
|
297
|
+
if (tagIsOpen) {
|
|
298
|
+
markup += "</span>";
|
|
299
|
+
pf = pb = pw = 0;
|
|
300
|
+
tagIsOpen = false;
|
|
301
|
+
}
|
|
302
|
+
markup += extra.end;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (tagIsOpen)
|
|
306
|
+
markup += "</span>";
|
|
307
|
+
element.childNodes[j].innerHTML = markup;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/webglrenderer.ts
|
|
312
|
+
import { cssColor as cssColor2 } from "@glyph-engine/core";
|
|
313
|
+
|
|
314
|
+
// src/glyphatlas.ts
|
|
315
|
+
import { getChar as getChar3, GRAPHEME, weightString as weightString3 } from "@glyph-engine/core";
|
|
316
|
+
var FLAG_COLOR_GLYPH = 1;
|
|
317
|
+
|
|
318
|
+
class GlyphAtlas {
|
|
319
|
+
size;
|
|
320
|
+
upload;
|
|
321
|
+
key = "";
|
|
322
|
+
slotW = 0;
|
|
323
|
+
slotH = 0;
|
|
324
|
+
perRow = 0;
|
|
325
|
+
capacity = 0;
|
|
326
|
+
next = 1;
|
|
327
|
+
generation = 0;
|
|
328
|
+
glyphs = new Map;
|
|
329
|
+
colored = new Uint8Array(0);
|
|
330
|
+
wide = new Uint8Array(0);
|
|
331
|
+
scratch = document.createElement("canvas");
|
|
332
|
+
#ctx = this.scratch.getContext("2d", { willReadFrequently: true });
|
|
333
|
+
constructor(size, upload) {
|
|
334
|
+
this.size = size;
|
|
335
|
+
this.upload = upload;
|
|
336
|
+
}
|
|
337
|
+
ensure(slotW, slotH, font) {
|
|
338
|
+
const key = font + "|" + slotW + "x" + slotH;
|
|
339
|
+
if (this.key === key)
|
|
340
|
+
return false;
|
|
341
|
+
this.key = key;
|
|
342
|
+
this.slotW = slotW;
|
|
343
|
+
this.slotH = slotH;
|
|
344
|
+
this.perRow = Math.floor(this.size / slotW);
|
|
345
|
+
this.capacity = this.perRow * Math.floor(this.size / slotH);
|
|
346
|
+
this.colored = new Uint8Array(this.capacity);
|
|
347
|
+
this.wide = new Uint8Array(this.capacity);
|
|
348
|
+
this.#forget();
|
|
349
|
+
this.scratch.width = slotW;
|
|
350
|
+
this.scratch.height = slotH;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
cellSlot(frame, k, defaultWeight, fontSize, family) {
|
|
354
|
+
const code = frame.chars[k];
|
|
355
|
+
if (code === 32 || code === 0)
|
|
356
|
+
return 0;
|
|
357
|
+
const key = code >= GRAPHEME ? getChar3(frame, k) : code;
|
|
358
|
+
return this.slot(frame.weight[k], key, getChar3(frame, k), defaultWeight, fontSize, family);
|
|
359
|
+
}
|
|
360
|
+
slot(weight, key, char, defaultWeight, fontSize, family) {
|
|
361
|
+
let slot = this.glyphs.get(weight)?.get(key);
|
|
362
|
+
if (slot !== undefined)
|
|
363
|
+
return slot;
|
|
364
|
+
if (this.next >= this.capacity)
|
|
365
|
+
this.#forget();
|
|
366
|
+
slot = this.next++;
|
|
367
|
+
const ctx = this.#ctx;
|
|
368
|
+
ctx.clearRect(0, 0, this.slotW, this.slotH);
|
|
369
|
+
ctx.font = (weight ? weightString3(weight) : defaultWeight) + " " + fontSize + "px " + family;
|
|
370
|
+
ctx.textBaseline = "top";
|
|
371
|
+
ctx.fillStyle = "white";
|
|
372
|
+
ctx.fillText(char, 0, 0);
|
|
373
|
+
this.colored[slot] = isColorGlyph(ctx.getImageData(0, 0, this.slotW, this.slotH).data) ? 1 : 0;
|
|
374
|
+
this.wide[slot] = cellSpan(ctx, ctx.font, char, this.slotW / 2) - 1;
|
|
375
|
+
this.upload(slot % this.perRow * this.slotW, Math.floor(slot / this.perRow) * this.slotH, this.scratch);
|
|
376
|
+
let byChar = this.glyphs.get(weight);
|
|
377
|
+
if (!byChar)
|
|
378
|
+
this.glyphs.set(weight, byChar = new Map);
|
|
379
|
+
byChar.set(key, slot);
|
|
380
|
+
return slot;
|
|
381
|
+
}
|
|
382
|
+
writeCells(data, frame, fg, bg, fontWeight, fontSize, family) {
|
|
383
|
+
const { cols, rows, fg: fgs, bg: bgs } = frame;
|
|
384
|
+
for (let j = 0;j < rows; j++) {
|
|
385
|
+
const row = j * cols;
|
|
386
|
+
let col = 0;
|
|
387
|
+
for (let i = 0;i < cols && col < cols; i++) {
|
|
388
|
+
const k = row + i;
|
|
389
|
+
const slot = this.cellSlot(frame, k, fontWeight, fontSize, family);
|
|
390
|
+
const f = fgs[k] || fg;
|
|
391
|
+
const b = bgs[k] || bg;
|
|
392
|
+
let o = (row + col++) * 4;
|
|
393
|
+
data[o] = slot;
|
|
394
|
+
data[o + 1] = f;
|
|
395
|
+
data[o + 2] = b;
|
|
396
|
+
data[o + 3] = this.colored[slot] ? FLAG_COLOR_GLYPH : 0;
|
|
397
|
+
if (this.wide[slot] && col < cols) {
|
|
398
|
+
o = (row + col++) * 4;
|
|
399
|
+
data[o] = 0;
|
|
400
|
+
data[o + 1] = f;
|
|
401
|
+
data[o + 2] = b;
|
|
402
|
+
data[o + 3] = 0;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
#forget() {
|
|
408
|
+
this.glyphs.clear();
|
|
409
|
+
this.colored.fill(0);
|
|
410
|
+
this.wide.fill(0);
|
|
411
|
+
this.next = 1;
|
|
412
|
+
this.generation++;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function isColorGlyph(px) {
|
|
416
|
+
for (let i = 0;i < px.length; i += 4) {
|
|
417
|
+
if (px[i + 3] > 0 && (px[i] !== px[i + 1] || px[i + 1] !== px[i + 2]))
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/shaders/grid.frag
|
|
424
|
+
var grid_default = `#version 300 es
|
|
425
|
+
// Draws the whole character grid: one fragment = one screen pixel.
|
|
426
|
+
// Finds its cell, reads the cell texel, samples the glyph atlas, mixes fg/bg.
|
|
427
|
+
// Layering: this cell's background, the left neighbour's glyph overflow (right
|
|
428
|
+
// half of a wide glyph), then this cell's glyph.
|
|
429
|
+
precision highp float;
|
|
430
|
+
precision highp int;
|
|
431
|
+
precision highp usampler2D;
|
|
432
|
+
|
|
433
|
+
uniform usampler2D u_cells; // per cell: x = atlas slot, y = fg RGBA8, z = bg RGBA8, w = flags
|
|
434
|
+
uniform sampler2D u_atlas; // glyphs drawn white by Canvas2D, premultiplied alpha
|
|
435
|
+
uniform vec2 u_cellSize; // cell size in device pixels (x may be fractional)
|
|
436
|
+
uniform vec2 u_offset; // grid origin in device pixels, from the top-left
|
|
437
|
+
uniform float u_height; // canvas height in device pixels (gl_FragCoord is bottom-up)
|
|
438
|
+
uniform ivec2 u_grid; // cols, rows
|
|
439
|
+
uniform ivec2 u_slotSize; // atlas slot size in pixels: two cells wide, for wide glyphs
|
|
440
|
+
uniform int u_slotsPerRow;
|
|
441
|
+
uniform vec4 u_background; // canvas background, also outside the grid
|
|
442
|
+
uniform uint u_backgroundPacked; // same colour as RGBA8: cells with it draw no background
|
|
443
|
+
|
|
444
|
+
out vec4 outColor;
|
|
445
|
+
|
|
446
|
+
const uint FLAG_COLOR_GLYPH = 1u; // emoji etc.: keep the glyph's own colours
|
|
447
|
+
|
|
448
|
+
vec4 unpack(uint c) {
|
|
449
|
+
return vec4(uvec4(c >> 24, (c >> 16) & 255u, (c >> 8) & 255u, c & 255u)) / 255.0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Draws the glyph of cell \`c\` over \`color\`; \`local\` is the pixel relative to
|
|
453
|
+
// that cell's top-left corner.
|
|
454
|
+
vec3 drawGlyph(vec3 color, uvec4 c, vec2 local) {
|
|
455
|
+
if (c.x == 0u) return color; // slot 0: space
|
|
456
|
+
int slot = int(c.x);
|
|
457
|
+
ivec2 origin = ivec2(slot % u_slotsPerRow, slot / u_slotsPerRow) * u_slotSize;
|
|
458
|
+
ivec2 p = ivec2(local);
|
|
459
|
+
if (p.x >= u_slotSize.x || p.y >= u_slotSize.y) return color;
|
|
460
|
+
vec4 glyph = texelFetch(u_atlas, origin + p, 0);
|
|
461
|
+
vec4 fg = unpack(c.y);
|
|
462
|
+
bool colorGlyph = (c.w & FLAG_COLOR_GLYPH) != 0u;
|
|
463
|
+
float coverage = colorGlyph ? glyph.a : glyph.a * fg.a;
|
|
464
|
+
vec3 ink = colorGlyph ? glyph.rgb : fg.rgb * coverage;
|
|
465
|
+
return color * (1.0 - coverage) + ink;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
void main() {
|
|
469
|
+
vec2 px = vec2(gl_FragCoord.x, u_height - gl_FragCoord.y) - u_offset;
|
|
470
|
+
ivec2 cell = ivec2(floor(px / u_cellSize));
|
|
471
|
+
if (px.x < 0.0 || px.y < 0.0 || cell.x >= u_grid.x || cell.y >= u_grid.y) {
|
|
472
|
+
outColor = u_background;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
vec3 color = u_background.rgb;
|
|
477
|
+
vec2 local = px - vec2(cell) * u_cellSize;
|
|
478
|
+
|
|
479
|
+
uvec4 c = texelFetch(u_cells, cell, 0);
|
|
480
|
+
if (c.z != u_backgroundPacked) {
|
|
481
|
+
vec4 bg = unpack(c.z); // translucent backgrounds blend, as with Canvas2D
|
|
482
|
+
color = mix(color, bg.rgb, bg.a);
|
|
483
|
+
}
|
|
484
|
+
if (cell.x > 0) {
|
|
485
|
+
uvec4 left = texelFetch(u_cells, cell - ivec2(1, 0), 0);
|
|
486
|
+
color = drawGlyph(color, left, local + vec2(u_cellSize.x, 0.0));
|
|
487
|
+
}
|
|
488
|
+
outColor = vec4(drawGlyph(color, c, local), 1.0);
|
|
489
|
+
}
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
// src/shaders/grid.vert
|
|
493
|
+
var grid_default2 = `#version 300 es
|
|
494
|
+
// Fullscreen triangle built from gl_VertexID: no vertex buffers needed.
|
|
495
|
+
void main() {
|
|
496
|
+
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
497
|
+
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
498
|
+
}
|
|
499
|
+
`;
|
|
500
|
+
|
|
501
|
+
// src/webglrenderer.ts
|
|
502
|
+
var webglrenderer_default = {
|
|
503
|
+
preferredElementNodeName: "CANVAS",
|
|
504
|
+
render: render3
|
|
505
|
+
};
|
|
506
|
+
var FALLBACK = "canvas";
|
|
507
|
+
var states2 = new WeakMap;
|
|
508
|
+
var ATLAS_UNIT = 1;
|
|
509
|
+
function render3(context, frame) {
|
|
510
|
+
const canvas = context.settings.element;
|
|
511
|
+
let state = states2.get(canvas);
|
|
512
|
+
if (!state) {
|
|
513
|
+
const gl = context.settings.textAlign == "center" ? null : canvas.getContext("webgl2", {
|
|
514
|
+
alpha: false,
|
|
515
|
+
antialias: false,
|
|
516
|
+
preserveDrawingBuffer: true
|
|
517
|
+
});
|
|
518
|
+
if (gl?.isContextLost())
|
|
519
|
+
return;
|
|
520
|
+
state = gl ? createState(gl) : FALLBACK;
|
|
521
|
+
if (gl) {
|
|
522
|
+
canvas.addEventListener("webglcontextlost", (e) => e.preventDefault(), {
|
|
523
|
+
once: true
|
|
524
|
+
});
|
|
525
|
+
canvas.addEventListener("webglcontextrestored", () => states2.delete(canvas), { once: true });
|
|
526
|
+
}
|
|
527
|
+
states2.set(canvas, state);
|
|
528
|
+
}
|
|
529
|
+
if (state === FALLBACK)
|
|
530
|
+
return canvasrenderer_default.render(context, frame);
|
|
531
|
+
if (state.gl.isContextLost())
|
|
532
|
+
return;
|
|
533
|
+
draw(state, context, frame);
|
|
534
|
+
}
|
|
535
|
+
function createState(gl) {
|
|
536
|
+
const program = linkProgram(gl, grid_default2, grid_default);
|
|
537
|
+
const u = (name) => gl.getUniformLocation(program, name);
|
|
538
|
+
const atlasTexture = gl.createTexture();
|
|
539
|
+
return {
|
|
540
|
+
gl,
|
|
541
|
+
program,
|
|
542
|
+
uniforms: {
|
|
543
|
+
cells: u("u_cells"),
|
|
544
|
+
atlas: u("u_atlas"),
|
|
545
|
+
cellSize: u("u_cellSize"),
|
|
546
|
+
offset: u("u_offset"),
|
|
547
|
+
height: u("u_height"),
|
|
548
|
+
grid: u("u_grid"),
|
|
549
|
+
slotSize: u("u_slotSize"),
|
|
550
|
+
slotsPerRow: u("u_slotsPerRow"),
|
|
551
|
+
background: u("u_background"),
|
|
552
|
+
backgroundPacked: u("u_backgroundPacked")
|
|
553
|
+
},
|
|
554
|
+
vao: gl.createVertexArray(),
|
|
555
|
+
cellTexture: gl.createTexture(),
|
|
556
|
+
cells: new Uint32Array(0),
|
|
557
|
+
cols: 0,
|
|
558
|
+
rows: 0,
|
|
559
|
+
atlasTexture,
|
|
560
|
+
atlas: new GlyphAtlas(Math.min(2048, gl.getParameter(gl.MAX_TEXTURE_SIZE)), (x, y, glyph) => {
|
|
561
|
+
gl.activeTexture(gl.TEXTURE0 + ATLAS_UNIT);
|
|
562
|
+
gl.bindTexture(gl.TEXTURE_2D, atlasTexture);
|
|
563
|
+
setUnpack(gl, true);
|
|
564
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, glyph);
|
|
565
|
+
})
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function draw(s, context, frame) {
|
|
569
|
+
const { gl, uniforms } = s;
|
|
570
|
+
const canvas = gl.canvas;
|
|
571
|
+
const settings = context.settings;
|
|
572
|
+
const m = context.metrics;
|
|
573
|
+
const scale = devicePixelRatio;
|
|
574
|
+
fitCanvas(canvas, context, scale);
|
|
575
|
+
const cols = context.cols;
|
|
576
|
+
const rows = context.rows;
|
|
577
|
+
const cw = m.cellWidth;
|
|
578
|
+
const ch = Math.round(m.lineHeight);
|
|
579
|
+
const fontSize = m.fontSize * scale;
|
|
580
|
+
ensureAtlas(s, 2 * Math.ceil(cw * scale), Math.ceil(ch * scale), fontSize + "px " + m.fontFamily);
|
|
581
|
+
ensureCells(s, cols, rows);
|
|
582
|
+
const bg = cssColor2(settings.backgroundColor || "white");
|
|
583
|
+
const fg = cssColor2(settings.color || "black");
|
|
584
|
+
const fontWeight = String(settings.fontWeight || "400");
|
|
585
|
+
const data = s.cells;
|
|
586
|
+
s.atlas.writeCells(data, frame, fg, bg, fontWeight, fontSize, m.fontFamily);
|
|
587
|
+
let ox = 0;
|
|
588
|
+
let oy = 0;
|
|
589
|
+
if (settings.canvasOffset) {
|
|
590
|
+
const offs = settings.canvasOffset;
|
|
591
|
+
ox = Math.round(offs.x == "auto" ? (canvas.width / scale - cols * cw) / 2 : offs.x);
|
|
592
|
+
oy = Math.round(offs.y == "auto" ? (canvas.height / scale - rows * ch) / 2 : offs.y);
|
|
593
|
+
}
|
|
594
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
595
|
+
gl.useProgram(s.program);
|
|
596
|
+
gl.bindVertexArray(s.vao);
|
|
597
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
598
|
+
gl.bindTexture(gl.TEXTURE_2D, s.cellTexture);
|
|
599
|
+
setUnpack(gl, false);
|
|
600
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, cols, rows, gl.RGBA_INTEGER, gl.UNSIGNED_INT, data);
|
|
601
|
+
gl.activeTexture(gl.TEXTURE0 + ATLAS_UNIT);
|
|
602
|
+
gl.bindTexture(gl.TEXTURE_2D, s.atlasTexture);
|
|
603
|
+
const a = s.atlas;
|
|
604
|
+
gl.uniform1i(uniforms.cells, 0);
|
|
605
|
+
gl.uniform1i(uniforms.atlas, ATLAS_UNIT);
|
|
606
|
+
gl.uniform2f(uniforms.cellSize, cw * scale, ch * scale);
|
|
607
|
+
gl.uniform2f(uniforms.offset, ox * scale, oy * scale);
|
|
608
|
+
gl.uniform1f(uniforms.height, canvas.height);
|
|
609
|
+
gl.uniform2i(uniforms.grid, cols, rows);
|
|
610
|
+
gl.uniform2i(uniforms.slotSize, a.slotW, a.slotH);
|
|
611
|
+
gl.uniform1i(uniforms.slotsPerRow, a.perRow);
|
|
612
|
+
gl.uniform4f(uniforms.background, (bg >>> 24) / 255, (bg >>> 16 & 255) / 255, (bg >>> 8 & 255) / 255, 1);
|
|
613
|
+
gl.uniform1ui(uniforms.backgroundPacked, bg);
|
|
614
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
615
|
+
gl.bindVertexArray(null);
|
|
616
|
+
}
|
|
617
|
+
function ensureCells(s, cols, rows) {
|
|
618
|
+
if (s.cols === cols && s.rows === rows)
|
|
619
|
+
return;
|
|
620
|
+
const { gl } = s;
|
|
621
|
+
s.cols = cols;
|
|
622
|
+
s.rows = rows;
|
|
623
|
+
s.cells = new Uint32Array(cols * rows * 4);
|
|
624
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
625
|
+
gl.bindTexture(gl.TEXTURE_2D, s.cellTexture);
|
|
626
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32UI, cols, rows, 0, gl.RGBA_INTEGER, gl.UNSIGNED_INT, null);
|
|
627
|
+
setNearest(gl);
|
|
628
|
+
}
|
|
629
|
+
function ensureAtlas(s, slotW, slotH, font) {
|
|
630
|
+
if (!s.atlas.ensure(slotW, slotH, font))
|
|
631
|
+
return;
|
|
632
|
+
const { gl } = s;
|
|
633
|
+
gl.activeTexture(gl.TEXTURE0 + ATLAS_UNIT);
|
|
634
|
+
gl.bindTexture(gl.TEXTURE_2D, s.atlasTexture);
|
|
635
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, s.atlas.size, s.atlas.size, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
636
|
+
setNearest(gl);
|
|
637
|
+
}
|
|
638
|
+
function setUnpack(gl, premultiply) {
|
|
639
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
|
|
640
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
641
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, premultiply);
|
|
642
|
+
}
|
|
643
|
+
function setNearest(gl) {
|
|
644
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
645
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
646
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
647
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
648
|
+
}
|
|
649
|
+
function linkProgram(gl, vert, frag) {
|
|
650
|
+
const compile = (type, src) => {
|
|
651
|
+
const shader = gl.createShader(type);
|
|
652
|
+
gl.shaderSource(shader, src);
|
|
653
|
+
gl.compileShader(shader);
|
|
654
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
655
|
+
throw new Error("Shader compile failed: " + gl.getShaderInfoLog(shader));
|
|
656
|
+
}
|
|
657
|
+
return shader;
|
|
658
|
+
};
|
|
659
|
+
const program = gl.createProgram();
|
|
660
|
+
const vs = compile(gl.VERTEX_SHADER, vert);
|
|
661
|
+
const fs = compile(gl.FRAGMENT_SHADER, frag);
|
|
662
|
+
gl.attachShader(program, vs);
|
|
663
|
+
gl.attachShader(program, fs);
|
|
664
|
+
gl.linkProgram(program);
|
|
665
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
666
|
+
throw new Error("Shader link failed: " + gl.getProgramInfoLog(program));
|
|
667
|
+
}
|
|
668
|
+
gl.deleteShader(vs);
|
|
669
|
+
gl.deleteShader(fs);
|
|
670
|
+
return program;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/webgpurenderer.ts
|
|
674
|
+
import { cssColor as cssColor3 } from "@glyph-engine/core";
|
|
675
|
+
|
|
676
|
+
// src/shaders/grid.wgsl
|
|
677
|
+
var grid_default3 = `// Draws the whole character grid, as grid.frag does for WebGL2: one fragment =
|
|
678
|
+
// one screen pixel. Finds its cell, reads it, samples the glyph atlas, mixes
|
|
679
|
+
// fg/bg. Layering: this cell's background, the left neighbour's glyph overflow
|
|
680
|
+
// (right half of a wide glyph), then this cell's glyph.
|
|
681
|
+
|
|
682
|
+
struct Draw {
|
|
683
|
+
cellSize: vec2f, // cell size in device pixels (x may be fractional)
|
|
684
|
+
offset: vec2f, // grid origin in device pixels, from the top-left
|
|
685
|
+
grid: vec2u, // cols, rows
|
|
686
|
+
slotSize: vec2u, // atlas slot size in pixels: two cells wide, for wide glyphs
|
|
687
|
+
background: vec4f, // canvas background, also outside the grid
|
|
688
|
+
slotsPerRow: u32,
|
|
689
|
+
backgroundPacked: u32, // same colour as RGBA8: cells with it draw no background
|
|
690
|
+
useOverlay: u32, // 1: cells flagged in \`overlay\` win over \`cells\`
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@group(0) @binding(0) var<uniform> d: Draw;
|
|
694
|
+
@group(0) @binding(1) var<storage, read> cells: array<vec4u>; // per cell: atlas slot, fg RGBA8, bg RGBA8, flags
|
|
695
|
+
@group(0) @binding(2) var<storage, read> overlay: array<vec4u>; // same layout: what pre()/post() drew over a GPU program
|
|
696
|
+
@group(0) @binding(3) var atlas: texture_2d<f32>; // glyphs drawn white by Canvas2D, premultiplied alpha
|
|
697
|
+
|
|
698
|
+
const FLAG_COLOR_GLYPH = 1u; // emoji etc.: keep the glyph's own colours
|
|
699
|
+
const FLAG_OVERLAY = 2u; // overlay cell present
|
|
700
|
+
|
|
701
|
+
// Fullscreen triangle built from the vertex index: no vertex buffers needed.
|
|
702
|
+
@vertex
|
|
703
|
+
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
704
|
+
let p = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
|
|
705
|
+
return vec4f(p * 2.0 - 1.0, 0.0, 1.0);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
fn unpack(c: u32) -> vec4f {
|
|
709
|
+
return vec4f(f32(c >> 24u), f32((c >> 16u) & 255u), f32((c >> 8u) & 255u), f32(c & 255u)) / 255.0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
fn cellAt(k: u32) -> vec4u {
|
|
713
|
+
if (d.useOverlay != 0u) {
|
|
714
|
+
let o = overlay[k];
|
|
715
|
+
if ((o.w & FLAG_OVERLAY) != 0u) { return o; }
|
|
716
|
+
}
|
|
717
|
+
return cells[k];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Draws the glyph of cell \`c\` over \`color\`; \`local\` is the pixel relative to
|
|
721
|
+
// that cell's top-left corner.
|
|
722
|
+
fn drawGlyph(color: vec3f, c: vec4u, local: vec2f) -> vec3f {
|
|
723
|
+
if (c.x == 0u) { return color; } // slot 0: space
|
|
724
|
+
let origin = vec2u(c.x % d.slotsPerRow, c.x / d.slotsPerRow) * d.slotSize;
|
|
725
|
+
let p = vec2u(local);
|
|
726
|
+
if (p.x >= d.slotSize.x || p.y >= d.slotSize.y) { return color; }
|
|
727
|
+
let glyph = textureLoad(atlas, origin + p, 0);
|
|
728
|
+
let fg = unpack(c.y);
|
|
729
|
+
let colorGlyph = (c.w & FLAG_COLOR_GLYPH) != 0u;
|
|
730
|
+
let coverage = select(glyph.a * fg.a, glyph.a, colorGlyph);
|
|
731
|
+
let ink = select(fg.rgb * coverage, glyph.rgb, colorGlyph);
|
|
732
|
+
return color * (1.0 - coverage) + ink;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
@fragment
|
|
736
|
+
fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
|
737
|
+
let px = pos.xy - d.offset;
|
|
738
|
+
let cell = vec2i(floor(px / d.cellSize));
|
|
739
|
+
if (px.x < 0.0 || px.y < 0.0 || cell.x >= i32(d.grid.x) || cell.y >= i32(d.grid.y)) {
|
|
740
|
+
return d.background;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
var color = d.background.rgb;
|
|
744
|
+
let local = px - vec2f(cell) * d.cellSize;
|
|
745
|
+
let k = u32(cell.y) * d.grid.x + u32(cell.x);
|
|
746
|
+
|
|
747
|
+
let c = cellAt(k);
|
|
748
|
+
if (c.z != d.backgroundPacked) {
|
|
749
|
+
let bg = unpack(c.z); // translucent backgrounds blend, as with Canvas2D
|
|
750
|
+
color = mix(color, bg.rgb, bg.a);
|
|
751
|
+
}
|
|
752
|
+
if (cell.x > 0) {
|
|
753
|
+
color = drawGlyph(color, cellAt(k - 1u), local + vec2f(d.cellSize.x, 0.0));
|
|
754
|
+
}
|
|
755
|
+
return vec4f(drawGlyph(color, c, local), 1.0);
|
|
756
|
+
}
|
|
757
|
+
`;
|
|
758
|
+
|
|
759
|
+
// src/shaders/program.wgsl
|
|
760
|
+
var program_default = `// Runs a program's WGSL main() for every cell, like the JS loop in computeFrame.
|
|
761
|
+
// The program's source replaces the PROGRAM line; everything above it is what
|
|
762
|
+
// a program can use.
|
|
763
|
+
|
|
764
|
+
struct Context {
|
|
765
|
+
time: f32, // ms
|
|
766
|
+
delta: f32, // ms since the previous frame
|
|
767
|
+
frame: u32,
|
|
768
|
+
cols: u32,
|
|
769
|
+
rows: u32,
|
|
770
|
+
aspect: f32, // cell width / height
|
|
771
|
+
cursor: vec2f, // in cells (fractional)
|
|
772
|
+
pressed: u32, // 1 while the pointer is down
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// What main() writes. Colours are 0-1 per channel, alpha included.
|
|
776
|
+
struct Cell {
|
|
777
|
+
char: u32, // a code point; glyph(i) gives the i-th of the program's glyphs
|
|
778
|
+
color: vec4f, // starts as settings.color
|
|
779
|
+
background: vec4f, // starts as settings.backgroundColor
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
struct GeEngine {
|
|
783
|
+
ctx: Context,
|
|
784
|
+
color: vec4f,
|
|
785
|
+
background: vec4f,
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@group(0) @binding(0) var<uniform> ge_engine: GeEngine;
|
|
789
|
+
@group(0) @binding(1) var<storage, read> ge_glyphs: array<u32>; // the program's glyphs, as code points
|
|
790
|
+
@group(0) @binding(2) var<storage, read> ge_slots: array<u32>; // code point (< 0x10000) -> atlas slot | flags << 16
|
|
791
|
+
@group(0) @binding(3) var<storage, read_write> ge_cells: array<vec4u>;
|
|
792
|
+
|
|
793
|
+
// The i-th glyph of the program's \`glyphs\` string (clamped to the last one).
|
|
794
|
+
fn glyph(i: u32) -> u32 {
|
|
795
|
+
return ge_glyphs[min(i, GLYPHS - 1u)];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// PROGRAM
|
|
799
|
+
|
|
800
|
+
fn ge_pack(c: vec4f) -> u32 {
|
|
801
|
+
let v = vec4u(round(clamp(c, vec4f(0.0), vec4f(1.0)) * 255.0));
|
|
802
|
+
return (v.r << 24u) | (v.g << 16u) | (v.b << 8u) | v.a;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
@compute @workgroup_size(8, 8)
|
|
806
|
+
fn ge_main(@builtin(global_invocation_id) id: vec3u) {
|
|
807
|
+
let ctx = ge_engine.ctx;
|
|
808
|
+
if (id.x >= ctx.cols || id.y >= ctx.rows) { return; }
|
|
809
|
+
var out = Cell(32u, ge_engine.color, ge_engine.background);
|
|
810
|
+
main(vec2f(id.xy), ctx, &out);
|
|
811
|
+
var entry = 0u; // glyphs not in the program's set draw nothing
|
|
812
|
+
if (out.char < 65536u) { entry = ge_slots[out.char]; }
|
|
813
|
+
ge_cells[id.y * ctx.cols + id.x] = vec4u(entry & 0xffffu, ge_pack(out.color), ge_pack(out.background), entry >> 16u);
|
|
814
|
+
}
|
|
815
|
+
`;
|
|
816
|
+
|
|
817
|
+
// src/webgpurenderer.ts
|
|
818
|
+
var webgpurenderer_default = {
|
|
819
|
+
preferredElementNodeName: "CANVAS",
|
|
820
|
+
render: render4
|
|
821
|
+
};
|
|
822
|
+
var FLAG_OVERLAY = 2;
|
|
823
|
+
var device;
|
|
824
|
+
async function initWebGPU() {
|
|
825
|
+
if (device !== undefined)
|
|
826
|
+
return !!device;
|
|
827
|
+
try {
|
|
828
|
+
const adapter = await navigator.gpu?.requestAdapter();
|
|
829
|
+
device = adapter ? await adapter.requestDevice() : null;
|
|
830
|
+
} catch {
|
|
831
|
+
device = null;
|
|
832
|
+
}
|
|
833
|
+
device?.lost.then(() => device = null);
|
|
834
|
+
return !!device;
|
|
835
|
+
}
|
|
836
|
+
var compiled = new Map;
|
|
837
|
+
var PRELUDE_LINES = program_default.slice(0, program_default.indexOf("// PROGRAM")).split(`
|
|
838
|
+
`).length;
|
|
839
|
+
async function compileGpuProgram(wgsl, glyphs) {
|
|
840
|
+
const dev = device;
|
|
841
|
+
if (!dev)
|
|
842
|
+
throw new Error("WebGPU is not available");
|
|
843
|
+
const id = glyphs + "\x00" + wgsl;
|
|
844
|
+
if (compiled.has(id))
|
|
845
|
+
return;
|
|
846
|
+
const points = [...glyphs].map((c) => c.codePointAt(0));
|
|
847
|
+
if (points.length === 0)
|
|
848
|
+
points.push(32);
|
|
849
|
+
const code = `const GLYPHS: u32 = ${points.length}u;
|
|
850
|
+
` + program_default.replace("// PROGRAM", wgsl);
|
|
851
|
+
const module = dev.createShaderModule({ code });
|
|
852
|
+
const errors = (await module.getCompilationInfo()).messages.filter((m) => m.type === "error");
|
|
853
|
+
if (errors.length) {
|
|
854
|
+
const lines = errors.map((m) => {
|
|
855
|
+
const line = m.lineNum - PRELUDE_LINES;
|
|
856
|
+
return (line > 0 ? `line ${line}:${m.linePos}: ` : "") + m.message;
|
|
857
|
+
});
|
|
858
|
+
throw new Error(`WGSL error in the program:
|
|
859
|
+
` + lines.join(`
|
|
860
|
+
`));
|
|
861
|
+
}
|
|
862
|
+
const entry = (type) => ({
|
|
863
|
+
binding: 0,
|
|
864
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
865
|
+
buffer: { type }
|
|
866
|
+
});
|
|
867
|
+
const layout = dev.createBindGroupLayout({
|
|
868
|
+
entries: [
|
|
869
|
+
{ ...entry("uniform"), binding: 0 },
|
|
870
|
+
{ ...entry("read-only-storage"), binding: 1 },
|
|
871
|
+
{ ...entry("read-only-storage"), binding: 2 },
|
|
872
|
+
{ ...entry("storage"), binding: 3 }
|
|
873
|
+
]
|
|
874
|
+
});
|
|
875
|
+
const pipeline = await dev.createComputePipelineAsync({
|
|
876
|
+
layout: dev.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
877
|
+
compute: { module, entryPoint: "ge_main" }
|
|
878
|
+
});
|
|
879
|
+
compiled.set(id, { pipeline, layout, glyphs: points });
|
|
880
|
+
}
|
|
881
|
+
var states3 = new WeakMap;
|
|
882
|
+
function render4(context, frame, gpu) {
|
|
883
|
+
const dev = device;
|
|
884
|
+
const canvas = context.settings.element;
|
|
885
|
+
let s = states3.get(canvas);
|
|
886
|
+
if (!dev && s)
|
|
887
|
+
return;
|
|
888
|
+
if (!dev || context.settings.textAlign == "center")
|
|
889
|
+
return webglrenderer_default.render(context, frame);
|
|
890
|
+
if (!s || s.device !== dev)
|
|
891
|
+
states3.set(canvas, s = createState2(dev, canvas));
|
|
892
|
+
draw2(s, context, frame, gpu);
|
|
893
|
+
}
|
|
894
|
+
function createState2(dev, canvas) {
|
|
895
|
+
const ctx = canvas.getContext("webgpu");
|
|
896
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
897
|
+
ctx.configure({ device: dev, format, alphaMode: "opaque" });
|
|
898
|
+
const module = dev.createShaderModule({ code: grid_default3 });
|
|
899
|
+
const layout = dev.createBindGroupLayout({
|
|
900
|
+
entries: [
|
|
901
|
+
{
|
|
902
|
+
binding: 0,
|
|
903
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
904
|
+
buffer: { type: "uniform" }
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
binding: 1,
|
|
908
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
909
|
+
buffer: { type: "read-only-storage" }
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
binding: 2,
|
|
913
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
914
|
+
buffer: { type: "read-only-storage" }
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
binding: 3,
|
|
918
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
919
|
+
texture: { sampleType: "float" }
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
});
|
|
923
|
+
const pipeline = dev.createRenderPipeline({
|
|
924
|
+
layout: dev.createPipelineLayout({ bindGroupLayouts: [layout] }),
|
|
925
|
+
vertex: { module, entryPoint: "vs" },
|
|
926
|
+
fragment: { module, entryPoint: "fs", targets: [{ format }] }
|
|
927
|
+
});
|
|
928
|
+
const size = Math.min(2048, dev.limits.maxTextureDimension2D);
|
|
929
|
+
const s = {
|
|
930
|
+
device: dev,
|
|
931
|
+
ctx,
|
|
932
|
+
pipeline,
|
|
933
|
+
layout,
|
|
934
|
+
drawUniforms: dev.createBuffer({
|
|
935
|
+
size: 64,
|
|
936
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
937
|
+
}),
|
|
938
|
+
drawData: new ArrayBuffer(64),
|
|
939
|
+
atlasTexture: createAtlasTexture(dev, size),
|
|
940
|
+
atlas: null,
|
|
941
|
+
cols: 0,
|
|
942
|
+
rows: 0,
|
|
943
|
+
cells: null,
|
|
944
|
+
overlay: null,
|
|
945
|
+
cellData: new Uint32Array(0),
|
|
946
|
+
overlayData: new Uint32Array(0),
|
|
947
|
+
overlayEmpty: false,
|
|
948
|
+
bindGroup: null,
|
|
949
|
+
engineUniforms: dev.createBuffer({
|
|
950
|
+
size: 80,
|
|
951
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
952
|
+
}),
|
|
953
|
+
engineData: new ArrayBuffer(80),
|
|
954
|
+
program: null
|
|
955
|
+
};
|
|
956
|
+
s.atlas = new GlyphAtlas(size, (x, y, glyph) => {
|
|
957
|
+
dev.queue.copyExternalImageToTexture({ source: glyph }, { texture: s.atlasTexture, origin: { x, y }, premultipliedAlpha: true }, [glyph.width, glyph.height]);
|
|
958
|
+
});
|
|
959
|
+
return s;
|
|
960
|
+
}
|
|
961
|
+
function createAtlasTexture(dev, size) {
|
|
962
|
+
return dev.createTexture({
|
|
963
|
+
size: [size, size],
|
|
964
|
+
format: "rgba8unorm",
|
|
965
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function draw2(s, context, frame, gpu) {
|
|
969
|
+
const dev = s.device;
|
|
970
|
+
const canvas = context.settings.element;
|
|
971
|
+
const settings = context.settings;
|
|
972
|
+
const m = context.metrics;
|
|
973
|
+
const scale = devicePixelRatio;
|
|
974
|
+
fitCanvas(canvas, context, scale);
|
|
975
|
+
const cols = context.cols;
|
|
976
|
+
const rows = context.rows;
|
|
977
|
+
const cw = m.cellWidth;
|
|
978
|
+
const ch = Math.round(m.lineHeight);
|
|
979
|
+
const fontSize = m.fontSize * scale;
|
|
980
|
+
if (s.atlas.ensure(2 * Math.ceil(cw * scale), Math.ceil(ch * scale), fontSize + "px " + m.fontFamily)) {
|
|
981
|
+
s.atlasTexture.destroy();
|
|
982
|
+
s.atlasTexture = createAtlasTexture(dev, s.atlas.size);
|
|
983
|
+
s.bindGroup = null;
|
|
984
|
+
}
|
|
985
|
+
ensureCells2(s, cols, rows);
|
|
986
|
+
const bg = cssColor3(settings.backgroundColor || "white");
|
|
987
|
+
const fg = cssColor3(settings.color || "black");
|
|
988
|
+
const fontWeight = String(settings.fontWeight || "400");
|
|
989
|
+
const encoder = dev.createCommandEncoder();
|
|
990
|
+
if (gpu) {
|
|
991
|
+
const data = s.overlayData;
|
|
992
|
+
let any = false;
|
|
993
|
+
for (let k = 0, n = cols * rows;k < n; k++) {
|
|
994
|
+
const o = k * 4;
|
|
995
|
+
if (frame.chars[k] === 0) {
|
|
996
|
+
data[o] = data[o + 1] = data[o + 2] = data[o + 3] = 0;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
any = true;
|
|
1000
|
+
const slot = s.atlas.cellSlot(frame, k, fontWeight, fontSize, m.fontFamily);
|
|
1001
|
+
data[o] = slot;
|
|
1002
|
+
data[o + 1] = frame.fg[k] || fg;
|
|
1003
|
+
data[o + 2] = frame.bg[k] || bg;
|
|
1004
|
+
data[o + 3] = FLAG_OVERLAY | (s.atlas.colored[slot] ? FLAG_COLOR_GLYPH : 0);
|
|
1005
|
+
}
|
|
1006
|
+
if (any || !s.overlayEmpty)
|
|
1007
|
+
dev.queue.writeBuffer(s.overlay, 0, data);
|
|
1008
|
+
s.overlayEmpty = !any;
|
|
1009
|
+
runProgram(s, encoder, context, gpu, fg, bg, fontWeight, fontSize, m.fontFamily);
|
|
1010
|
+
} else {
|
|
1011
|
+
s.atlas.writeCells(s.cellData, frame, fg, bg, fontWeight, fontSize, m.fontFamily);
|
|
1012
|
+
dev.queue.writeBuffer(s.cells, 0, s.cellData);
|
|
1013
|
+
}
|
|
1014
|
+
let ox = 0;
|
|
1015
|
+
let oy = 0;
|
|
1016
|
+
if (settings.canvasOffset) {
|
|
1017
|
+
const offs = settings.canvasOffset;
|
|
1018
|
+
ox = Math.round(offs.x == "auto" ? (canvas.width / scale - cols * cw) / 2 : offs.x);
|
|
1019
|
+
oy = Math.round(offs.y == "auto" ? (canvas.height / scale - rows * ch) / 2 : offs.y);
|
|
1020
|
+
}
|
|
1021
|
+
const f = new Float32Array(s.drawData);
|
|
1022
|
+
const u = new Uint32Array(s.drawData);
|
|
1023
|
+
f.set([cw * scale, ch * scale, ox * scale, oy * scale], 0);
|
|
1024
|
+
u.set([cols, rows, s.atlas.slotW, s.atlas.slotH], 4);
|
|
1025
|
+
f.set([(bg >>> 24) / 255, (bg >>> 16 & 255) / 255, (bg >>> 8 & 255) / 255, 1], 8);
|
|
1026
|
+
u.set([s.atlas.perRow, bg, gpu ? 1 : 0], 12);
|
|
1027
|
+
dev.queue.writeBuffer(s.drawUniforms, 0, s.drawData);
|
|
1028
|
+
s.bindGroup ??= dev.createBindGroup({
|
|
1029
|
+
layout: s.layout,
|
|
1030
|
+
entries: [
|
|
1031
|
+
{ binding: 0, resource: { buffer: s.drawUniforms } },
|
|
1032
|
+
{ binding: 1, resource: { buffer: s.cells } },
|
|
1033
|
+
{ binding: 2, resource: { buffer: s.overlay } },
|
|
1034
|
+
{ binding: 3, resource: s.atlasTexture.createView() }
|
|
1035
|
+
]
|
|
1036
|
+
});
|
|
1037
|
+
const pass = encoder.beginRenderPass({
|
|
1038
|
+
colorAttachments: [
|
|
1039
|
+
{
|
|
1040
|
+
view: s.ctx.getCurrentTexture().createView(),
|
|
1041
|
+
loadOp: "clear",
|
|
1042
|
+
storeOp: "store",
|
|
1043
|
+
clearValue: [0, 0, 0, 1]
|
|
1044
|
+
}
|
|
1045
|
+
]
|
|
1046
|
+
});
|
|
1047
|
+
pass.setPipeline(s.pipeline);
|
|
1048
|
+
pass.setBindGroup(0, s.bindGroup);
|
|
1049
|
+
pass.draw(3);
|
|
1050
|
+
pass.end();
|
|
1051
|
+
dev.queue.submit([encoder.finish()]);
|
|
1052
|
+
}
|
|
1053
|
+
function runProgram(s, encoder, context, gpu, fg, bg, fontWeight, fontSize, family) {
|
|
1054
|
+
const dev = s.device;
|
|
1055
|
+
const id = gpu.glyphs + "\x00" + gpu.wgsl;
|
|
1056
|
+
const c = compiled.get(id);
|
|
1057
|
+
if (!c)
|
|
1058
|
+
throw new Error("WGSL program not compiled: run() compiles it before the first frame");
|
|
1059
|
+
let p = s.program;
|
|
1060
|
+
if (!p || p.id !== id) {
|
|
1061
|
+
p?.glyphBuffer.destroy();
|
|
1062
|
+
p?.slotBuffer.destroy();
|
|
1063
|
+
const glyphBuffer = dev.createBuffer({
|
|
1064
|
+
size: c.glyphs.length * 4,
|
|
1065
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
1066
|
+
});
|
|
1067
|
+
dev.queue.writeBuffer(glyphBuffer, 0, new Uint32Array(c.glyphs));
|
|
1068
|
+
const slotBuffer = dev.createBuffer({
|
|
1069
|
+
size: 65536 * 4,
|
|
1070
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
1071
|
+
});
|
|
1072
|
+
s.program = p = {
|
|
1073
|
+
id,
|
|
1074
|
+
glyphBuffer,
|
|
1075
|
+
slotBuffer,
|
|
1076
|
+
generation: -1,
|
|
1077
|
+
bindGroup: null
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (p.generation !== s.atlas.generation) {
|
|
1081
|
+
const slots = new Uint32Array(65536);
|
|
1082
|
+
for (const cp of c.glyphs) {
|
|
1083
|
+
if (cp >= 65536 || cp === 32)
|
|
1084
|
+
continue;
|
|
1085
|
+
const slot = s.atlas.slot(0, cp, String.fromCodePoint(cp), fontWeight, fontSize, family);
|
|
1086
|
+
slots[cp] = slot | (s.atlas.colored[slot] ? FLAG_COLOR_GLYPH : 0) << 16;
|
|
1087
|
+
}
|
|
1088
|
+
dev.queue.writeBuffer(p.slotBuffer, 0, slots);
|
|
1089
|
+
p.generation = s.atlas.generation;
|
|
1090
|
+
}
|
|
1091
|
+
const f = new Float32Array(s.engineData);
|
|
1092
|
+
const u = new Uint32Array(s.engineData);
|
|
1093
|
+
f[0] = context.time;
|
|
1094
|
+
f[1] = context.delta;
|
|
1095
|
+
u[2] = context.frame;
|
|
1096
|
+
u[3] = context.cols;
|
|
1097
|
+
u[4] = context.rows;
|
|
1098
|
+
f[5] = context.metrics.aspect;
|
|
1099
|
+
f[6] = gpu.cursor.x;
|
|
1100
|
+
f[7] = gpu.cursor.y;
|
|
1101
|
+
u[8] = gpu.cursor.pressed ? 1 : 0;
|
|
1102
|
+
f.set(unpack(fg), 12);
|
|
1103
|
+
f.set(unpack(bg), 16);
|
|
1104
|
+
dev.queue.writeBuffer(s.engineUniforms, 0, s.engineData);
|
|
1105
|
+
p.bindGroup ??= dev.createBindGroup({
|
|
1106
|
+
layout: c.layout,
|
|
1107
|
+
entries: [
|
|
1108
|
+
{ binding: 0, resource: { buffer: s.engineUniforms } },
|
|
1109
|
+
{ binding: 1, resource: { buffer: p.glyphBuffer } },
|
|
1110
|
+
{ binding: 2, resource: { buffer: p.slotBuffer } },
|
|
1111
|
+
{ binding: 3, resource: { buffer: s.cells } }
|
|
1112
|
+
]
|
|
1113
|
+
});
|
|
1114
|
+
const pass = encoder.beginComputePass();
|
|
1115
|
+
pass.setPipeline(c.pipeline);
|
|
1116
|
+
pass.setBindGroup(0, p.bindGroup);
|
|
1117
|
+
pass.dispatchWorkgroups(Math.ceil(context.cols / 8), Math.ceil(context.rows / 8));
|
|
1118
|
+
pass.end();
|
|
1119
|
+
}
|
|
1120
|
+
var unpack = (c) => [c >>> 24, c >>> 16 & 255, c >>> 8 & 255, c & 255].map((v) => v / 255);
|
|
1121
|
+
function ensureCells2(s, cols, rows) {
|
|
1122
|
+
if (s.cols === cols && s.rows === rows)
|
|
1123
|
+
return;
|
|
1124
|
+
s.cols = cols;
|
|
1125
|
+
s.rows = rows;
|
|
1126
|
+
const size = Math.max(16, cols * rows * 16);
|
|
1127
|
+
const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
|
|
1128
|
+
s.cells?.destroy();
|
|
1129
|
+
s.overlay?.destroy();
|
|
1130
|
+
s.cells = s.device.createBuffer({ size, usage });
|
|
1131
|
+
s.overlay = s.device.createBuffer({ size, usage });
|
|
1132
|
+
s.cellData = new Uint32Array(cols * rows * 4);
|
|
1133
|
+
s.overlayData = new Uint32Array(cols * rows * 4);
|
|
1134
|
+
s.overlayEmpty = true;
|
|
1135
|
+
s.bindGroup = null;
|
|
1136
|
+
if (s.program)
|
|
1137
|
+
s.program.bindGroup = null;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/run.ts
|
|
1141
|
+
var renderers = {
|
|
1142
|
+
canvas: canvasrenderer_default,
|
|
1143
|
+
text: textrenderer_default,
|
|
1144
|
+
webgl: webglrenderer_default,
|
|
1145
|
+
webgpu: webgpurenderer_default
|
|
1146
|
+
};
|
|
1147
|
+
var defaultSettings = {
|
|
1148
|
+
element: null,
|
|
1149
|
+
cols: 0,
|
|
1150
|
+
rows: 0,
|
|
1151
|
+
once: false,
|
|
1152
|
+
fps: 30,
|
|
1153
|
+
renderer: "text",
|
|
1154
|
+
allowSelect: false,
|
|
1155
|
+
restoreState: false
|
|
1156
|
+
};
|
|
1157
|
+
var CSSStyles = ["backgroundColor", "color", "fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight", "textAlign"];
|
|
1158
|
+
function run(program, runSettings = {}, userData = {}) {
|
|
1159
|
+
return new Promise(function(resolve, reject) {
|
|
1160
|
+
const settings = {
|
|
1161
|
+
...defaultSettings,
|
|
1162
|
+
...runSettings,
|
|
1163
|
+
...program.settings
|
|
1164
|
+
};
|
|
1165
|
+
const state = {
|
|
1166
|
+
time: 0,
|
|
1167
|
+
frame: 0,
|
|
1168
|
+
cycle: 0,
|
|
1169
|
+
fps: settings.fps
|
|
1170
|
+
};
|
|
1171
|
+
const LOCAL_STORAGE_KEY_STATE = "currentState";
|
|
1172
|
+
if (settings.restoreState) {
|
|
1173
|
+
storage_default.restore(LOCAL_STORAGE_KEY_STATE, state);
|
|
1174
|
+
state.cycle++;
|
|
1175
|
+
addEventListener("pagehide", () => storage_default.store(LOCAL_STORAGE_KEY_STATE, state));
|
|
1176
|
+
}
|
|
1177
|
+
const renderer = renderers[settings.renderer] || renderers["text"];
|
|
1178
|
+
if (!settings.element) {
|
|
1179
|
+
settings.element = document.createElement(renderer.preferredElementNodeName);
|
|
1180
|
+
document.body.appendChild(settings.element);
|
|
1181
|
+
} else if (settings.element.nodeName == "CANVAS" != (renderer.preferredElementNodeName == "CANVAS")) {
|
|
1182
|
+
console.warn(`The "${settings.renderer}" renderer expects a ${renderer.preferredElementNodeName} target element.`);
|
|
1183
|
+
}
|
|
1184
|
+
for (const s of CSSStyles) {
|
|
1185
|
+
const v = settings[s];
|
|
1186
|
+
if (v)
|
|
1187
|
+
settings.element.style[s] = v;
|
|
1188
|
+
}
|
|
1189
|
+
const eventQueue = [];
|
|
1190
|
+
const pointer = {
|
|
1191
|
+
x: 0,
|
|
1192
|
+
y: 0,
|
|
1193
|
+
pressed: false,
|
|
1194
|
+
px: 0,
|
|
1195
|
+
py: 0,
|
|
1196
|
+
ppressed: false
|
|
1197
|
+
};
|
|
1198
|
+
settings.element.addEventListener("pointermove", (e) => {
|
|
1199
|
+
const rect = settings.element?.getBoundingClientRect();
|
|
1200
|
+
if (!rect)
|
|
1201
|
+
return;
|
|
1202
|
+
pointer.x = e.clientX - rect.left;
|
|
1203
|
+
pointer.y = e.clientY - rect.top;
|
|
1204
|
+
eventQueue.push("pointerMove");
|
|
1205
|
+
});
|
|
1206
|
+
settings.element.addEventListener("pointerdown", (e) => {
|
|
1207
|
+
pointer.pressed = true;
|
|
1208
|
+
eventQueue.push("pointerDown");
|
|
1209
|
+
});
|
|
1210
|
+
settings.element.addEventListener("pointerup", (e) => {
|
|
1211
|
+
pointer.pressed = false;
|
|
1212
|
+
eventQueue.push("pointerUp");
|
|
1213
|
+
});
|
|
1214
|
+
const touchHandler = (e) => {
|
|
1215
|
+
const rect = settings.element?.getBoundingClientRect();
|
|
1216
|
+
if (!rect)
|
|
1217
|
+
return;
|
|
1218
|
+
pointer.x = e.touches[0].clientX - rect.left;
|
|
1219
|
+
pointer.y = e.touches[0].clientY - rect.top;
|
|
1220
|
+
eventQueue.push("pointerMove");
|
|
1221
|
+
};
|
|
1222
|
+
settings.element.addEventListener("touchmove", touchHandler);
|
|
1223
|
+
settings.element.addEventListener("touchstart", touchHandler);
|
|
1224
|
+
settings.element.addEventListener("touchend", touchHandler);
|
|
1225
|
+
settings.element.style.fontStretch = "normal";
|
|
1226
|
+
if (!settings.allowSelect)
|
|
1227
|
+
disableSelect(settings.element);
|
|
1228
|
+
document.fonts.ready.then((e) => {
|
|
1229
|
+
let count = 3;
|
|
1230
|
+
(function __run_thrice__() {
|
|
1231
|
+
if (--count > 0) {
|
|
1232
|
+
requestAnimationFrame(__run_thrice__);
|
|
1233
|
+
} else {
|
|
1234
|
+
prepareGpu().then(boot, reject);
|
|
1235
|
+
}
|
|
1236
|
+
})();
|
|
1237
|
+
});
|
|
1238
|
+
let gpu;
|
|
1239
|
+
async function prepareGpu() {
|
|
1240
|
+
const device = settings.renderer == "webgpu" ? await initWebGPU() : null;
|
|
1241
|
+
if (program.wgsl && device) {
|
|
1242
|
+
await compileGpuProgram(program.wgsl, program.glyphs ?? " ");
|
|
1243
|
+
gpu = { wgsl: program.wgsl, glyphs: program.glyphs ?? " " };
|
|
1244
|
+
} else if (program.wgsl && typeof program.main != "function") {
|
|
1245
|
+
throw new Error(device === false ? "This program needs WebGPU, which this browser does not provide." : 'This program runs on the GPU: set renderer: "webgpu".');
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function boot() {
|
|
1249
|
+
const el = settings.element;
|
|
1250
|
+
let rect = el.getBoundingClientRect();
|
|
1251
|
+
new ResizeObserver(() => rect = el.getBoundingClientRect()).observe(el);
|
|
1252
|
+
const host = {
|
|
1253
|
+
requestFrame: (cb) => requestAnimationFrame(cb),
|
|
1254
|
+
metrics: calcMetrics(el),
|
|
1255
|
+
size: () => rect,
|
|
1256
|
+
pointer,
|
|
1257
|
+
events: eventQueue,
|
|
1258
|
+
render: (context, frame, cursor) => renderer.render(context, frame, gpu && { ...gpu, cursor }),
|
|
1259
|
+
onGpu: !!gpu,
|
|
1260
|
+
storeState: (state) => storage_default.store(LOCAL_STORAGE_KEY_STATE, state)
|
|
1261
|
+
};
|
|
1262
|
+
startLoop(program, settings, userData, state, host, resolve);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
function disableSelect(el) {
|
|
1267
|
+
el.style.userSelect = "none";
|
|
1268
|
+
el.style.webkitUserSelect = "none";
|
|
1269
|
+
el.style.userSelect = "none";
|
|
1270
|
+
el.dataset.selectionEnabled = "false";
|
|
1271
|
+
}
|
|
1272
|
+
function enableSelect(el) {
|
|
1273
|
+
el.style.userSelect = "auto";
|
|
1274
|
+
el.style.webkitUserSelect = "auto";
|
|
1275
|
+
el.style.userSelect = "auto";
|
|
1276
|
+
el.dataset.selectionEnabled = "true";
|
|
1277
|
+
}
|
|
1278
|
+
function copyContent(el) {
|
|
1279
|
+
const selectionEnabled = !el.dataset.selectionEnabled === false;
|
|
1280
|
+
if (!selectionEnabled)
|
|
1281
|
+
enableSelect(el);
|
|
1282
|
+
const range = document.createRange();
|
|
1283
|
+
range.selectNode(el);
|
|
1284
|
+
const sel = window.getSelection();
|
|
1285
|
+
if (!sel)
|
|
1286
|
+
throw new Error("Could not get selection");
|
|
1287
|
+
sel.removeAllRanges();
|
|
1288
|
+
sel.addRange(range);
|
|
1289
|
+
navigator.clipboard.writeText(el.innerText);
|
|
1290
|
+
sel.removeAllRanges();
|
|
1291
|
+
if (!selectionEnabled)
|
|
1292
|
+
disableSelect(el);
|
|
1293
|
+
}
|
|
1294
|
+
function calcMetrics(el) {
|
|
1295
|
+
const style = getComputedStyle(el);
|
|
1296
|
+
const fontFamily = style.getPropertyValue("font-family");
|
|
1297
|
+
const fontSize = parseFloat(style.getPropertyValue("font-size"));
|
|
1298
|
+
const lineHeight = parseFloat(style.getPropertyValue("line-height"));
|
|
1299
|
+
let cellWidth;
|
|
1300
|
+
if (el.nodeName == "CANVAS") {
|
|
1301
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
1302
|
+
if (!ctx)
|
|
1303
|
+
throw new Error("Could not get 2d context");
|
|
1304
|
+
ctx.font = fontSize + "px " + fontFamily;
|
|
1305
|
+
cellWidth = ctx.measureText("".padEnd(50, "X")).width / 50;
|
|
1306
|
+
} else {
|
|
1307
|
+
const span = document.createElement("span");
|
|
1308
|
+
el.appendChild(span);
|
|
1309
|
+
span.innerHTML = "".padEnd(50, "X");
|
|
1310
|
+
cellWidth = span.getBoundingClientRect().width / 50;
|
|
1311
|
+
el.removeChild(span);
|
|
1312
|
+
}
|
|
1313
|
+
const metrics = {
|
|
1314
|
+
aspect: cellWidth / lineHeight,
|
|
1315
|
+
cellWidth,
|
|
1316
|
+
lineHeight,
|
|
1317
|
+
fontFamily,
|
|
1318
|
+
fontSize,
|
|
1319
|
+
_update: function() {
|
|
1320
|
+
const tmp = calcMetrics(el);
|
|
1321
|
+
for (var k in tmp) {}
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
return metrics;
|
|
1325
|
+
}
|
|
1326
|
+
// src/modules/camera.ts
|
|
1327
|
+
var camera_default = { init };
|
|
1328
|
+
var video = null;
|
|
1329
|
+
function init(callback) {
|
|
1330
|
+
video = video || getUserMedia(callback);
|
|
1331
|
+
return video;
|
|
1332
|
+
}
|
|
1333
|
+
function getUserMedia(callback) {
|
|
1334
|
+
if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
|
|
1335
|
+
throw new DOMException("getUserMedia not supported in this browser");
|
|
1336
|
+
}
|
|
1337
|
+
const video = document.createElement("video");
|
|
1338
|
+
video.setAttribute("playsinline", "");
|
|
1339
|
+
const constraints = {
|
|
1340
|
+
audio: false,
|
|
1341
|
+
video: { facingMode: "user" }
|
|
1342
|
+
};
|
|
1343
|
+
navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {
|
|
1344
|
+
if ("srcObject" in video) {
|
|
1345
|
+
video.srcObject = stream;
|
|
1346
|
+
} else {
|
|
1347
|
+
video.src = window.URL.createObjectURL(stream);
|
|
1348
|
+
}
|
|
1349
|
+
}).catch(function(err) {
|
|
1350
|
+
let msg = "No camera available.";
|
|
1351
|
+
if (err.code == 1)
|
|
1352
|
+
msg = "User denied access to use camera.";
|
|
1353
|
+
console.log(msg);
|
|
1354
|
+
console.error(err);
|
|
1355
|
+
});
|
|
1356
|
+
video.addEventListener("loadedmetadata", function() {
|
|
1357
|
+
video.play();
|
|
1358
|
+
if (typeof callback === "function")
|
|
1359
|
+
callback(video.srcObject);
|
|
1360
|
+
});
|
|
1361
|
+
return video;
|
|
1362
|
+
}
|
|
1363
|
+
// src/modules/canvas.ts
|
|
1364
|
+
import { num } from "@glyph-engine/core";
|
|
1365
|
+
var { clamp, map, mix } = num;
|
|
1366
|
+
var MODE_COVER = Symbol();
|
|
1367
|
+
var MODE_FIT = Symbol();
|
|
1368
|
+
var MODE_CENTER = Symbol();
|
|
1369
|
+
var BLACK = { r: 0, g: 0, b: 0, a: 1, v: 0 };
|
|
1370
|
+
class Canvas {
|
|
1371
|
+
canvas;
|
|
1372
|
+
ctx;
|
|
1373
|
+
pixels = [];
|
|
1374
|
+
constructor(sourceCanvas) {
|
|
1375
|
+
this.canvas = sourceCanvas || document.createElement("canvas");
|
|
1376
|
+
if (!this.canvas) {
|
|
1377
|
+
throw new Error(`Canvas 2d context is not supported on this browser.
|
|
1378
|
+
Can’t sort chars for brightness.`);
|
|
1379
|
+
}
|
|
1380
|
+
this.canvas.width = 1;
|
|
1381
|
+
this.canvas.height = 1;
|
|
1382
|
+
const ctx = this.canvas.getContext("2d");
|
|
1383
|
+
if (!ctx) {
|
|
1384
|
+
throw new Error(`Canvas 2d context is not supported on this browser.
|
|
1385
|
+
Can’t sort chars for brightness.`);
|
|
1386
|
+
}
|
|
1387
|
+
this.ctx = ctx;
|
|
1388
|
+
this.ctx.putImageData(this.ctx.createImageData(1, 1), 0, 0);
|
|
1389
|
+
this.pixels = [];
|
|
1390
|
+
this.loadPixels();
|
|
1391
|
+
}
|
|
1392
|
+
get width() {
|
|
1393
|
+
return this.canvas.width;
|
|
1394
|
+
}
|
|
1395
|
+
get height() {
|
|
1396
|
+
return this.canvas.height;
|
|
1397
|
+
}
|
|
1398
|
+
resize(dWidth, dHeight) {
|
|
1399
|
+
this.canvas.width = dWidth;
|
|
1400
|
+
this.canvas.height = dHeight;
|
|
1401
|
+
this.pixels.length = 0;
|
|
1402
|
+
return this;
|
|
1403
|
+
}
|
|
1404
|
+
copy(source, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) {
|
|
1405
|
+
sx = sx || 0;
|
|
1406
|
+
sy = sy || 0;
|
|
1407
|
+
sWidth = sWidth || (source instanceof HTMLImageElement || source instanceof HTMLVideoElement ? source.videoWidth : source.width);
|
|
1408
|
+
sHeight = sHeight || (source instanceof HTMLImageElement || source instanceof HTMLVideoElement ? source.videoHeight : source.height);
|
|
1409
|
+
dx = dx || 0;
|
|
1410
|
+
dy = dy || 0;
|
|
1411
|
+
dWidth = dWidth || this.canvas.width;
|
|
1412
|
+
dHeight = dHeight || this.canvas.height;
|
|
1413
|
+
this.ctx.drawImage(source, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
|
|
1414
|
+
this.loadPixels();
|
|
1415
|
+
return this;
|
|
1416
|
+
}
|
|
1417
|
+
image(source) {
|
|
1418
|
+
if (!source)
|
|
1419
|
+
return this;
|
|
1420
|
+
const w = source.videoWidth || source.width;
|
|
1421
|
+
const h = source.videoHeight || source.height;
|
|
1422
|
+
this.resize(w, h);
|
|
1423
|
+
this.copy(source, 0, 0, w, h, 0, 0, w, h);
|
|
1424
|
+
return this;
|
|
1425
|
+
}
|
|
1426
|
+
cover(source, aspect = 1) {
|
|
1427
|
+
centerImage(source, this.canvas, 1, aspect, MODE_COVER);
|
|
1428
|
+
this.loadPixels();
|
|
1429
|
+
return this;
|
|
1430
|
+
}
|
|
1431
|
+
fit(source, aspect = 1) {
|
|
1432
|
+
centerImage(source, this.canvas, 1, aspect, MODE_FIT);
|
|
1433
|
+
this.loadPixels();
|
|
1434
|
+
return this;
|
|
1435
|
+
}
|
|
1436
|
+
center(source, scaleX = 1, scaleY = 1) {
|
|
1437
|
+
centerImage(source, this.canvas, scaleX, scaleY, MODE_CENTER);
|
|
1438
|
+
this.loadPixels();
|
|
1439
|
+
return this;
|
|
1440
|
+
}
|
|
1441
|
+
mirrorX() {
|
|
1442
|
+
const w = this.canvas.width;
|
|
1443
|
+
const h = this.canvas.height;
|
|
1444
|
+
const buf = this.pixels;
|
|
1445
|
+
const half = Math.floor(w / 2);
|
|
1446
|
+
for (let j = 0;j < h; j++) {
|
|
1447
|
+
for (let i = 0;i < half; i++) {
|
|
1448
|
+
const a = w * j + i;
|
|
1449
|
+
const b = w * (j + 1) - i - 1;
|
|
1450
|
+
const t = buf[b];
|
|
1451
|
+
buf[b] = buf[a];
|
|
1452
|
+
buf[a] = t;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return this;
|
|
1456
|
+
}
|
|
1457
|
+
normalize() {
|
|
1458
|
+
normalizeGray(this.pixels, this.pixels, 0, 1);
|
|
1459
|
+
return this;
|
|
1460
|
+
}
|
|
1461
|
+
quantize(palette) {
|
|
1462
|
+
paletteQuantize(this.pixels, this.pixels, palette);
|
|
1463
|
+
return this;
|
|
1464
|
+
}
|
|
1465
|
+
get(x, y) {
|
|
1466
|
+
if (x < 0 || x >= this.canvas.width)
|
|
1467
|
+
return BLACK;
|
|
1468
|
+
if (y < 0 || y >= this.canvas.height)
|
|
1469
|
+
return BLACK;
|
|
1470
|
+
return this.pixels[x + y * this.canvas.width];
|
|
1471
|
+
}
|
|
1472
|
+
sample(sx, sy, gray = false) {
|
|
1473
|
+
const w = this.canvas.width;
|
|
1474
|
+
const h = this.canvas.height;
|
|
1475
|
+
const x = sx * w - 0.5;
|
|
1476
|
+
const y = sy * h - 0.5;
|
|
1477
|
+
let l = Math.floor(x);
|
|
1478
|
+
let b = Math.floor(y);
|
|
1479
|
+
let r = l + 1;
|
|
1480
|
+
let t = b + 1;
|
|
1481
|
+
const lr = x - l;
|
|
1482
|
+
const bt = y - b;
|
|
1483
|
+
if (gray) {
|
|
1484
|
+
const p1 = mix(this.get(l, b).v || 0, this.get(r, b).v || 0, lr);
|
|
1485
|
+
const p2 = mix(this.get(l, t).v || 0, this.get(r, t).v || 0, lr);
|
|
1486
|
+
return mix(p1, p2, bt);
|
|
1487
|
+
} else {
|
|
1488
|
+
const p1 = mixColors(this.get(l, b), this.get(r, b), lr);
|
|
1489
|
+
const p2 = mixColors(this.get(l, t), this.get(r, t), lr);
|
|
1490
|
+
return mixColors(p1, p2, bt);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
loadPixels() {
|
|
1494
|
+
this.pixels.length = 0;
|
|
1495
|
+
const w = this.canvas.width;
|
|
1496
|
+
const h = this.canvas.height;
|
|
1497
|
+
const data = this.ctx.getImageData(0, 0, w, h).data;
|
|
1498
|
+
let idx = 0;
|
|
1499
|
+
for (let i = 0;i < data.length; i += 4) {
|
|
1500
|
+
const r = data[i];
|
|
1501
|
+
const g = data[i + 1];
|
|
1502
|
+
const b = data[i + 2];
|
|
1503
|
+
const a = data[i + 3] / 255;
|
|
1504
|
+
this.pixels[idx++] = {
|
|
1505
|
+
r,
|
|
1506
|
+
g,
|
|
1507
|
+
b,
|
|
1508
|
+
a,
|
|
1509
|
+
v: toGray(r, g, b)
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
return this;
|
|
1513
|
+
}
|
|
1514
|
+
writeTo(buf) {
|
|
1515
|
+
if (Array.isArray(buf)) {
|
|
1516
|
+
for (let i = 0;i < this.pixels.length; i++)
|
|
1517
|
+
buf[i] = this.pixels[i];
|
|
1518
|
+
}
|
|
1519
|
+
return this;
|
|
1520
|
+
}
|
|
1521
|
+
display(target, x = 0, y = 0) {
|
|
1522
|
+
target = target || document.body;
|
|
1523
|
+
this.canvas.style.position = "absolute";
|
|
1524
|
+
this.canvas.style.left = x + "px";
|
|
1525
|
+
this.canvas.style.top = y + "px";
|
|
1526
|
+
this.canvas.style.width = "auto";
|
|
1527
|
+
this.canvas.style.height = "auto";
|
|
1528
|
+
this.canvas.style.zIndex = "10";
|
|
1529
|
+
document.body.appendChild(this.canvas);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function mixColors(a, b, amt) {
|
|
1533
|
+
return {
|
|
1534
|
+
r: mix(a.r, b.r, amt),
|
|
1535
|
+
g: mix(a.g, b.g, amt),
|
|
1536
|
+
b: mix(a.b, b.b, amt),
|
|
1537
|
+
v: mix(a.v || 0, b.v || 0, amt)
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function getElementSize(source) {
|
|
1541
|
+
const type = source.nodeName;
|
|
1542
|
+
const width = type == "VIDEO" ? source.videoWidth : source.width || 0;
|
|
1543
|
+
const height = type == "VIDEO" ? source.videoHeight : source.height || 0;
|
|
1544
|
+
return { width, height };
|
|
1545
|
+
}
|
|
1546
|
+
function centerImage(sourceCanvas, targetCanvas, scaleX = 1, scaleY = 1, mode = MODE_CENTER) {
|
|
1547
|
+
const s = getElementSize(sourceCanvas);
|
|
1548
|
+
const sa = scaleX * s.width / (s.height * scaleY);
|
|
1549
|
+
const tw = targetCanvas.width;
|
|
1550
|
+
const th = targetCanvas.height;
|
|
1551
|
+
const ta = tw / th;
|
|
1552
|
+
let dw = 0, dh = 0;
|
|
1553
|
+
if (mode == MODE_COVER) {
|
|
1554
|
+
if (sa > ta) {
|
|
1555
|
+
dw = th * sa;
|
|
1556
|
+
dh = th;
|
|
1557
|
+
} else {
|
|
1558
|
+
dw = tw;
|
|
1559
|
+
dh = tw / sa;
|
|
1560
|
+
}
|
|
1561
|
+
} else if (mode == MODE_FIT) {
|
|
1562
|
+
if (sa > ta) {
|
|
1563
|
+
dw = tw;
|
|
1564
|
+
dh = tw / sa;
|
|
1565
|
+
} else {
|
|
1566
|
+
dw = th * sa;
|
|
1567
|
+
dh = th;
|
|
1568
|
+
}
|
|
1569
|
+
} else if (mode == MODE_CENTER) {
|
|
1570
|
+
dw = s.width * scaleX;
|
|
1571
|
+
dh = s.height * scaleY;
|
|
1572
|
+
}
|
|
1573
|
+
const ctx = targetCanvas.getContext("2d");
|
|
1574
|
+
if (!ctx)
|
|
1575
|
+
throw new Error("Canvas 2d context is not supported on this browser.");
|
|
1576
|
+
ctx.fillStyle = "black";
|
|
1577
|
+
ctx.fillRect(0, 0, tw, th);
|
|
1578
|
+
ctx.save();
|
|
1579
|
+
ctx.translate(tw / 2, th / 2);
|
|
1580
|
+
if (!dw || !dh)
|
|
1581
|
+
return;
|
|
1582
|
+
ctx.drawImage(sourceCanvas, -dw / 2, -dh / 2, dw, dh);
|
|
1583
|
+
ctx.restore();
|
|
1584
|
+
}
|
|
1585
|
+
function toGray(r, g, b) {
|
|
1586
|
+
return Math.round(r * 0.2126 + g * 0.7152 + b * 0.0722) / 255;
|
|
1587
|
+
}
|
|
1588
|
+
function paletteQuantize(arrayIn, arrayOut, palette) {
|
|
1589
|
+
arrayOut = arrayOut || [];
|
|
1590
|
+
const distFn = (a, b) => {
|
|
1591
|
+
const r = (a.r + b.r) * 0.5;
|
|
1592
|
+
let s = 0;
|
|
1593
|
+
s += (2 + r / 256) * Math.pow(a.r - b.r, 2);
|
|
1594
|
+
s += 4 * Math.pow(a.g - b.g, 2);
|
|
1595
|
+
s += (2 + (255 - r) / 256) * Math.pow(a.b - b.b, 2);
|
|
1596
|
+
return Math.sqrt(s);
|
|
1597
|
+
};
|
|
1598
|
+
for (let i = 0;i < arrayIn.length; i++) {
|
|
1599
|
+
const a = arrayIn[i];
|
|
1600
|
+
let dist = Number.MAX_VALUE;
|
|
1601
|
+
let nearest;
|
|
1602
|
+
for (const b of palette) {
|
|
1603
|
+
const d = distFn(a, b);
|
|
1604
|
+
if (d < dist) {
|
|
1605
|
+
dist = d;
|
|
1606
|
+
nearest = b;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
arrayOut[i] = { ...nearest, v: arrayIn[i].v };
|
|
1610
|
+
}
|
|
1611
|
+
return arrayOut;
|
|
1612
|
+
}
|
|
1613
|
+
function normalizeGray(arrayIn, arrayOut, lower = 0, upper = 1) {
|
|
1614
|
+
arrayOut = arrayOut || [];
|
|
1615
|
+
let min = Number.MAX_VALUE;
|
|
1616
|
+
let max = 0;
|
|
1617
|
+
for (let i = 0;i < arrayIn.length; i++) {
|
|
1618
|
+
min = Math.min(arrayIn[i].v || 0, min);
|
|
1619
|
+
max = Math.max(arrayIn[i].v || 0, max);
|
|
1620
|
+
}
|
|
1621
|
+
for (let i = 0;i < arrayIn.length; i++) {
|
|
1622
|
+
const v = min == max ? min : map(arrayIn[i].v || 0, min, max, lower, upper);
|
|
1623
|
+
arrayOut[i] = { ...arrayOut[i], v };
|
|
1624
|
+
}
|
|
1625
|
+
return arrayOut;
|
|
1626
|
+
}
|
|
1627
|
+
// src/modules/filedownload.ts
|
|
1628
|
+
var mimeTypes = {
|
|
1629
|
+
js: "text/javascript",
|
|
1630
|
+
txt: "text/plain",
|
|
1631
|
+
png: "image/png",
|
|
1632
|
+
jpg: "text/jpeg"
|
|
1633
|
+
};
|
|
1634
|
+
function saveSourceAsFile(src, filename) {
|
|
1635
|
+
const ext = getFileExt(filename);
|
|
1636
|
+
const type = mimeTypes[ext];
|
|
1637
|
+
const blob = type ? new Blob([src], { type }) : new Blob([src]);
|
|
1638
|
+
saveBlobAsFile(blob, filename);
|
|
1639
|
+
}
|
|
1640
|
+
function getFileExt(filename) {
|
|
1641
|
+
const ext = filename.split(".").pop() || "";
|
|
1642
|
+
const exts = ["js", "txt", "png", "jpg"];
|
|
1643
|
+
if (exts.includes(ext)) {
|
|
1644
|
+
return ext;
|
|
1645
|
+
}
|
|
1646
|
+
return "txt";
|
|
1647
|
+
}
|
|
1648
|
+
function saveBlobAsFile(blob, filename) {
|
|
1649
|
+
const a = document.createElement("a");
|
|
1650
|
+
a.download = filename;
|
|
1651
|
+
a.rel = "noopener";
|
|
1652
|
+
a.href = URL.createObjectURL(blob);
|
|
1653
|
+
setTimeout(() => {
|
|
1654
|
+
URL.revokeObjectURL(a.href);
|
|
1655
|
+
}, 1e4);
|
|
1656
|
+
setTimeout(() => {
|
|
1657
|
+
click(a);
|
|
1658
|
+
}, 0);
|
|
1659
|
+
}
|
|
1660
|
+
function click(node) {
|
|
1661
|
+
try {
|
|
1662
|
+
node.dispatchEvent(new MouseEvent("click"));
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
var e = document.createEvent("MouseEvents");
|
|
1665
|
+
e.initMouseEvent("click");
|
|
1666
|
+
node.dispatchEvent(e);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/modules/exportframe.ts
|
|
1671
|
+
var warnedNoCanvas = false;
|
|
1672
|
+
function exportFrame(context, filename, from = 1, to = from) {
|
|
1673
|
+
const canvas = context.settings.element;
|
|
1674
|
+
if (canvas?.nodeName != "CANVAS") {
|
|
1675
|
+
if (!warnedNoCanvas)
|
|
1676
|
+
console.warn("exportframe.js: Can’t export, a canvas renderer is required.");
|
|
1677
|
+
warnedNoCanvas = true;
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (!filename) {
|
|
1681
|
+
console.warn("exportframe.js: Filename not provided.");
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const m = filename.match(/(.+)\.([0-9a-z]+$)/i) || [];
|
|
1685
|
+
if (m.length < 3) {
|
|
1686
|
+
console.warn("exportframe.js: Invalid filename.");
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const base = m[1];
|
|
1690
|
+
const ext = m[2];
|
|
1691
|
+
const f = context.frame;
|
|
1692
|
+
if (f >= from && f <= to) {
|
|
1693
|
+
const out = base + "_" + f.toString().padStart(5, "0") + "." + ext;
|
|
1694
|
+
console.info("exportframe.js: Exporting frame " + out + ". Will stop at " + to + ".");
|
|
1695
|
+
canvas.toBlob((blob) => {
|
|
1696
|
+
if (!blob) {
|
|
1697
|
+
console.warn("exportframe.js: No data to export.");
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
saveBlobAsFile(blob, out);
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
// src/modules/load.ts
|
|
1705
|
+
var load_default = { json, image, text };
|
|
1706
|
+
function image(url) {
|
|
1707
|
+
return new Promise((resolve, reject) => {
|
|
1708
|
+
const img = new Image;
|
|
1709
|
+
img.onload = () => resolve(img);
|
|
1710
|
+
img.onerror = () => {
|
|
1711
|
+
console.log("Loader: error loading image " + url);
|
|
1712
|
+
resolve(null);
|
|
1713
|
+
};
|
|
1714
|
+
img.src = url;
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
function text(url) {
|
|
1718
|
+
return fetch(url).then((response) => {
|
|
1719
|
+
return response.text();
|
|
1720
|
+
}).catch((err) => {
|
|
1721
|
+
console.log("Loader: error loading text " + url);
|
|
1722
|
+
return "";
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function json(url) {
|
|
1726
|
+
return fetch(url).then((response) => {
|
|
1727
|
+
return response.json();
|
|
1728
|
+
}).catch((err) => {
|
|
1729
|
+
console.log("Loader: error loading json " + url);
|
|
1730
|
+
return {};
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/modules/image.ts
|
|
1735
|
+
var image_default = {
|
|
1736
|
+
load
|
|
1737
|
+
};
|
|
1738
|
+
function load(path) {
|
|
1739
|
+
const source = document.createElement("canvas");
|
|
1740
|
+
source.width = 1;
|
|
1741
|
+
source.height = 1;
|
|
1742
|
+
const can = new Canvas(source);
|
|
1743
|
+
load_default.image(path).then((img) => {
|
|
1744
|
+
if (!img) {
|
|
1745
|
+
console.warn("Image " + path + " could not be loaded.");
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const imageElement = img;
|
|
1749
|
+
console.log("Image " + path + " loaded. Size: " + imageElement.width + "×" + imageElement.height);
|
|
1750
|
+
can.resize(imageElement.width, imageElement.height);
|
|
1751
|
+
can.copy(imageElement, 0, 0, imageElement.width, imageElement.height, 0, 0, imageElement.width, imageElement.height);
|
|
1752
|
+
}).catch((err) => {
|
|
1753
|
+
console.warn("There was an error loading image " + path + ".");
|
|
1754
|
+
});
|
|
1755
|
+
return can;
|
|
1756
|
+
}
|
|
1757
|
+
// src/modules/sort.ts
|
|
1758
|
+
function sort(charSet, fontFamily, ascending = false) {
|
|
1759
|
+
if (typeof document === "undefined")
|
|
1760
|
+
return charSet;
|
|
1761
|
+
const size = 30;
|
|
1762
|
+
const ctx = document.createElement("canvas").getContext("2d");
|
|
1763
|
+
if (!ctx) {
|
|
1764
|
+
console.warn(`Canvas 2d context is not supported on this browser.
|
|
1765
|
+
Can’t sort chars for brightness.`);
|
|
1766
|
+
return charSet;
|
|
1767
|
+
}
|
|
1768
|
+
ctx.canvas.width = size * 2;
|
|
1769
|
+
ctx.canvas.height = size * 2;
|
|
1770
|
+
ctx.canvas.style.right = "0";
|
|
1771
|
+
ctx.canvas.style.top = "0";
|
|
1772
|
+
ctx.canvas.style.position = "absolute";
|
|
1773
|
+
document.body.appendChild(ctx.canvas);
|
|
1774
|
+
if (ctx.getImageData(0, 0, 1, 1).data.length == 0) {
|
|
1775
|
+
console.warn(`getImageData() is not supported on this browser.
|
|
1776
|
+
Can’t sort chars for brightness.`);
|
|
1777
|
+
return charSet;
|
|
1778
|
+
}
|
|
1779
|
+
let out = [];
|
|
1780
|
+
for (let i = 0;i < charSet.length; i++) {
|
|
1781
|
+
ctx.fillStyle = "black";
|
|
1782
|
+
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
1783
|
+
ctx.fillStyle = "rgb(255,255,255)";
|
|
1784
|
+
ctx.font = size + "px " + fontFamily;
|
|
1785
|
+
ctx.textAlign = "center";
|
|
1786
|
+
ctx.textBaseline = "middle";
|
|
1787
|
+
ctx.fillText(charSet[i], ctx.canvas.width / 2, ctx.canvas.height / 2);
|
|
1788
|
+
out[i] = {
|
|
1789
|
+
count: 0,
|
|
1790
|
+
char: charSet[i],
|
|
1791
|
+
index: i
|
|
1792
|
+
};
|
|
1793
|
+
const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
|
|
1794
|
+
for (let y = 0;y < ctx.canvas.height; y++) {
|
|
1795
|
+
const oy = y * ctx.canvas.width;
|
|
1796
|
+
for (let x = 0;x < ctx.canvas.width; x++) {
|
|
1797
|
+
let r = data[4 * (x + oy)];
|
|
1798
|
+
out[i].count += r;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
document.body.removeChild(ctx.canvas);
|
|
1803
|
+
if (ascending) {
|
|
1804
|
+
return out.sort((a, b) => a.count - b.count).map((x) => x.char).join("");
|
|
1805
|
+
} else {
|
|
1806
|
+
return out.sort((a, b) => b.count - a.count).map((x) => x.char).join("");
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
export {
|
|
1810
|
+
calcMetrics,
|
|
1811
|
+
exports_camera as camera,
|
|
1812
|
+
exports_canvas as canvas,
|
|
1813
|
+
copyContent,
|
|
1814
|
+
run as default,
|
|
1815
|
+
exports_exportframe as exportframe,
|
|
1816
|
+
exports_filedownload as filedownload,
|
|
1817
|
+
exports_image as image,
|
|
1818
|
+
exports_load as load,
|
|
1819
|
+
run,
|
|
1820
|
+
exports_sort as sort
|
|
1821
|
+
};
|