@pixerate/editor 0.96.0 → 0.97.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.
@@ -1080,11 +1080,18 @@ function boxLine(src, x0, dst, bw) {
1080
1080
  }
1081
1081
  }
1082
1082
  var BLUR_EPS = 1e-7;
1083
+ var BLUR_FIX = 2 ** 32;
1083
1084
  function tentLine(src, dst, bw, mid) {
1084
1085
  const [r] = boxGeom(bw);
1085
- boxLine(src, -r, mid.subarray(0, src.length + 2 * r), bw);
1086
- boxLine(mid.subarray(0, src.length + 2 * r), r, dst, bw);
1087
- for (let i = 0; i < dst.length; i++) if (Math.abs(dst[i]) < BLUR_EPS) dst[i] = 0;
1086
+ const m = mid.subarray(0, src.length + 2 * r);
1087
+ for (let i = 0; i < src.length; i++) src[i] = Math.round(src[i] * BLUR_FIX);
1088
+ boxLine(src, -r, m, bw);
1089
+ for (let i = 0; i < m.length; i++) m[i] = Math.round(m[i]);
1090
+ boxLine(m, r, dst, bw);
1091
+ for (let i = 0; i < dst.length; i++) {
1092
+ const v = dst[i] / BLUR_FIX;
1093
+ dst[i] = Math.abs(v) < BLUR_EPS ? 0 : v;
1094
+ }
1088
1095
  }
1089
1096
  function tent(m, w) {
1090
1097
  if (!Number.isFinite(w) || m.v.length === 0) return;
@@ -1386,25 +1393,41 @@ function effectsAreLocal(items) {
1386
1393
  }
1387
1394
  });
1388
1395
  }
