@pixerate/editor 0.95.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.
@@ -6,6 +6,7 @@ import {
6
6
  isBlendMode,
7
7
  isSeparableBlendMode,
8
8
  mixPigment8,
9
+ mixPigmentRgb,
9
10
  readRegion,
10
11
  surfaceFromPixels,
11
12
  tileKey,
@@ -372,6 +373,7 @@ function oneOf(r, key, path, values) {
372
373
  }
373
374
  function blend(r, key, path) {
374
375
  if (!isBlendMode(r[key])) fail(`${path}.${key}`, `must be a blend mode such as normal, multiply or screen (got ${JSON.stringify(r[key])}).`);
376
+ if (r[key] === "pigment") fail(`${path}.${key}`, "pigment is a layer blend mode only; effects cannot use it.");
375
377
  }
376
378
  function contour(v, path) {
377
379
  if (v === void 0) return;
@@ -1078,11 +1080,18 @@ function boxLine(src, x0, dst, bw) {
1078
1080
  }
1079
1081
  }
1080
1082
  var BLUR_EPS = 1e-7;
1083
+ var BLUR_FIX = 2 ** 32;
1081
1084
  function tentLine(src, dst, bw, mid) {
1082
1085
  const [r] = boxGeom(bw);
1083
- boxLine(src, -r, mid.subarray(0, src.length + 2 * r), bw);
1084
- boxLine(mid.subarray(0, src.length + 2 * r), r, dst, bw);
1085
- 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
+ }
1086
1095
  }
1087
1096
  function tent(m, w) {
1088
1097
  if (!Number.isFinite(w) || m.v.length === 0) return;
@@ -1384,25 +1393,41 @@ function effectsAreLocal(items) {
1384
1393
  }
1385
1394
  });
1386
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
+ }
1387
1426
  function buildEffectMaps(items, shape, rect, ctx) {
1388
1427
  const w = rect.width;
1389
1428
  const h = rect.height;
1390
1429
  const s = { w, h, v: shape };
1391
- let x0 = Infinity;
1392
- let y0 = Infinity;
1393
- let x1 = -Infinity;
1394
- let y1 = -Infinity;
1395
- for (let y = 0; y < h; y++) {
1396
- for (let x = 0; x < w; x++) {
1397
- if (shape[y * w + x] > INSIDE_EPS) {
1398
- if (x < x0) x0 = x;
1399
- if (x > x1) x1 = x;
1400
- if (y < y0) y0 = y;
1401
- if (y > y1) y1 = y;
1402
- }
1403
- }
1404
- }
1405
- 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);
1406
1431
  const anchor = bounds ? { x: bounds.x, y: bounds.y } : { x: rect.x, y: rect.y };
1407
1432
  const per = [];
1408
1433
  const bevelPaint = [];
@@ -1450,8 +1475,84 @@ function buildEffectMaps(items, shape, rect, ctx) {
1450
1475
  for (const d of [din, dout, doutExact]) if (d) bytes += d.byteLength;
1451
1476
  return { rect, items, bounds, shape, per, bevelPaint, din, dout, doutExact, frames, bytes };
1452
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
+ }
1453
1553
  var rgbScratchB = [0, 0, 0];
1454
1554
  var rgbScratchS = [0, 0, 0];
