wff-web 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 +15 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1446 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
// src/color.ts
|
|
2
|
+
function parseColor(value) {
|
|
3
|
+
if (value == null) return "#000000";
|
|
4
|
+
if (value.length === 9 && value.startsWith("#")) {
|
|
5
|
+
const a = parseInt(value.slice(1, 3), 16);
|
|
6
|
+
const r = parseInt(value.slice(3, 5), 16);
|
|
7
|
+
const g = parseInt(value.slice(5, 7), 16);
|
|
8
|
+
const b = parseInt(value.slice(7, 9), 16);
|
|
9
|
+
const alpha = Number((a / 255).toFixed(3));
|
|
10
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/styles.ts
|
|
16
|
+
function applyFill(ctx, el) {
|
|
17
|
+
const fillEl = el.querySelector(":scope > Fill");
|
|
18
|
+
if (!fillEl) return;
|
|
19
|
+
const gradient = createGradient(ctx, fillEl);
|
|
20
|
+
if (gradient) {
|
|
21
|
+
ctx.fillStyle = gradient;
|
|
22
|
+
} else {
|
|
23
|
+
ctx.fillStyle = parseColor(fillEl.getAttribute("color"));
|
|
24
|
+
}
|
|
25
|
+
ctx.fill();
|
|
26
|
+
}
|
|
27
|
+
function applyStroke(ctx, el) {
|
|
28
|
+
const strokeEl = el.querySelector(":scope > Stroke");
|
|
29
|
+
if (!strokeEl) return;
|
|
30
|
+
ctx.strokeStyle = parseColor(strokeEl.getAttribute("color"));
|
|
31
|
+
ctx.lineWidth = parseFloat(strokeEl.getAttribute("thickness") ?? "1");
|
|
32
|
+
const cap = strokeEl.getAttribute("cap");
|
|
33
|
+
if (cap === "ROUND") ctx.lineCap = "round";
|
|
34
|
+
else if (cap === "SQUARE") ctx.lineCap = "square";
|
|
35
|
+
else ctx.lineCap = "butt";
|
|
36
|
+
const dashAttr = strokeEl.getAttribute("dashIntervals");
|
|
37
|
+
if (dashAttr) {
|
|
38
|
+
ctx.setLineDash(dashAttr.split(/\s+/).map(Number));
|
|
39
|
+
} else {
|
|
40
|
+
ctx.setLineDash([]);
|
|
41
|
+
}
|
|
42
|
+
ctx.lineDashOffset = parseFloat(
|
|
43
|
+
strokeEl.getAttribute("dashPhase") ?? "0"
|
|
44
|
+
);
|
|
45
|
+
ctx.stroke();
|
|
46
|
+
}
|
|
47
|
+
function createGradient(ctx, fillEl) {
|
|
48
|
+
const linear = fillEl.querySelector(":scope > LinearGradient");
|
|
49
|
+
if (linear) {
|
|
50
|
+
return createLinearGradient(ctx, linear);
|
|
51
|
+
}
|
|
52
|
+
const radial = fillEl.querySelector(":scope > RadialGradient");
|
|
53
|
+
if (radial) {
|
|
54
|
+
return createRadialGradient(ctx, radial);
|
|
55
|
+
}
|
|
56
|
+
const sweep = fillEl.querySelector(":scope > SweepGradient");
|
|
57
|
+
if (sweep) {
|
|
58
|
+
return createSweepGradient(ctx, sweep);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function addColorStops(gradient, el, positionScale = 1) {
|
|
63
|
+
const colorsAttr = el.getAttribute("colors") ?? "";
|
|
64
|
+
const positionsAttr = el.getAttribute("positions") ?? "";
|
|
65
|
+
const colors = colorsAttr.split(/\s+/).filter(Boolean);
|
|
66
|
+
const positions = positionsAttr.split(/\s+/).filter(Boolean).map(Number);
|
|
67
|
+
const clampedScale = clampStop(positionScale);
|
|
68
|
+
let lastStop = 0;
|
|
69
|
+
for (let i = 0; i < colors.length; i++) {
|
|
70
|
+
const basePos = i < positions.length ? positions[i] : colors.length <= 1 ? 0 : i / (colors.length - 1);
|
|
71
|
+
const pos = clampStop(basePos * clampedScale);
|
|
72
|
+
lastStop = pos;
|
|
73
|
+
gradient.addColorStop(pos, parseColor(colors[i]));
|
|
74
|
+
}
|
|
75
|
+
if (colors.length > 0 && clampedScale < 1 && lastStop < 1) {
|
|
76
|
+
gradient.addColorStop(1, parseColor(colors.at(-1)));
|
|
77
|
+
}
|
|
78
|
+
return gradient;
|
|
79
|
+
}
|
|
80
|
+
function createLinearGradient(ctx, el) {
|
|
81
|
+
const x0 = parseFloat(el.getAttribute("startX") ?? "0");
|
|
82
|
+
const y0 = parseFloat(el.getAttribute("startY") ?? "0");
|
|
83
|
+
const x1 = parseFloat(el.getAttribute("endX") ?? "0");
|
|
84
|
+
const y1 = parseFloat(el.getAttribute("endY") ?? "0");
|
|
85
|
+
return addColorStops(ctx.createLinearGradient(x0, y0, x1, y1), el);
|
|
86
|
+
}
|
|
87
|
+
function createRadialGradient(ctx, el) {
|
|
88
|
+
const cx = parseFloat(el.getAttribute("centerX") ?? "0");
|
|
89
|
+
const cy = parseFloat(el.getAttribute("centerY") ?? "0");
|
|
90
|
+
const r = parseFloat(el.getAttribute("radius") ?? "0");
|
|
91
|
+
return addColorStops(ctx.createRadialGradient(cx, cy, 0, cx, cy, r), el);
|
|
92
|
+
}
|
|
93
|
+
function createSweepGradient(ctx, el) {
|
|
94
|
+
const cx = parseFloat(el.getAttribute("centerX") ?? "0");
|
|
95
|
+
const cy = parseFloat(el.getAttribute("centerY") ?? "0");
|
|
96
|
+
const startAngle = parseFloat(el.getAttribute("startAngle") ?? "0");
|
|
97
|
+
const endAngle = parseFloat(el.getAttribute("endAngle") ?? "360");
|
|
98
|
+
const startRad = (startAngle - 90) * Math.PI / 180;
|
|
99
|
+
const sweepFraction = getSweepFraction(startAngle, endAngle);
|
|
100
|
+
return addColorStops(
|
|
101
|
+
ctx.createConicGradient(startRad, cx, cy),
|
|
102
|
+
el,
|
|
103
|
+
sweepFraction
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function getSweepFraction(startAngle, endAngle) {
|
|
107
|
+
const rawSweep = endAngle - startAngle;
|
|
108
|
+
if (!Number.isFinite(rawSweep) || Math.abs(rawSweep) >= 360) {
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
const normalizedSweep = (rawSweep % 360 + 360) % 360;
|
|
112
|
+
return normalizedSweep === 0 ? 1 : normalizedSweep / 360;
|
|
113
|
+
}
|
|
114
|
+
function clampStop(value) {
|
|
115
|
+
if (!Number.isFinite(value)) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
return Math.min(1, Math.max(0, value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/variants.ts
|
|
122
|
+
function applyVariants(el, ambient) {
|
|
123
|
+
for (const child of el.children) {
|
|
124
|
+
if (child.tagName === "Variant") {
|
|
125
|
+
const mode = child.getAttribute("mode");
|
|
126
|
+
if (mode === "AMBIENT" && ambient) {
|
|
127
|
+
const target = child.getAttribute("target");
|
|
128
|
+
const value = child.getAttribute("value");
|
|
129
|
+
if (target !== null && value !== null) {
|
|
130
|
+
el.setAttribute(target, value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/masking.ts
|
|
138
|
+
var BLEND_MODE_MAP = {
|
|
139
|
+
SRC_OVER: "source-over",
|
|
140
|
+
MULTIPLY: "multiply",
|
|
141
|
+
SCREEN: "screen",
|
|
142
|
+
OVERLAY: "overlay",
|
|
143
|
+
DARKEN: "darken",
|
|
144
|
+
LIGHTEN: "lighten"
|
|
145
|
+
};
|
|
146
|
+
function applyBlendMode(ctx, el) {
|
|
147
|
+
const mode = el.getAttribute("blendMode");
|
|
148
|
+
if (mode && BLEND_MODE_MAP[mode]) {
|
|
149
|
+
ctx.globalCompositeOperation = BLEND_MODE_MAP[mode];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function hasMasking(el) {
|
|
153
|
+
for (const child of el.children) {
|
|
154
|
+
const rm = child.getAttribute("renderMode");
|
|
155
|
+
if (rm === "SOURCE" || rm === "MASK") return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
async function renderWithMasking(ctx, el, width, height, renderChild, renderCtx) {
|
|
160
|
+
const offscreen = new OffscreenCanvas(width, height);
|
|
161
|
+
const offCtx = offscreen.getContext("2d");
|
|
162
|
+
for (const child of el.children) {
|
|
163
|
+
const rm = child.getAttribute("renderMode");
|
|
164
|
+
if (rm === "SOURCE" || !rm) {
|
|
165
|
+
if (child.tagName !== "Variant") {
|
|
166
|
+
await renderChild(
|
|
167
|
+
offCtx,
|
|
168
|
+
child,
|
|
169
|
+
renderCtx
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const child of el.children) {
|
|
175
|
+
const rm = child.getAttribute("renderMode");
|
|
176
|
+
if (rm === "MASK") {
|
|
177
|
+
offCtx.globalCompositeOperation = "destination-in";
|
|
178
|
+
await renderChild(
|
|
179
|
+
offCtx,
|
|
180
|
+
child,
|
|
181
|
+
renderCtx
|
|
182
|
+
);
|
|
183
|
+
offCtx.globalCompositeOperation = "source-over";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const child of el.children) {
|
|
187
|
+
const rm = child.getAttribute("renderMode");
|
|
188
|
+
if (rm === "ALL") {
|
|
189
|
+
await renderChild(
|
|
190
|
+
offCtx,
|
|
191
|
+
child,
|
|
192
|
+
renderCtx
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
ctx.drawImage(offscreen, 0, 0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/expressions.ts
|
|
200
|
+
function tokenize(input) {
|
|
201
|
+
const tokens = [];
|
|
202
|
+
let i = 0;
|
|
203
|
+
while (i < input.length) {
|
|
204
|
+
if (/\s/.test(input[i])) {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (input[i] === "[") {
|
|
209
|
+
const end = input.indexOf("]", i);
|
|
210
|
+
if (end === -1) throw new Error(`Unterminated source reference at ${i}`);
|
|
211
|
+
tokens.push({ type: "source", value: input.slice(i + 1, end) });
|
|
212
|
+
i = end + 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (input[i] === '"' || input[i] === "'") {
|
|
216
|
+
const quote = input[i];
|
|
217
|
+
let str = "";
|
|
218
|
+
i++;
|
|
219
|
+
while (i < input.length && input[i] !== quote) {
|
|
220
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
221
|
+
i++;
|
|
222
|
+
str += input[i];
|
|
223
|
+
} else {
|
|
224
|
+
str += input[i];
|
|
225
|
+
}
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
i++;
|
|
229
|
+
tokens.push({ type: "string", value: str });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (/[0-9]/.test(input[i]) || input[i] === "." && /[0-9]/.test(input[i + 1] ?? "")) {
|
|
233
|
+
let num = "";
|
|
234
|
+
while (i < input.length && /[0-9.]/.test(input[i])) {
|
|
235
|
+
num += input[i];
|
|
236
|
+
i++;
|
|
237
|
+
}
|
|
238
|
+
tokens.push({ type: "number", value: parseFloat(num) });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const two = input.slice(i, i + 2);
|
|
242
|
+
if (two === "==" || two === "!=" || two === "<=" || two === ">=" || two === "&&" || two === "||") {
|
|
243
|
+
tokens.push({ type: two, value: two });
|
|
244
|
+
i += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const ch = input[i];
|
|
248
|
+
if ("+-*/%<>!~|&?:(),".includes(ch)) {
|
|
249
|
+
tokens.push({ type: ch, value: ch });
|
|
250
|
+
i++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (/[a-zA-Z_]/.test(input[i])) {
|
|
254
|
+
let ident = "";
|
|
255
|
+
while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) {
|
|
256
|
+
ident += input[i];
|
|
257
|
+
i++;
|
|
258
|
+
}
|
|
259
|
+
tokens.push({ type: "ident", value: ident });
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
throw new Error(`Unexpected character '${input[i]}' at position ${i}`);
|
|
263
|
+
}
|
|
264
|
+
return tokens;
|
|
265
|
+
}
|
|
266
|
+
var Parser = class {
|
|
267
|
+
tokens;
|
|
268
|
+
pos;
|
|
269
|
+
constructor(tokens) {
|
|
270
|
+
this.tokens = tokens;
|
|
271
|
+
this.pos = 0;
|
|
272
|
+
}
|
|
273
|
+
peek() {
|
|
274
|
+
return this.tokens[this.pos];
|
|
275
|
+
}
|
|
276
|
+
consume() {
|
|
277
|
+
const tok = this.tokens[this.pos];
|
|
278
|
+
if (!tok) throw new Error("Unexpected end of expression");
|
|
279
|
+
this.pos++;
|
|
280
|
+
return tok;
|
|
281
|
+
}
|
|
282
|
+
expect(type) {
|
|
283
|
+
const tok = this.consume();
|
|
284
|
+
if (tok.type !== type) {
|
|
285
|
+
throw new Error(`Expected token '${type}', got '${tok.type}'`);
|
|
286
|
+
}
|
|
287
|
+
return tok;
|
|
288
|
+
}
|
|
289
|
+
parse() {
|
|
290
|
+
const node = this.parseTernary();
|
|
291
|
+
if (this.pos < this.tokens.length) {
|
|
292
|
+
throw new Error(`Unexpected token '${this.tokens[this.pos].value}' at position ${this.pos}`);
|
|
293
|
+
}
|
|
294
|
+
return node;
|
|
295
|
+
}
|
|
296
|
+
// Ternary: condition ? consequent : alternate
|
|
297
|
+
parseTernary() {
|
|
298
|
+
const condition = this.parseOr();
|
|
299
|
+
if (this.peek()?.type === "?") {
|
|
300
|
+
this.consume();
|
|
301
|
+
const consequent = this.parseTernary();
|
|
302
|
+
this.expect(":");
|
|
303
|
+
const alternate = this.parseTernary();
|
|
304
|
+
return { type: "ternary", condition, consequent, alternate };
|
|
305
|
+
}
|
|
306
|
+
return condition;
|
|
307
|
+
}
|
|
308
|
+
// Logical OR
|
|
309
|
+
parseOr() {
|
|
310
|
+
let left = this.parseAnd();
|
|
311
|
+
while (this.peek()?.type === "||") {
|
|
312
|
+
const op = this.consume().type;
|
|
313
|
+
const right = this.parseAnd();
|
|
314
|
+
left = { type: "binary", op, left, right };
|
|
315
|
+
}
|
|
316
|
+
return left;
|
|
317
|
+
}
|
|
318
|
+
// Logical AND
|
|
319
|
+
parseAnd() {
|
|
320
|
+
let left = this.parseBitwiseOr();
|
|
321
|
+
while (this.peek()?.type === "&&") {
|
|
322
|
+
const op = this.consume().type;
|
|
323
|
+
const right = this.parseBitwiseOr();
|
|
324
|
+
left = { type: "binary", op, left, right };
|
|
325
|
+
}
|
|
326
|
+
return left;
|
|
327
|
+
}
|
|
328
|
+
// Bitwise OR
|
|
329
|
+
parseBitwiseOr() {
|
|
330
|
+
let left = this.parseBitwiseAnd();
|
|
331
|
+
while (this.peek()?.type === "|") {
|
|
332
|
+
const op = this.consume().type;
|
|
333
|
+
const right = this.parseBitwiseAnd();
|
|
334
|
+
left = { type: "binary", op, left, right };
|
|
335
|
+
}
|
|
336
|
+
return left;
|
|
337
|
+
}
|
|
338
|
+
// Bitwise AND
|
|
339
|
+
parseBitwiseAnd() {
|
|
340
|
+
let left = this.parseEquality();
|
|
341
|
+
while (this.peek()?.type === "&") {
|
|
342
|
+
const op = this.consume().type;
|
|
343
|
+
const right = this.parseEquality();
|
|
344
|
+
left = { type: "binary", op, left, right };
|
|
345
|
+
}
|
|
346
|
+
return left;
|
|
347
|
+
}
|
|
348
|
+
// Equality: == !=
|
|
349
|
+
parseEquality() {
|
|
350
|
+
let left = this.parseComparison();
|
|
351
|
+
while (this.peek()?.type === "==" || this.peek()?.type === "!=") {
|
|
352
|
+
const op = this.consume().type;
|
|
353
|
+
const right = this.parseComparison();
|
|
354
|
+
left = { type: "binary", op, left, right };
|
|
355
|
+
}
|
|
356
|
+
return left;
|
|
357
|
+
}
|
|
358
|
+
// Comparison: < <= > >=
|
|
359
|
+
parseComparison() {
|
|
360
|
+
let left = this.parseAddition();
|
|
361
|
+
while (this.peek()?.type === "<" || this.peek()?.type === "<=" || this.peek()?.type === ">" || this.peek()?.type === ">=") {
|
|
362
|
+
const op = this.consume().type;
|
|
363
|
+
const right = this.parseAddition();
|
|
364
|
+
left = { type: "binary", op, left, right };
|
|
365
|
+
}
|
|
366
|
+
return left;
|
|
367
|
+
}
|
|
368
|
+
// Addition: + -
|
|
369
|
+
parseAddition() {
|
|
370
|
+
let left = this.parseMultiplication();
|
|
371
|
+
while (this.peek()?.type === "+" || this.peek()?.type === "-") {
|
|
372
|
+
const op = this.consume().type;
|
|
373
|
+
const right = this.parseMultiplication();
|
|
374
|
+
left = { type: "binary", op, left, right };
|
|
375
|
+
}
|
|
376
|
+
return left;
|
|
377
|
+
}
|
|
378
|
+
// Multiplication: * / %
|
|
379
|
+
parseMultiplication() {
|
|
380
|
+
let left = this.parseUnary();
|
|
381
|
+
while (this.peek()?.type === "*" || this.peek()?.type === "/" || this.peek()?.type === "%") {
|
|
382
|
+
const op = this.consume().type;
|
|
383
|
+
const right = this.parseUnary();
|
|
384
|
+
left = { type: "binary", op, left, right };
|
|
385
|
+
}
|
|
386
|
+
return left;
|
|
387
|
+
}
|
|
388
|
+
// Unary: ! - ~
|
|
389
|
+
parseUnary() {
|
|
390
|
+
const tok = this.peek();
|
|
391
|
+
if (tok?.type === "!" || tok?.type === "-" || tok?.type === "~") {
|
|
392
|
+
this.consume();
|
|
393
|
+
const operand = this.parseUnary();
|
|
394
|
+
return { type: "unary", op: tok.type, operand };
|
|
395
|
+
}
|
|
396
|
+
return this.parsePrimary();
|
|
397
|
+
}
|
|
398
|
+
// Primary: number, string, source ref, function call, parenthesized
|
|
399
|
+
parsePrimary() {
|
|
400
|
+
const tok = this.peek();
|
|
401
|
+
if (!tok) throw new Error("Unexpected end of expression");
|
|
402
|
+
if (tok.type === "number") {
|
|
403
|
+
this.consume();
|
|
404
|
+
return { type: "number", value: tok.value };
|
|
405
|
+
}
|
|
406
|
+
if (tok.type === "string") {
|
|
407
|
+
this.consume();
|
|
408
|
+
return { type: "string", value: tok.value };
|
|
409
|
+
}
|
|
410
|
+
if (tok.type === "source") {
|
|
411
|
+
this.consume();
|
|
412
|
+
return { type: "source", name: tok.value };
|
|
413
|
+
}
|
|
414
|
+
if (tok.type === "ident") {
|
|
415
|
+
this.consume();
|
|
416
|
+
const name = tok.value;
|
|
417
|
+
if (this.peek()?.type === "(") {
|
|
418
|
+
this.consume();
|
|
419
|
+
const args = [];
|
|
420
|
+
if (this.peek()?.type !== ")") {
|
|
421
|
+
args.push(this.parseTernary());
|
|
422
|
+
while (this.peek()?.type === ",") {
|
|
423
|
+
this.consume();
|
|
424
|
+
args.push(this.parseTernary());
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.expect(")");
|
|
428
|
+
return { type: "call", name, args };
|
|
429
|
+
}
|
|
430
|
+
return { type: "string", value: name };
|
|
431
|
+
}
|
|
432
|
+
if (tok.type === "(") {
|
|
433
|
+
this.consume();
|
|
434
|
+
const node = this.parseTernary();
|
|
435
|
+
this.expect(")");
|
|
436
|
+
return node;
|
|
437
|
+
}
|
|
438
|
+
throw new Error(`Unexpected token '${tok.value}' (${tok.type})`);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
function numArg(args, i, fname) {
|
|
442
|
+
const v = args[i];
|
|
443
|
+
if (typeof v !== "number") throw new Error(`${fname}: argument ${i} must be a number, got ${v}`);
|
|
444
|
+
return v;
|
|
445
|
+
}
|
|
446
|
+
var BUILTINS = {
|
|
447
|
+
round: (a) => Math.round(numArg(a, 0, "round")),
|
|
448
|
+
floor: (a) => Math.floor(numArg(a, 0, "floor")),
|
|
449
|
+
ceil: (a) => Math.ceil(numArg(a, 0, "ceil")),
|
|
450
|
+
fract: (a) => {
|
|
451
|
+
const x = numArg(a, 0, "fract");
|
|
452
|
+
return x - Math.floor(x);
|
|
453
|
+
},
|
|
454
|
+
abs: (a) => Math.abs(numArg(a, 0, "abs")),
|
|
455
|
+
sqrt: (a) => Math.sqrt(numArg(a, 0, "sqrt")),
|
|
456
|
+
pow: (a) => Math.pow(numArg(a, 0, "pow"), numArg(a, 1, "pow")),
|
|
457
|
+
sin: (a) => Math.sin(numArg(a, 0, "sin")),
|
|
458
|
+
cos: (a) => Math.cos(numArg(a, 0, "cos")),
|
|
459
|
+
tan: (a) => Math.tan(numArg(a, 0, "tan")),
|
|
460
|
+
asin: (a) => Math.asin(numArg(a, 0, "asin")),
|
|
461
|
+
acos: (a) => Math.acos(numArg(a, 0, "acos")),
|
|
462
|
+
atan: (a) => Math.atan(numArg(a, 0, "atan")),
|
|
463
|
+
deg: (a) => numArg(a, 0, "deg") * (180 / Math.PI),
|
|
464
|
+
rad: (a) => numArg(a, 0, "rad") * (Math.PI / 180),
|
|
465
|
+
clamp: (a) => {
|
|
466
|
+
const x = numArg(a, 0, "clamp");
|
|
467
|
+
const min = numArg(a, 1, "clamp");
|
|
468
|
+
const max = numArg(a, 2, "clamp");
|
|
469
|
+
return Math.min(Math.max(x, min), max);
|
|
470
|
+
},
|
|
471
|
+
log: (a) => Math.log(numArg(a, 0, "log")),
|
|
472
|
+
log2: (a) => Math.log2(numArg(a, 0, "log2")),
|
|
473
|
+
log10: (a) => Math.log10(numArg(a, 0, "log10")),
|
|
474
|
+
exp: (a) => Math.exp(numArg(a, 0, "exp")),
|
|
475
|
+
numberFormat: (a) => {
|
|
476
|
+
const value = numArg(a, 0, "numberFormat");
|
|
477
|
+
const minIntDigits = numArg(a, 1, "numberFormat");
|
|
478
|
+
return String(Math.trunc(value)).padStart(minIntDigits, "0");
|
|
479
|
+
},
|
|
480
|
+
subText: (a) => {
|
|
481
|
+
const str = String(a[0]);
|
|
482
|
+
const start = numArg(a, 1, "subText");
|
|
483
|
+
const end = numArg(a, 2, "subText");
|
|
484
|
+
return str.slice(start, end);
|
|
485
|
+
},
|
|
486
|
+
textLength: (a) => String(a[0]).length,
|
|
487
|
+
icuText: (a) => String(a[0])
|
|
488
|
+
// return the pattern as-is
|
|
489
|
+
};
|
|
490
|
+
function evalNode(node, ctx) {
|
|
491
|
+
switch (node.type) {
|
|
492
|
+
case "number":
|
|
493
|
+
return node.value;
|
|
494
|
+
case "string":
|
|
495
|
+
return node.value;
|
|
496
|
+
case "source": {
|
|
497
|
+
const val = ctx.sources[node.name];
|
|
498
|
+
if (val === void 0) {
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
return val;
|
|
502
|
+
}
|
|
503
|
+
case "unary": {
|
|
504
|
+
const operand = evalNode(node.operand, ctx);
|
|
505
|
+
switch (node.op) {
|
|
506
|
+
case "!":
|
|
507
|
+
return Number(!operand);
|
|
508
|
+
case "-":
|
|
509
|
+
return -operand;
|
|
510
|
+
case "~":
|
|
511
|
+
return ~operand;
|
|
512
|
+
default:
|
|
513
|
+
throw new Error(`Unknown unary operator: ${node.op}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
case "binary": {
|
|
517
|
+
const left = evalNode(node.left, ctx);
|
|
518
|
+
const right = evalNode(node.right, ctx);
|
|
519
|
+
switch (node.op) {
|
|
520
|
+
case "+":
|
|
521
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
522
|
+
return String(left) + String(right);
|
|
523
|
+
}
|
|
524
|
+
return left + right;
|
|
525
|
+
case "-":
|
|
526
|
+
return left - right;
|
|
527
|
+
case "*":
|
|
528
|
+
return left * right;
|
|
529
|
+
case "/":
|
|
530
|
+
return left / right;
|
|
531
|
+
case "%":
|
|
532
|
+
return left % right;
|
|
533
|
+
case "==":
|
|
534
|
+
return left == right ? 1 : 0;
|
|
535
|
+
case "!=":
|
|
536
|
+
return left != right ? 1 : 0;
|
|
537
|
+
case "<":
|
|
538
|
+
return left < right ? 1 : 0;
|
|
539
|
+
case "<=":
|
|
540
|
+
return left <= right ? 1 : 0;
|
|
541
|
+
case ">":
|
|
542
|
+
return left > right ? 1 : 0;
|
|
543
|
+
case ">=":
|
|
544
|
+
return left >= right ? 1 : 0;
|
|
545
|
+
case "&&":
|
|
546
|
+
return left && right ? 1 : 0;
|
|
547
|
+
case "||":
|
|
548
|
+
return left || right ? 1 : 0;
|
|
549
|
+
case "|":
|
|
550
|
+
return left | right;
|
|
551
|
+
case "&":
|
|
552
|
+
return left & right;
|
|
553
|
+
default:
|
|
554
|
+
throw new Error(`Unknown binary operator: ${node.op}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
case "ternary": {
|
|
558
|
+
const cond = evalNode(node.condition, ctx);
|
|
559
|
+
return cond ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
|
|
560
|
+
}
|
|
561
|
+
case "call": {
|
|
562
|
+
const fn = BUILTINS[node.name];
|
|
563
|
+
if (!fn) throw new Error(`Unknown function: ${node.name}`);
|
|
564
|
+
const args = node.args.map((a) => evalNode(a, ctx));
|
|
565
|
+
return fn(args);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function evaluateExpression(expr, context) {
|
|
570
|
+
const tokens = tokenize(expr);
|
|
571
|
+
const parser = new Parser(tokens);
|
|
572
|
+
const ast = parser.parse();
|
|
573
|
+
return evalNode(ast, context);
|
|
574
|
+
}
|
|
575
|
+
function zeroPad(value) {
|
|
576
|
+
return String(value).padStart(2, "0");
|
|
577
|
+
}
|
|
578
|
+
function getDayOfYear(date) {
|
|
579
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
580
|
+
const diff = date.getTime() - start.getTime();
|
|
581
|
+
const oneDay = 1e3 * 60 * 60 * 24;
|
|
582
|
+
return Math.floor(diff / oneDay);
|
|
583
|
+
}
|
|
584
|
+
function buildDataSources(time, config, is24Hour) {
|
|
585
|
+
const second = time.getSeconds();
|
|
586
|
+
const minute = time.getMinutes();
|
|
587
|
+
const hour0_23 = time.getHours();
|
|
588
|
+
const hour0_11 = hour0_23 % 12;
|
|
589
|
+
const hour1_12 = hour0_11 === 0 ? 12 : hour0_11;
|
|
590
|
+
const hour1_24 = hour0_23 === 0 ? 24 : hour0_23;
|
|
591
|
+
const day = time.getDate();
|
|
592
|
+
const dayOfWeek = time.getDay() + 1;
|
|
593
|
+
const dayOfYear = getDayOfYear(time);
|
|
594
|
+
const month = time.getMonth() + 1;
|
|
595
|
+
const year = time.getFullYear();
|
|
596
|
+
const ampmState = hour0_23 >= 12 ? 1 : 0;
|
|
597
|
+
const is24HourMode = is24Hour !== false ? 1 : 0;
|
|
598
|
+
const utcTimestamp = Math.floor(time.getTime() / 1e3);
|
|
599
|
+
const sources = {
|
|
600
|
+
SECOND: second,
|
|
601
|
+
MINUTE: minute,
|
|
602
|
+
HOUR_0_23: hour0_23,
|
|
603
|
+
HOUR_1_12: hour1_12,
|
|
604
|
+
HOUR_0_11: hour0_11,
|
|
605
|
+
HOUR_1_24: hour1_24,
|
|
606
|
+
DAY: day,
|
|
607
|
+
DAY_OF_WEEK: dayOfWeek,
|
|
608
|
+
DAY_OF_YEAR: dayOfYear,
|
|
609
|
+
MONTH: month,
|
|
610
|
+
YEAR: year,
|
|
611
|
+
AMPM_STATE: ampmState,
|
|
612
|
+
IS_24_HOUR_MODE: is24HourMode,
|
|
613
|
+
UTC_TIMESTAMP: utcTimestamp,
|
|
614
|
+
// Zero-padded string variants
|
|
615
|
+
SECOND_Z: zeroPad(second),
|
|
616
|
+
MINUTE_Z: zeroPad(minute),
|
|
617
|
+
HOUR_0_23_Z: zeroPad(hour0_23),
|
|
618
|
+
HOUR_1_12_Z: zeroPad(hour1_12),
|
|
619
|
+
HOUR_0_11_Z: zeroPad(hour0_11),
|
|
620
|
+
HOUR_1_24_Z: zeroPad(hour1_24),
|
|
621
|
+
DAY_Z: zeroPad(day),
|
|
622
|
+
MONTH_Z: zeroPad(month),
|
|
623
|
+
// Digit extraction
|
|
624
|
+
SECOND_TENS_DIGIT: Math.floor(second / 10),
|
|
625
|
+
SECOND_UNITS_DIGIT: second % 10,
|
|
626
|
+
MINUTE_TENS_DIGIT: Math.floor(minute / 10),
|
|
627
|
+
MINUTE_UNITS_DIGIT: minute % 10,
|
|
628
|
+
HOUR_0_23_TENS_DIGIT: Math.floor(hour0_23 / 10),
|
|
629
|
+
HOUR_0_23_UNITS_DIGIT: hour0_23 % 10,
|
|
630
|
+
HOUR_1_12_TENS_DIGIT: Math.floor(hour1_12 / 10),
|
|
631
|
+
HOUR_1_12_UNITS_DIGIT: hour1_12 % 10,
|
|
632
|
+
HOUR_0_11_TENS_DIGIT: Math.floor(hour0_11 / 10),
|
|
633
|
+
HOUR_0_11_UNITS_DIGIT: hour0_11 % 10,
|
|
634
|
+
HOUR_1_24_TENS_DIGIT: Math.floor(hour1_24 / 10),
|
|
635
|
+
HOUR_1_24_UNITS_DIGIT: hour1_24 % 10
|
|
636
|
+
};
|
|
637
|
+
if (config) {
|
|
638
|
+
for (const [key, value] of Object.entries(config)) {
|
|
639
|
+
sources[`CONFIGURATION.${key}`] = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { sources };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/animation.ts
|
|
646
|
+
function ease(t, interpolation, controls) {
|
|
647
|
+
t = Math.max(0, Math.min(1, t));
|
|
648
|
+
switch (interpolation) {
|
|
649
|
+
case "LINEAR":
|
|
650
|
+
return t;
|
|
651
|
+
case "EASE_IN":
|
|
652
|
+
return t * t;
|
|
653
|
+
case "EASE_OUT":
|
|
654
|
+
return 1 - (1 - t) * (1 - t);
|
|
655
|
+
case "EASE_IN_OUT":
|
|
656
|
+
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
657
|
+
case "OVERSHOOT":
|
|
658
|
+
return 2.70158 * t * t * t - 1.70158 * t * t;
|
|
659
|
+
case "CUBIC_BEZIER": {
|
|
660
|
+
if (!controls) return t;
|
|
661
|
+
const parts = controls.split(",").map(Number);
|
|
662
|
+
const [x1, y1, x2, y2] = parts;
|
|
663
|
+
return cubicBezier(x1, y1, x2, y2, t);
|
|
664
|
+
}
|
|
665
|
+
default:
|
|
666
|
+
return t;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function cubicBezier(x1, y1, x2, y2, x) {
|
|
670
|
+
let lo = 0;
|
|
671
|
+
let hi = 1;
|
|
672
|
+
for (let i = 0; i < 20; i++) {
|
|
673
|
+
const mid = (lo + hi) / 2;
|
|
674
|
+
const bx = 3 * (1 - mid) * (1 - mid) * mid * x1 + 3 * (1 - mid) * mid * mid * x2 + mid * mid * mid;
|
|
675
|
+
if (bx < x) lo = mid;
|
|
676
|
+
else hi = mid;
|
|
677
|
+
}
|
|
678
|
+
const t = (lo + hi) / 2;
|
|
679
|
+
return 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t;
|
|
680
|
+
}
|
|
681
|
+
function applyTransforms(el, expressionCtx, elapsedMs) {
|
|
682
|
+
for (const child of el.children) {
|
|
683
|
+
if (child.tagName !== "Transform") continue;
|
|
684
|
+
const target = child.getAttribute("target");
|
|
685
|
+
if (!target) continue;
|
|
686
|
+
const valueExpr = child.getAttribute("value");
|
|
687
|
+
const mode = child.getAttribute("mode") ?? "TO";
|
|
688
|
+
const animEl = Array.from(child.children).find(
|
|
689
|
+
(c) => c.tagName === "Animation"
|
|
690
|
+
);
|
|
691
|
+
if (animEl) {
|
|
692
|
+
const fromAttr = child.getAttribute("from");
|
|
693
|
+
const toAttr = child.getAttribute("to");
|
|
694
|
+
if (fromAttr !== null && toAttr !== null) {
|
|
695
|
+
const from = parseFloat(fromAttr);
|
|
696
|
+
const to = parseFloat(toAttr);
|
|
697
|
+
const duration = parseFloat(animEl.getAttribute("duration") ?? "1") * 1e3;
|
|
698
|
+
const repeat = parseInt(animEl.getAttribute("repeat") ?? "0");
|
|
699
|
+
const interpolation = animEl.getAttribute("interpolation") ?? "LINEAR";
|
|
700
|
+
const controls = animEl.getAttribute("controls") ?? void 0;
|
|
701
|
+
const fpsAttr = animEl.getAttribute("fps");
|
|
702
|
+
const fps = fpsAttr ? parseInt(fpsAttr) : void 0;
|
|
703
|
+
let elapsed = elapsedMs;
|
|
704
|
+
if (repeat === -1) {
|
|
705
|
+
elapsed = duration > 0 ? elapsed % duration : 0;
|
|
706
|
+
} else if (repeat > 0) {
|
|
707
|
+
const totalDuration = duration * (repeat + 1);
|
|
708
|
+
if (elapsed > totalDuration) elapsed = totalDuration;
|
|
709
|
+
if (elapsed < totalDuration) {
|
|
710
|
+
elapsed = elapsed % duration;
|
|
711
|
+
} else {
|
|
712
|
+
elapsed = duration;
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
if (elapsed > duration) elapsed = duration;
|
|
716
|
+
}
|
|
717
|
+
if (fps && fps > 0) {
|
|
718
|
+
const frameMs = 1e3 / fps;
|
|
719
|
+
elapsed = Math.floor(elapsed / frameMs) * frameMs;
|
|
720
|
+
}
|
|
721
|
+
let t = duration > 0 ? elapsed / duration : 1;
|
|
722
|
+
t = ease(t, interpolation, controls);
|
|
723
|
+
const value = from + (to - from) * t;
|
|
724
|
+
applyValue(el, target, mode, value);
|
|
725
|
+
}
|
|
726
|
+
} else if (valueExpr) {
|
|
727
|
+
const value = evaluateExpression(valueExpr, expressionCtx);
|
|
728
|
+
applyValue(el, target, mode, Number(value));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function applyValue(el, target, mode, value) {
|
|
733
|
+
if (mode === "TO") {
|
|
734
|
+
el.setAttribute(target, String(value));
|
|
735
|
+
} else if (mode === "BY") {
|
|
736
|
+
const base = parseFloat(el.getAttribute(target) ?? "0");
|
|
737
|
+
el.setAttribute(target, String(base + value));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/layout.ts
|
|
742
|
+
async function renderGroup(ctx, el, renderChild, renderCtx) {
|
|
743
|
+
applyVariants(el, renderCtx.ambient);
|
|
744
|
+
applyTransforms(el, renderCtx.expressionCtx, renderCtx.elapsedMs ?? 0);
|
|
745
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
746
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
747
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
748
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
749
|
+
const pivotX = parseFloat(el.getAttribute("pivotX") ?? "0.5");
|
|
750
|
+
const pivotY = parseFloat(el.getAttribute("pivotY") ?? "0.5");
|
|
751
|
+
const angle = parseFloat(el.getAttribute("angle") ?? "0");
|
|
752
|
+
const alpha = parseFloat(el.getAttribute("alpha") ?? "255");
|
|
753
|
+
const scaleX = parseFloat(el.getAttribute("scaleX") ?? "1");
|
|
754
|
+
const scaleY = parseFloat(el.getAttribute("scaleY") ?? "1");
|
|
755
|
+
ctx.save();
|
|
756
|
+
ctx.translate(x, y);
|
|
757
|
+
const px = pivotX * w;
|
|
758
|
+
const py = pivotY * h;
|
|
759
|
+
ctx.translate(px, py);
|
|
760
|
+
ctx.rotate(angle * Math.PI / 180);
|
|
761
|
+
ctx.scale(scaleX, scaleY);
|
|
762
|
+
ctx.translate(-px, -py);
|
|
763
|
+
ctx.globalAlpha *= alpha / 255;
|
|
764
|
+
applyBlendMode(ctx, el);
|
|
765
|
+
if (hasMasking(el)) {
|
|
766
|
+
await renderWithMasking(ctx, el, w, h, renderChild, renderCtx);
|
|
767
|
+
} else {
|
|
768
|
+
for (const child of el.children) {
|
|
769
|
+
if (child.tagName !== "Variant") {
|
|
770
|
+
await renderChild(ctx, child, renderCtx);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
ctx.restore();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/conditions.ts
|
|
778
|
+
async function renderCondition(ctx, el, renderChild, renderCtx) {
|
|
779
|
+
const namedResults = {};
|
|
780
|
+
const expressionsEl = el.querySelector(":scope > Expressions");
|
|
781
|
+
if (expressionsEl) {
|
|
782
|
+
for (const exprEl of expressionsEl.children) {
|
|
783
|
+
if (exprEl.tagName === "Expression") {
|
|
784
|
+
const name = exprEl.getAttribute("name") ?? "";
|
|
785
|
+
const expr = exprEl.getAttribute("expression") ?? "0";
|
|
786
|
+
const augCtx2 = {
|
|
787
|
+
sources: { ...renderCtx.expressionCtx.sources, ...namedResults }
|
|
788
|
+
};
|
|
789
|
+
namedResults[name] = evaluateExpression(expr, augCtx2);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const augCtx = {
|
|
794
|
+
sources: { ...renderCtx.expressionCtx.sources, ...namedResults }
|
|
795
|
+
};
|
|
796
|
+
const augRenderCtx = { ...renderCtx, expressionCtx: augCtx };
|
|
797
|
+
for (const child of el.children) {
|
|
798
|
+
if (child.tagName === "Compare") {
|
|
799
|
+
const expr = child.getAttribute("expression") ?? "0";
|
|
800
|
+
const result = evaluateExpression(expr, augCtx);
|
|
801
|
+
if (result) {
|
|
802
|
+
for (const grandchild of child.children) {
|
|
803
|
+
await renderChild(ctx, grandchild, augRenderCtx);
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const child of el.children) {
|
|
810
|
+
if (child.tagName === "Default") {
|
|
811
|
+
for (const grandchild of child.children) {
|
|
812
|
+
await renderChild(ctx, grandchild, augRenderCtx);
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/text.ts
|
|
820
|
+
var WEIGHT_MAP = {
|
|
821
|
+
THIN: 100,
|
|
822
|
+
EXTRA_LIGHT: 200,
|
|
823
|
+
LIGHT: 300,
|
|
824
|
+
NORMAL: 400,
|
|
825
|
+
MEDIUM: 500,
|
|
826
|
+
SEMI_BOLD: 600,
|
|
827
|
+
BOLD: 700,
|
|
828
|
+
EXTRA_BOLD: 800,
|
|
829
|
+
BLACK: 900
|
|
830
|
+
};
|
|
831
|
+
function parseFontElement(fontEl) {
|
|
832
|
+
const family = fontEl?.getAttribute("family") ?? "sans-serif";
|
|
833
|
+
const size = parseFloat(fontEl?.getAttribute("size") ?? "16");
|
|
834
|
+
const color = parseColor(fontEl?.getAttribute("color") ?? "#FFFFFF");
|
|
835
|
+
const weightStr = fontEl?.getAttribute("weight") ?? "NORMAL";
|
|
836
|
+
const weight = WEIGHT_MAP[weightStr] ?? 400;
|
|
837
|
+
const slant = fontEl?.getAttribute("slant");
|
|
838
|
+
const style = slant === "ITALIC" ? "italic" : "normal";
|
|
839
|
+
const letterSpacingAttr = fontEl?.getAttribute("letterSpacing");
|
|
840
|
+
const letterSpacing = letterSpacingAttr ? parseFloat(letterSpacingAttr) : 0;
|
|
841
|
+
const underline = fontEl?.querySelector(":scope > Underline") != null;
|
|
842
|
+
const strikeThrough = fontEl?.querySelector(":scope > StrikeThrough") != null;
|
|
843
|
+
const resolvedFamily = family === "SYNC_TO_DEVICE" ? "sans-serif" : family;
|
|
844
|
+
return { style, weight, size, family: resolvedFamily, color, letterSpacing, underline, strikeThrough };
|
|
845
|
+
}
|
|
846
|
+
function applyFont(ctx, spec) {
|
|
847
|
+
ctx.font = `${spec.style} ${spec.weight} ${spec.size}px ${spec.family}`;
|
|
848
|
+
ctx.fillStyle = spec.color;
|
|
849
|
+
if (spec.letterSpacing !== 0) {
|
|
850
|
+
const spacingPx = spec.letterSpacing * spec.size;
|
|
851
|
+
ctx.letterSpacing = `${spacingPx}px`;
|
|
852
|
+
} else {
|
|
853
|
+
ctx.letterSpacing = "0px";
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function resolveTextContent(raw, expressionCtx) {
|
|
857
|
+
return raw.replace(/\[([^\]]+)\]/g, (_match, name) => {
|
|
858
|
+
const val = expressionCtx.sources[name];
|
|
859
|
+
return val !== void 0 ? String(val) : "";
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
function resolveTextAlign(align) {
|
|
863
|
+
switch (align) {
|
|
864
|
+
case "START":
|
|
865
|
+
return "left";
|
|
866
|
+
case "CENTER":
|
|
867
|
+
return "center";
|
|
868
|
+
case "END":
|
|
869
|
+
return "right";
|
|
870
|
+
default:
|
|
871
|
+
return "left";
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function truncateWithEllipsis(ctx, text, maxWidth) {
|
|
875
|
+
if (ctx.measureText(text).width <= maxWidth) return text;
|
|
876
|
+
const ellipsis = "\u2026";
|
|
877
|
+
let result = text;
|
|
878
|
+
while (result.length > 0 && ctx.measureText(result + ellipsis).width > maxWidth) {
|
|
879
|
+
result = result.slice(0, -1);
|
|
880
|
+
}
|
|
881
|
+
return result + ellipsis;
|
|
882
|
+
}
|
|
883
|
+
function drawDecorations(ctx, text, x, y, spec, align) {
|
|
884
|
+
if (!spec.underline && !spec.strikeThrough) return;
|
|
885
|
+
const metrics = ctx.measureText(text);
|
|
886
|
+
const textWidth = metrics.width;
|
|
887
|
+
let lineX;
|
|
888
|
+
switch (align) {
|
|
889
|
+
case "center":
|
|
890
|
+
lineX = x - textWidth / 2;
|
|
891
|
+
break;
|
|
892
|
+
case "right":
|
|
893
|
+
lineX = x - textWidth;
|
|
894
|
+
break;
|
|
895
|
+
default:
|
|
896
|
+
lineX = x;
|
|
897
|
+
}
|
|
898
|
+
ctx.save();
|
|
899
|
+
ctx.strokeStyle = spec.color;
|
|
900
|
+
ctx.lineWidth = Math.max(1, spec.size / 14);
|
|
901
|
+
if (spec.underline) {
|
|
902
|
+
const underlineY = y + spec.size * 0.15;
|
|
903
|
+
ctx.beginPath();
|
|
904
|
+
ctx.moveTo(lineX, underlineY);
|
|
905
|
+
ctx.lineTo(lineX + textWidth, underlineY);
|
|
906
|
+
ctx.stroke();
|
|
907
|
+
}
|
|
908
|
+
if (spec.strikeThrough) {
|
|
909
|
+
const strikeY = y - spec.size * 0.25;
|
|
910
|
+
ctx.beginPath();
|
|
911
|
+
ctx.moveTo(lineX, strikeY);
|
|
912
|
+
ctx.lineTo(lineX + textWidth, strikeY);
|
|
913
|
+
ctx.stroke();
|
|
914
|
+
}
|
|
915
|
+
ctx.restore();
|
|
916
|
+
}
|
|
917
|
+
function renderPartText(ctx, el, renderCtx) {
|
|
918
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
919
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
920
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
921
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
922
|
+
const textEl = el.querySelector(":scope > Text");
|
|
923
|
+
if (!textEl) return;
|
|
924
|
+
ctx.save();
|
|
925
|
+
ctx.translate(x, y);
|
|
926
|
+
renderTextElement(ctx, textEl, w, h, renderCtx);
|
|
927
|
+
ctx.restore();
|
|
928
|
+
}
|
|
929
|
+
function renderTextElement(ctx, textEl, containerWidth, containerHeight, renderCtx) {
|
|
930
|
+
const fontEl = textEl.querySelector(":scope > Font");
|
|
931
|
+
const spec = parseFontElement(fontEl);
|
|
932
|
+
const align = textEl.getAttribute("align") ?? "START";
|
|
933
|
+
const ellipsis = textEl.getAttribute("ellipsis") === "true";
|
|
934
|
+
let rawText = "";
|
|
935
|
+
for (const node of textEl.childNodes) {
|
|
936
|
+
if (node.nodeType === 3) {
|
|
937
|
+
rawText += node.textContent ?? "";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
rawText = rawText.trim();
|
|
941
|
+
const resolvedText = resolveTextContent(rawText, renderCtx.expressionCtx);
|
|
942
|
+
applyFont(ctx, spec);
|
|
943
|
+
const textAlign = resolveTextAlign(align);
|
|
944
|
+
ctx.textAlign = textAlign;
|
|
945
|
+
ctx.textBaseline = "middle";
|
|
946
|
+
let displayText = resolvedText;
|
|
947
|
+
if (ellipsis && containerWidth > 0) {
|
|
948
|
+
displayText = truncateWithEllipsis(ctx, displayText, containerWidth);
|
|
949
|
+
}
|
|
950
|
+
let textX;
|
|
951
|
+
switch (textAlign) {
|
|
952
|
+
case "center":
|
|
953
|
+
textX = containerWidth / 2;
|
|
954
|
+
break;
|
|
955
|
+
case "right":
|
|
956
|
+
textX = containerWidth;
|
|
957
|
+
break;
|
|
958
|
+
default:
|
|
959
|
+
textX = 0;
|
|
960
|
+
}
|
|
961
|
+
const textY = containerHeight / 2;
|
|
962
|
+
ctx.fillText(displayText, textX, textY);
|
|
963
|
+
drawDecorations(ctx, displayText, textX, textY, spec, textAlign);
|
|
964
|
+
}
|
|
965
|
+
var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
966
|
+
function formatTimeText(format, date, hourFormat) {
|
|
967
|
+
const is24 = hourFormat !== "12";
|
|
968
|
+
const h24 = date.getHours();
|
|
969
|
+
const h12Raw = h24 % 12;
|
|
970
|
+
const h12 = h12Raw === 0 ? 12 : h12Raw;
|
|
971
|
+
const hDisplay24 = h24;
|
|
972
|
+
const hDisplay12 = h12;
|
|
973
|
+
const min = date.getMinutes();
|
|
974
|
+
const sec = date.getSeconds();
|
|
975
|
+
const ampm = h24 >= 12 ? "PM" : "AM";
|
|
976
|
+
const dayName = DAY_NAMES[date.getDay()];
|
|
977
|
+
const tokens = [
|
|
978
|
+
["hh", String(hDisplay12).padStart(2, "0")],
|
|
979
|
+
["HH", String(hDisplay24).padStart(2, "0")],
|
|
980
|
+
["mm", String(min).padStart(2, "0")],
|
|
981
|
+
["ss", String(sec).padStart(2, "0")],
|
|
982
|
+
["EEE", dayName],
|
|
983
|
+
["h", String(hDisplay12)],
|
|
984
|
+
["H", String(hDisplay24)],
|
|
985
|
+
["m", String(min)],
|
|
986
|
+
["s", String(sec)],
|
|
987
|
+
["a", ampm]
|
|
988
|
+
];
|
|
989
|
+
let result = format;
|
|
990
|
+
const replacements = [];
|
|
991
|
+
for (const [token, value] of tokens) {
|
|
992
|
+
const placeholder = `\0${replacements.length}\0`;
|
|
993
|
+
replacements.push(value);
|
|
994
|
+
result = result.split(token).join(placeholder);
|
|
995
|
+
}
|
|
996
|
+
for (let i = 0; i < replacements.length; i++) {
|
|
997
|
+
result = result.split(`\0${i}\0`).join(replacements[i]);
|
|
998
|
+
}
|
|
999
|
+
return result;
|
|
1000
|
+
}
|
|
1001
|
+
function renderDigitalClock(ctx, el, renderCtx) {
|
|
1002
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1003
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1004
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1005
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1006
|
+
ctx.save();
|
|
1007
|
+
ctx.translate(x, y);
|
|
1008
|
+
for (const child of el.children) {
|
|
1009
|
+
if (child.tagName === "TimeText") {
|
|
1010
|
+
renderTimeText(ctx, child, w, h, renderCtx);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
ctx.restore();
|
|
1014
|
+
}
|
|
1015
|
+
function renderTimeText(ctx, el, parentWidth, parentHeight, renderCtx) {
|
|
1016
|
+
applyVariants(el, renderCtx.ambient);
|
|
1017
|
+
const alpha = parseFloat(el.getAttribute("alpha") ?? "255");
|
|
1018
|
+
if (alpha <= 0) return;
|
|
1019
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1020
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1021
|
+
const w = parseFloat(el.getAttribute("width") ?? "0") || parentWidth;
|
|
1022
|
+
const h = parseFloat(el.getAttribute("height") ?? "0") || parentHeight;
|
|
1023
|
+
const format = el.getAttribute("format") ?? "HH:mm";
|
|
1024
|
+
const hourFormat = el.getAttribute("hourFormat") ?? "24";
|
|
1025
|
+
const align = el.getAttribute("align") ?? "START";
|
|
1026
|
+
const utcTimestamp = renderCtx.expressionCtx.sources["UTC_TIMESTAMP"];
|
|
1027
|
+
const date = typeof utcTimestamp === "number" ? new Date(utcTimestamp * 1e3) : /* @__PURE__ */ new Date();
|
|
1028
|
+
const formattedText = formatTimeText(format, date, hourFormat);
|
|
1029
|
+
const fontEl = el.querySelector(":scope > Font");
|
|
1030
|
+
const spec = parseFontElement(fontEl);
|
|
1031
|
+
const sizeAttr = el.getAttribute("size");
|
|
1032
|
+
if (sizeAttr) {
|
|
1033
|
+
spec.size = parseFloat(sizeAttr);
|
|
1034
|
+
}
|
|
1035
|
+
ctx.save();
|
|
1036
|
+
ctx.translate(x, y);
|
|
1037
|
+
ctx.globalAlpha *= alpha / 255;
|
|
1038
|
+
applyFont(ctx, spec);
|
|
1039
|
+
const textAlign = resolveTextAlign(align);
|
|
1040
|
+
ctx.textAlign = textAlign;
|
|
1041
|
+
ctx.textBaseline = "middle";
|
|
1042
|
+
let textX;
|
|
1043
|
+
switch (textAlign) {
|
|
1044
|
+
case "center":
|
|
1045
|
+
textX = w / 2;
|
|
1046
|
+
break;
|
|
1047
|
+
case "right":
|
|
1048
|
+
textX = w;
|
|
1049
|
+
break;
|
|
1050
|
+
default:
|
|
1051
|
+
textX = 0;
|
|
1052
|
+
}
|
|
1053
|
+
const textY = h / 2;
|
|
1054
|
+
ctx.fillText(formattedText, textX, textY);
|
|
1055
|
+
drawDecorations(ctx, formattedText, textX, textY, spec, textAlign);
|
|
1056
|
+
ctx.restore();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// src/images.ts
|
|
1060
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
1061
|
+
async function renderPartImage(ctx, el, renderCtx, assets) {
|
|
1062
|
+
applyVariants(el, renderCtx.ambient);
|
|
1063
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1064
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1065
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1066
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1067
|
+
const imageEl = findImageChild(el);
|
|
1068
|
+
if (!imageEl) return;
|
|
1069
|
+
const resource = imageEl.getAttribute("resource") ?? "";
|
|
1070
|
+
if (!resource) return;
|
|
1071
|
+
const bitmap = await getOrDecodeImage(resource, assets);
|
|
1072
|
+
if (!bitmap) return;
|
|
1073
|
+
ctx.save();
|
|
1074
|
+
ctx.drawImage(bitmap, x, y, w, h);
|
|
1075
|
+
ctx.restore();
|
|
1076
|
+
}
|
|
1077
|
+
function findImageChild(el) {
|
|
1078
|
+
for (const child of el.children) {
|
|
1079
|
+
if (child.tagName === "Image") return child;
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
async function getOrDecodeImage(resource, assets) {
|
|
1084
|
+
if (imageCache.has(resource)) {
|
|
1085
|
+
return imageCache.get(resource);
|
|
1086
|
+
}
|
|
1087
|
+
const buffer = assets.get(resource);
|
|
1088
|
+
if (!buffer) return null;
|
|
1089
|
+
const blob = new Blob([buffer]);
|
|
1090
|
+
const bitmap = await createImageBitmap(blob);
|
|
1091
|
+
imageCache.set(resource, bitmap);
|
|
1092
|
+
return bitmap;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/clock.ts
|
|
1096
|
+
async function renderAnalogClock(ctx, el, renderChild, renderCtx) {
|
|
1097
|
+
applyVariants(el, renderCtx.ambient);
|
|
1098
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1099
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1100
|
+
const alpha = parseFloat(el.getAttribute("alpha") ?? "255");
|
|
1101
|
+
if (alpha <= 0) return;
|
|
1102
|
+
const sources = renderCtx.expressionCtx.sources;
|
|
1103
|
+
const hour = sources.HOUR_0_23 ?? 0;
|
|
1104
|
+
const minute = sources.MINUTE ?? 0;
|
|
1105
|
+
const second = sources.SECOND ?? 0;
|
|
1106
|
+
ctx.save();
|
|
1107
|
+
ctx.translate(x, y);
|
|
1108
|
+
ctx.globalAlpha *= alpha / 255;
|
|
1109
|
+
for (const child of el.children) {
|
|
1110
|
+
const tag = child.tagName;
|
|
1111
|
+
if (tag === "HourHand" || tag === "MinuteHand" || tag === "SecondHand") {
|
|
1112
|
+
let angle;
|
|
1113
|
+
if (tag === "HourHand") {
|
|
1114
|
+
angle = (hour % 12 + minute / 60) * 30;
|
|
1115
|
+
} else if (tag === "MinuteHand") {
|
|
1116
|
+
angle = (minute + second / 60) * 6;
|
|
1117
|
+
} else {
|
|
1118
|
+
angle = second * 6;
|
|
1119
|
+
}
|
|
1120
|
+
await renderHand(ctx, child, angle, renderChild, renderCtx);
|
|
1121
|
+
} else if (tag !== "Variant") {
|
|
1122
|
+
await renderChild(ctx, child, renderCtx);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
ctx.restore();
|
|
1126
|
+
}
|
|
1127
|
+
function resolveExprRef(value, expressionCtx) {
|
|
1128
|
+
if (!value?.includes("[")) return value;
|
|
1129
|
+
return value.replace(/\[([^\]]+)\]/g, (_, name) => {
|
|
1130
|
+
const val = expressionCtx.sources[name];
|
|
1131
|
+
return val !== void 0 ? String(val) : "#000000";
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
async function drawTintedImage(ctx, bitmap, w, h, tintColor) {
|
|
1135
|
+
if (!tintColor) {
|
|
1136
|
+
ctx.drawImage(bitmap, 0, 0, w, h);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const offscreen = new OffscreenCanvas(w, h);
|
|
1140
|
+
const offCtx = offscreen.getContext("2d");
|
|
1141
|
+
offCtx.drawImage(bitmap, 0, 0, w, h);
|
|
1142
|
+
offCtx.globalCompositeOperation = "source-in";
|
|
1143
|
+
offCtx.fillStyle = parseColor(tintColor);
|
|
1144
|
+
offCtx.fillRect(0, 0, w, h);
|
|
1145
|
+
ctx.drawImage(offscreen, 0, 0);
|
|
1146
|
+
}
|
|
1147
|
+
async function renderHand(ctx, el, angle, renderChild, renderCtx) {
|
|
1148
|
+
applyVariants(el, renderCtx.ambient);
|
|
1149
|
+
const alpha = parseFloat(el.getAttribute("alpha") ?? "255");
|
|
1150
|
+
if (alpha <= 0) return;
|
|
1151
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1152
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1153
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1154
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1155
|
+
const pivotX = parseFloat(el.getAttribute("pivotX") ?? "0.5");
|
|
1156
|
+
const pivotY = parseFloat(el.getAttribute("pivotY") ?? "0.5");
|
|
1157
|
+
const tintColor = resolveExprRef(el.getAttribute("tintColor"), renderCtx.expressionCtx);
|
|
1158
|
+
ctx.save();
|
|
1159
|
+
ctx.translate(x, y);
|
|
1160
|
+
ctx.globalAlpha *= alpha / 255;
|
|
1161
|
+
const px = pivotX * w;
|
|
1162
|
+
const py = pivotY * h;
|
|
1163
|
+
ctx.translate(px, py);
|
|
1164
|
+
ctx.rotate(angle * Math.PI / 180);
|
|
1165
|
+
ctx.translate(-px, -py);
|
|
1166
|
+
const resource = el.getAttribute("resource");
|
|
1167
|
+
if (resource) {
|
|
1168
|
+
const bitmap = await getOrDecodeImage(resource, renderCtx.assets);
|
|
1169
|
+
if (bitmap) {
|
|
1170
|
+
await drawTintedImage(ctx, bitmap, w, h, tintColor);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
for (const child of el.children) {
|
|
1174
|
+
if (child.tagName !== "Variant") {
|
|
1175
|
+
await renderChild(ctx, child, renderCtx);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
ctx.restore();
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/shapes.ts
|
|
1182
|
+
function resolveColorExprs(el, expressionCtx) {
|
|
1183
|
+
for (const child of el.children) {
|
|
1184
|
+
if (child.tagName !== "Fill" && child.tagName !== "Stroke") continue;
|
|
1185
|
+
const color = child.getAttribute("color");
|
|
1186
|
+
if (!color?.includes("[")) continue;
|
|
1187
|
+
const resolved = color.replace(/\[([^\]]+)\]/g, (_, name) => {
|
|
1188
|
+
const val = expressionCtx.sources[name];
|
|
1189
|
+
return val !== void 0 ? String(val) : "#000000";
|
|
1190
|
+
});
|
|
1191
|
+
child.setAttribute("color", resolved);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
async function renderElement(ctx, el, renderCtx) {
|
|
1195
|
+
const tag = el.tagName;
|
|
1196
|
+
switch (tag) {
|
|
1197
|
+
case "Group":
|
|
1198
|
+
case "PartDraw":
|
|
1199
|
+
await renderGroup(ctx, el, renderElement, renderCtx);
|
|
1200
|
+
break;
|
|
1201
|
+
case "Condition":
|
|
1202
|
+
await renderCondition(ctx, el, renderElement, renderCtx);
|
|
1203
|
+
break;
|
|
1204
|
+
case "Arc":
|
|
1205
|
+
applyVariants(el, renderCtx.ambient);
|
|
1206
|
+
applyTransforms(el, renderCtx.expressionCtx, renderCtx.elapsedMs);
|
|
1207
|
+
resolveColorExprs(el, renderCtx.expressionCtx);
|
|
1208
|
+
applyBlendMode(ctx, el);
|
|
1209
|
+
renderArc(ctx, el);
|
|
1210
|
+
break;
|
|
1211
|
+
case "Rectangle":
|
|
1212
|
+
applyVariants(el, renderCtx.ambient);
|
|
1213
|
+
applyTransforms(el, renderCtx.expressionCtx, renderCtx.elapsedMs);
|
|
1214
|
+
resolveColorExprs(el, renderCtx.expressionCtx);
|
|
1215
|
+
applyBlendMode(ctx, el);
|
|
1216
|
+
renderRectangle(ctx, el);
|
|
1217
|
+
break;
|
|
1218
|
+
case "RoundRectangle":
|
|
1219
|
+
applyVariants(el, renderCtx.ambient);
|
|
1220
|
+
applyTransforms(el, renderCtx.expressionCtx, renderCtx.elapsedMs);
|
|
1221
|
+
resolveColorExprs(el, renderCtx.expressionCtx);
|
|
1222
|
+
applyBlendMode(ctx, el);
|
|
1223
|
+
renderRoundRectangle(ctx, el);
|
|
1224
|
+
break;
|
|
1225
|
+
case "Ellipse":
|
|
1226
|
+
applyVariants(el, renderCtx.ambient);
|
|
1227
|
+
applyTransforms(el, renderCtx.expressionCtx, renderCtx.elapsedMs);
|
|
1228
|
+
resolveColorExprs(el, renderCtx.expressionCtx);
|
|
1229
|
+
applyBlendMode(ctx, el);
|
|
1230
|
+
renderEllipse(ctx, el);
|
|
1231
|
+
break;
|
|
1232
|
+
case "Line":
|
|
1233
|
+
applyVariants(el, renderCtx.ambient);
|
|
1234
|
+
applyTransforms(el, renderCtx.expressionCtx, renderCtx.elapsedMs);
|
|
1235
|
+
resolveColorExprs(el, renderCtx.expressionCtx);
|
|
1236
|
+
applyBlendMode(ctx, el);
|
|
1237
|
+
renderLine(ctx, el);
|
|
1238
|
+
break;
|
|
1239
|
+
case "PartText":
|
|
1240
|
+
renderPartText(ctx, el, renderCtx);
|
|
1241
|
+
break;
|
|
1242
|
+
case "DigitalClock":
|
|
1243
|
+
renderDigitalClock(ctx, el, renderCtx);
|
|
1244
|
+
break;
|
|
1245
|
+
case "PartImage":
|
|
1246
|
+
await renderPartImage(ctx, el, renderCtx, renderCtx.assets);
|
|
1247
|
+
break;
|
|
1248
|
+
case "AnalogClock":
|
|
1249
|
+
await renderAnalogClock(ctx, el, renderElement, renderCtx);
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function renderRectangle(ctx, el) {
|
|
1254
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1255
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1256
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1257
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1258
|
+
ctx.beginPath();
|
|
1259
|
+
ctx.rect(x, y, w, h);
|
|
1260
|
+
applyFill(ctx, el);
|
|
1261
|
+
applyStroke(ctx, el);
|
|
1262
|
+
}
|
|
1263
|
+
function renderRoundRectangle(ctx, el) {
|
|
1264
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1265
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1266
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1267
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1268
|
+
const rx = parseFloat(el.getAttribute("cornerRadiusX") ?? "0");
|
|
1269
|
+
const ry = parseFloat(el.getAttribute("cornerRadiusY") ?? rx.toString());
|
|
1270
|
+
ctx.beginPath();
|
|
1271
|
+
const radius = { x: rx, y: ry };
|
|
1272
|
+
ctx.roundRect(x, y, w, h, [radius, radius, radius, radius]);
|
|
1273
|
+
applyFill(ctx, el);
|
|
1274
|
+
applyStroke(ctx, el);
|
|
1275
|
+
}
|
|
1276
|
+
function renderArc(ctx, el) {
|
|
1277
|
+
const cx = parseFloat(el.getAttribute("centerX") ?? "0");
|
|
1278
|
+
const cy = parseFloat(el.getAttribute("centerY") ?? "0");
|
|
1279
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1280
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1281
|
+
const startAngle = parseFloat(el.getAttribute("startAngle") ?? "0");
|
|
1282
|
+
const endAngle = parseFloat(el.getAttribute("endAngle") ?? "360");
|
|
1283
|
+
const direction = el.getAttribute("direction") ?? "CLOCKWISE";
|
|
1284
|
+
const startRad = (startAngle - 90) * Math.PI / 180;
|
|
1285
|
+
const endRad = (endAngle - 90) * Math.PI / 180;
|
|
1286
|
+
const counterclockwise = direction === "COUNTER_CLOCKWISE";
|
|
1287
|
+
ctx.beginPath();
|
|
1288
|
+
if (w === h) {
|
|
1289
|
+
ctx.arc(cx, cy, w / 2, startRad, endRad, counterclockwise);
|
|
1290
|
+
} else {
|
|
1291
|
+
ctx.ellipse(cx, cy, w / 2, h / 2, 0, startRad, endRad, counterclockwise);
|
|
1292
|
+
}
|
|
1293
|
+
applyFill(ctx, el);
|
|
1294
|
+
applyStroke(ctx, el);
|
|
1295
|
+
}
|
|
1296
|
+
function renderEllipse(ctx, el) {
|
|
1297
|
+
const x = parseFloat(el.getAttribute("x") ?? "0");
|
|
1298
|
+
const y = parseFloat(el.getAttribute("y") ?? "0");
|
|
1299
|
+
const w = parseFloat(el.getAttribute("width") ?? "0");
|
|
1300
|
+
const h = parseFloat(el.getAttribute("height") ?? "0");
|
|
1301
|
+
ctx.beginPath();
|
|
1302
|
+
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, Math.PI * 2);
|
|
1303
|
+
applyFill(ctx, el);
|
|
1304
|
+
applyStroke(ctx, el);
|
|
1305
|
+
}
|
|
1306
|
+
function renderLine(ctx, el) {
|
|
1307
|
+
const x1 = parseFloat(el.getAttribute("startX") ?? "0");
|
|
1308
|
+
const y1 = parseFloat(el.getAttribute("startY") ?? "0");
|
|
1309
|
+
const x2 = parseFloat(el.getAttribute("endX") ?? "0");
|
|
1310
|
+
const y2 = parseFloat(el.getAttribute("endY") ?? "0");
|
|
1311
|
+
ctx.beginPath();
|
|
1312
|
+
ctx.moveTo(x1, y1);
|
|
1313
|
+
ctx.lineTo(x2, y2);
|
|
1314
|
+
applyStroke(ctx, el);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/index.ts
|
|
1318
|
+
function parseUserConfigurations(doc, userConfig) {
|
|
1319
|
+
const result = {};
|
|
1320
|
+
const userConfEl = doc.querySelector("UserConfigurations");
|
|
1321
|
+
if (!userConfEl) return result;
|
|
1322
|
+
for (const child of userConfEl.children) {
|
|
1323
|
+
const id = child.getAttribute("id");
|
|
1324
|
+
if (!id) continue;
|
|
1325
|
+
if (child.tagName === "ColorConfiguration") {
|
|
1326
|
+
const defaultValue = child.getAttribute("defaultValue") ?? "0";
|
|
1327
|
+
const selectedId = userConfig?.[id] !== void 0 ? String(userConfig[id]) : defaultValue;
|
|
1328
|
+
for (const opt of child.querySelectorAll("ColorOption")) {
|
|
1329
|
+
if (opt.getAttribute("id") === selectedId) {
|
|
1330
|
+
const colors = (opt.getAttribute("colors") ?? "").split(/\s+/).filter(Boolean);
|
|
1331
|
+
colors.forEach((color, i) => {
|
|
1332
|
+
result[`${id}.${i}`] = color;
|
|
1333
|
+
});
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
} else if (child.tagName === "ListConfiguration") {
|
|
1338
|
+
const defaultValue = child.getAttribute("defaultValue") ?? "0";
|
|
1339
|
+
result[id] = userConfig?.[id] !== void 0 ? String(userConfig[id]) : defaultValue;
|
|
1340
|
+
} else if (child.tagName === "BooleanConfiguration") {
|
|
1341
|
+
const defaultValue = child.getAttribute("defaultValue") ?? "TRUE";
|
|
1342
|
+
const val = userConfig?.[id] !== void 0 ? String(userConfig[id]) : defaultValue;
|
|
1343
|
+
result[id] = val === "TRUE" ? 1 : 0;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
async function renderFrame(canvas, ctx, doc, options, elapsedMs, time) {
|
|
1349
|
+
const root = doc.documentElement;
|
|
1350
|
+
const width = parseInt(root.getAttribute("width") ?? "450", 10);
|
|
1351
|
+
const height = parseInt(root.getAttribute("height") ?? "450", 10);
|
|
1352
|
+
const clipShape = root.getAttribute("clipShape");
|
|
1353
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
1354
|
+
const metaElements = root.querySelectorAll("Metadata");
|
|
1355
|
+
for (const el of metaElements) {
|
|
1356
|
+
const key = el.getAttribute("key");
|
|
1357
|
+
const value = el.getAttribute("value");
|
|
1358
|
+
if (key !== null && value !== null) {
|
|
1359
|
+
metadata.set(key, value);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
ctx.resetTransform?.();
|
|
1363
|
+
ctx.clearRect(0, 0, width, height);
|
|
1364
|
+
ctx.save();
|
|
1365
|
+
ctx.globalAlpha = 1;
|
|
1366
|
+
ctx.globalCompositeOperation = "source-over";
|
|
1367
|
+
if (clipShape === "CIRCLE") {
|
|
1368
|
+
ctx.beginPath();
|
|
1369
|
+
ctx.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, Math.PI * 2);
|
|
1370
|
+
ctx.clip();
|
|
1371
|
+
}
|
|
1372
|
+
const scene = root.querySelector("Scene");
|
|
1373
|
+
const backgroundColor = scene?.getAttribute("backgroundColor") ?? "#000000";
|
|
1374
|
+
ctx.fillStyle = backgroundColor;
|
|
1375
|
+
ctx.fillRect(0, 0, width, height);
|
|
1376
|
+
const parsedConfig = parseUserConfigurations(doc, options.configuration);
|
|
1377
|
+
const mergedConfig = { ...parsedConfig, ...options.configuration };
|
|
1378
|
+
const expressionCtx = buildDataSources(time, mergedConfig, true);
|
|
1379
|
+
const renderCtx = {
|
|
1380
|
+
expressionCtx,
|
|
1381
|
+
ambient: options.ambient ?? false,
|
|
1382
|
+
assets: options.assets ?? /* @__PURE__ */ new Map(),
|
|
1383
|
+
elapsedMs
|
|
1384
|
+
};
|
|
1385
|
+
if (scene) {
|
|
1386
|
+
for (const child of scene.children) {
|
|
1387
|
+
await renderElement(ctx, child, renderCtx);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
ctx.restore();
|
|
1391
|
+
return metadata;
|
|
1392
|
+
}
|
|
1393
|
+
async function renderWatchFace(canvas, options) {
|
|
1394
|
+
const doc = new DOMParser().parseFromString(options.xml, "text/xml");
|
|
1395
|
+
const root = doc.documentElement;
|
|
1396
|
+
if (root.tagName === "parsererror" || root.querySelector("parsererror")) {
|
|
1397
|
+
throw new Error("Invalid WFF XML: " + root.textContent?.slice(0, 200));
|
|
1398
|
+
}
|
|
1399
|
+
const xmlWidth = parseInt(root.getAttribute("width") ?? "450", 10);
|
|
1400
|
+
const xmlHeight = parseInt(root.getAttribute("height") ?? "450", 10);
|
|
1401
|
+
const width = options.width ?? xmlWidth;
|
|
1402
|
+
const height = options.height ?? xmlHeight;
|
|
1403
|
+
canvas.width = width;
|
|
1404
|
+
canvas.height = height;
|
|
1405
|
+
const ctx = canvas.getContext("2d");
|
|
1406
|
+
if (!ctx) {
|
|
1407
|
+
const metadata2 = /* @__PURE__ */ new Map();
|
|
1408
|
+
return { metadata: metadata2 };
|
|
1409
|
+
}
|
|
1410
|
+
if (options.animate) {
|
|
1411
|
+
const startTime = performance.now();
|
|
1412
|
+
let animFrameId;
|
|
1413
|
+
let metadata2 = /* @__PURE__ */ new Map();
|
|
1414
|
+
const frame = async () => {
|
|
1415
|
+
const now = /* @__PURE__ */ new Date();
|
|
1416
|
+
const elapsed = performance.now() - startTime;
|
|
1417
|
+
const freshDoc2 = new DOMParser().parseFromString(options.xml, "text/xml");
|
|
1418
|
+
if (freshDoc2.documentElement.tagName === "parsererror") return;
|
|
1419
|
+
metadata2 = await renderFrame(canvas, ctx, freshDoc2, options, elapsed, now);
|
|
1420
|
+
animFrameId = requestAnimationFrame(() => {
|
|
1421
|
+
void frame();
|
|
1422
|
+
});
|
|
1423
|
+
};
|
|
1424
|
+
animFrameId = requestAnimationFrame(() => {
|
|
1425
|
+
void frame();
|
|
1426
|
+
});
|
|
1427
|
+
const freshDoc = new DOMParser().parseFromString(options.xml, "text/xml");
|
|
1428
|
+
const initialMetadata = await renderFrame(
|
|
1429
|
+
canvas,
|
|
1430
|
+
ctx,
|
|
1431
|
+
freshDoc,
|
|
1432
|
+
options,
|
|
1433
|
+
0,
|
|
1434
|
+
options.time ?? /* @__PURE__ */ new Date()
|
|
1435
|
+
);
|
|
1436
|
+
return {
|
|
1437
|
+
metadata: initialMetadata,
|
|
1438
|
+
stop: () => cancelAnimationFrame(animFrameId)
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const metadata = await renderFrame(canvas, ctx, doc, options, 0, options.time ?? /* @__PURE__ */ new Date());
|
|
1442
|
+
return { metadata };
|
|
1443
|
+
}
|
|
1444
|
+
export {
|
|
1445
|
+
renderWatchFace
|
|
1446
|
+
};
|