1396
+ function shapeBounds(shape, rect) {
1397
+ const { width: w, height: h } = rect;
1398
+ const rowHit = (y) => {
1399
+ for (let x = 0, i = y * w; x < w; x++, i++) if (shape[i] > INSIDE_EPS) return true;
1400
+ return false;
1401
+ };
1402
+ let y0 = 0;
1403
+ while (y0 < h && !rowHit(y0)) y0++;
1404
+ if (y0 === h) return null;
1405
+ let y1 = h - 1;
1406
+ while (!rowHit(y1)) y1--;
1407
+ let x0 = w;
1408
+ let x1 = -1;
1409
+ for (let y = y0; y <= y1; y++) {
1410
+ const o = y * w;
1411
+ for (let x = 0; x < x0; x++) {
1412
+ if (shape[o + x] > INSIDE_EPS) {
1413
+ x0 = x;
1414
+ break;
1415
+ }
1416
+ }
1417
+ for (let x = w - 1; x > x1; x--) {
1418
+ if (shape[o + x] > INSIDE_EPS) {
1419
+ x1 = x;
1420
+ break;
1421
+ }
1422
+ }
1423
+ }
1424
+ return { x: rect.x + x0, y: rect.y + y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
1425
+ }
1389
1426
  function buildEffectMaps(items, shape, rect, ctx) {
1390
1427
  const w = rect.width;
1391
1428
  const h = rect.height;
1392
1429
  const s = { w, h, v: shape };
1393
- let x0 = Infinity;
1394
- let y0 = Infinity;
1395
- let x1 = -Infinity;
1396
- let y1 = -Infinity;
1397
- for (let y = 0; y < h; y++) {
1398
- for (let x = 0; x < w; x++) {
1399
- if (shape[y * w + x] > INSIDE_EPS) {
1400
- if (x < x0) x0 = x;
1401
- if (x > x1) x1 = x;
1402
- if (y < y0) y0 = y;
1403
- if (y > y1) y1 = y;
1404
- }
1405
- }
1406
- }
1407
- const bounds = x0 === Infinity ? null : { x: rect.x + x0, y: rect.y + y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
1430
+ const bounds = shapeBounds(shape, rect);
1408
1431
  const anchor = bounds ? { x: bounds.x, y: bounds.y } : { x: rect.x, y: rect.y };
1409
1432
  const per = [];
1410
1433
  const bevelPaint = [];
@@ -1452,6 +1475,81 @@ function buildEffectMaps(items, shape, rect, ctx) {
1452
1475
  for (const d of [din, dout, doutExact]) if (d) bytes += d.byteLength;
1453
1476
  return { rect, items, bounds, shape, per, bevelPaint, din, dout, doutExact, frames, bytes };
1454
1477
  }
1478
+ var rectArea = (r) => r.width * r.height;
1479
+ function clipRect(a, b) {
1480
+ const x0 = Math.max(a.x, b.x);
1481
+ const y0 = Math.max(a.y, b.y);
1482
+ const x1 = Math.min(a.x + a.width, b.x + b.width);
1483
+ const y1 = Math.min(a.y + a.height, b.y + b.height);
1484
+ return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
1485
+ }
1486
+ var growRect = (r, m) => ({ x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m });
1487
+ function unionRect(a, b) {
1488
+ const x0 = Math.min(a.x, b.x);
1489
+ const y0 = Math.min(a.y, b.y);
1490
+ return { x: x0, y: y0, width: Math.max(a.x + a.width, b.x + b.width) - x0, height: Math.max(a.y + a.height, b.y + b.height) - y0 };
1491
+ }
1492
+ function rectMinus(r, cut) {
1493
+ const i = clipRect(r, cut);
1494
+ if (!i) return [r];
1495
+ const parts = [
1496
+ { x: r.x, y: r.y, width: r.width, height: i.y - r.y },
1497
+ { x: r.x, y: i.y + i.height, width: r.width, height: r.y + r.height - (i.y + i.height) },
1498
+ { x: r.x, y: i.y, width: i.x - r.x, height: i.height },
1499
+ { x: i.x + i.width, y: i.y, width: r.x + r.width - (i.x + i.width), height: i.height }
1500
+ ];
1501
+ return parts.filter((p) => p.width > 0 && p.height > 0);
1502
+ }
1503
+ function pasteMap(dst, to, src, from, a) {
1504
+ for (let y = a.y; y < a.y + a.height; y++) {
1505
+ const s = (y - from.y) * from.width + (a.x - from.x);
1506
+ dst.set(src.subarray(s, s + a.width), (y - to.y) * to.width + (a.x - to.x));
1507
+ }
1508
+ }
1509
+ function patchEffectMaps(prev, items, rect, changed, margin, shapeOver, ctx) {
1510
+ if (!effectsAreLocal(items) || rectArea(rect) <= 0) return null;
1511
+ let areas = [];
1512
+ for (const c of changed) {
1513
+ const a = clipRect(growRect(c, margin), rect);
1514
+ if (a) areas.push(a);
1515
+ }
1516
+ areas.push(...rectMinus(rect, prev.rect));
1517
+ for (let merged = true; merged; ) {
1518
+ merged = false;
1519
+ for (let i = 0; i < areas.length && !merged; i++) {
1520
+ for (let j = i + 1; j < areas.length; j++) {
1521
+ if (clipRect(growRect(areas[i], 2 * margin), areas[j])) {
1522
+ areas[i] = unionRect(areas[i], areas[j]);
1523
+ areas.splice(j, 1);
1524
+ merged = true;
1525
+ break;
1526
+ }
1527
+ }
1528
+ }
1529
+ }
1530
+ const windows = areas.map((a) => clipRect(growRect(a, margin), rect));
1531
+ if (windows.reduce((n, w) => n + rectArea(w), 0) > rectArea(rect) / 2) return null;
1532
+ const same = rect.x === prev.rect.x && rect.y === prev.rect.y && rect.width === prev.rect.width && rect.height === prev.rect.height;
1533
+ const take = (v) => same ? v : crop(v, prev.rect, rect, 0);
1534
+ const shape = take(prev.shape);
1535
+ const per = prev.per.map((maps) => maps.map(take));
1536
+ const din = prev.din && take(prev.din);
1537
+ const dout = prev.dout && take(prev.dout);
1538
+ const doutExact = prev.doutExact && take(prev.doutExact);
1539
+ areas.forEach((a, k) => {
1540
+ const w = windows[k];
1541
+ const part = buildEffectMaps(items, shapeOver(w), w, ctx);
1542
+ pasteMap(shape, rect, part.shape, w, a);
1543
+ per.forEach((maps, i) => maps.forEach((m, j) => pasteMap(m, rect, part.per[i][j], w, a)));
1544
+ if (din) pasteMap(din, rect, part.din, w, a);
1545
+ if (dout) pasteMap(dout, rect, part.dout, w, a);
1546
+ if (doutExact) pasteMap(doutExact, rect, part.doutExact, w, a);
1547
+ });
1548
+ let bytes = shape.byteLength;
1549
+ for (const maps of per) for (const m of maps) bytes += m.byteLength;
1550
+ for (const d of [din, dout, doutExact]) if (d) bytes += d.byteLength;
1551
+ return { rect, items, bounds: shapeBounds(shape, rect), shape, per, bevelPaint: prev.bevelPaint, din, dout, doutExact, frames: /* @__PURE__ */ new Map(), bytes };
1552
+ }
1455
1553
  var rgbScratchB = [0, 0, 0];
1456
1554
  var rgbScratchS = [0, 0, 0];
1457
1555
  var rgbScratchM = [0, 0, 0];
@@ -2084,8 +2182,11 @@ var RasterCompositor = class {
2084
2182
  patternTiles = /* @__PURE__ */ new WeakMap();
2085
2183
  reachCache = /* @__PURE__ */ new WeakMap();
2086
2184
  fxContext = null;
2087
- lastStats = { tiles: 0, cached: 0, stepsRun: 0 };
2185
+ lastStats = { tiles: 0, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 };
2088
2186
  hint = -1;
2187
+ /** The `dirty` rectangles of the current call, as passed. */
2188
+ callDirty = null;
2189
+ callStats = this.lastStats;
2089
2190
  /** Drops every cached tile and plane. */
2090
2191
  clear() {
2091
2192
  this.tiles.clear();
@@ -2133,7 +2234,9 @@ var RasterCompositor = class {
2133
2234
  const steps = this.buildSteps(layers);
2134
2235
  this.hint = options.checkpointLayerId ? this.stepIndexOf(steps, options.checkpointLayerId) : -1;
2135
2236
  this.stamp++;
2136
- const stats = { tiles: 0, cached: 0, stepsRun: 0 };
2237
+ this.callDirty = options.dirty ?? null;
2238
+ const stats = { tiles: 0, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 };
2239
+ this.callStats = stats;
2137
2240
  const grow = options.dirty ? this.effectsReach(layers) : 0;
2138
2241
  const dirty = grow > 0 ? options.dirty.map((d) => inflateRect(d, grow)) : options.dirty;
2139
2242
  for (const { tx, ty } of tilesInRect(rect)) {
@@ -2149,8 +2252,10 @@ var RasterCompositor = class {
2149
2252
  tile(layers, tx, ty, sources) {
2150
2253
  this.setSources(sources);
2151
2254
  this.stamp++;
2152
- const stats = { tiles: 1, cached: 0, stepsRun: 0 };
2255
+ const stats = { tiles: 1, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 };
2256
+ this.callStats = stats;
2153
2257
  this.hint = -1;
2258
+ this.callDirty = null;
2154
2259
  const data = this.compositeTile(layers, this.buildSteps(layers), tx, ty, stats);
2155
2260
  this.lastStats = stats;
2156
2261
  this.evict();
@@ -2220,7 +2325,8 @@ var RasterCompositor = class {
2220
2325
  let first = 0;
2221
2326
  if (entry) {
2222
2327
  while (first < sigs.length && first < entry.sigs.length && sameSig(entry.sigs[first], sigs[first])) first++;
2223
- if (first === sigs.length && first === entry.sigs.length) {
2328
+ if (first === sigs.length && first === entry.sigs.length || this.changedOnlyNearby(steps, sigs, entry, dirty)) {
2329
+ entry.sigs = sigs;
2224
2330
  entry.stamp = this.stamp;
2225
2331
  stats.cached++;
2226
2332
  return entry.result;
@@ -2285,6 +2391,20 @@ var RasterCompositor = class {
2285
2391
  this.cacheBytes += this.entryBytes(next) - prevBytes;
2286
2392
  return result;
2287
2393
  }
2394
+ /**
2395
+ * Whether a tile with no part in this call's (grown) `dirty` rectangles changed only in what local
2396
+ * effects read beyond it: every changed step is a pixel layer with local effects whose changes stay
2397
+ * inside `dirty` (and so are farther than its margin), which leaves the tile's pixels as they were.
2398
+ */
2399
+ changedOnlyNearby(steps, sigs, entry, dirty) {
2400
+ if (this.callDirty === null || dirty || entry.stamp !== this.stamp - 1 || sigs.length !== entry.sigs.length) return false;
2401
+ return steps.every((step, i) => {
2402
+ if (sameSig(entry.sigs[i], sigs[i])) return true;
2403
+ if (step.kind !== "content" || entry.sigs[i].key !== sigs[i].key) return false;
2404
+ const { layer, clipped } = step;
2405
+ return this.hasEffects(layer) && effectsAreLocal(activeEffectItems(layer.effects)) && this.dirtyCoversShape(layer) && clipped.every((c) => this.dirtyCoversShape(c));
2406
+ });
2407
+ }
2288
2408
  entryBytes(e) {
2289
2409
  let n = e.result === ZERO_TILE ? 0 : TILE_BYTES;
2290
2410
  for (const cp of e.checkpoints.values()) n += cp.data.byteLength;
@@ -2415,14 +2535,20 @@ var RasterCompositor = class {
2415
2535
  if (!b) return { x: 0, y: 0, width: 0, height: 0 };
2416
2536
  return intersectRect({ x: b.x + dx - m, y: b.y + dy - m, width: b.width + 2 * m, height: b.height + 2 * m }, canvas);
2417
2537
  }
2538
+ /** The signature of a layer's shape (its surface tiles and mask refs) per tile of `region`. */
2539
+ shapeSigs(layer, region) {
2540
+ const sigs = /* @__PURE__ */ new Map();
2541
+ if (region.width <= 0 || region.height <= 0) return sigs;
2542
+ const steps = isGroup(layer) ? this.buildSteps(layer.children ?? []) : null;
2543
+ for (const t of tilesInRect(region)) {
2544
+ sigs.set(tileKey(t.tx, t.ty), steps ? this.plainGroupSig(layer, steps, t.tx, t.ty) : this.plainContentSig(layer, t.tx, t.ty));
2545
+ }
2546
+ return sigs;
2547
+ }
2418
2548
  /** The surface tiles (and mask refs) a layer's shape reads over `region`. */
2419
2549
  shapeRefs(layer, region) {
2420
2550
  const refs = [];
2421
- if (region.width <= 0 || region.height <= 0) return refs;
2422
- for (const t of tilesInRect(region)) {
2423
- const sig = isGroup(layer) ? this.plainGroupSig(layer, this.buildSteps(layer.children ?? []), t.tx, t.ty) : this.plainContentSig(layer, t.tx, t.ty);
2424
- for (const r of sig.refs) refs.push(r);
2425
- }
2551
+ for (const sig of this.shapeSigs(layer, region).values()) for (const r of sig.refs) refs.push(r);
2426
2552
  return refs;
2427
2553
  }
2428
2554
  lightKey() {
@@ -2448,18 +2574,28 @@ var RasterCompositor = class {
2448
2574
  const memo = this.effectMemo.get(layer);
2449
2575
  if (memo && memo.stamp === this.stamp) return memo.maps;
2450
2576
  const region = this.effectRegion(layer);
2451
- const key = `${this.propsKey(layer)}${this.lightKey()}|${region.x},${region.y},${region.width},${region.height}`;
2452
- const refs = this.shapeRefs(layer, region);
2577
+ const key = this.propsKey(layer) + this.lightKey();
2578
+ const tiles = this.shapeSigs(layer, region);
2453
2579
  const hit = this.effectCache.get(layer.id);
2454
- let maps;
2455
- if (hit && hit.key === key && sameSig({ key, refs: hit.refs }, { key, refs })) {
2456
- this.effectCache.delete(layer.id);
2457
- this.effectCache.set(layer.id, hit);
2458
- maps = hit.maps;
2580
+ this.effectCache.delete(layer.id);
2581
+ let entry;
2582
+ if (hit && hit.key === key && sameRect(hit.region, region) && [...tiles].every(([k, sig]) => sameSig(hit.tiles.get(k), sig))) {
2583
+ entry = hit;
2459
2584
  } else {
2460
- maps = buildEffectMaps(activeEffectItems(layer.effects), this.layerShape(layer, region), region, this.effectContext());
2461
- this.effectCache.delete(layer.id);
2462
- this.effectCache.set(layer.id, { key, refs, maps });
2585
+ const items = activeEffectItems(layer.effects);
2586
+ const ctx = this.effectContext();
2587
+ let maps = null;
2588
+ if (hit && hit.key === key && items.every((e) => effectReach(e) <= MAX_EFFECT_REACH)) {
2589
+ const changed = this.shapeChanges(layer, hit, tiles, region);
2590
+ maps = patchEffectMaps(hit.maps, items, region, changed, effectsMargin(layer.effects), (r) => this.layerShape(layer, r), ctx);
2591
+ }
2592
+ if (maps) this.callStats.effectsPatched++;
2593
+ else this.callStats.effectsBuilt++;
2594
+ entry = { key, region, tiles, maps: maps ?? buildEffectMaps(items, this.layerShape(layer, region), region, ctx), stamp: 0 };
2595
+ }
2596
+ entry.stamp = this.stamp;
2597
+ this.effectCache.set(layer.id, entry);
2598
+ if (entry !== hit) {
2463
2599
  let bytes = 0;
2464
2600
  for (const e of this.effectCache.values()) bytes += e.maps.bytes;
2465
2601
  while (this.effectCache.size > 1 && (this.effectCache.size > 32 || bytes > this.maxCacheBytes / 2)) {
@@ -2468,8 +2604,42 @@ var RasterCompositor = class {
2468
2604
  this.effectCache.delete(oldest);
2469
2605
  }
2470
2606
  }
2471
- this.effectMemo.set(layer, { stamp: this.stamp, maps });
2472
- return maps;
2607
+ this.effectMemo.set(layer, { stamp: this.stamp, maps: entry.maps });
2608
+ return entry.maps;
2609
+ }
2610
+ /**
2611
+ * Where a layer's shape may differ from the one `prev` was built from: the tiles whose signature
2612
+ * changed (a tile new to the region that reads nothing did not), narrowed to this call's `dirty`
2613
+ * rectangles when `prev` is from the previous call and they cover every change of the shape.
2614
+ */
2615
+ shapeChanges(layer, prev, tiles, region) {
2616
+ const out = [];
2617
+ for (const { tx, ty } of tilesInRect(region)) {
2618
+ const sig = tiles.get(tileKey(tx, ty));
2619
+ const old = prev.tiles.get(tileKey(tx, ty));
2620
+ if (old ? sameSig(old, sig) : sig.refs.every((r2) => r2 === null || r2 === 0)) continue;
2621
+ const r = intersectRect({ x: tx * TILE, y: ty * TILE, width: TILE, height: TILE }, region);
2622
+ if (r.width > 0 && r.height > 0) out.push(r);
2623
+ }
2624
+ const dirty = this.callDirty;
2625
+ if (!dirty || prev.stamp !== this.stamp - 1 || !this.dirtyCoversShape(layer)) return out;
2626
+ const narrowed = [];
2627
+ for (const r of out) {
2628
+ for (const d of dirty) {
2629
+ const a = intersectRect(r, { x: Math.floor(d.x), y: Math.floor(d.y), width: Math.ceil(d.x + d.width) - Math.floor(d.x), height: Math.ceil(d.y + d.height) - Math.floor(d.y) });
2630
+ if (a.width > 0 && a.height > 0) narrowed.push(a);
2631
+ }
2632
+ }
2633
+ return narrowed;
2634
+ }
2635
+ /**
2636
+ * Whether a change to a layer stays inside the `dirty` rectangles that name it: a pixel layer
2637
+ * without filters or a feathered or parametric mask (those spread a change beyond it).
2638
+ */
2639
+ dirtyCoversShape(layer) {
2640
+ if (!isPixel(layer) || this.profile.pixelFilters?.(layer)) return false;
2641
+ const mask = layer.mask;
2642
+ return !mask || mask.enabled === false || !mask.source && !((mask.feather ?? 0) > 0);
2473
2643
  }
2474
2644
  /** A layer's alpha over `r` after its mask (0..1, quantised to bytes as tiles see it). */
2475
2645
  layerShape(layer, r) {
@@ -2796,6 +2966,9 @@ function copyRect(data, r, a) {
2796
2966
  function pasteRect(data, r, part, a) {
2797
2967
  for (let y = 0; y < a.height; y++) data.set(part.subarray(y * a.width * 4, (y + 1) * a.width * 4), ((a.y - r.y + y) * r.width + (a.x - r.x)) * 4);
2798
2968
  }
2969
+ function sameRect(a, b) {
2970
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
2971
+ }
2799
2972
  function inflateRect(r, m) {
2800
2973
  return { x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m };
2801
2974
  }
@@ -3689,6 +3862,7 @@ export {
3689
3862
  activeEffectItems,
3690
3863
  effectsAreLocal,
3691
3864
  buildEffectMaps,
3865
+ patchEffectMaps,
3692
3866
  gradientT,
3693
3867
  compositeLayerEffects,
3694
3868
  renderLayerEffects,
@@ -3709,4 +3883,4 @@ export {
3709
3883
  parseDataUrl,
3710
3884
  pngDataUrl
3711
3885
  };
3712
- //# sourceMappingURL=chunk-KFTQ2PKP.js.map
3886
+ //# sourceMappingURL=chunk-OEVSVA6V.js.map