1555
+ var rgbScratchM = [0, 0, 0];
1455
1556
  function compositeAt(dst, i, mode, separable, sr, sg, sb, sa) {
1456
1557
  if (!(sa > 0)) return;
1457
1558
  const ab = dst[i + 3];
@@ -1463,6 +1564,20 @@ function compositeAt(dst, i, mode, separable, sr, sg, sb, sa) {
1463
1564
  const b0 = dst[i];
1464
1565
  const b1 = dst[i + 1];
1465
1566
  const b2 = dst[i + 2];
1567
+ if (mode === "pigment") {
1568
+ rgbScratchB[0] = b0;
1569
+ rgbScratchB[1] = b1;
1570
+ rgbScratchB[2] = b2;
1571
+ rgbScratchS[0] = sr;
1572
+ rgbScratchS[1] = sg;
1573
+ rgbScratchS[2] = sb;
1574
+ mixPigmentRgb(rgbScratchB, rgbScratchS, sa / ao, rgbScratchM);
1575
+ dst[i] = rgbScratchM[0];
1576
+ dst[i + 1] = rgbScratchM[1];
1577
+ dst[i + 2] = rgbScratchM[2];
1578
+ dst[i + 3] = ao;
1579
+ return;
1580
+ }
1466
1581
  let m0;
1467
1582
  let m1;
1468
1583
  let m2;
@@ -2067,8 +2182,11 @@ var RasterCompositor = class {
2067
2182
  patternTiles = /* @__PURE__ */ new WeakMap();
2068
2183
  reachCache = /* @__PURE__ */ new WeakMap();
2069
2184
  fxContext = null;
2070
- lastStats = { tiles: 0, cached: 0, stepsRun: 0 };
2185
+ lastStats = { tiles: 0, cached: 0, stepsRun: 0, effectsBuilt: 0, effectsPatched: 0 };
2071
2186
  hint = -1;
2187
+ /** The `dirty` rectangles of the current call, as passed. */
2188
+ callDirty = null;
2189
+ callStats = this.lastStats;
2072
2190
  /** Drops every cached tile and plane. */
2073
2191
  clear() {
2074
2192
  this.tiles.clear();
@@ -2116,7 +2234,9 @@ var RasterCompositor = class {
2116
2234
  const steps = this.buildSteps(layers);
2117
2235
  this.hint = options.checkpointLayerId ? this.stepIndexOf(steps, options.checkpointLayerId) : -1;
2118
2236
  this.stamp++;
2119
- 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;
2120
2240
  const grow = options.dirty ? this.effectsReach(layers) : 0;
2121
2241
  const dirty = grow > 0 ? options.dirty.map((d) => inflateRect(d, grow)) : options.dirty;
2122
2242
  for (const { tx, ty } of tilesInRect(rect)) {
@@ -2132,8 +2252,10 @@ var RasterCompositor = class {
2132
2252
  tile(layers, tx, ty, sources) {
2133
2253
  this.setSources(sources);
2134
2254
  this.stamp++;
2135
- 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;
2136
2257
  this.hint = -1;
2258
+ this.callDirty = null;
2137
2259
  const data = this.compositeTile(layers, this.buildSteps(layers), tx, ty, stats);
2138
2260
  this.lastStats = stats;
2139
2261
  this.evict();
@@ -2203,7 +2325,8 @@ var RasterCompositor = class {
2203
2325
  let first = 0;
2204
2326
  if (entry) {
2205
2327
  while (first < sigs.length && first < entry.sigs.length && sameSig(entry.sigs[first], sigs[first])) first++;
2206
- 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;
2207
2330
  entry.stamp = this.stamp;
2208
2331
  stats.cached++;
2209
2332
  return entry.result;
@@ -2268,6 +2391,20 @@ var RasterCompositor = class {
2268
2391
  this.cacheBytes += this.entryBytes(next) - prevBytes;
2269
2392
  return result;
2270
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
+ }
2271
2408
  entryBytes(e) {
2272
2409
  let n = e.result === ZERO_TILE ? 0 : TILE_BYTES;
2273
2410
  for (const cp of e.checkpoints.values()) n += cp.data.byteLength;
@@ -2398,14 +2535,20 @@ var RasterCompositor = class {
2398
2535
  if (!b) return { x: 0, y: 0, width: 0, height: 0 };
2399
2536
  return intersectRect({ x: b.x + dx - m, y: b.y + dy - m, width: b.width + 2 * m, height: b.height + 2 * m }, canvas);
2400
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
+ }
2401
2548
  /** The surface tiles (and mask refs) a layer's shape reads over `region`. */
2402
2549
  shapeRefs(layer, region) {
2403
2550
  const refs = [];
2404
- if (region.width <= 0 || region.height <= 0) return refs;
2405
- for (const t of tilesInRect(region)) {
2406
- const sig = isGroup(layer) ? this.plainGroupSig(layer, this.buildSteps(layer.children ?? []), t.tx, t.ty) : this.plainContentSig(layer, t.tx, t.ty);
2407
- for (const r of sig.refs) refs.push(r);
2408
- }
2551
+ for (const sig of this.shapeSigs(layer, region).values()) for (const r of sig.refs) refs.push(r);
2409
2552
  return refs;
2410
2553
  }
2411
2554
  lightKey() {
@@ -2431,18 +2574,28 @@ var RasterCompositor = class {
2431
2574
  const memo = this.effectMemo.get(layer);
2432
2575
  if (memo && memo.stamp === this.stamp) return memo.maps;
2433
2576
  const region = this.effectRegion(layer);
2434
- const key = `${this.propsKey(layer)}${this.lightKey()}|${region.x},${region.y},${region.width},${region.height}`;
2435
- const refs = this.shapeRefs(layer, region);
2577
+ const key = this.propsKey(layer) + this.lightKey();
2578
+ const tiles = this.shapeSigs(layer, region);
2436
2579
  const hit = this.effectCache.get(layer.id);
2437
- let maps;
2438
- if (hit && hit.key === key && sameSig({ key, refs: hit.refs }, { key, refs })) {
2439
- this.effectCache.delete(layer.id);
2440
- this.effectCache.set(layer.id, hit);
2441
- 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;
2442
2584
  } else {
2443
- maps = buildEffectMaps(activeEffectItems(layer.effects), this.layerShape(layer, region), region, this.effectContext());
2444
- this.effectCache.delete(layer.id);
2445
- 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) {
2446
2599
  let bytes = 0;
2447
2600
  for (const e of this.effectCache.values()) bytes += e.maps.bytes;
2448
2601
  while (this.effectCache.size > 1 && (this.effectCache.size > 32 || bytes > this.maxCacheBytes / 2)) {
@@ -2451,8 +2604,42 @@ var RasterCompositor = class {
2451
2604
  this.effectCache.delete(oldest);
2452
2605
  }
2453
2606
  }
2454
- this.effectMemo.set(layer, { stamp: this.stamp, maps });
2455
- 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);
2456
2643
  }
2457
2644
  /** A layer's alpha over `r` after its mask (0..1, quantised to bytes as tiles see it). */
2458
2645
  layerShape(layer, r) {
@@ -2779,6 +2966,9 @@ function copyRect(data, r, a) {
2779
2966
  function pasteRect(data, r, part, a) {
2780
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);
2781
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
+ }
2782
2972
  function inflateRect(r, m) {
2783
2973
  return { x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m };
2784
2974
  }
@@ -3672,6 +3862,7 @@ export {
3672
3862
  activeEffectItems,
3673
3863
  effectsAreLocal,
3674
3864
  buildEffectMaps,
3865
+ patchEffectMaps,
3675
3866
  gradientT,
3676
3867
  compositeLayerEffects,
3677
3868
  renderLayerEffects,
@@ -3692,4 +3883,4 @@ export {
3692
3883
  parseDataUrl,
3693
3884
  pngDataUrl
3694
3885
  };
3695
- //# sourceMappingURL=chunk-E2ILRYTJ.js.map
3886
+ //# sourceMappingURL=chunk-OEVSVA6V.js.map