@pixerate/editor 0.125.0 → 0.127.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/dist/animation/index.d.cts +2 -3
- package/dist/animation/index.d.ts +2 -3
- package/dist/animation-editor/index.cjs +71 -73
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +6 -8
- package/dist/animation-editor/index.d.ts +6 -8
- package/dist/animation-editor/index.js +11 -13
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/{bezier-CBzKKDjl.d.cts → bezier-DBz0IgEt.d.cts} +22 -2
- package/dist/{bezier-CWv6LQal.d.ts → bezier-DWI5MEkI.d.ts} +22 -2
- package/dist/{brushstrokeRender-LRV7GXGD.js → brushstrokeRender-7RUITO42.js} +3 -3
- package/dist/{brushstrokeRender-EELTV7U4.cjs → brushstrokeRender-OW6ENCZG.cjs} +7 -7
- package/dist/{brushstrokeRender-EELTV7U4.cjs.map → brushstrokeRender-OW6ENCZG.cjs.map} +1 -1
- package/dist/canvas/index.cjs +423 -37
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +189 -11
- package/dist/canvas/index.d.ts +189 -11
- package/dist/canvas/index.js +395 -9
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-7LQO6OHH.cjs → chunk-33SO3QWP.cjs} +4 -4
- package/dist/{chunk-7LQO6OHH.cjs.map → chunk-33SO3QWP.cjs.map} +1 -1
- package/dist/{chunk-MPQAO33O.js → chunk-A6KHMKI4.js} +3 -3
- package/dist/{chunk-V5YGPRCZ.cjs → chunk-BEGEWQHD.cjs} +41 -41
- package/dist/{chunk-V5YGPRCZ.cjs.map → chunk-BEGEWQHD.cjs.map} +1 -1
- package/dist/{chunk-N3FVUOZD.cjs → chunk-BJTCSLF6.cjs} +7 -7
- package/dist/{chunk-N3FVUOZD.cjs.map → chunk-BJTCSLF6.cjs.map} +1 -1
- package/dist/{chunk-WUDPFWAN.js → chunk-F7Q2NE2Y.js} +3 -3
- package/dist/{chunk-G6WXWS4Y.js → chunk-FICSDDDF.js} +2 -2
- package/dist/{chunk-GUZ47CJM.cjs → chunk-IZ4WRXZA.cjs} +1 -1
- package/dist/{chunk-GUZ47CJM.cjs.map → chunk-IZ4WRXZA.cjs.map} +1 -1
- package/dist/{chunk-QDJKUBWE.cjs → chunk-JODTP4DP.cjs} +176 -5
- package/dist/chunk-JODTP4DP.cjs.map +1 -0
- package/dist/{chunk-GLWZXQ4Q.js → chunk-KHNZ6MOU.js} +1 -1
- package/dist/chunk-KHNZ6MOU.js.map +1 -0
- package/dist/{chunk-GCJWMEYW.cjs → chunk-KSJLDRHK.cjs} +46 -46
- package/dist/{chunk-GCJWMEYW.cjs.map → chunk-KSJLDRHK.cjs.map} +1 -1
- package/dist/{chunk-QQKTOA4U.cjs → chunk-ORE334I6.cjs} +132 -132
- package/dist/{chunk-QQKTOA4U.cjs.map → chunk-ORE334I6.cjs.map} +1 -1
- package/dist/{chunk-X6KTSFAE.js → chunk-Q4NBEVV5.js} +2 -2
- package/dist/{chunk-XZWOLXWA.js → chunk-QFSTQHZY.js} +174 -3
- package/dist/chunk-QFSTQHZY.js.map +1 -0
- package/dist/{chunk-FPRIUR4T.js → chunk-ROZFX4QI.js} +3 -3
- package/dist/{chunk-UXFI3CDZ.cjs → chunk-SFCPGMCT.cjs} +8 -8
- package/dist/{chunk-UXFI3CDZ.cjs.map → chunk-SFCPGMCT.cjs.map} +1 -1
- package/dist/{chunk-J67OLQUQ.js → chunk-UABQTYQF.js} +3 -3
- package/dist/{chunk-O5RVCDEC.cjs → chunk-XLUYXUBD.cjs} +88 -88
- package/dist/{chunk-O5RVCDEC.cjs.map → chunk-XLUYXUBD.cjs.map} +1 -1
- package/dist/{chunk-FG274FYT.js → chunk-YGJIER2R.js} +2 -2
- package/dist/effects/index.cjs +32 -34
- package/dist/effects/index.cjs.map +1 -1
- package/dist/effects/index.d.cts +6 -38
- package/dist/effects/index.d.ts +6 -38
- package/dist/effects/index.js +3 -5
- package/dist/effects/index.js.map +1 -1
- package/dist/evaluate-BbCeF6pt.d.ts +114 -0
- package/dist/evaluate-BsxYieLN.d.cts +114 -0
- package/dist/{export-DVl4fWy5.d.cts → export-Cbr9IqpJ.d.cts} +1 -1
- package/dist/{export-Bp7ji2r4.d.cts → export-CcldobdN.d.cts} +3 -3
- package/dist/{export-C1s4eolZ.d.ts → export-ClPKUB-v.d.ts} +1 -1
- package/dist/{export-DAyPY94f.d.ts → export-PGhOpDmz.d.ts} +3 -3
- package/dist/{helpers-i95Ob6Vi.d.cts → helpers-C5aAd44p.d.cts} +67 -4
- package/dist/{helpers-BBLlekuM.d.ts → helpers-CPWcK504.d.ts} +67 -4
- package/dist/image-editor/index.cjs +619 -621
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +9 -11
- package/dist/image-editor/index.d.ts +9 -11
- package/dist/image-editor/index.js +6 -8
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{opentype-14PDI5D2.d.ts → opentype-01rMIuQR.d.ts} +1 -1
- package/dist/{opentype-D80AVmMu.d.cts → opentype-BgqvmGT-.d.cts} +1 -1
- package/dist/paint-editor/index.cjs +737 -117
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +248 -13
- package/dist/paint-editor/index.d.ts +248 -13
- package/dist/paint-editor/index.js +635 -15
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/{pathfinder-DjISxkUv.d.ts → pathfinder-D-h0Imus.d.ts} +1 -1
- package/dist/{pathfinder-C0UoFypx.d.cts → pathfinder-DgG0Z9Lr.d.cts} +1 -1
- package/dist/{player-cGLrngL_.d.ts → player-BK2kKbfd.d.ts} +1 -1
- package/dist/{player-CBtEoU2x.d.cts → player-CmbY3218.d.cts} +1 -1
- package/dist/pool-C7GtrL34.d.cts +34 -0
- package/dist/pool-CTrtciaN.d.ts +34 -0
- package/dist/{svg-DJKriJ4A.d.ts → svg-BRLwcibA.d.ts} +2 -3
- package/dist/{svg-BkRL-fZJ.d.cts → svg-DLBBPQP9.d.cts} +2 -3
- package/dist/{tools-CM8Oe8F3.d.ts → tools-AugT22z3.d.ts} +1 -1
- package/dist/{tools-DZCv2kX1.d.cts → tools-BpCmUNxb.d.cts} +1 -1
- package/dist/vector/index.cjs +923 -925
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +10 -12
- package/dist/vector/index.d.ts +10 -12
- package/dist/vector/index.js +11 -13
- package/dist/vector/index.js.map +1 -1
- package/dist/video-editor/index.cjs +269 -271
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +6 -8
- package/dist/video-editor/index.d.ts +6 -8
- package/dist/video-editor/index.js +3 -5
- package/dist/video-editor/index.js.map +1 -1
- package/dist/{warp-CH9MM435.d.cts → warp-C13L_GRn.d.cts} +1 -1
- package/dist/{warp-k_PdtXOG.d.ts → warp-q8FK5HHi.d.ts} +1 -1
- package/package.json +1 -1
- package/skills/pixerate-canvas/SKILL.md +19 -0
- package/skills/pixerate-paint-editor/SKILL.md +40 -1
- package/dist/behavior-Ymbld6jL.d.cts +0 -60
- package/dist/behavior-Ymbld6jL.d.ts +0 -60
- package/dist/chunk-GLWZXQ4Q.js.map +0 -1
- package/dist/chunk-IOXKXQNW.cjs +0 -178
- package/dist/chunk-IOXKXQNW.cjs.map +0 -1
- package/dist/chunk-QDJKUBWE.cjs.map +0 -1
- package/dist/chunk-TGKT5QAB.js +0 -178
- package/dist/chunk-TGKT5QAB.js.map +0 -1
- package/dist/chunk-XZWOLXWA.js.map +0 -1
- package/dist/evaluate-2S3vSPLy.d.cts +0 -55
- package/dist/evaluate-CfmT6xA3.d.ts +0 -55
- package/dist/gradient-KDfCvhuj.d.cts +0 -66
- package/dist/gradient-KDfCvhuj.d.ts +0 -66
- /package/dist/{brushstrokeRender-LRV7GXGD.js.map → brushstrokeRender-7RUITO42.js.map} +0 -0
- /package/dist/{chunk-MPQAO33O.js.map → chunk-A6KHMKI4.js.map} +0 -0
- /package/dist/{chunk-WUDPFWAN.js.map → chunk-F7Q2NE2Y.js.map} +0 -0
- /package/dist/{chunk-G6WXWS4Y.js.map → chunk-FICSDDDF.js.map} +0 -0
- /package/dist/{chunk-X6KTSFAE.js.map → chunk-Q4NBEVV5.js.map} +0 -0
- /package/dist/{chunk-FPRIUR4T.js.map → chunk-ROZFX4QI.js.map} +0 -0
- /package/dist/{chunk-J67OLQUQ.js.map → chunk-UABQTYQF.js.map} +0 -0
- /package/dist/{chunk-FG274FYT.js.map → chunk-YGJIER2R.js.map} +0 -0
|
@@ -73,16 +73,18 @@ import {
|
|
|
73
73
|
} from "../chunk-UYQUPUR6.js";
|
|
74
74
|
import {
|
|
75
75
|
toVideoFfmpeg
|
|
76
|
-
} from "../chunk-
|
|
76
|
+
} from "../chunk-FICSDDDF.js";
|
|
77
77
|
import {
|
|
78
78
|
deserializeLayers,
|
|
79
79
|
exportPsd,
|
|
80
80
|
resolveExternalAssets,
|
|
81
81
|
serializeLayers,
|
|
82
82
|
serializeLayersExternal
|
|
83
|
-
} from "../chunk-
|
|
83
|
+
} from "../chunk-F7Q2NE2Y.js";
|
|
84
84
|
import "../chunk-CTTF4KYV.js";
|
|
85
|
-
import
|
|
85
|
+
import {
|
|
86
|
+
EffectRendererPool
|
|
87
|
+
} from "../chunk-FJ3324TZ.js";
|
|
86
88
|
import {
|
|
87
89
|
RasterCompositor,
|
|
88
90
|
findLayer,
|
|
@@ -209,10 +211,15 @@ import {
|
|
|
209
211
|
realPresenceClock,
|
|
210
212
|
tweenPresenceCursor
|
|
211
213
|
} from "../chunk-B6DSIEXA.js";
|
|
212
|
-
import "../chunk-
|
|
213
|
-
import "../chunk-
|
|
214
|
-
import "../chunk-
|
|
215
|
-
import
|
|
214
|
+
import "../chunk-ROZFX4QI.js";
|
|
215
|
+
import "../chunk-YGJIER2R.js";
|
|
216
|
+
import "../chunk-KHNZ6MOU.js";
|
|
217
|
+
import {
|
|
218
|
+
EffectUnsupportedError,
|
|
219
|
+
EffectsSlot,
|
|
220
|
+
createEffectToolProvider,
|
|
221
|
+
resolveEffectValues
|
|
222
|
+
} from "../chunk-QFSTQHZY.js";
|
|
216
223
|
import "../chunk-4FX5ACVG.js";
|
|
217
224
|
import "../chunk-33AHNSEB.js";
|
|
218
225
|
import "../chunk-ZRJMJIQU.js";
|
|
@@ -1483,6 +1490,7 @@ function varyStroke(stroke, m, v) {
|
|
|
1483
1490
|
// src/paint-editor/render.ts
|
|
1484
1491
|
var PAPER_NODE_ID = "__paper";
|
|
1485
1492
|
var TRACING_NODE_ID = "__tracing";
|
|
1493
|
+
var PAPER_EFFECT_NODE_ID = "__paper_effect";
|
|
1486
1494
|
function paperKey(p, light) {
|
|
1487
1495
|
const metal = p.metallic ?? 0;
|
|
1488
1496
|
return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry, metal, metal > 0 && light ? [light.azimuth, light.elevation] : null]);
|
|
@@ -3216,6 +3224,130 @@ ${tl.ffmpeg.command}
|
|
|
3216
3224
|
return writeZip(entries, { compression: "store" });
|
|
3217
3225
|
}
|
|
3218
3226
|
|
|
3227
|
+
// src/paint-editor/effects.ts
|
|
3228
|
+
var PAINT_EFFECT_PLACES = {
|
|
3229
|
+
/** Effect paper: drawn over the whole sheet. */
|
|
3230
|
+
paper: ["generator"],
|
|
3231
|
+
/** An effect layer: drawn at the canvas size. */
|
|
3232
|
+
layer: ["generator", "overlay", "title"],
|
|
3233
|
+
/** An effect fill: composited onto a layer's pixels. */
|
|
3234
|
+
fill: ["generator", "overlay", "title"],
|
|
3235
|
+
/** Light (or any filter) on a layer's dried paint. */
|
|
3236
|
+
light: ["filter"]
|
|
3237
|
+
};
|
|
3238
|
+
var PLACE_WORDS = {
|
|
3239
|
+
paper: "effect paper takes generator effects",
|
|
3240
|
+
layer: "an effect layer takes generator, overlay or title effects",
|
|
3241
|
+
fill: "an effect fill takes generator, overlay or title effects",
|
|
3242
|
+
light: "light on dried paint takes filter effects"
|
|
3243
|
+
};
|
|
3244
|
+
function checkPaintEffectPlace(d, place) {
|
|
3245
|
+
const allowed = PAINT_EFFECT_PLACES[place];
|
|
3246
|
+
if (d.kind !== "motion" && d.kind !== "vector" && d.uses.some((u) => allowed.includes(u))) return;
|
|
3247
|
+
let instead = "";
|
|
3248
|
+
if (d.uses.includes("filter") && place !== "light") instead = "; run it on a layer's dried paint with apply_effect instead";
|
|
3249
|
+
else if (place === "light" && d.uses.some((u) => PAINT_EFFECT_PLACES.layer.includes(u))) instead = "; add it as an effect layer (add_effect_layer) or fill a layer with it (fill_layer_with_effect) instead";
|
|
3250
|
+
else if (place === "paper" && d.uses.some((u) => u === "overlay" || u === "title")) instead = "; add it as an effect layer with add_effect_layer instead";
|
|
3251
|
+
throw new EffectUnsupportedError(`"${d.id}" (${d.name}) is a ${d.uses.join(" / ") || d.kind} effect, and ${PLACE_WORDS[place]}${instead}.`, d.id);
|
|
3252
|
+
}
|
|
3253
|
+
var PAPER_EFFECT_BLENDS = ["multiply", "normal", "overlay", "soft-light", "screen", "color"];
|
|
3254
|
+
var paperVersion = 1;
|
|
3255
|
+
function nextPaperEffectVersion() {
|
|
3256
|
+
return paperVersion++;
|
|
3257
|
+
}
|
|
3258
|
+
function overPixels(base, top, opacity = 1) {
|
|
3259
|
+
const out = new Uint8ClampedArray(base.length);
|
|
3260
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
3261
|
+
const ta = top[i + 3] / 255 * opacity;
|
|
3262
|
+
const ba = base[i + 3] / 255;
|
|
3263
|
+
const a = ta + ba * (1 - ta);
|
|
3264
|
+
out[i + 3] = Math.round(a * 255);
|
|
3265
|
+
if (a > 0) for (let c = 0; c < 3; c++) out[i + c] = (top[i + c] * ta + base[i + c] * ba * (1 - ta)) / a;
|
|
3266
|
+
}
|
|
3267
|
+
return out;
|
|
3268
|
+
}
|
|
3269
|
+
function mixPixels(before, after, amount) {
|
|
3270
|
+
const out = new Uint8ClampedArray(before.length);
|
|
3271
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
3272
|
+
for (let c = 0; c < 3; c++) out[i + c] = before[i + c] + (after[i + c] - before[i + c]) * amount;
|
|
3273
|
+
out[i + 3] = before[i + 3];
|
|
3274
|
+
}
|
|
3275
|
+
return out;
|
|
3276
|
+
}
|
|
3277
|
+
var PaintEffectsRenderer = class {
|
|
3278
|
+
pool = new EffectRendererPool({ maxRenderers: 3 });
|
|
3279
|
+
descriptions = /* @__PURE__ */ new Map();
|
|
3280
|
+
provider = null;
|
|
3281
|
+
/** Forgets cached descriptions and renderers when the provider changes. */
|
|
3282
|
+
follow(provider) {
|
|
3283
|
+
if (provider === this.provider) return;
|
|
3284
|
+
this.provider = provider;
|
|
3285
|
+
this.descriptions.clear();
|
|
3286
|
+
this.pool.clear();
|
|
3287
|
+
}
|
|
3288
|
+
describe(provider, id) {
|
|
3289
|
+
this.follow(provider);
|
|
3290
|
+
let d = this.descriptions.get(id);
|
|
3291
|
+
if (!d) {
|
|
3292
|
+
d = Promise.resolve().then(() => provider.describe(id));
|
|
3293
|
+
d.catch(() => this.descriptions.delete(id));
|
|
3294
|
+
this.descriptions.set(id, d);
|
|
3295
|
+
}
|
|
3296
|
+
return d;
|
|
3297
|
+
}
|
|
3298
|
+
async render(provider, id, size, request) {
|
|
3299
|
+
this.follow(provider);
|
|
3300
|
+
const out = await this.pool.render(provider, id, { ...size, depth: 8 }, request);
|
|
3301
|
+
if (out.width !== size.width || out.height !== size.height || out.data.length !== size.width * size.height * 4) {
|
|
3302
|
+
throw new Error(`The effect "${id}" returned ${out.width}\xD7${out.height} pixels for a ${size.width}\xD7${size.height} canvas.`);
|
|
3303
|
+
}
|
|
3304
|
+
return out;
|
|
3305
|
+
}
|
|
3306
|
+
dispose() {
|
|
3307
|
+
this.pool.clear();
|
|
3308
|
+
this.descriptions.clear();
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
var sourceOut = (s) => ({
|
|
3312
|
+
effect: s.effect,
|
|
3313
|
+
...s.provider ? { provider: s.provider } : {},
|
|
3314
|
+
...s.name ? { name: s.name } : {},
|
|
3315
|
+
values: JSON.parse(JSON.stringify(s.values)),
|
|
3316
|
+
...s.time ? { time: s.time } : {}
|
|
3317
|
+
});
|
|
3318
|
+
function readPaintEffectSource(raw) {
|
|
3319
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3320
|
+
const r = raw;
|
|
3321
|
+
if (typeof r.effect !== "string" || !r.effect || !r.values || typeof r.values !== "object" || Array.isArray(r.values)) return null;
|
|
3322
|
+
return {
|
|
3323
|
+
effect: r.effect,
|
|
3324
|
+
...typeof r.provider === "string" ? { provider: r.provider } : {},
|
|
3325
|
+
...typeof r.name === "string" ? { name: r.name } : {},
|
|
3326
|
+
values: r.values,
|
|
3327
|
+
...typeof r.time === "number" && r.time > 0 ? { time: r.time } : {}
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
function paperEffectToJSON(p) {
|
|
3331
|
+
return { ...sourceOut(p), opacity: p.opacity, blend: p.blend, png: pngDataUrl(p.image.width, p.image.height, p.image.data) };
|
|
3332
|
+
}
|
|
3333
|
+
function paperEffectFromJSON(raw, width, height) {
|
|
3334
|
+
const source = readPaintEffectSource(raw);
|
|
3335
|
+
if (!source) throw new Error("it is not an effect (effect id and values)");
|
|
3336
|
+
const r = raw;
|
|
3337
|
+
const parsed = typeof r.png === "string" ? parseDataUrl(r.png) : null;
|
|
3338
|
+
if (!parsed) throw new Error("its pixels are missing");
|
|
3339
|
+
const image = decodePng(parsed.bytes);
|
|
3340
|
+
if (image.width !== width || image.height !== height) throw new Error(`its pixels are ${image.width}\xD7${image.height}, not the canvas size ${width}\xD7${height}`);
|
|
3341
|
+
const opacity = typeof r.opacity === "number" && r.opacity >= 0 && r.opacity <= 1 ? r.opacity : 1;
|
|
3342
|
+
const blend = PAPER_EFFECT_BLENDS.includes(r.blend) ? r.blend : "multiply";
|
|
3343
|
+
return { ...source, opacity, blend, image: { width, height, data: new Uint8ClampedArray(image.data) }, version: nextPaperEffectVersion() };
|
|
3344
|
+
}
|
|
3345
|
+
function effectLayersToJSON(map, keep) {
|
|
3346
|
+
const out = {};
|
|
3347
|
+
for (const [id, s] of Object.entries(map)) if (keep(id)) out[id] = sourceOut(s);
|
|
3348
|
+
return Object.keys(out).length ? out : void 0;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3219
3351
|
// src/paint-editor/controller.ts
|
|
3220
3352
|
var PaintEditorError = class extends Error {
|
|
3221
3353
|
constructor(message) {
|
|
@@ -3291,6 +3423,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3291
3423
|
liveThick = null;
|
|
3292
3424
|
/** Set while an operation that changes wet paint runs: the simulation before it (for undo). */
|
|
3293
3425
|
wetOp = null;
|
|
3426
|
+
/** The effects provider slot (see `setEffectsProvider`). */
|
|
3427
|
+
effects = new EffectsSlot();
|
|
3428
|
+
effectsRenderer = new PaintEffectsRenderer();
|
|
3429
|
+
paperEffectNodeCache = null;
|
|
3294
3430
|
constructor(options = {}) {
|
|
3295
3431
|
this.historyLimit = Math.max(1, options.historyLimit ?? 200);
|
|
3296
3432
|
this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });
|
|
@@ -3404,6 +3540,21 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3404
3540
|
this.trimWetHistory();
|
|
3405
3541
|
this.emit();
|
|
3406
3542
|
}
|
|
3543
|
+
/**
|
|
3544
|
+
* Commits `next` as a new undo step, or with `commitToHistory` false folds it into the last step
|
|
3545
|
+
* when that step has the same label (the later values of one slider drag).
|
|
3546
|
+
*/
|
|
3547
|
+
commitOrAmend(label, next, commitToHistory) {
|
|
3548
|
+
const last = this.undoStack[this.undoStack.length - 1];
|
|
3549
|
+
if (commitToHistory || !last || last.label !== label || last.wet || this.wetOp) {
|
|
3550
|
+
this.commit(label, next);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
last.after = next;
|
|
3554
|
+
this.redoStack = [];
|
|
3555
|
+
this.doc = next;
|
|
3556
|
+
this.emit();
|
|
3557
|
+
}
|
|
3407
3558
|
/** Keeps the wet snapshots in history within the wet undo memory, dropping the oldest. */
|
|
3408
3559
|
trimWetHistory() {
|
|
3409
3560
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3812,6 +3963,257 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3812
3963
|
}
|
|
3813
3964
|
return new TiledSurface(4, tiles, 0);
|
|
3814
3965
|
}
|
|
3966
|
+
// --- Provider effects ------------------------------------------------------------
|
|
3967
|
+
/** The registered effects provider, or `null`. */
|
|
3968
|
+
get effectsProvider() {
|
|
3969
|
+
return this.effects.provider;
|
|
3970
|
+
}
|
|
3971
|
+
/** Registers (or with `null` removes) the effects provider. Not an edit: no undo step. */
|
|
3972
|
+
setEffectsProvider(provider) {
|
|
3973
|
+
this.effects.set(provider);
|
|
3974
|
+
}
|
|
3975
|
+
/** Registers `provider` and returns a function that removes it again (if it is still the one set). */
|
|
3976
|
+
useEffects(provider) {
|
|
3977
|
+
return this.effects.use(provider);
|
|
3978
|
+
}
|
|
3979
|
+
/** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
|
|
3980
|
+
async describeEffect(effect, place) {
|
|
3981
|
+
const d = await this.effectsRenderer.describe(this.effects.require(), effect);
|
|
3982
|
+
if (place) checkPaintEffectPlace(d, place);
|
|
3983
|
+
return d;
|
|
3984
|
+
}
|
|
3985
|
+
/** Every effect layer (top first) with its effect. */
|
|
3986
|
+
listEffectLayers() {
|
|
3987
|
+
const map = this.doc.effectLayers ?? {};
|
|
3988
|
+
return this.listLayers().filter((l) => map[l.id]).map((l) => ({ layerId: l.id, name: l.name, source: map[l.id] }));
|
|
3989
|
+
}
|
|
3990
|
+
/** The effect a layer is drawn from, or `undefined` for other layers. */
|
|
3991
|
+
getEffectLayer(layerId) {
|
|
3992
|
+
return findLayer(this.doc.layers, layerId) ? this.doc.effectLayers?.[layerId] : void 0;
|
|
3993
|
+
}
|
|
3994
|
+
/** The effect paper (without its pixels), or `null`. */
|
|
3995
|
+
getPaperEffect() {
|
|
3996
|
+
const p = this.doc.paperEffect;
|
|
3997
|
+
if (!p) return null;
|
|
3998
|
+
const { image: _i, version: _v, ...rest } = p;
|
|
3999
|
+
return { ...rest, values: { ...rest.values } };
|
|
4000
|
+
}
|
|
4001
|
+
/** Values over `base` (`null` resets a control to its default), checked against the effect's controls. */
|
|
4002
|
+
effectValues(d, base, changes) {
|
|
4003
|
+
const merged = { ...base };
|
|
4004
|
+
const given = {};
|
|
4005
|
+
for (const [k, v] of Object.entries(changes ?? {})) {
|
|
4006
|
+
if (!(k in d.controls.properties)) throw new PaintEditorError(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ") || "none"} (describe_effect).`);
|
|
4007
|
+
if (v === null) merged[k] = d.defaults[k];
|
|
4008
|
+
else given[k] = v;
|
|
4009
|
+
}
|
|
4010
|
+
return { ...merged, ...resolveEffectValues({ ...d, defaults: merged }, given) };
|
|
4011
|
+
}
|
|
4012
|
+
/** Renders a generator, overlay or title effect at the canvas size. */
|
|
4013
|
+
renderEffectCanvas(d, values, time = 0) {
|
|
4014
|
+
const { width, height } = this.doc.canvas;
|
|
4015
|
+
return this.effectsRenderer.render(this.effects.require(), d.id, { width, height }, { time, values });
|
|
4016
|
+
}
|
|
4017
|
+
sourceOf(d, values, time) {
|
|
4018
|
+
return { effect: d.id, provider: this.effects.require().id, name: d.name, values, ...time ? { time } : {} };
|
|
4019
|
+
}
|
|
4020
|
+
checkTime(time) {
|
|
4021
|
+
if (time !== void 0 && !(Number.isFinite(time) && time >= 0)) throw new PaintEditorError(`time ${time} is not a moment of the effect; use 0 or more milliseconds.`);
|
|
4022
|
+
}
|
|
4023
|
+
checkOpacity(opacity) {
|
|
4024
|
+
if (opacity !== void 0 && !(opacity >= 0 && opacity <= 1)) throw new PaintEditorError(`opacity ${opacity} is out of range; use 0..1.`);
|
|
4025
|
+
}
|
|
4026
|
+
/** A pixel layer that takes effect pixels: unlocked, visible, no sticker layer, no wet paint. */
|
|
4027
|
+
effectTarget(layerId, what) {
|
|
4028
|
+
const l = this.paintTarget(layerId, what);
|
|
4029
|
+
if (this.doc.stickers?.[l.id]?.length) throw new PaintEditorError(`Layer "${l.name}" is a sticker layer; ${what} another layer (add_layer).`);
|
|
4030
|
+
if (this.wet.wetLayerIds().includes(l.id)) throw new PaintEditorError(`Layer "${l.name}" holds wet paint; effects go on dried paint, so dry it first (dry_paint) and try again.`);
|
|
4031
|
+
return l;
|
|
4032
|
+
}
|
|
4033
|
+
/** After an effect rendered: the layer must be as it was when the render started. */
|
|
4034
|
+
sameLayerAfterRender(before) {
|
|
4035
|
+
this.requireIdle("applying an effect");
|
|
4036
|
+
const now = this.getLayer(before.id);
|
|
4037
|
+
if (!isPixelLayer(now)) throw new PaintEditorError(`Layer "${before.name}" was removed while the effect rendered.`);
|
|
4038
|
+
if (now.surface !== before.surface || now.offset.x !== before.offset.x || now.offset.y !== before.offset.y) throw new PaintEditorError(`Layer "${before.name}" changed while the effect rendered; apply the effect again.`);
|
|
4039
|
+
return now;
|
|
4040
|
+
}
|
|
4041
|
+
/** The canvas rectangle in a layer's own coordinates. */
|
|
4042
|
+
canvasRectOf(l) {
|
|
4043
|
+
return { x: -l.offset.x, y: -l.offset.y, width: this.doc.canvas.width, height: this.doc.canvas.height };
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Sets the effect paper: a provider generator drawn over the paper and under the paint, at the
|
|
4047
|
+
* canvas size (`multiply` by default, so the paper's colour and grain still show). `null` removes
|
|
4048
|
+
* it. One undo step. Returns the effect paper (without pixels), or `null`.
|
|
4049
|
+
*/
|
|
4050
|
+
async setPaperEffect(effect, options = {}) {
|
|
4051
|
+
this.requireIdle("changing the paper");
|
|
4052
|
+
if (effect === null) {
|
|
4053
|
+
if (!this.doc.paperEffect) return null;
|
|
4054
|
+
const { paperEffect: _p, ...rest } = this.doc;
|
|
4055
|
+
this.commit("Remove effect paper", rest);
|
|
4056
|
+
return null;
|
|
4057
|
+
}
|
|
4058
|
+
this.checkOpacity(options.opacity);
|
|
4059
|
+
this.checkTime(options.time);
|
|
4060
|
+
if (options.blend !== void 0 && !PAPER_EFFECT_BLENDS.includes(options.blend)) throw new PaintEditorError(`Unknown effect paper blend "${options.blend}"; use ${PAPER_EFFECT_BLENDS.join(", ")}.`);
|
|
4061
|
+
const d = await this.describeEffect(effect, "paper");
|
|
4062
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4063
|
+
const pixels = await this.renderEffectCanvas(d, values, options.time);
|
|
4064
|
+
this.requireIdle("changing the paper");
|
|
4065
|
+
const was = this.doc.paperEffect;
|
|
4066
|
+
const paperEffect = {
|
|
4067
|
+
...this.sourceOf(d, values, options.time),
|
|
4068
|
+
opacity: options.opacity ?? was?.opacity ?? 1,
|
|
4069
|
+
blend: options.blend ?? was?.blend ?? "multiply",
|
|
4070
|
+
image: { width: pixels.width, height: pixels.height, data: new Uint8ClampedArray(pixels.data) },
|
|
4071
|
+
version: nextPaperEffectVersion()
|
|
4072
|
+
};
|
|
4073
|
+
this.commit("Effect paper", { ...this.doc, paperEffect });
|
|
4074
|
+
return this.getPaperEffect();
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Changes the effect paper: new control `values` (merged over its own; `null` resets one) or
|
|
4078
|
+
* `time` draw it again at the canvas size; `opacity` and `blend` only change how it is drawn.
|
|
4079
|
+
* One undo step; with `commitToHistory` false it joins the previous effect paper step (a slider
|
|
4080
|
+
* drag). Returns the effect paper (without pixels).
|
|
4081
|
+
*/
|
|
4082
|
+
async updatePaperEffect(changes, commitToHistory = true) {
|
|
4083
|
+
this.requireIdle("changing the paper");
|
|
4084
|
+
const was = this.doc.paperEffect;
|
|
4085
|
+
if (!was) throw new PaintEditorError("There is no effect paper; set one with set_paper_effect.");
|
|
4086
|
+
this.checkOpacity(changes.opacity);
|
|
4087
|
+
this.checkTime(changes.time);
|
|
4088
|
+
if (changes.blend !== void 0 && !PAPER_EFFECT_BLENDS.includes(changes.blend)) throw new PaintEditorError(`Unknown effect paper blend "${changes.blend}"; use ${PAPER_EFFECT_BLENDS.join(", ")}.`);
|
|
4089
|
+
let next = { ...was, ...changes.opacity !== void 0 ? { opacity: changes.opacity } : {}, ...changes.blend !== void 0 ? { blend: changes.blend } : {} };
|
|
4090
|
+
const { width, height } = this.doc.canvas;
|
|
4091
|
+
if (changes.values !== void 0 || changes.time !== void 0 || was.image.width !== width || was.image.height !== height) {
|
|
4092
|
+
const d = await this.describeEffect(was.effect, "paper");
|
|
4093
|
+
const values = this.effectValues(d, was.values, changes.values);
|
|
4094
|
+
const time = changes.time ?? was.time ?? 0;
|
|
4095
|
+
const pixels = await this.renderEffectCanvas(d, values, time);
|
|
4096
|
+
this.requireIdle("changing the paper");
|
|
4097
|
+
if (this.doc.paperEffect !== was) throw new PaintEditorError("The effect paper changed while it rendered; change it again.");
|
|
4098
|
+
next = { ...next, ...this.sourceOf(d, values, time), image: { width: pixels.width, height: pixels.height, data: new Uint8ClampedArray(pixels.data) }, version: nextPaperEffectVersion() };
|
|
4099
|
+
if (!time) delete next.time;
|
|
4100
|
+
}
|
|
4101
|
+
this.commitOrAmend("Effect paper", { ...this.doc, paperEffect: next }, commitToHistory);
|
|
4102
|
+
return this.getPaperEffect();
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Adds an effect layer above the active layer (or `aboveId`): a pixel layer drawn by a provider
|
|
4106
|
+
* generator, overlay or title effect at the canvas size. It keeps its effect, so
|
|
4107
|
+
* `updateEffectLayer` draws it again. One undo step. Returns the layer and its effect.
|
|
4108
|
+
*/
|
|
4109
|
+
async addEffectLayer(effect, options = {}) {
|
|
4110
|
+
this.requireIdle("adding a layer");
|
|
4111
|
+
this.checkOpacity(options.opacity);
|
|
4112
|
+
this.checkTime(options.time);
|
|
4113
|
+
if (options.blend !== void 0 && !isBlendMode(options.blend)) throw new PaintEditorError(`Unknown blend mode "${options.blend}". Call get_canvas_info for the blend modes.`);
|
|
4114
|
+
if (options.aboveId !== void 0) this.requireLayer(options.aboveId);
|
|
4115
|
+
const d = await this.describeEffect(effect, "layer");
|
|
4116
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4117
|
+
const pixels = await this.renderEffectCanvas(d, values, options.time);
|
|
4118
|
+
this.requireIdle("adding a layer");
|
|
4119
|
+
const above = options.aboveId ?? this.doc.activeLayerId;
|
|
4120
|
+
const loc = above ? findLayer(this.doc.layers, above) : null;
|
|
4121
|
+
if (options.aboveId !== void 0 && !loc) throw new PaintEditorError(`Layer "${options.aboveId}" was removed while the effect rendered.`);
|
|
4122
|
+
const layer = { ...createPaintLayer(options.name?.trim() || d.name), surface: surfaceFromPixels(pixels, 4), opacity: options.opacity ?? 1, blend: options.blend ?? "normal" };
|
|
4123
|
+
const layers = insertLayer(this.doc.layers, layer, loc?.parent?.id ?? null, loc ? loc.index + 1 : void 0);
|
|
4124
|
+
const source = this.sourceOf(d, values, options.time);
|
|
4125
|
+
this.commit("Add effect layer", { ...this.doc, layers, activeLayerId: layer.id, effectLayers: { ...this.doc.effectLayers ?? {}, [layer.id]: source } });
|
|
4126
|
+
return { layer: this.describeLayer(layer), source };
|
|
4127
|
+
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Draws an effect layer again with new `values` (merged over its own; `null` resets one) or
|
|
4130
|
+
* `time`, at the canvas size, replacing its pixels (paint on it is lost). One undo step; with
|
|
4131
|
+
* `commitToHistory` false it joins the previous effect layer step (a slider drag).
|
|
4132
|
+
*/
|
|
4133
|
+
async updateEffectLayer(layerId, changes, commitToHistory = true) {
|
|
4134
|
+
this.requireIdle("changing an effect layer");
|
|
4135
|
+
const was = this.getEffectLayer(layerId);
|
|
4136
|
+
if (!was) throw new PaintEditorError(`"${layerId}" is not an effect layer; add one with add_effect_layer.`);
|
|
4137
|
+
this.checkTime(changes.time);
|
|
4138
|
+
const l = this.effectTarget(layerId, "draw the effect on");
|
|
4139
|
+
const d = await this.describeEffect(was.effect, "layer");
|
|
4140
|
+
const values = this.effectValues(d, was.values, changes.values);
|
|
4141
|
+
const time = changes.time ?? was.time ?? 0;
|
|
4142
|
+
const pixels = await this.renderEffectCanvas(d, values, time);
|
|
4143
|
+
const now = this.getLayer(l.id);
|
|
4144
|
+
this.requireIdle("changing an effect layer");
|
|
4145
|
+
if (!isPixelLayer(now)) throw new PaintEditorError(`Layer "${l.name}" was removed while the effect rendered.`);
|
|
4146
|
+
const source = this.sourceOf(d, values, time);
|
|
4147
|
+
this.commitOrAmend("Effect layer", { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...now, surface: surfaceFromPixels(pixels, 4), offset: { x: 0, y: 0 } })), effectLayers: { ...this.doc.effectLayers ?? {}, [l.id]: source } }, commitToHistory);
|
|
4148
|
+
return source;
|
|
4149
|
+
}
|
|
4150
|
+
/** Turns an effect layer into a plain pixel layer, keeping its pixels (one undo step). */
|
|
4151
|
+
rasterizeEffectLayer(layerId) {
|
|
4152
|
+
this.requireIdle("changing an effect layer");
|
|
4153
|
+
if (!this.getEffectLayer(layerId)) throw new PaintEditorError(`"${layerId}" is not an effect layer; list_applied_effects lists them.`);
|
|
4154
|
+
const { [layerId]: _gone, ...rest } = this.doc.effectLayers ?? {};
|
|
4155
|
+
this.commit("Rasterize effect layer", { ...this.doc, effectLayers: rest });
|
|
4156
|
+
return this.describeLayer(this.requireLayer(layerId));
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Fills a layer with a provider generator, overlay or title effect: its pixels, rendered at the
|
|
4160
|
+
* canvas size, are composited onto the layer's paint at `opacity` (only where it is already
|
|
4161
|
+
* painted with `lockTransparency`, or when the layer's transparency is locked). One undo step.
|
|
4162
|
+
*/
|
|
4163
|
+
async fillLayerWithEffect(effect, options = {}) {
|
|
4164
|
+
this.requireIdle("filling");
|
|
4165
|
+
this.checkOpacity(options.opacity);
|
|
4166
|
+
this.checkTime(options.time);
|
|
4167
|
+
const l = this.effectTarget(options.layerId, "fill");
|
|
4168
|
+
const d = await this.describeEffect(effect, "fill");
|
|
4169
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4170
|
+
const pixels = await this.renderEffectCanvas(d, values, options.time);
|
|
4171
|
+
const now = this.sameLayerAfterRender(l);
|
|
4172
|
+
const rect = this.canvasRectOf(now);
|
|
4173
|
+
const before = readRegion(now.surface, rect);
|
|
4174
|
+
let after = overPixels(before, pixels.data, options.opacity ?? 1);
|
|
4175
|
+
if (options.lockTransparency || isLayerLocked(now, "transparency")) after = mixPixels(before, after, 1);
|
|
4176
|
+
const surface = writeRegion(now.surface, rect, after);
|
|
4177
|
+
this.commit("Effect fill", { ...this.doc, layers: mapLayer(this.doc.layers, now.id, () => ({ ...now, surface })) });
|
|
4178
|
+
return { layerId: now.id, source: this.sourceOf(d, values, options.time) };
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Runs a filter effect (light, glow, colour grade …) on a layer's dried paint and bakes it in, at
|
|
4182
|
+
* `amount` strength (0..1, default 1). Transparent areas stay transparent; thick paint keeps its
|
|
4183
|
+
* relief, and an effect with a `height` input gets the relief as a height map. Wet paint is
|
|
4184
|
+
* refused (dry it first). One undo step.
|
|
4185
|
+
*/
|
|
4186
|
+
async applyLightEffect(layerId, effect, options = {}) {
|
|
4187
|
+
this.requireIdle("applying an effect");
|
|
4188
|
+
const amount = options.amount ?? 1;
|
|
4189
|
+
if (!(amount >= 0 && amount <= 1)) throw new PaintEditorError(`amount ${options.amount} is out of range; use 0..1.`);
|
|
4190
|
+
this.checkTime(options.time);
|
|
4191
|
+
const l = this.effectTarget(layerId, "light");
|
|
4192
|
+
if (!contentBounds(l.surface)) throw new PaintEditorError(`Layer "${l.name}" has no paint to light; paint on it first, or add an effect layer (add_effect_layer).`);
|
|
4193
|
+
const d = await this.describeEffect(effect, "light");
|
|
4194
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4195
|
+
const rect = this.canvasRectOf(l);
|
|
4196
|
+
const before = readRegion(l.surface, rect);
|
|
4197
|
+
const inputs = { source: { width: rect.width, height: rect.height, data: before } };
|
|
4198
|
+
const heightInput = d.inputs.find((i) => i.kind === "image" && /^(height|depth|relief)$/i.test(i.name));
|
|
4199
|
+
const relief = this.doc.thick?.[l.id];
|
|
4200
|
+
if (heightInput && relief) {
|
|
4201
|
+
const h = readRegion(relief.height, rect);
|
|
4202
|
+
const rgba = new Uint8ClampedArray(rect.width * rect.height * 4);
|
|
4203
|
+
for (let i = 0; i < h.length; i++) rgba.set([h[i], h[i], h[i], 255], i * 4);
|
|
4204
|
+
inputs[heightInput.name] = { width: rect.width, height: rect.height, data: rgba };
|
|
4205
|
+
}
|
|
4206
|
+
const sourceInput = d.inputs.find((i) => i.kind === "image" && i.name !== heightInput?.name)?.name ?? "source";
|
|
4207
|
+
if (sourceInput !== "source") {
|
|
4208
|
+
inputs[sourceInput] = inputs.source;
|
|
4209
|
+
delete inputs.source;
|
|
4210
|
+
}
|
|
4211
|
+
const out = await this.effectsRenderer.render(this.effects.require(), d.id, { width: rect.width, height: rect.height }, { time: options.time ?? 0, values, inputs });
|
|
4212
|
+
const now = this.sameLayerAfterRender(l);
|
|
4213
|
+
const surface = writeRegion(now.surface, rect, mixPixels(before, out.data, amount));
|
|
4214
|
+
this.commit("Light effect", { ...this.doc, layers: mapLayer(this.doc.layers, now.id, () => ({ ...now, surface })) });
|
|
4215
|
+
return { layerId: now.id, source: this.sourceOf(d, values, options.time), amount };
|
|
4216
|
+
}
|
|
3815
4217
|
// --- Brushes -------------------------------------------------------------------
|
|
3816
4218
|
findBrush(id) {
|
|
3817
4219
|
return BUILTIN_BRUSHES.find((b) => b.id === id) ?? this.userBrushes.find((b) => b.id === id);
|
|
@@ -4464,6 +4866,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4464
4866
|
/** Stops the wet simulation's worker threads (call when the editor goes away). */
|
|
4465
4867
|
dispose() {
|
|
4466
4868
|
this.wet.destroy();
|
|
4869
|
+
this.effectsRenderer.dispose();
|
|
4467
4870
|
}
|
|
4468
4871
|
/** Resolves when wet paint is between steps (edits, undo and exports can run). */
|
|
4469
4872
|
wetIdle() {
|
|
@@ -4651,6 +5054,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4651
5054
|
if (!t || !t.visible) return [];
|
|
4652
5055
|
return [this.tracingNode(t)];
|
|
4653
5056
|
}
|
|
5057
|
+
/** The effect paper as a compositor layer (one object per effect paper, so caches hold). */
|
|
5058
|
+
paperEffectNode(p) {
|
|
5059
|
+
const c = this.paperEffectNodeCache;
|
|
5060
|
+
if (c && c.image === p.image && c.opacity === p.opacity && c.blend === p.blend) return c;
|
|
5061
|
+
const node = { type: "image", id: PAPER_EFFECT_NODE_ID, name: "Effect paper", visible: true, opacity: p.opacity, blend: p.blend, image: p.image, version: p.version };
|
|
5062
|
+
this.paperEffectNodeCache = node;
|
|
5063
|
+
return node;
|
|
5064
|
+
}
|
|
4654
5065
|
/** The tracing image resampled onto the canvas (cached until it or the canvas changes). */
|
|
4655
5066
|
tracingNode(t) {
|
|
4656
5067
|
const { width, height } = this.doc.canvas;
|
|
@@ -4700,6 +5111,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4700
5111
|
layers = picked.map((l) => ({ ...l, visible: true }));
|
|
4701
5112
|
}
|
|
4702
5113
|
const under = options.paper === false ? [] : [paperNode(this.doc.paper, this.lighting)];
|
|
5114
|
+
if (options.paper !== false && this.doc.paperEffect) under.push(this.paperEffectNode(this.doc.paperEffect));
|
|
4703
5115
|
if (view) under.push(...this.underlayLayers());
|
|
4704
5116
|
return [...under, ...layers];
|
|
4705
5117
|
}
|
|
@@ -4774,6 +5186,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4774
5186
|
for (const [id, list] of Object.entries(this.doc.stickers ?? {})) if (list.length && findLayer(this.doc.layers, id)) stickers[id] = list.map((x) => ({ ...x }));
|
|
4775
5187
|
if (Object.keys(stickers).length) ext.stickers = stickers;
|
|
4776
5188
|
else delete ext.stickers;
|
|
5189
|
+
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
5190
|
+
if (effectLayers) ext.effectLayers = effectLayers;
|
|
5191
|
+
else delete ext.effectLayers;
|
|
5192
|
+
if (this.doc.paperEffect) ext.paperEffect = paperEffectToJSON(this.doc.paperEffect);
|
|
5193
|
+
else delete ext.paperEffect;
|
|
4777
5194
|
if (this.lighting.azimuth !== DEFAULT_LIGHTING.azimuth || this.lighting.elevation !== DEFAULT_LIGHTING.elevation || this.lighting.relief !== DEFAULT_LIGHTING.relief) ext.lighting = { ...this.lighting };
|
|
4778
5195
|
else delete ext.lighting;
|
|
4779
5196
|
return ext;
|
|
@@ -4792,6 +5209,23 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4792
5209
|
}
|
|
4793
5210
|
if (Object.keys(stickers).length) state = { ...state, stickers };
|
|
4794
5211
|
}
|
|
5212
|
+
if (ext.effectLayers && typeof ext.effectLayers === "object") {
|
|
5213
|
+
const effectLayers = {};
|
|
5214
|
+
for (const [id, v] of Object.entries(ext.effectLayers)) {
|
|
5215
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
5216
|
+
const source = readPaintEffectSource(v);
|
|
5217
|
+
if (source) effectLayers[id] = source;
|
|
5218
|
+
else problems.push(`The effect of layer "${id}" could not be read; it loads as a plain layer.`);
|
|
5219
|
+
}
|
|
5220
|
+
if (Object.keys(effectLayers).length) state = { ...state, effectLayers };
|
|
5221
|
+
}
|
|
5222
|
+
if (ext.paperEffect !== void 0) {
|
|
5223
|
+
try {
|
|
5224
|
+
state = { ...state, paperEffect: paperEffectFromJSON(ext.paperEffect, state.canvas.width, state.canvas.height) };
|
|
5225
|
+
} catch (e) {
|
|
5226
|
+
problems.push(`The effect paper could not be read (${e.message}); the plain paper shows.`);
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
4795
5229
|
const raw = ext.layerChannels;
|
|
4796
5230
|
if (!raw || typeof raw !== "object") return state;
|
|
4797
5231
|
const thick = {};
|
|
@@ -4828,6 +5262,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4828
5262
|
delete this.extensions.layerChannels;
|
|
4829
5263
|
delete this.extensions.lighting;
|
|
4830
5264
|
delete this.extensions.stickers;
|
|
5265
|
+
delete this.extensions.effectLayers;
|
|
5266
|
+
delete this.extensions.paperEffect;
|
|
4831
5267
|
this.viewCompositor.clear();
|
|
4832
5268
|
this.emit();
|
|
4833
5269
|
return loaded.problems;
|
|
@@ -5025,7 +5461,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5025
5461
|
/** Adds a floating reference image to look at and pick colours from. */
|
|
5026
5462
|
addReferenceImage(options) {
|
|
5027
5463
|
const { src, pixels } = this.importImage(options, "The reference image");
|
|
5028
|
-
const
|
|
5464
|
+
const ref2 = {
|
|
5029
5465
|
id: `ref${this.nextReferenceId++}`,
|
|
5030
5466
|
name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,
|
|
5031
5467
|
src,
|
|
@@ -5036,8 +5472,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5036
5472
|
displayWidth: options.displayWidth ?? Math.min(240, pixels.width),
|
|
5037
5473
|
visible: true
|
|
5038
5474
|
};
|
|
5039
|
-
this.setGuides({ ...this.guides, references: [...this.guides.references,
|
|
5040
|
-
return
|
|
5475
|
+
this.setGuides({ ...this.guides, references: [...this.guides.references, ref2] });
|
|
5476
|
+
return ref2;
|
|
5041
5477
|
}
|
|
5042
5478
|
requireReference(id) {
|
|
5043
5479
|
const r = this.guides.references.find((x) => x.id === id);
|
|
@@ -5101,12 +5537,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5101
5537
|
}
|
|
5102
5538
|
}
|
|
5103
5539
|
if (Array.isArray(r.references)) {
|
|
5104
|
-
for (const
|
|
5105
|
-
if (!
|
|
5540
|
+
for (const ref2 of r.references) {
|
|
5541
|
+
if (!ref2 || typeof ref2.src !== "string" || typeof ref2.id !== "string") continue;
|
|
5106
5542
|
try {
|
|
5107
|
-
const px = this.decodeImage(
|
|
5108
|
-
guides.references.push({ id:
|
|
5109
|
-
const n = Number(/^ref(\d+)$/.exec(
|
|
5543
|
+
const px = this.decodeImage(ref2.src, `Reference "${ref2.name}"`);
|
|
5544
|
+
guides.references.push({ id: ref2.id, name: String(ref2.name ?? ref2.id), src: ref2.src, width: px.width, height: px.height, x: Number(ref2.x) || 0, y: Number(ref2.y) || 0, displayWidth: Number(ref2.displayWidth) || 240, visible: ref2.visible !== false });
|
|
5545
|
+
const n = Number(/^ref(\d+)$/.exec(ref2.id)?.[1] ?? 0);
|
|
5110
5546
|
if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;
|
|
5111
5547
|
} catch (e) {
|
|
5112
5548
|
problems.push(e.message);
|
|
@@ -7938,6 +8374,176 @@ function quickStencil(kind, canvas) {
|
|
|
7938
8374
|
return { kind: "line", from: { x: w / 2, y: 0 }, to: { x: w / 2, y: h } };
|
|
7939
8375
|
}
|
|
7940
8376
|
}
|
|
8377
|
+
|
|
8378
|
+
// src/paint-editor/effect-tools.ts
|
|
8379
|
+
var PAPER_TARGET = "paper";
|
|
8380
|
+
var ref = (s) => ({ effect: s.effect, ...s.name ? { name: s.name } : {}, ...s.provider ? { provider: s.provider } : {} });
|
|
8381
|
+
function createPaintEffectsToolProvider(controller, options = {}) {
|
|
8382
|
+
const readOnlyReason = () => {
|
|
8383
|
+
const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
|
|
8384
|
+
return ro ? typeof ro === "string" ? ro : "The painting is read-only." : null;
|
|
8385
|
+
};
|
|
8386
|
+
const paperInfo = () => {
|
|
8387
|
+
const p = controller.getPaperEffect();
|
|
8388
|
+
return p ? [{ id: PAPER_TARGET, ...ref(p), role: "effect paper", enabled: true, opacity: p.opacity, blend: p.blend, values: p.values, ...p.time ? { time: p.time } : {} }] : [];
|
|
8389
|
+
};
|
|
8390
|
+
const layerInfo = (layerId) => {
|
|
8391
|
+
const s = controller.getEffectLayer(layerId);
|
|
8392
|
+
return s ? [{ id: "effect", ...ref(s), role: "effect layer", enabled: true, values: s.values, ...s.time ? { time: s.time } : {} }] : [];
|
|
8393
|
+
};
|
|
8394
|
+
const requireLayer = (id) => {
|
|
8395
|
+
const l = controller.getLayer(id);
|
|
8396
|
+
if (!l) {
|
|
8397
|
+
const ids = controller.listLayers().map((x) => `"${x.id}"`);
|
|
8398
|
+
throw new Error(`There is no layer "${id}"; the targets are "${PAPER_TARGET}" and the layers ${ids.join(", ") || "(none)"} (list_layers lists them).`);
|
|
8399
|
+
}
|
|
8400
|
+
return l;
|
|
8401
|
+
};
|
|
8402
|
+
const effectsOf = (target) => target === PAPER_TARGET ? paperInfo() : layerInfo(target);
|
|
8403
|
+
const instanceCheck = (target, instance) => {
|
|
8404
|
+
const want = target === PAPER_TARGET ? PAPER_TARGET : "effect";
|
|
8405
|
+
if (target !== PAPER_TARGET) requireLayer(target);
|
|
8406
|
+
if (instance !== want || !effectsOf(target).length) {
|
|
8407
|
+
const have = effectsOf(target).map((e) => `"${e.id}"`);
|
|
8408
|
+
throw new Error(
|
|
8409
|
+
`${target === PAPER_TARGET ? "The paper" : `Layer "${target}"`} has no provider effect "${instance}"; its effects are ${have.join(", ") || "none"} (list_applied_effects). Light baked into paint is part of the pixels: undo takes it back.`
|
|
8410
|
+
);
|
|
8411
|
+
}
|
|
8412
|
+
};
|
|
8413
|
+
const noStackArgs = (changes) => {
|
|
8414
|
+
if (changes.index !== void 0) throw new Error("index does not apply: the paper and each effect layer have one effect.");
|
|
8415
|
+
if (changes.enabled !== void 0) throw new Error("enabled does not apply here: hide an effect layer with update_layer visible false, or remove the effect paper.");
|
|
8416
|
+
};
|
|
8417
|
+
const values = (v) => v;
|
|
8418
|
+
return createEffectToolProvider({
|
|
8419
|
+
id: options.id ?? "paint-effects",
|
|
8420
|
+
namespace: options.namespace,
|
|
8421
|
+
target: { noun: "layer", id: `A pixel layer id from list_layers ("${PAPER_TARGET}" is the effect paper, for list, update and remove).` },
|
|
8422
|
+
applyUses: ["filter"],
|
|
8423
|
+
applyDetails: "Light on dried paint: the filter (search_effects with use filter, e.g. a light, glow or grade) runs on the layer's paint at the canvas size and is baked into its pixels; transparent areas stay transparent, and thick paint keeps its relief (an effect with a height input gets it). opacity is the strength (default 1). Wet paint is refused: dry it first (dry_paint). It is one undo step and is not listed afterwards (it is part of the pixels). Generators go on the paper (set_paper_effect), on their own layer (add_effect_layer) or into a layer (fill_layer_with_effect).",
|
|
8424
|
+
provider: () => controller.effectsProvider,
|
|
8425
|
+
readOnlyReason,
|
|
8426
|
+
list: (target) => {
|
|
8427
|
+
if (target === PAPER_TARGET) return [{ target: PAPER_TARGET, name: "Paper", effects: paperInfo() }];
|
|
8428
|
+
if (target) {
|
|
8429
|
+
const l = requireLayer(target);
|
|
8430
|
+
return [{ target, name: l.name, effects: layerInfo(target) }];
|
|
8431
|
+
}
|
|
8432
|
+
const out = [];
|
|
8433
|
+
if (paperInfo().length) out.push({ target: PAPER_TARGET, name: "Paper", effects: paperInfo() });
|
|
8434
|
+
for (const e of controller.listEffectLayers()) out.push({ target: e.layerId, name: e.name, effects: layerInfo(e.layerId) });
|
|
8435
|
+
return out;
|
|
8436
|
+
},
|
|
8437
|
+
apply: async (target, effect, args) => {
|
|
8438
|
+
if (target === PAPER_TARGET) throw new Error("The paper takes generator effects through set_paper_effect; apply_effect runs a filter on a layer's paint.");
|
|
8439
|
+
noStackArgs(args);
|
|
8440
|
+
requireLayer(target);
|
|
8441
|
+
const r = await controller.applyLightEffect(target, effect, { values: values(args.values), amount: args.opacity });
|
|
8442
|
+
const applied = { id: "light", ...ref(r.source), role: "baked light", enabled: true, opacity: r.amount, values: r.source.values };
|
|
8443
|
+
return { target, effect: applied, effects: layerInfo(target), baked: true };
|
|
8444
|
+
},
|
|
8445
|
+
update: async (target, instance, changes) => {
|
|
8446
|
+
instanceCheck(target, instance);
|
|
8447
|
+
noStackArgs(changes);
|
|
8448
|
+
if (target === PAPER_TARGET) {
|
|
8449
|
+
await controller.updatePaperEffect({ values: changes.values, opacity: changes.opacity });
|
|
8450
|
+
const effects2 = paperInfo();
|
|
8451
|
+
return { target, effect: effects2[0], effects: effects2 };
|
|
8452
|
+
}
|
|
8453
|
+
if (changes.opacity !== void 0) throw new Error("An effect layer's opacity is the layer's: change it with update_layer.");
|
|
8454
|
+
await controller.updateEffectLayer(target, { values: changes.values });
|
|
8455
|
+
const effects = layerInfo(target);
|
|
8456
|
+
return { target, effect: effects[0], effects };
|
|
8457
|
+
},
|
|
8458
|
+
remove: async (target, instance) => {
|
|
8459
|
+
instanceCheck(target, instance);
|
|
8460
|
+
const was = effectsOf(target)[0];
|
|
8461
|
+
if (target === PAPER_TARGET) await controller.setPaperEffect(null);
|
|
8462
|
+
else controller.rasterizeEffectLayer(target);
|
|
8463
|
+
return { target, effect: was, effects: [], ...target === PAPER_TARGET ? {} : { keptPixels: true } };
|
|
8464
|
+
},
|
|
8465
|
+
subscribe: (notify) => {
|
|
8466
|
+
const a = controller.subscribe(() => notify());
|
|
8467
|
+
const b = controller.effects.subscribe(() => notify());
|
|
8468
|
+
return () => {
|
|
8469
|
+
a();
|
|
8470
|
+
b();
|
|
8471
|
+
};
|
|
8472
|
+
},
|
|
8473
|
+
extraTools: (ctx) => [
|
|
8474
|
+
{
|
|
8475
|
+
name: "set_paper_effect",
|
|
8476
|
+
title: "Set the effect paper",
|
|
8477
|
+
description: `Draws a provider generator (search_effects with use generator) over the paper and under the paint, at the canvas size: multiplied by default, so the paper's colour and grain still show. Replaces any effect paper; it is saved and exported with the painting. effect null removes it. One undo step. Returns the effect paper (update_effect / remove_effect with target and effect "paper" change it later).`,
|
|
8478
|
+
inputSchema: {
|
|
8479
|
+
type: "object",
|
|
8480
|
+
properties: {
|
|
8481
|
+
effect: { anyOf: [ctx.effectIdSchema(["generator"]), { type: "null" }], description: "A generator effect id from search_effects, or null to remove the effect paper." },
|
|
8482
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
8483
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Strength 0\u20131 (default 1, or the current effect paper's)." },
|
|
8484
|
+
blend: { type: "string", enum: [...PAPER_EFFECT_BLENDS], description: "How it draws over the paper (default multiply, which keeps the paper's grain; normal covers it)." },
|
|
8485
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." }
|
|
8486
|
+
},
|
|
8487
|
+
required: ["effect"],
|
|
8488
|
+
additionalProperties: false
|
|
8489
|
+
},
|
|
8490
|
+
annotations: { title: "Set the effect paper", ...ctx.annotations.writeIdempotent },
|
|
8491
|
+
handler: async (args) => {
|
|
8492
|
+
if (args.effect !== null) ctx.requireProvider();
|
|
8493
|
+
return { paperEffect: await controller.setPaperEffect(args.effect, { values: values(args.values), opacity: args.opacity, blend: args.blend, time: args.time }) };
|
|
8494
|
+
}
|
|
8495
|
+
},
|
|
8496
|
+
{
|
|
8497
|
+
name: "add_effect_layer",
|
|
8498
|
+
title: "Add an effect layer",
|
|
8499
|
+
description: 'Adds a pixel layer drawn by a provider generator, overlay or title effect (search_effects with use generator, overlay or title) at the canvas size, above the active layer unless aboveId is given, and makes it active. Paint on it like any layer; update_effect (effect "effect") draws it again with new values, replacing its pixels; remove_effect keeps its pixels as a plain layer. One undo step. Returns the new layer and its effect.',
|
|
8500
|
+
inputSchema: {
|
|
8501
|
+
type: "object",
|
|
8502
|
+
properties: {
|
|
8503
|
+
effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
|
|
8504
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
8505
|
+
name: { type: "string", minLength: 1, description: "The layer name (default: the effect's name)." },
|
|
8506
|
+
aboveId: { type: "string", minLength: 1, description: "Put it directly above this layer (default: above the active layer, else on top)." },
|
|
8507
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131 (default 1)." },
|
|
8508
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Layer blend mode (default normal); screen or linear-dodge for light, multiply for textures." },
|
|
8509
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." }
|
|
8510
|
+
},
|
|
8511
|
+
required: ["effect"],
|
|
8512
|
+
additionalProperties: false
|
|
8513
|
+
},
|
|
8514
|
+
annotations: { title: "Add an effect layer", ...ctx.annotations.write },
|
|
8515
|
+
handler: async (args) => {
|
|
8516
|
+
ctx.requireProvider();
|
|
8517
|
+
const r = await controller.addEffectLayer(args.effect, { values: values(args.values), name: args.name, aboveId: args.aboveId, opacity: args.opacity, blend: args.blend, time: args.time });
|
|
8518
|
+
return { layer: r.layer, effects: layerInfo(r.layer.id) };
|
|
8519
|
+
}
|
|
8520
|
+
},
|
|
8521
|
+
{
|
|
8522
|
+
name: "fill_layer_with_effect",
|
|
8523
|
+
title: "Fill a layer with an effect",
|
|
8524
|
+
description: "Composites a provider generator, overlay or title effect (rendered at the canvas size) onto a layer's pixels at opacity, as a fill: the layer keeps no link to the effect. lockTransparency (or a transparency-locked layer) only colours what is already painted. Wet paint is refused: dry it first (dry_paint). One undo step. Returns the layer id and the effect used.",
|
|
8525
|
+
inputSchema: {
|
|
8526
|
+
type: "object",
|
|
8527
|
+
properties: {
|
|
8528
|
+
effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
|
|
8529
|
+
layerId: { type: "string", minLength: 1, description: "The pixel layer to fill (default: the active layer)." },
|
|
8530
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
8531
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Fill strength 0\u20131 (default 1)." },
|
|
8532
|
+
lockTransparency: { type: "boolean", description: "Only colour pixels that are already painted (default false)." },
|
|
8533
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." }
|
|
8534
|
+
},
|
|
8535
|
+
required: ["effect"],
|
|
8536
|
+
additionalProperties: false
|
|
8537
|
+
},
|
|
8538
|
+
annotations: { title: "Fill a layer with an effect", ...ctx.annotations.write },
|
|
8539
|
+
handler: async (args) => {
|
|
8540
|
+
ctx.requireProvider();
|
|
8541
|
+
return controller.fillLayerWithEffect(args.effect, { layerId: args.layerId, values: values(args.values), opacity: args.opacity, lockTransparency: args.lockTransparency, time: args.time });
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
]
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
7941
8547
|
export {
|
|
7942
8548
|
BLEND_MENU_GROUPS,
|
|
7943
8549
|
BLEND_MODES,
|
|
@@ -7991,6 +8597,7 @@ export {
|
|
|
7991
8597
|
PAINT_BRUSH_CATEGORIES,
|
|
7992
8598
|
PAINT_BRUSH_ENGINES,
|
|
7993
8599
|
PAINT_COLOR_JSON_SCHEMA,
|
|
8600
|
+
PAINT_EFFECT_PLACES,
|
|
7994
8601
|
PAINT_FORMAT,
|
|
7995
8602
|
PAINT_GRADIENT_FULL_SPEED,
|
|
7996
8603
|
PAINT_GRADIENT_JSON_SCHEMA,
|
|
@@ -8005,10 +8612,13 @@ export {
|
|
|
8005
8612
|
PAINT_SCHEMA_VERSION,
|
|
8006
8613
|
PAINT_SOURCE_JSON_SCHEMA,
|
|
8007
8614
|
PAINT_TOOLS,
|
|
8615
|
+
PAPER_EFFECT_BLENDS,
|
|
8616
|
+
PAPER_EFFECT_NODE_ID,
|
|
8008
8617
|
PAPER_FAMILIES,
|
|
8009
8618
|
PAPER_LIBRARY,
|
|
8010
8619
|
PAPER_NODE_ID,
|
|
8011
8620
|
PAPER_SLIDERS,
|
|
8621
|
+
PAPER_TARGET,
|
|
8012
8622
|
PARTICLE_KINDS,
|
|
8013
8623
|
PARTICLE_KIND_LABELS,
|
|
8014
8624
|
PARTICLE_RENDERS,
|
|
@@ -8019,6 +8629,7 @@ export {
|
|
|
8019
8629
|
PaintDocumentError,
|
|
8020
8630
|
PaintEditorController,
|
|
8021
8631
|
PaintEditorError,
|
|
8632
|
+
PaintEffectsRenderer,
|
|
8022
8633
|
PaintGesture,
|
|
8023
8634
|
PaintGradientSampler,
|
|
8024
8635
|
PaintPresence,
|
|
@@ -8065,6 +8676,7 @@ export {
|
|
|
8065
8676
|
brushTakesPaint,
|
|
8066
8677
|
brushToJSON,
|
|
8067
8678
|
buildTimeLapse,
|
|
8679
|
+
checkPaintEffectPlace,
|
|
8068
8680
|
clonePaper,
|
|
8069
8681
|
colorHarmony,
|
|
8070
8682
|
composeAffine,
|
|
@@ -8072,6 +8684,7 @@ export {
|
|
|
8072
8684
|
createPaintCompositor,
|
|
8073
8685
|
createPaintDocument,
|
|
8074
8686
|
createPaintEditorToolProvider,
|
|
8687
|
+
createPaintEffectsToolProvider,
|
|
8075
8688
|
createPaintLayer,
|
|
8076
8689
|
createPaintReplayTools,
|
|
8077
8690
|
createPaintStickerTools,
|
|
@@ -8088,6 +8701,7 @@ export {
|
|
|
8088
8701
|
downsampleRgba,
|
|
8089
8702
|
drawSticker,
|
|
8090
8703
|
drawWaterOverlay,
|
|
8704
|
+
effectLayersToJSON,
|
|
8091
8705
|
emptiedLayers,
|
|
8092
8706
|
emptyThickChannels,
|
|
8093
8707
|
fitZoom,
|
|
@@ -8112,6 +8726,7 @@ export {
|
|
|
8112
8726
|
lightingKey,
|
|
8113
8727
|
mapPenPoint,
|
|
8114
8728
|
mix32,
|
|
8729
|
+
mixPixels,
|
|
8115
8730
|
mixerLoadForStroke,
|
|
8116
8731
|
mixerTip,
|
|
8117
8732
|
multiColorForDab,
|
|
@@ -8120,6 +8735,7 @@ export {
|
|
|
8120
8735
|
multiColorSliders,
|
|
8121
8736
|
multiColorToPaint,
|
|
8122
8737
|
newPaintLayerId,
|
|
8738
|
+
nextPaperEffectVersion,
|
|
8123
8739
|
normalizeBehavior,
|
|
8124
8740
|
normalizeBristle,
|
|
8125
8741
|
normalizeBrushStroke,
|
|
@@ -8142,11 +8758,14 @@ export {
|
|
|
8142
8758
|
normalizeView,
|
|
8143
8759
|
normalizeWetBrush,
|
|
8144
8760
|
normalizeWetParams,
|
|
8761
|
+
overPixels,
|
|
8145
8762
|
padSampleRadius,
|
|
8146
8763
|
paintGradientMappingFor,
|
|
8147
8764
|
paintSourceColor,
|
|
8148
8765
|
paintSourceCss,
|
|
8149
8766
|
paintToMultiColor,
|
|
8767
|
+
paperEffectFromJSON,
|
|
8768
|
+
paperEffectToJSON,
|
|
8150
8769
|
paperGrain,
|
|
8151
8770
|
paperKey,
|
|
8152
8771
|
paperNode,
|
|
@@ -8163,6 +8782,7 @@ export {
|
|
|
8163
8782
|
quickStencil,
|
|
8164
8783
|
rand01,
|
|
8165
8784
|
randSigned,
|
|
8785
|
+
readPaintEffectSource,
|
|
8166
8786
|
readPaintSource,
|
|
8167
8787
|
readPath,
|
|
8168
8788
|
readSwatch,
|