@idle-screens/schema 3.12.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +44 -17
- package/dist/index.d.ts +2 -1
- package/dist/index.js +498 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -279,7 +279,13 @@ function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
|
279
279
|
const walk = (a, b, key, root = false) => {
|
|
280
280
|
if (typeof a === "number" && typeof b === "number") {
|
|
281
281
|
const v = a + (b - a) * kk;
|
|
282
|
-
|
|
282
|
+
if (key === "count") return Math.max(1, Math.round(v));
|
|
283
|
+
if (key === "octaves") return Math.round(v);
|
|
284
|
+
if (key === "quantize") {
|
|
285
|
+
const q = Math.round(v);
|
|
286
|
+
return q < 2 ? 0 : q;
|
|
287
|
+
}
|
|
288
|
+
return v;
|
|
283
289
|
}
|
|
284
290
|
if (typeof a === "string" && typeof b === "string" && HEX.test(a) && HEX.test(b)) {
|
|
285
291
|
return lerpHex(a, b, kk);
|
|
@@ -2019,12 +2025,75 @@ function strokeTaper(u) {
|
|
|
2019
2025
|
function strokeWidthPx(s, scale) {
|
|
2020
2026
|
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2021
2027
|
}
|
|
2022
|
-
function
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
return
|
|
2028
|
+
function featherRamp(u, feather) {
|
|
2029
|
+
const band = Math.max(0, Math.min(1, feather)) / 2;
|
|
2030
|
+
const d = Math.min(u, 1 - u);
|
|
2031
|
+
if (band <= 0) return d >= 0 ? 1 : 0;
|
|
2032
|
+
return Math.max(0, Math.min(1, d / band));
|
|
2033
|
+
}
|
|
2034
|
+
var FEATHER_MASK_SIZE = 128;
|
|
2035
|
+
function ribbonSides(pts, half) {
|
|
2036
|
+
const n = pts.length;
|
|
2037
|
+
const left = [];
|
|
2038
|
+
const right = [];
|
|
2039
|
+
if (n < 2) return { left, right };
|
|
2040
|
+
const dir = (a, b) => {
|
|
2041
|
+
const dx = b.x - a.x;
|
|
2042
|
+
const dy = b.y - a.y;
|
|
2043
|
+
const l = Math.hypot(dx, dy);
|
|
2044
|
+
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
2045
|
+
};
|
|
2046
|
+
for (let i = 0; i < n; i++) {
|
|
2047
|
+
const tIn = i > 0 ? dir(pts[i - 1], pts[i]) : dir(pts[0], pts[1]);
|
|
2048
|
+
const tOut = i < n - 1 ? dir(pts[i], pts[i + 1]) : tIn;
|
|
2049
|
+
let tx = tIn.x + tOut.x;
|
|
2050
|
+
let ty = tIn.y + tOut.y;
|
|
2051
|
+
const tl = Math.hypot(tx, ty);
|
|
2052
|
+
if (tl < 1e-9) {
|
|
2053
|
+
tx = tOut.x;
|
|
2054
|
+
ty = tOut.y;
|
|
2055
|
+
} else {
|
|
2056
|
+
tx /= tl;
|
|
2057
|
+
ty /= tl;
|
|
2058
|
+
}
|
|
2059
|
+
const hw = half[i] / Math.max(0.5, tx * tOut.x + ty * tOut.y);
|
|
2060
|
+
const p = pts[i];
|
|
2061
|
+
left.push({ x: p.x - ty * hw, y: p.y + tx * hw });
|
|
2062
|
+
right.push({ x: p.x + ty * hw, y: p.y - tx * hw });
|
|
2063
|
+
}
|
|
2064
|
+
return { left, right };
|
|
2065
|
+
}
|
|
2066
|
+
function strokeOutline(pts, lw, taper = true) {
|
|
2067
|
+
const n = pts.length;
|
|
2068
|
+
if (n < 2) return [];
|
|
2069
|
+
const half = (i) => Math.max(0.5, taper ? lw * strokeTaper(i / (n - 1)) : lw) / 2;
|
|
2070
|
+
const dir = (a, b) => {
|
|
2071
|
+
const dx = b.x - a.x;
|
|
2072
|
+
const dy = b.y - a.y;
|
|
2073
|
+
const l = Math.hypot(dx, dy);
|
|
2074
|
+
return l > 1e-9 ? { x: dx / l, y: dy / l } : { x: 1, y: 0 };
|
|
2075
|
+
};
|
|
2076
|
+
const { left, right } = ribbonSides(pts, Array.from({ length: n }, (_, i) => half(i)));
|
|
2077
|
+
const cap = (c, r, from) => {
|
|
2078
|
+
const out = [];
|
|
2079
|
+
const steps = 6;
|
|
2080
|
+
for (let k = 1; k < steps; k++) {
|
|
2081
|
+
const a = from - Math.PI * k / steps;
|
|
2082
|
+
out.push({ x: c.x + Math.cos(a) * r, y: c.y + Math.sin(a) * r });
|
|
2083
|
+
}
|
|
2084
|
+
return out;
|
|
2085
|
+
};
|
|
2086
|
+
const endT = dir(pts[n - 2], pts[n - 1]);
|
|
2087
|
+
const startT = dir(pts[0], pts[1]);
|
|
2088
|
+
const endA = Math.atan2(endT.y, endT.x) + Math.PI / 2;
|
|
2089
|
+
const startA = Math.atan2(startT.y, startT.x) - Math.PI / 2;
|
|
2090
|
+
return [
|
|
2091
|
+
...left,
|
|
2092
|
+
...cap(pts[n - 1], half(n - 1), endA),
|
|
2093
|
+
...right.reverse(),
|
|
2094
|
+
...cap(pts[0], half(0), startA)
|
|
2095
|
+
];
|
|
2026
2096
|
}
|
|
2027
|
-
var FEATHER_STEPS = 6;
|
|
2028
2097
|
|
|
2029
2098
|
// src/simulate.ts
|
|
2030
2099
|
function entityScales(spec, w, h) {
|
|
@@ -3088,6 +3157,79 @@ function screen(fp, pat, alpha, off, w, h) {
|
|
|
3088
3157
|
p.restore();
|
|
3089
3158
|
}
|
|
3090
3159
|
|
|
3160
|
+
// src/trail.ts
|
|
3161
|
+
function trailFillBudget(layerCount) {
|
|
3162
|
+
return Math.max(4, Math.min(48, Math.floor(240 / Math.max(1, layerCount))));
|
|
3163
|
+
}
|
|
3164
|
+
function trailEchoes(trail, t, headAlpha, headSize, posAt, w, h, wrap2, fillBudget, minAlpha = 0, additive = false) {
|
|
3165
|
+
const fade = trail.fade ?? 1;
|
|
3166
|
+
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
3167
|
+
const alphaAtK = (k) => headAlpha * (1 - k * fade);
|
|
3168
|
+
const radiusAtK = (k) => headSize / 2 * (1 - k * 0.7);
|
|
3169
|
+
const jumps = (a, b) => wrap2 && (Math.abs(b.x - a.x) > w / 2 || Math.abs(b.y - a.y) > h / 2);
|
|
3170
|
+
const head = posAt(t);
|
|
3171
|
+
const base = [];
|
|
3172
|
+
let prev = head;
|
|
3173
|
+
for (let s = 1; s <= n; s++) {
|
|
3174
|
+
const k = s / n;
|
|
3175
|
+
const pastT = t - k * trail.length;
|
|
3176
|
+
if (pastT < 0) break;
|
|
3177
|
+
const pos = posAt(pastT);
|
|
3178
|
+
if (jumps(prev, pos)) break;
|
|
3179
|
+
prev = pos;
|
|
3180
|
+
const a = alphaAtK(k);
|
|
3181
|
+
if (a <= minAlpha) break;
|
|
3182
|
+
const r = radiusAtK(k);
|
|
3183
|
+
if (r < 0.2) break;
|
|
3184
|
+
base.push({ t: pastT, x: pos.x, y: pos.y, k, alpha: a, r });
|
|
3185
|
+
}
|
|
3186
|
+
if (base.length === 0) return base;
|
|
3187
|
+
const need = [];
|
|
3188
|
+
let total = 0;
|
|
3189
|
+
for (let i = 0; i < base.length; i++) {
|
|
3190
|
+
const a = i === 0 ? head : base[i - 1];
|
|
3191
|
+
const b = base[i];
|
|
3192
|
+
const d = Math.hypot(b.x - a.x, b.y - a.y);
|
|
3193
|
+
const m = d > b.r ? Math.ceil(d / b.r) - 1 : 0;
|
|
3194
|
+
need.push(m);
|
|
3195
|
+
total += m;
|
|
3196
|
+
}
|
|
3197
|
+
if (total === 0) return base;
|
|
3198
|
+
const scale = total > fillBudget ? fillBudget / total : 1;
|
|
3199
|
+
const out = [];
|
|
3200
|
+
let last = head;
|
|
3201
|
+
for (let i = 0; i < base.length; i++) {
|
|
3202
|
+
const b = base[i];
|
|
3203
|
+
const m = Math.floor(need[i] * scale);
|
|
3204
|
+
if (m === 0) {
|
|
3205
|
+
out.push(b);
|
|
3206
|
+
last = b;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
const a = i === 0 ? head : base[i - 1];
|
|
3210
|
+
const spacing = Math.hypot(b.x - a.x, b.y - a.y) / (m + 1);
|
|
3211
|
+
const share = (alpha, r) => {
|
|
3212
|
+
const k = Math.min(1, spacing / (2 * r));
|
|
3213
|
+
return additive ? alpha * k : 1 - Math.pow(1 - Math.min(1, alpha), k);
|
|
3214
|
+
};
|
|
3215
|
+
const t0 = i === 0 ? t : base[i - 1].t;
|
|
3216
|
+
const k0 = i === 0 ? 0 : base[i - 1].k;
|
|
3217
|
+
for (let j = 1; j <= m; j++) {
|
|
3218
|
+
const f = j / (m + 1);
|
|
3219
|
+
const tt = t0 + (b.t - t0) * f;
|
|
3220
|
+
const k = k0 + (b.k - k0) * f;
|
|
3221
|
+
const pos = posAt(tt);
|
|
3222
|
+
if (jumps(last, pos)) continue;
|
|
3223
|
+
last = pos;
|
|
3224
|
+
const r = radiusAtK(k);
|
|
3225
|
+
out.push({ t: tt, x: pos.x, y: pos.y, k, alpha: share(alphaAtK(k), r), r });
|
|
3226
|
+
}
|
|
3227
|
+
out.push({ ...b, alpha: share(b.alpha, b.r) });
|
|
3228
|
+
last = b;
|
|
3229
|
+
}
|
|
3230
|
+
return out;
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3091
3233
|
// src/compile.ts
|
|
3092
3234
|
var DEFAULT_STEER_DUR = 1e3;
|
|
3093
3235
|
function applyRetainedDeltas(spec, deltas) {
|
|
@@ -3285,6 +3427,15 @@ var SpecInstance = class {
|
|
|
3285
3427
|
fieldShortSide;
|
|
3286
3428
|
/** The `field` background's cached raster; null until a field is first painted. */
|
|
3287
3429
|
field = null;
|
|
3430
|
+
featherWhite = /* @__PURE__ */ new Map();
|
|
3431
|
+
/** Per-entity pre-rendered feathered rects (see `blitFeather`), and the device pixels they hold. */
|
|
3432
|
+
featherBlits = /* @__PURE__ */ new WeakMap();
|
|
3433
|
+
featherBlitPx = 0;
|
|
3434
|
+
featherBlitN = 0;
|
|
3435
|
+
paintSeq = 0;
|
|
3436
|
+
/** Per layer: ribbon area painted this frame (px²), and whether its trails ribbon (see ribbonAllowed). */
|
|
3437
|
+
ribbonBudget = /* @__PURE__ */ new WeakMap();
|
|
3438
|
+
featherTints = /* @__PURE__ */ new Map();
|
|
3288
3439
|
/**
|
|
3289
3440
|
* The presentation pass when this instance applies a `finish`: the
|
|
3290
3441
|
* visible canvas plus the cached screen tiles. When set, `this.canvas` is
|
|
@@ -3449,36 +3600,81 @@ var SpecInstance = class {
|
|
|
3449
3600
|
const trail = built.layer.trail;
|
|
3450
3601
|
if (!trail) return;
|
|
3451
3602
|
const { ctx, w, h } = this;
|
|
3452
|
-
const fade = trail.fade ?? 1;
|
|
3453
|
-
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
3454
3603
|
const headAlpha = alphaAt(e, t) * lifeA;
|
|
3455
3604
|
const headSize = sizeAt(e, t);
|
|
3456
3605
|
const sprite = built.layer.sprite;
|
|
3606
|
+
const shaped = isShapedSprite(sprite) && sprite.kind !== "circle";
|
|
3607
|
+
const isSoft = sprite.kind === "circle" && !!sprite.soft;
|
|
3608
|
+
const hardCircle = sprite.kind === "circle" && !sprite.soft;
|
|
3609
|
+
const ribbon = hardCircle && this.ribbonAllowed(built);
|
|
3610
|
+
const posAt = (pt) => this.entityPos(e, pt, parentE);
|
|
3611
|
+
const fill = shaped || hardCircle ? 0 : trailFillBudget(built.entities.length);
|
|
3612
|
+
const echoes = trailEchoes(trail, t, headAlpha, headSize, posAt, w, h, built.layer.wrap !== false, fill, 0, built.layer.blend === "lighter");
|
|
3613
|
+
if (echoes.length === 0) return;
|
|
3614
|
+
const head = posAt(t);
|
|
3615
|
+
if (hardCircle) {
|
|
3616
|
+
const rb = this.ribbonBudget.get(built);
|
|
3617
|
+
let prev = head;
|
|
3618
|
+
let r0 = headSize / 2;
|
|
3619
|
+
for (const ec of echoes) {
|
|
3620
|
+
rb.area += Math.hypot(ec.x - prev.x, ec.y - prev.y) * (r0 + ec.r);
|
|
3621
|
+
prev = ec;
|
|
3622
|
+
r0 = ec.r;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
if (shaped) {
|
|
3626
|
+
for (const ec of echoes) this.drawEntity(built, e, ec.t, 1, parentE, void 0, { x: ec.x, y: ec.y, size: ec.r * 2, alpha: ec.alpha });
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3457
3629
|
const resolvedColor = isShapedSprite(sprite) ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" || sprite.kind === "textBlock" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3630
|
+
if (ribbon) {
|
|
3631
|
+
const pts = [head];
|
|
3632
|
+
const radii = [headSize / 2];
|
|
3633
|
+
const alphas = [headAlpha];
|
|
3634
|
+
let prevT = t;
|
|
3635
|
+
for (const ec of echoes) {
|
|
3636
|
+
const p0 = pts[pts.length - 1];
|
|
3637
|
+
const r0 = radii[radii.length - 1];
|
|
3638
|
+
const a0 = alphas[alphas.length - 1];
|
|
3639
|
+
const sub = Math.max(1, Math.min(8, Math.ceil(Math.hypot(ec.x - p0.x, ec.y - p0.y) / 12)));
|
|
3640
|
+
for (let j = 1; j < sub; j++) {
|
|
3641
|
+
const f = j / sub;
|
|
3642
|
+
pts.push(posAt(prevT + (ec.t - prevT) * f));
|
|
3643
|
+
radii.push(r0 + (ec.r - r0) * f);
|
|
3644
|
+
alphas.push(a0 + (ec.alpha - a0) * f);
|
|
3645
|
+
}
|
|
3646
|
+
pts.push(ec);
|
|
3647
|
+
radii.push(ec.r);
|
|
3648
|
+
alphas.push(ec.alpha);
|
|
3649
|
+
prevT = ec.t;
|
|
3650
|
+
}
|
|
3651
|
+
const { left, right } = ribbonSides(pts, radii);
|
|
3652
|
+
ctx.fillStyle = resolvedColor;
|
|
3653
|
+
const tol = Math.max(4e-3, headAlpha * 0.04);
|
|
3654
|
+
let from = 0;
|
|
3655
|
+
while (from < pts.length - 1) {
|
|
3656
|
+
const a0 = alphas[from + 1];
|
|
3657
|
+
let to = from + 1;
|
|
3658
|
+
let sum = a0;
|
|
3659
|
+
while (to < pts.length - 1 && Math.abs(alphas[to + 1] - a0) <= tol) {
|
|
3660
|
+
sum += alphas[to + 1];
|
|
3661
|
+
to++;
|
|
3662
|
+
}
|
|
3663
|
+
ctx.globalAlpha = sum / (to - from);
|
|
3664
|
+
ctx.beginPath();
|
|
3665
|
+
ctx.moveTo(left[from].x, left[from].y);
|
|
3666
|
+
for (let i = from + 1; i <= to; i++) ctx.lineTo(left[i].x, left[i].y);
|
|
3667
|
+
for (let i = to; i >= from; i--) ctx.lineTo(right[i].x, right[i].y);
|
|
3668
|
+
ctx.closePath();
|
|
3669
|
+
ctx.fill();
|
|
3670
|
+
from = to;
|
|
3671
|
+
}
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
for (const ec of echoes) {
|
|
3675
|
+
ctx.globalAlpha = ec.alpha;
|
|
3480
3676
|
if (isSoft) {
|
|
3481
|
-
const g = ctx.createRadialGradient(
|
|
3677
|
+
const g = ctx.createRadialGradient(ec.x, ec.y, 0, ec.x, ec.y, ec.r);
|
|
3482
3678
|
g.addColorStop(0, resolvedColor);
|
|
3483
3679
|
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
3484
3680
|
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
@@ -3487,18 +3683,202 @@ var SpecInstance = class {
|
|
|
3487
3683
|
ctx.fillStyle = resolvedColor;
|
|
3488
3684
|
}
|
|
3489
3685
|
ctx.beginPath();
|
|
3490
|
-
ctx.arc(
|
|
3686
|
+
ctx.arc(ec.x, ec.y, ec.r, 0, Math.PI * 2);
|
|
3491
3687
|
ctx.fill();
|
|
3492
3688
|
}
|
|
3493
3689
|
}
|
|
3494
|
-
|
|
3690
|
+
/**
|
|
3691
|
+
* Whether this layer's hard-circle trails draw as ribbons this frame. A
|
|
3692
|
+
* ribbon paints its trail's whole length, so a layer of many fast, long
|
|
3693
|
+
* trails can paint far more pixels than its dots did — under `lighter` on
|
|
3694
|
+
* a CPU rasteriser that was 2.4× the frame time (60 trails, 3 s, 0.4/s).
|
|
3695
|
+
* Past a tenth of the viewport's area in ribbon (measured on the
|
|
3696
|
+
* previous frame) the layer falls back to the time-sample discs it always
|
|
3697
|
+
* drew; it returns to ribbons below 70 % of that, so a layer near the line
|
|
3698
|
+
* does not flicker between the two.
|
|
3699
|
+
*/
|
|
3700
|
+
ribbonAllowed(built) {
|
|
3701
|
+
let rb = this.ribbonBudget.get(built);
|
|
3702
|
+
if (!rb) {
|
|
3703
|
+
rb = { seq: this.paintSeq, area: 0, ribbon: true };
|
|
3704
|
+
this.ribbonBudget.set(built, rb);
|
|
3705
|
+
}
|
|
3706
|
+
if (rb.seq !== this.paintSeq) {
|
|
3707
|
+
const budget = 0.1 * this.w * this.h;
|
|
3708
|
+
if (rb.ribbon && rb.area > budget) rb.ribbon = false;
|
|
3709
|
+
else if (!rb.ribbon && rb.area < 0.7 * budget) rb.ribbon = true;
|
|
3710
|
+
rb.seq = this.paintSeq;
|
|
3711
|
+
rb.area = 0;
|
|
3712
|
+
}
|
|
3713
|
+
return rb.ribbon;
|
|
3714
|
+
}
|
|
3715
|
+
/** A fresh 2D canvas (DOM or Offscreen), or null where none can be made. */
|
|
3716
|
+
makeCanvas(n) {
|
|
3717
|
+
try {
|
|
3718
|
+
const canvas = typeof document !== "undefined" ? document.createElement("canvas") : new OffscreenCanvas(n, n);
|
|
3719
|
+
canvas.width = n;
|
|
3720
|
+
canvas.height = n;
|
|
3721
|
+
const c = canvas.getContext("2d");
|
|
3722
|
+
return c ? { canvas, ctx: c } : null;
|
|
3723
|
+
} catch {
|
|
3724
|
+
return null;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* A feathered rect from a per-entity bitmap of it, pre-rendered at device
|
|
3729
|
+
* resolution with its rotation baked in, copied 1:1 at a whole-pixel
|
|
3730
|
+
* offset. A smooth falloff is a sampled image draw — on a CPU rasteriser
|
|
3731
|
+
* (a Raspberry Pi wall) ~3× the six flat fills it replaced, ~10× when
|
|
3732
|
+
* rotated and stretched — while a 1:1 integer copy costs about one flat
|
|
3733
|
+
* fill. The bitmap is rebuilt only when the rect's size, rotation or
|
|
3734
|
+
* feather changes (in device pixels, to a quarter pixel / 0.06°), and
|
|
3735
|
+
* re-tinted (one copy + one flat fill) only when its colour changes, so a
|
|
3736
|
+
* drifting block or a colour glide stays cheap. A rect whose shape
|
|
3737
|
+
* changes every frame (`grow`, `spin`), a canvas under a rotating or
|
|
3738
|
+
* mirroring transform, or a scene past the 8 Mpx cache budget draws the
|
|
3739
|
+
* stretched mask directly instead. Returns false when it did not draw.
|
|
3740
|
+
*/
|
|
3741
|
+
blitFeather(e, p, sz, rh, rot, color2, feather) {
|
|
3742
|
+
const ctx = this.ctx;
|
|
3743
|
+
const m = typeof ctx.getTransform === "function" ? ctx.getTransform() : null;
|
|
3744
|
+
if (!m || m.b !== 0 || m.c !== 0 || m.a <= 0 || m.d <= 0) return false;
|
|
3745
|
+
const cos = Math.abs(Math.cos(rot));
|
|
3746
|
+
const sin = Math.abs(Math.sin(rot));
|
|
3747
|
+
const bw = Math.ceil((cos * sz + sin * rh) * m.a) + 2;
|
|
3748
|
+
const bh = Math.ceil((sin * sz + cos * rh) * m.d) + 2;
|
|
3749
|
+
const shapeKey = `${feather}|${Math.round(sz * m.a * 4)}|${Math.round(rh * m.d * 4)}|${Math.round(rot * 1e3)}|${bw}x${bh}`;
|
|
3750
|
+
let b = this.featherBlits.get(e);
|
|
3751
|
+
if (!b || b.shapeKey !== shapeKey) {
|
|
3752
|
+
if (b) {
|
|
3753
|
+
if (b.seenKey !== shapeKey) {
|
|
3754
|
+
b.seenKey = shapeKey;
|
|
3755
|
+
b.seenAt = this.paintSeq;
|
|
3756
|
+
}
|
|
3757
|
+
if (b.seenAt === this.paintSeq) return false;
|
|
3758
|
+
}
|
|
3759
|
+
const px = bw * bh;
|
|
3760
|
+
const reuse = b && b.w === bw && b.h === bh;
|
|
3761
|
+
if (!reuse && this.featherBlitPx + px - (b ? b.w * b.h : 0) > 8e6) return false;
|
|
3762
|
+
const white = this.featherWhite.get(feather);
|
|
3763
|
+
if (!white) return false;
|
|
3764
|
+
const wc = reuse ? b.white : this.makeCanvas(1);
|
|
3765
|
+
const tc = reuse ? b.tint : this.makeCanvas(1);
|
|
3766
|
+
if (!wc || !tc) return false;
|
|
3767
|
+
if (!reuse) {
|
|
3768
|
+
wc.canvas.width = bw;
|
|
3769
|
+
wc.canvas.height = bh;
|
|
3770
|
+
tc.canvas.width = bw;
|
|
3771
|
+
tc.canvas.height = bh;
|
|
3772
|
+
this.featherBlitPx += px - (b ? b.w * b.h : 0);
|
|
3773
|
+
}
|
|
3774
|
+
const w2 = wc.ctx;
|
|
3775
|
+
w2.setTransform(1, 0, 0, 1, 0, 0);
|
|
3776
|
+
w2.clearRect(0, 0, bw, bh);
|
|
3777
|
+
w2.translate(bw / 2, bh / 2);
|
|
3778
|
+
w2.scale(m.a, m.d);
|
|
3779
|
+
if (rot) w2.rotate(rot);
|
|
3780
|
+
w2.imageSmoothingEnabled = true;
|
|
3781
|
+
w2.drawImage(white, -sz / 2, -rh / 2, sz, rh);
|
|
3782
|
+
w2.setTransform(1, 0, 0, 1, 0, 0);
|
|
3783
|
+
b = { shapeKey, w: bw, h: bh, white: wc, tint: tc, tintKey: "", tintAt: 0, stagger: this.featherBlitN++ % 8, seenKey: shapeKey, seenAt: this.paintSeq };
|
|
3784
|
+
this.featherBlits.set(e, b);
|
|
3785
|
+
}
|
|
3786
|
+
if (b.tintKey !== color2 && (b.tintKey === "" || this.paintSeq - b.tintAt >= 8)) {
|
|
3787
|
+
const t2 = b.tint.ctx;
|
|
3788
|
+
t2.setTransform(1, 0, 0, 1, 0, 0);
|
|
3789
|
+
t2.globalCompositeOperation = "copy";
|
|
3790
|
+
t2.drawImage(b.white.canvas, 0, 0);
|
|
3791
|
+
t2.globalCompositeOperation = "source-in";
|
|
3792
|
+
t2.fillStyle = color2;
|
|
3793
|
+
t2.fillRect(0, 0, b.w, b.h);
|
|
3794
|
+
t2.globalCompositeOperation = "source-over";
|
|
3795
|
+
b.tintKey = color2;
|
|
3796
|
+
b.tintAt = this.paintSeq - (b.stagger ?? 0);
|
|
3797
|
+
b.stagger = 0;
|
|
3798
|
+
}
|
|
3799
|
+
const cx = m.a * p.x + m.e;
|
|
3800
|
+
const cy = m.d * p.y + m.f;
|
|
3801
|
+
ctx.save();
|
|
3802
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3803
|
+
ctx.drawImage(b.tint.canvas, Math.round(cx - b.w / 2), Math.round(cy - b.h / 2));
|
|
3804
|
+
ctx.restore();
|
|
3805
|
+
return true;
|
|
3806
|
+
}
|
|
3807
|
+
/**
|
|
3808
|
+
* A feathered rect's mask in `color`: FEATHER_MASK_SIZE² texels of
|
|
3809
|
+
* `featherRamp(u) × featherRamp(v)`, drawn stretched to the rect with
|
|
3810
|
+
* smoothing on, so the falloff is continuous at any size (X383).
|
|
3811
|
+
*
|
|
3812
|
+
* The ramp itself is built ONCE per feather (a white alpha mask, the only
|
|
3813
|
+
* per-pixel work). A colour is a cheap tint of it — clear, draw the mask,
|
|
3814
|
+
* `source-in` fill — into a pooled canvas, cached per colour × feather.
|
|
3815
|
+
* A colour glide or a palette larger than the pool re-tints a pooled
|
|
3816
|
+
* canvas (three GPU draws) rather than rebuilding pixels in JS.
|
|
3817
|
+
*/
|
|
3818
|
+
featherMask(color2, feather) {
|
|
3819
|
+
const key = `${color2}|${feather}`;
|
|
3820
|
+
const hit = this.featherTints.get(key);
|
|
3821
|
+
if (hit) {
|
|
3822
|
+
this.featherTints.delete(key);
|
|
3823
|
+
this.featherTints.set(key, hit);
|
|
3824
|
+
return hit.canvas;
|
|
3825
|
+
}
|
|
3826
|
+
const N = FEATHER_MASK_SIZE;
|
|
3827
|
+
let white = this.featherWhite.get(feather);
|
|
3828
|
+
if (!white) {
|
|
3829
|
+
const made = this.makeCanvas(N);
|
|
3830
|
+
if (!made) return null;
|
|
3831
|
+
const img = made.ctx.createImageData(N, N);
|
|
3832
|
+
const data = img.data;
|
|
3833
|
+
const ramp = [];
|
|
3834
|
+
for (let i = 0; i < N; i++) ramp.push(featherRamp((i + 0.5) / N, feather));
|
|
3835
|
+
let o = 0;
|
|
3836
|
+
for (let y = 0; y < N; y++) {
|
|
3837
|
+
const ry = ramp[y];
|
|
3838
|
+
for (let x = 0; x < N; x++) {
|
|
3839
|
+
data[o++] = 255;
|
|
3840
|
+
data[o++] = 255;
|
|
3841
|
+
data[o++] = 255;
|
|
3842
|
+
data[o++] = Math.round(255 * ry * ramp[x]);
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
made.ctx.putImageData(img, 0, 0);
|
|
3846
|
+
if (this.featherWhite.size >= 8) this.featherWhite.delete(this.featherWhite.keys().next().value);
|
|
3847
|
+
white = made.canvas;
|
|
3848
|
+
this.featherWhite.set(feather, white);
|
|
3849
|
+
}
|
|
3850
|
+
let slot = null;
|
|
3851
|
+
if (this.featherTints.size >= 48) {
|
|
3852
|
+
const oldest = this.featherTints.keys().next().value;
|
|
3853
|
+
slot = this.featherTints.get(oldest);
|
|
3854
|
+
this.featherTints.delete(oldest);
|
|
3855
|
+
} else {
|
|
3856
|
+
slot = this.makeCanvas(N);
|
|
3857
|
+
if (!slot) return null;
|
|
3858
|
+
}
|
|
3859
|
+
const tctx = slot.ctx;
|
|
3860
|
+
tctx.globalCompositeOperation = "copy";
|
|
3861
|
+
tctx.drawImage(white, 0, 0);
|
|
3862
|
+
tctx.globalCompositeOperation = "source-in";
|
|
3863
|
+
tctx.fillStyle = color2;
|
|
3864
|
+
tctx.fillRect(0, 0, N, N);
|
|
3865
|
+
tctx.globalCompositeOperation = "source-over";
|
|
3866
|
+
this.featherTints.set(key, slot);
|
|
3867
|
+
return slot.canvas;
|
|
3868
|
+
}
|
|
3869
|
+
/**
|
|
3870
|
+
* Paint one entity at time `t`. `echo` paints a trail echo instead: the
|
|
3871
|
+
* entity as it was at `t` (rotation, heading, values), but at the echo's
|
|
3872
|
+
* position, size and alpha.
|
|
3873
|
+
*/
|
|
3874
|
+
drawEntity(built, e, t, lifeA, parentE, paint, echo) {
|
|
3495
3875
|
const { ctx } = this;
|
|
3496
|
-
const p = this.entityPos(e, t, parentE);
|
|
3876
|
+
const p = echo ? { x: echo.x, y: echo.y, flip: false } : this.entityPos(e, t, parentE);
|
|
3497
3877
|
const sprite = paint?.sprite ?? built.layer.sprite;
|
|
3498
|
-
const sz = sizeAt(e, t);
|
|
3878
|
+
const sz = echo ? echo.size : sizeAt(e, t);
|
|
3499
3879
|
const rot = rotationAt(e, t);
|
|
3500
3880
|
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
3501
|
-
ctx.globalAlpha = alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
3881
|
+
ctx.globalAlpha = echo ? echo.alpha : alphaAt(e, t) * lifeA * (paint?.alpha ?? 1);
|
|
3502
3882
|
if (sprite.kind === "ring") {
|
|
3503
3883
|
const r = sz / 2;
|
|
3504
3884
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -3529,23 +3909,18 @@ var SpecInstance = class {
|
|
|
3529
3909
|
if (sprite.kind === "rect") {
|
|
3530
3910
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
3531
3911
|
const rh = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
3912
|
+
const feather = sprite.feather ?? 0;
|
|
3913
|
+
const mask = feather > 0 ? this.featherMask(resolvedColor, feather) : null;
|
|
3914
|
+
if (mask && this.blitFeather(e, p, sz, rh, rot, resolvedColor, feather)) return;
|
|
3532
3915
|
ctx.save();
|
|
3533
3916
|
ctx.translate(p.x, p.y);
|
|
3534
3917
|
if (rot) ctx.rotate(rot);
|
|
3535
3918
|
ctx.fillStyle = resolvedColor;
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
for (let j = 1; j <= FEATHER_STEPS; j++) {
|
|
3542
|
-
const f = 1 - feather * (j - 1) / FEATHER_STEPS;
|
|
3543
|
-
const fw = sz * f;
|
|
3544
|
-
const fh = rh * f;
|
|
3545
|
-
ctx.globalAlpha = base * alphas[j - 1];
|
|
3546
|
-
ctx.fillRect(-fw / 2, -fh / 2, fw, fh);
|
|
3547
|
-
}
|
|
3548
|
-
ctx.globalAlpha = base;
|
|
3919
|
+
if (mask) {
|
|
3920
|
+
const prevSmooth = ctx.imageSmoothingEnabled;
|
|
3921
|
+
ctx.imageSmoothingEnabled = true;
|
|
3922
|
+
ctx.drawImage(mask, -sz / 2, -rh / 2, sz, rh);
|
|
3923
|
+
ctx.imageSmoothingEnabled = prevSmooth;
|
|
3549
3924
|
} else {
|
|
3550
3925
|
ctx.fillRect(-sz / 2, -rh / 2, sz, rh);
|
|
3551
3926
|
}
|
|
@@ -3603,13 +3978,13 @@ var SpecInstance = class {
|
|
|
3603
3978
|
ctx.lineCap = "round";
|
|
3604
3979
|
ctx.lineJoin = "round";
|
|
3605
3980
|
if (sprite.taper) {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3981
|
+
const outline = strokeOutline(pts, lw);
|
|
3982
|
+
ctx.fillStyle = resolvedColor;
|
|
3983
|
+
ctx.beginPath();
|
|
3984
|
+
ctx.moveTo(outline[0].x, outline[0].y);
|
|
3985
|
+
for (let i = 1; i < outline.length; i++) ctx.lineTo(outline[i].x, outline[i].y);
|
|
3986
|
+
ctx.closePath();
|
|
3987
|
+
ctx.fill();
|
|
3613
3988
|
} else {
|
|
3614
3989
|
ctx.lineWidth = lw;
|
|
3615
3990
|
ctx.beginPath();
|
|
@@ -3863,6 +4238,7 @@ var SpecInstance = class {
|
|
|
3863
4238
|
* showing through — the ghosting smear. */
|
|
3864
4239
|
paintFrame(t, bgAlpha) {
|
|
3865
4240
|
const { ctx } = this;
|
|
4241
|
+
this.paintSeq++;
|
|
3866
4242
|
if (this.authored) this.resolveAuthored(t);
|
|
3867
4243
|
if (!this.transparent) {
|
|
3868
4244
|
ctx.globalAlpha = bgAlpha;
|
|
@@ -5300,31 +5676,36 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5300
5676
|
else if (blend === "multiply") cells[idx] = cur * (1 - a * (1 - lum));
|
|
5301
5677
|
else cells[idx] = cur * (1 - a) + lum * a;
|
|
5302
5678
|
};
|
|
5303
|
-
const splatTrail = (layer, e, tPass, lifeA, alphaScale) => {
|
|
5679
|
+
const splatTrail = (layer, e, tPass, lifeA, alphaScale, layerCount) => {
|
|
5304
5680
|
const trail = layer.trail;
|
|
5305
5681
|
if (!trail) return;
|
|
5306
|
-
const
|
|
5307
|
-
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
5308
|
-
const headAlpha = alphaAt(e, tPass) * lifeA;
|
|
5682
|
+
const headAlpha = alphaAt(e, tPass) * lifeA * alphaScale;
|
|
5309
5683
|
const headSize = sizeAt(e, tPass);
|
|
5310
5684
|
const lum = spriteLuma(layer, e);
|
|
5311
5685
|
const soft = layer.sprite.kind === "circle" && !!layer.sprite.soft;
|
|
5312
|
-
const
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5686
|
+
const echoes = trailEchoes(
|
|
5687
|
+
trail,
|
|
5688
|
+
tPass,
|
|
5689
|
+
headAlpha,
|
|
5690
|
+
headSize,
|
|
5691
|
+
(pt) => posOf(scene, e, pt),
|
|
5692
|
+
w,
|
|
5693
|
+
h,
|
|
5694
|
+
layer.wrap !== false,
|
|
5695
|
+
// As the renderer: a shape echoes at its time samples only; circles fill
|
|
5696
|
+
// (a hard circle's ribbon reads, on the coarse grid, as filled discs).
|
|
5697
|
+
isShapedSprite(layer.sprite) && layer.sprite.kind !== "circle" ? 0 : trailFillBudget(layerCount),
|
|
5698
|
+
4e-3,
|
|
5699
|
+
layer.blend === "lighter"
|
|
5700
|
+
);
|
|
5701
|
+
for (const ec of echoes) {
|
|
5702
|
+
const a = ec.alpha;
|
|
5703
|
+
const rad = ec.r;
|
|
5704
|
+
if (layer.sprite.kind === "polygon" && rad * 2 >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
5705
|
+
splatPolygonOutline(layer, layer.sprite, e, ec, rad * 2, Math.min(1, a), lum, ec.t);
|
|
5706
|
+
continue;
|
|
5707
|
+
}
|
|
5708
|
+
const pos = ec;
|
|
5328
5709
|
const c0 = Math.max(0, Math.floor((pos.x - rad) / cellW));
|
|
5329
5710
|
const c1 = Math.min(cols - 1, Math.floor((pos.x + rad) / cellW));
|
|
5330
5711
|
const r0 = Math.max(0, Math.floor((pos.y - rad) / cellH));
|
|
@@ -5403,7 +5784,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
5403
5784
|
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
|
|
5404
5785
|
if (lifeA <= 0) continue;
|
|
5405
5786
|
for (const e of entities) {
|
|
5406
|
-
splatTrail(layer, e, tPass, lifeA, alphaScale);
|
|
5787
|
+
splatTrail(layer, e, tPass, lifeA, alphaScale, entities.length);
|
|
5407
5788
|
const a = Math.min(1, alphaAt(e, tPass) * lifeA) * alphaScale;
|
|
5408
5789
|
if (a <= 4e-3) continue;
|
|
5409
5790
|
const lum = spriteLuma(layer, e);
|
|
@@ -6354,7 +6735,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6354
6735
|
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
6355
6736
|
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
6356
6737
|
const str = s.kind === "textBlock" ? s.text : s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
6357
|
-
textBoxes.push({ li, label, alpha, str, ...box });
|
|
6738
|
+
textBoxes.push({ li, label, alpha, str, ...box, rawW: raw.x1 - raw.x0, rawH: raw.y1 - raw.y0 });
|
|
6358
6739
|
}
|
|
6359
6740
|
}
|
|
6360
6741
|
const asFraction = (b) => ({
|
|
@@ -6409,6 +6790,45 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
6409
6790
|
});
|
|
6410
6791
|
}
|
|
6411
6792
|
}
|
|
6793
|
+
const cellFlagged = /* @__PURE__ */ new Set();
|
|
6794
|
+
for (let i = 0; i < textBoxes.length; i++) {
|
|
6795
|
+
const a = textBoxes[i];
|
|
6796
|
+
if (cellFlagged.has(a.li)) continue;
|
|
6797
|
+
const layer = spec.layers[a.li];
|
|
6798
|
+
const layout = layer.layout;
|
|
6799
|
+
if (layout?.type !== "list" && layout?.type !== "table") continue;
|
|
6800
|
+
for (let j = i + 1; j < textBoxes.length; j++) {
|
|
6801
|
+
const b = textBoxes[j];
|
|
6802
|
+
if (b.li !== a.li) continue;
|
|
6803
|
+
const ix = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
|
6804
|
+
const iy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
|
6805
|
+
if (ix <= 0 || iy <= 0) continue;
|
|
6806
|
+
const smaller = Math.min((a.x1 - a.x0) * (a.y1 - a.y0), (b.x1 - b.x0) * (b.y1 - b.y0));
|
|
6807
|
+
if (smaller <= 0 || ix * iy / smaller <= 0.1) continue;
|
|
6808
|
+
cellFlagged.add(a.li);
|
|
6809
|
+
const cells = textBoxes.filter((x) => x.li === a.li);
|
|
6810
|
+
const dflt = scale === 1 ? LIMITS.defaultListGapPx : LIMITS.defaultListGap;
|
|
6811
|
+
const g = layout.gap ?? dflt;
|
|
6812
|
+
const gx = typeof g === "number" ? g : g.x ?? dflt;
|
|
6813
|
+
const gy = typeof g === "number" ? g : g.y ?? dflt;
|
|
6814
|
+
const cols = layout.type === "list" ? 1 : Math.max(1, Math.round(layout.columns));
|
|
6815
|
+
const unitPx = (v) => scale === 1 ? v : v * scale;
|
|
6816
|
+
const fromPx = (px) => scale === 1 ? Math.ceil(px) : Math.ceil(px / scale * 100) / 100;
|
|
6817
|
+
const maxW = cells.reduce((m, c) => Math.max(m, c.rawW), 0);
|
|
6818
|
+
const maxH = cells.reduce((m, c) => Math.max(m, c.rawH), 0);
|
|
6819
|
+
const fixes = [];
|
|
6820
|
+
const across = cols > 1 && maxW > unitPx(gx);
|
|
6821
|
+
if (across) fixes.push(`its labels are up to ~${Math.round(maxW)}px wide against a ${Math.round(unitPx(gx))}px column pitch (gap${typeof g === "number" ? "" : ".x"} ${gx}) \u2014 raise gap${typeof g === "number" ? "" : ".x"} to \u2265 ${fromPx(maxW * 1.1)}`);
|
|
6822
|
+
if (!across || maxH > unitPx(gy)) fixes.push(`rows are ~${Math.round(maxH)}px tall against a ${Math.round(unitPx(gy))}px row pitch (gap${typeof g === "number" ? "" : ".y"} ${gy}) \u2014 raise gap${typeof g === "number" ? "" : ".y"} to \u2265 ${fromPx(maxH * 1.1)}`);
|
|
6823
|
+
warnings.push({
|
|
6824
|
+
path: `layers[${a.li}].layout.gap`,
|
|
6825
|
+
code: "layout-text-overlap",
|
|
6826
|
+
message: `${a.label} cells overlap at ${w}\xD7${h}: a ${layout.type}'s \`gap\` is the pitch between cell anchors, not the space between cells; ${fixes.join("; and ")}, or shrink the text`,
|
|
6827
|
+
boxes: [asFraction(a), asFraction(b)]
|
|
6828
|
+
});
|
|
6829
|
+
break;
|
|
6830
|
+
}
|
|
6831
|
+
}
|
|
6412
6832
|
const readFlagged = /* @__PURE__ */ new Set();
|
|
6413
6833
|
for (const b of textBoxes) {
|
|
6414
6834
|
if (readFlagged.has(b.li)) continue;
|