@pixerate/editor 0.242.0 → 0.244.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-editor/index.cjs +59 -59
- package/dist/animation-editor/index.js +3 -3
- package/dist/{brushstrokeRender-MOYGEXOC.js → brushstrokeRender-4542T5HP.js} +3 -3
- package/dist/{brushstrokeRender-UKADOV5D.cjs → brushstrokeRender-QA5WVIDT.cjs} +5 -5
- package/dist/canvas/index.cjs +8 -8
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-ILM6GYAX.js → chunk-4EQY4TZJ.js} +1 -1
- package/dist/{chunk-5BR7LC24.cjs → chunk-4T47Z4J3.cjs} +68 -6
- package/dist/{chunk-ZDPLZDTT.js → chunk-5RFENYO5.js} +1 -1
- package/dist/{chunk-NE6OOKWJ.js → chunk-7YQNKSQ7.js} +1 -1
- package/dist/{chunk-AFLMXITL.cjs → chunk-BDIH4DYR.cjs} +23 -23
- package/dist/{chunk-HGGJ36HR.cjs → chunk-HZ547SGQ.cjs} +1 -1
- package/dist/{chunk-KCMV2534.cjs → chunk-IZL5V5RK.cjs} +2 -2
- package/dist/{chunk-QP2B3MID.js → chunk-L5PB7EG3.js} +1 -1
- package/dist/{chunk-HW72ABGR.cjs → chunk-M6JORA5Z.cjs} +86 -86
- package/dist/{chunk-UDBDYZET.js → chunk-OSTKS3YN.js} +1 -1
- package/dist/{chunk-HLBTRSQJ.js → chunk-P3NIPIKS.js} +263 -63
- package/dist/{chunk-7F4G74UQ.cjs → chunk-SD23XMVJ.cjs} +35 -35
- package/dist/{chunk-XBZK47LV.js → chunk-TBJEMELS.js} +67 -5
- package/dist/{chunk-XFYLA3ZO.cjs → chunk-VMROIWLL.cjs} +290 -90
- package/dist/image-editor/index.cjs +785 -777
- package/dist/image-editor/index.js +13 -5
- package/dist/kernel-wasm-4KVONDAU.js +9 -0
- package/dist/kernel-wasm-TTYRBGAK.cjs +9 -0
- package/dist/paint/index.cjs +10 -2
- package/dist/paint/index.js +9 -1
- package/dist/paint/wet/film.d.cts +19 -1
- package/dist/paint/wet/film.d.ts +19 -1
- package/dist/paint/wet/gpu-kernels.d.cts +2 -2
- package/dist/paint/wet/gpu-kernels.d.ts +2 -2
- package/dist/paint/wet/kernel-wasm.d.cts +5 -5
- package/dist/paint/wet/kernel-wasm.d.ts +5 -5
- package/dist/paint/wet/kernel.d.cts +1 -1
- package/dist/paint/wet/kernel.d.ts +1 -1
- package/dist/paint/wet/medium.d.cts +3 -1
- package/dist/paint/wet/medium.d.ts +3 -1
- package/dist/paint/wet/model.d.cts +19 -4
- package/dist/paint/wet/model.d.ts +19 -4
- package/dist/paint/wet/world.d.cts +34 -8
- package/dist/paint/wet/world.d.ts +34 -8
- package/dist/paint-editor/backlight.d.cts +77 -0
- package/dist/paint-editor/backlight.d.ts +77 -0
- package/dist/paint-editor/brushes.d.cts +18 -2
- package/dist/paint-editor/brushes.d.ts +18 -2
- package/dist/paint-editor/controller.d.cts +39 -0
- package/dist/paint-editor/controller.d.ts +39 -0
- package/dist/paint-editor/document.d.cts +3 -0
- package/dist/paint-editor/document.d.ts +3 -0
- package/dist/paint-editor/index.cjs +1335 -529
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +856 -50
- package/dist/paint-editor/light.d.cts +74 -0
- package/dist/paint-editor/light.d.ts +74 -0
- package/dist/paint-editor/paper.d.cts +7 -0
- package/dist/paint-editor/paper.d.ts +7 -0
- package/dist/paint-editor/stroke.d.cts +14 -0
- package/dist/paint-editor/stroke.d.ts +14 -0
- package/dist/paint-editor/thick-tools.d.cts +2 -2
- package/dist/paint-editor/thick-tools.d.ts +2 -2
- package/dist/paint-editor/thick.d.cts +5 -1
- package/dist/paint-editor/thick.d.ts +5 -1
- package/dist/paint-editor/ui.d.cts +2 -0
- package/dist/paint-editor/ui.d.ts +2 -0
- package/dist/paint-editor/wet.d.cts +9 -0
- package/dist/paint-editor/wet.d.ts +9 -0
- package/dist/vector/index.cjs +962 -962
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +11 -11
- package/skills/pixerate-paint-editor/references/canvas-hosted.md +8 -0
- package/skills/pixerate-paint-editor/references/light-paint.md +48 -0
- package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -0
- package/skills/pixerate-paint-watercolour/SKILL.md +10 -1
- package/skills/pixerate-paint-watercolour/evals/glowing-wash.json +22 -0
- package/skills/pixerate-paint-watercolour/evals/stained-glass.json +16 -0
- package/dist/kernel-wasm-6FPOLVUZ.cjs +0 -9
- package/dist/kernel-wasm-WIMMJUF2.js +0 -9
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveExternalAssets,
|
|
8
8
|
serializeLayers,
|
|
9
9
|
serializeLayersExternal
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-7YQNKSQ7.js";
|
|
11
11
|
import "../chunk-3CI7FRMK.js";
|
|
12
12
|
import "../chunk-ZNA3IQZF.js";
|
|
13
13
|
import {
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
pigmentOf,
|
|
45
45
|
pointerPressure,
|
|
46
46
|
wetSupport
|
|
47
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-P3NIPIKS.js";
|
|
48
48
|
import "../chunk-HVQVHUTV.js";
|
|
49
49
|
import {
|
|
50
50
|
writeZip
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
realPresenceClock,
|
|
56
56
|
tweenPresenceCursor
|
|
57
57
|
} from "../chunk-IVSWRVB4.js";
|
|
58
|
-
import "../chunk-
|
|
58
|
+
import "../chunk-4EQY4TZJ.js";
|
|
59
59
|
import {
|
|
60
60
|
BRUSH_ID,
|
|
61
61
|
BRUSH_ROTATIONS,
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
DEFAULT_BRUSH_ID,
|
|
67
67
|
DEFAULT_DYNAMICS,
|
|
68
68
|
DEFAULT_IMPASTO,
|
|
69
|
+
DEFAULT_LIGHT,
|
|
69
70
|
DEFAULT_MULTI_COLOR,
|
|
70
71
|
DEFAULT_PAPER,
|
|
71
72
|
DEFAULT_PARTICLE,
|
|
@@ -107,6 +108,7 @@ import {
|
|
|
107
108
|
normalizeBristle,
|
|
108
109
|
normalizeDynamics,
|
|
109
110
|
normalizeImpasto,
|
|
111
|
+
normalizeLight,
|
|
110
112
|
normalizeMultiColor,
|
|
111
113
|
normalizePaintBrush,
|
|
112
114
|
normalizePaper,
|
|
@@ -131,7 +133,7 @@ import {
|
|
|
131
133
|
tiltToOrientation,
|
|
132
134
|
toStrokePoint,
|
|
133
135
|
usesPaintExchange
|
|
134
|
-
} from "../chunk-
|
|
136
|
+
} from "../chunk-TBJEMELS.js";
|
|
135
137
|
import "../chunk-M6DNZNVY.js";
|
|
136
138
|
import "../chunk-WEIK3UYB.js";
|
|
137
139
|
import "../chunk-PWBZH3FK.js";
|
|
@@ -256,7 +258,10 @@ import {
|
|
|
256
258
|
} from "../chunk-BP6KSGVD.js";
|
|
257
259
|
import "../chunk-6ADHGQ73.js";
|
|
258
260
|
import "../chunk-JEGJP4OW.js";
|
|
259
|
-
import
|
|
261
|
+
import {
|
|
262
|
+
linearToSrgb,
|
|
263
|
+
srgbToLinear
|
|
264
|
+
} from "../chunk-F67XMVDI.js";
|
|
260
265
|
import "../chunk-6ONMQ6WF.js";
|
|
261
266
|
|
|
262
267
|
// src/paint-editor/document.ts
|
|
@@ -838,7 +843,7 @@ var ThickStrokeDriver = class {
|
|
|
838
843
|
if (im.tool === "knife") {
|
|
839
844
|
st.smudge.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: k }, rect, tip);
|
|
840
845
|
this.knifeHeight(st, d, rect, tip, k, im.drag);
|
|
841
|
-
this.paintMaterial(rect, tip, k * 0.6, im.gloss, im.metallic);
|
|
846
|
+
this.paintMaterial(rect, tip, k * 0.6, im.gloss, im.metallic, im.rim, im.clarity);
|
|
842
847
|
return;
|
|
843
848
|
}
|
|
844
849
|
const ex = st.exchange;
|
|
@@ -878,34 +883,34 @@ var ThickStrokeDriver = class {
|
|
|
878
883
|
const u = ((dx + 0.5 - d.x) * nxs + (dy + 0.5 - d.y) * nys) / r;
|
|
879
884
|
const h0 = this.getH(lx, ly);
|
|
880
885
|
let h = h0;
|
|
881
|
-
const
|
|
886
|
+
const full2 = 255 * thick * k;
|
|
882
887
|
switch (im.tool) {
|
|
883
888
|
case "brush": {
|
|
884
889
|
const ridge = 0.6 + 0.4 * Math.sin(u * 9 + hash2(Math.round(u * 6), 3, seed) * 6.28) + 0.15 * (hash2(lx, ly, seed) - 0.5);
|
|
885
|
-
const target =
|
|
890
|
+
const target = full2 * c * ridge;
|
|
886
891
|
h = Math.max(h0 * (1 - 0.25 * c * im.drag), Math.min(255, h0 * 0.35 + target));
|
|
887
892
|
break;
|
|
888
893
|
}
|
|
889
894
|
case "tube": {
|
|
890
895
|
const dome = Math.sqrt(Math.max(0, c));
|
|
891
|
-
h = Math.max(h0, Math.min(255,
|
|
896
|
+
h = Math.max(h0, Math.min(255, full2 * dome * 1.1));
|
|
892
897
|
break;
|
|
893
898
|
}
|
|
894
899
|
case "gloop": {
|
|
895
900
|
const dome = Math.sqrt(Math.max(0, 1 - Math.min(1, Math.abs(u)) ** 2));
|
|
896
|
-
h = Math.max(h0, Math.min(255,
|
|
901
|
+
h = Math.max(h0, Math.min(255, full2 * dome * (c > 0.2 ? 1 : c * 5)));
|
|
897
902
|
break;
|
|
898
903
|
}
|
|
899
904
|
case "roller": {
|
|
900
905
|
const stipple = 0.75 + 0.5 * hash2(lx >> 1, ly >> 1, seed);
|
|
901
|
-
h = h0 + (Math.min(255,
|
|
906
|
+
h = h0 + (Math.min(255, full2 * 0.6 * stipple) - h0) * Math.min(1, c * 1.5);
|
|
902
907
|
break;
|
|
903
908
|
}
|
|
904
909
|
}
|
|
905
910
|
this.setH(lx, ly, Math.round(Math.max(0, Math.min(255, h))));
|
|
906
911
|
}
|
|
907
912
|
}
|
|
908
|
-
this.paintMaterial(rect, tip, k, im.gloss * (1 - 0.3 * im.thinner), im.metallic);
|
|
913
|
+
this.paintMaterial(rect, tip, k, im.gloss * (1 - 0.3 * im.thinner), im.metallic, im.rim, im.clarity);
|
|
909
914
|
}
|
|
910
915
|
/** The knife: flattens the relief under it and carries some of it along (a patch, like a smudge). */
|
|
911
916
|
knifeHeight(st, d, rect, tip, k, drag) {
|
|
@@ -934,7 +939,12 @@ var ThickStrokeDriver = class {
|
|
|
934
939
|
for (let p = 0; p < n; p++) pick[p] = Math.max(0, cur[p] - next[p]) + (carried ? carried[p] * (1 - drag) * 0.5 : 0);
|
|
935
940
|
st.carried = pick;
|
|
936
941
|
}
|
|
937
|
-
|
|
942
|
+
/**
|
|
943
|
+
* Material: R gloss, G metallic, B rim (light paint, 0..255), A opacity for light paint's clarity
|
|
944
|
+
* (255 solid .. 1 clearest; 0 is no material, read as solid).
|
|
945
|
+
*/
|
|
946
|
+
paintMaterial(rect, tip, k, gloss, metal, rim = 0, clarity = 0) {
|
|
947
|
+
const opacity = 255 - clarity * 254;
|
|
938
948
|
const off = this.init.target.offset;
|
|
939
949
|
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
940
950
|
for (let xx = 0; xx < rect.width; xx++, p++) {
|
|
@@ -946,7 +956,9 @@ var ThickStrokeDriver = class {
|
|
|
946
956
|
const i = ((ly & 255) * TILE + (lx & 255)) * 4;
|
|
947
957
|
t[i] = Math.round(t[i] + (gloss * 255 - t[i]) * c);
|
|
948
958
|
t[i + 1] = Math.round(t[i + 1] + (metal * 255 - t[i + 1]) * c);
|
|
949
|
-
t[i +
|
|
959
|
+
t[i + 2] = Math.round(t[i + 2] + (rim * 255 - t[i + 2]) * c);
|
|
960
|
+
const a = t[i + 3] || 255;
|
|
961
|
+
t[i + 3] = Math.max(1, Math.round(a + (opacity - a) * c));
|
|
950
962
|
}
|
|
951
963
|
}
|
|
952
964
|
}
|
|
@@ -981,6 +993,261 @@ function thickFromJSON(raw) {
|
|
|
981
993
|
};
|
|
982
994
|
}
|
|
983
995
|
|
|
996
|
+
// src/paint-editor/light.ts
|
|
997
|
+
var GLOW = 1.5;
|
|
998
|
+
var DEFAULT_LIGHT_SPILL = { radius: 24, strength: 1 };
|
|
999
|
+
function normalizeLightSpill(raw, base = DEFAULT_LIGHT_SPILL) {
|
|
1000
|
+
const radius = raw.radius ?? base.radius;
|
|
1001
|
+
const strength = raw.strength ?? base.strength;
|
|
1002
|
+
if (!(Number.isFinite(radius) && radius >= 0 && radius <= 512)) throw new Error(`radius ${radius} is out of range; use 0..512 px (0 turns spill off).`);
|
|
1003
|
+
if (!(Number.isFinite(strength) && strength >= 0 && strength <= 2)) throw new Error(`strength ${strength} is out of range; use 0..2.`);
|
|
1004
|
+
return { radius, strength };
|
|
1005
|
+
}
|
|
1006
|
+
function emptyLight() {
|
|
1007
|
+
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
1008
|
+
}
|
|
1009
|
+
var full = (t, channels) => {
|
|
1010
|
+
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
1011
|
+
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
1012
|
+
const u = t.uniform ?? 0;
|
|
1013
|
+
if (channels === 1) out.fill(u & 255);
|
|
1014
|
+
else {
|
|
1015
|
+
const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
|
|
1016
|
+
for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
|
|
1017
|
+
}
|
|
1018
|
+
return out;
|
|
1019
|
+
};
|
|
1020
|
+
function lightToShadow(light) {
|
|
1021
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1022
|
+
for (const [key, tile] of light.tiles) {
|
|
1023
|
+
const l = full(tile, 1);
|
|
1024
|
+
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1025
|
+
for (let i = 0, j = 0; i < l.length; i++, j += 4) {
|
|
1026
|
+
if (!l[i]) continue;
|
|
1027
|
+
d[j] = d[j + 1] = d[j + 2] = 255;
|
|
1028
|
+
d[j + 3] = l[i];
|
|
1029
|
+
}
|
|
1030
|
+
const t = finishTile(d, 4, 0);
|
|
1031
|
+
if (t) tiles.set(key, t);
|
|
1032
|
+
}
|
|
1033
|
+
return new TiledSurface(4, tiles, 0);
|
|
1034
|
+
}
|
|
1035
|
+
function shadowToLight(shadow) {
|
|
1036
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1037
|
+
for (const [key, tile] of shadow.tiles) {
|
|
1038
|
+
const s = full(tile, 4);
|
|
1039
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1040
|
+
for (let i = 0, j = 0; i < d.length; i++, j += 4) if (s[j + 3]) d[i] = Math.round((s[j] + s[j + 1] + s[j + 2]) / 3 * s[j + 3] / 255);
|
|
1041
|
+
const t = finishTile(d, 1, 0);
|
|
1042
|
+
if (t) tiles.set(key, t);
|
|
1043
|
+
}
|
|
1044
|
+
return new TiledSurface(1, tiles, 0);
|
|
1045
|
+
}
|
|
1046
|
+
function lightTwinBrush(brush) {
|
|
1047
|
+
return normalizePaintBrush(
|
|
1048
|
+
{
|
|
1049
|
+
tip: { ...brush.tip, blend: "normal", pressureOpacity: brush.tip.pressureOpacity || brush.light.pressure },
|
|
1050
|
+
behavior: { ...brush.behavior, mixing: "rgb" },
|
|
1051
|
+
paint: null,
|
|
1052
|
+
...brush.engine === "mixer" ? { mixer: { ...brush.mixer, mixing: "rgb" } } : {},
|
|
1053
|
+
...brush.engine === "blender" ? { blender: { ...brush.blender, mixing: "rgb" } } : {},
|
|
1054
|
+
...brush.engine === "smudge" ? { smudge: { ...brush.smudge, mixing: "rgb" } } : {}
|
|
1055
|
+
},
|
|
1056
|
+
brush
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
function lightInfo(light) {
|
|
1060
|
+
if (!light) return { bounds: null, peak: 0 };
|
|
1061
|
+
let peak = 0;
|
|
1062
|
+
for (const t of light.tiles.values()) {
|
|
1063
|
+
if (t.uniform !== null) peak = Math.max(peak, t.uniform & 255);
|
|
1064
|
+
else for (let i = 0; i < t.data.length; i++) if (t.data[i] > peak) peak = t.data[i];
|
|
1065
|
+
}
|
|
1066
|
+
return { bounds: peak ? light.bounds : null, peak };
|
|
1067
|
+
}
|
|
1068
|
+
function scaleLight(light, k) {
|
|
1069
|
+
if (k >= 1) return light;
|
|
1070
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1071
|
+
if (k > 0) {
|
|
1072
|
+
for (const [key, tile] of light.tiles) {
|
|
1073
|
+
const d = Uint8ClampedArray.from(full(tile, 1), (v) => Math.round(v * k));
|
|
1074
|
+
const t = finishTile(d, 1, 0);
|
|
1075
|
+
if (t) tiles.set(key, t);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return new TiledSurface(1, tiles, 0);
|
|
1079
|
+
}
|
|
1080
|
+
function mergeLight(lower, upper, upperColor) {
|
|
1081
|
+
if (!upper?.tiles.size) return lower;
|
|
1082
|
+
const keys = /* @__PURE__ */ new Set([...lower?.tiles.keys() ?? [], ...upper.tiles.keys()]);
|
|
1083
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
1084
|
+
for (const key of keys) {
|
|
1085
|
+
const lo = lower?.tiles.get(key);
|
|
1086
|
+
const up = upper.tiles.get(key);
|
|
1087
|
+
const col = upperColor.tiles.get(key);
|
|
1088
|
+
const l = lo ? full(lo, 1) : null;
|
|
1089
|
+
const u = up ? full(up, 1) : null;
|
|
1090
|
+
const c = col ? full(col, 4) : null;
|
|
1091
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1092
|
+
for (let i = 0; i < d.length; i++) {
|
|
1093
|
+
const a = c ? c[i * 4 + 3] / 255 : 0;
|
|
1094
|
+
d[i] = Math.round((u ? u[i] : 0) + (l ? l[i] : 0) * (1 - a));
|
|
1095
|
+
}
|
|
1096
|
+
const t = finishTile(d, 1, 0);
|
|
1097
|
+
if (t) tiles.set(key, t);
|
|
1098
|
+
}
|
|
1099
|
+
return new TiledSurface(1, tiles, 0);
|
|
1100
|
+
}
|
|
1101
|
+
function facing(h, x, y) {
|
|
1102
|
+
const at = (xx, yy) => h[Math.min(TILE - 1, Math.max(0, yy)) * TILE + Math.min(TILE - 1, Math.max(0, xx))];
|
|
1103
|
+
const k = 7 / 255;
|
|
1104
|
+
const dx = (at(x + 1, y) - at(x - 1, y)) / 2 * k;
|
|
1105
|
+
const dy = (at(x, y + 1) - at(x, y - 1)) / 2 * k;
|
|
1106
|
+
return 1 / Math.hypot(dx, dy, 1);
|
|
1107
|
+
}
|
|
1108
|
+
var LightCache = class {
|
|
1109
|
+
tiles = /* @__PURE__ */ new Map();
|
|
1110
|
+
glow(color, light, thick) {
|
|
1111
|
+
const clear = (m) => !!m && (m.uniform === null ? m.data.some((v, i) => i % 4 === 3 && v > 0 && v < 255) : (m.uniform & 255) > 0 && (m.uniform & 255) < 255);
|
|
1112
|
+
if (!light?.tiles.size && !(thick && [...thick.material.tiles.values()].some(clear))) return color;
|
|
1113
|
+
const out = new Map(color.tiles);
|
|
1114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1115
|
+
for (const [key, tile] of color.tiles) {
|
|
1116
|
+
const l = light?.tiles.get(key);
|
|
1117
|
+
const m = thick?.material.tiles.get(key);
|
|
1118
|
+
const h = thick?.height.tiles.get(key);
|
|
1119
|
+
if (!l && !clear(m)) continue;
|
|
1120
|
+
seen.add(key);
|
|
1121
|
+
const inputs = [tile, l ?? null, m ?? null, h ?? null];
|
|
1122
|
+
const cached = this.tiles.get(key);
|
|
1123
|
+
if (cached && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
1124
|
+
out.set(key, cached.tile);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const c = full(tile, 4);
|
|
1128
|
+
const ld = l ? full(l, 1) : null;
|
|
1129
|
+
const md = m ? full(m, 4) : null;
|
|
1130
|
+
const hd = h ? full(h, 1) : null;
|
|
1131
|
+
const d = new Uint8ClampedArray(c);
|
|
1132
|
+
for (let y = 0, i = 0; y < TILE; y++) {
|
|
1133
|
+
for (let x = 0; x < TILE; x++, i++) {
|
|
1134
|
+
const j = i * 4;
|
|
1135
|
+
if (!d[j + 3]) continue;
|
|
1136
|
+
const rim = md ? md[j + 2] / 255 : 0;
|
|
1137
|
+
const opacity = md && md[j + 3] ? md[j + 3] : 255;
|
|
1138
|
+
const f = (rim > 0 || opacity < 255) && hd ? facing(hd, x, y) : 1;
|
|
1139
|
+
const g = ld && ld[i] ? ld[i] / 255 * (1 - rim + rim * 2 * (1 - f) ** 3) : 0;
|
|
1140
|
+
if (g > 0) {
|
|
1141
|
+
for (let ch = 0; ch < 3; ch++) d[j + ch] = Math.round(linearToSrgb(Math.min(1, srgbToLinear(d[j + ch] / 255) * (1 + GLOW * g))) * 255);
|
|
1142
|
+
}
|
|
1143
|
+
if (opacity < 255) d[j + 3] = Math.round(d[j + 3] * (1 - (255 - opacity) / 254 * f));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const lit = { data: d, uniform: null };
|
|
1147
|
+
this.tiles.set(key, { inputs, tile: lit });
|
|
1148
|
+
out.set(key, lit);
|
|
1149
|
+
}
|
|
1150
|
+
for (const key of [...this.tiles.keys()]) if (!seen.has(key)) this.tiles.delete(key);
|
|
1151
|
+
return new TiledSurface(4, out, color.defaultValue);
|
|
1152
|
+
}
|
|
1153
|
+
spillCache = null;
|
|
1154
|
+
/**
|
|
1155
|
+
* The layer's spill (layer space, RGBA for Linear Dodge (Add)): `light × colour` blurred by
|
|
1156
|
+
* `radius` px at a reduced resolution, times `strength`; `null` without light or spill.
|
|
1157
|
+
*/
|
|
1158
|
+
spill(color, light, spill) {
|
|
1159
|
+
const inputs = [color, light, spill.radius, spill.strength];
|
|
1160
|
+
if (this.spillCache && this.spillCache.inputs.every((v, i) => v === inputs[i])) return this.spillCache.surface;
|
|
1161
|
+
const surface = computeSpill(color, light, spill);
|
|
1162
|
+
this.spillCache = { inputs, surface };
|
|
1163
|
+
return surface;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
function computeSpill(color, light, spill) {
|
|
1167
|
+
const b = light ? lightInfo(light).bounds : null;
|
|
1168
|
+
if (!light || !b || spill.radius <= 0 || spill.strength <= 0) return null;
|
|
1169
|
+
const r = Math.ceil(spill.radius);
|
|
1170
|
+
const region = { x: b.x - r, y: b.y - r, width: b.width + 2 * r, height: b.height + 2 * r };
|
|
1171
|
+
const col = readRegion(color, region);
|
|
1172
|
+
const lit = readRegion(light, region);
|
|
1173
|
+
const f = Math.max(1, Math.floor(spill.radius / 6));
|
|
1174
|
+
const w = Math.ceil(region.width / f);
|
|
1175
|
+
const h = Math.ceil(region.height / f);
|
|
1176
|
+
const e = [new Float32Array(w * h), new Float32Array(w * h), new Float32Array(w * h)];
|
|
1177
|
+
for (let y = 0; y < region.height; y++) {
|
|
1178
|
+
for (let x = 0; x < region.width; x++) {
|
|
1179
|
+
const i = y * region.width + x;
|
|
1180
|
+
const l = lit[i];
|
|
1181
|
+
if (!l) continue;
|
|
1182
|
+
const j = i * 4;
|
|
1183
|
+
const k = l / 255 * (col[j + 3] / 255) / (f * f);
|
|
1184
|
+
const cell = Math.floor(y / f) * w + Math.floor(x / f);
|
|
1185
|
+
for (let ch = 0; ch < 3; ch++) e[ch][cell] += srgbToLinear(col[j + ch] / 255) * k;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const box = Math.max(1, Math.round(spill.radius / f / 2.5));
|
|
1189
|
+
for (const plane of e) for (let pass = 0; pass < 3; pass++) boxBlur(plane, w, h, box);
|
|
1190
|
+
const out = new Uint8ClampedArray(region.width * region.height * 4);
|
|
1191
|
+
for (let y = 0; y < region.height; y++) {
|
|
1192
|
+
for (let x = 0; x < region.width; x++) {
|
|
1193
|
+
const fx = Math.min(w - 1, Math.max(0, (x + 0.5) / f - 0.5));
|
|
1194
|
+
const fy = Math.min(h - 1, Math.max(0, (y + 0.5) / f - 0.5));
|
|
1195
|
+
const x0 = Math.floor(fx);
|
|
1196
|
+
const y0 = Math.floor(fy);
|
|
1197
|
+
const x1 = Math.min(w - 1, x0 + 1);
|
|
1198
|
+
const y1 = Math.min(h - 1, y0 + 1);
|
|
1199
|
+
const tx = fx - x0;
|
|
1200
|
+
const ty = fy - y0;
|
|
1201
|
+
const rgb = e.map((p) => (p[y0 * w + x0] * (1 - tx) + p[y0 * w + x1] * tx) * (1 - ty) + (p[y1 * w + x0] * (1 - tx) + p[y1 * w + x1] * tx) * ty);
|
|
1202
|
+
const add = rgb.map((v) => Math.min(1, linearToSrgb(Math.min(1, v * spill.strength))));
|
|
1203
|
+
const a = Math.max(add[0], add[1], add[2]);
|
|
1204
|
+
if (a <= 1 / 255) continue;
|
|
1205
|
+
const o = (y * region.width + x) * 4;
|
|
1206
|
+
out[o] = Math.round(add[0] / a * 255);
|
|
1207
|
+
out[o + 1] = Math.round(add[1] / a * 255);
|
|
1208
|
+
out[o + 2] = Math.round(add[2] / a * 255);
|
|
1209
|
+
out[o + 3] = Math.round(a * 255);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return surfaceFromPixels({ width: region.width, height: region.height, data: out }, 4, region.x, region.y);
|
|
1213
|
+
}
|
|
1214
|
+
function boxBlur(p, w, h, r) {
|
|
1215
|
+
const tmp = new Float32Array(Math.max(w, h));
|
|
1216
|
+
const n = 2 * r + 1;
|
|
1217
|
+
for (let y = 0; y < h; y++) {
|
|
1218
|
+
let sum = 0;
|
|
1219
|
+
for (let k = -r; k <= r; k++) sum += p[y * w + Math.min(w - 1, Math.max(0, k))];
|
|
1220
|
+
for (let x = 0; x < w; x++) {
|
|
1221
|
+
tmp[x] = sum / n;
|
|
1222
|
+
sum += p[y * w + Math.min(w - 1, x + r + 1)] - p[y * w + Math.max(0, x - r)];
|
|
1223
|
+
}
|
|
1224
|
+
for (let x = 0; x < w; x++) p[y * w + x] = tmp[x];
|
|
1225
|
+
}
|
|
1226
|
+
for (let x = 0; x < w; x++) {
|
|
1227
|
+
let sum = 0;
|
|
1228
|
+
for (let k = -r; k <= r; k++) sum += p[Math.min(h - 1, Math.max(0, k)) * w + x];
|
|
1229
|
+
for (let y = 0; y < h; y++) {
|
|
1230
|
+
tmp[y] = sum / n;
|
|
1231
|
+
sum += p[Math.min(h - 1, y + r + 1) * w + x] - p[Math.max(0, y - r) * w + x];
|
|
1232
|
+
}
|
|
1233
|
+
for (let y = 0; y < h; y++) p[y * w + x] = tmp[y];
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function lightToJSON(light) {
|
|
1237
|
+
const b = lightInfo(light).bounds;
|
|
1238
|
+
if (!b) return null;
|
|
1239
|
+
return { rect: { ...b }, light: pngDataUrl(b.width, b.height, readRegion(light, b), 1) };
|
|
1240
|
+
}
|
|
1241
|
+
function lightFromJSON(raw) {
|
|
1242
|
+
const r = raw;
|
|
1243
|
+
if (!r || typeof r !== "object" || !r.rect || typeof r.light !== "string") throw new Error("not a saved light channel");
|
|
1244
|
+
const parsed = parseDataUrl(r.light);
|
|
1245
|
+
if (!parsed) throw new Error("the light is not a PNG data URL");
|
|
1246
|
+
const img = decodePng(parsed.bytes, { keepGray: true });
|
|
1247
|
+
const data = img.channels === 1 ? img.data : Uint8ClampedArray.from({ length: img.width * img.height }, (_, i) => img.data[i * 4]);
|
|
1248
|
+
return surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
984
1251
|
// src/paint-editor/stroke.ts
|
|
985
1252
|
function defaultTile(surface) {
|
|
986
1253
|
const data = new Uint8ClampedArray(TILE * TILE * surface.channels);
|
|
@@ -1000,7 +1267,7 @@ function union(a, b) {
|
|
|
1000
1267
|
const y = Math.min(a.y, b.y);
|
|
1001
1268
|
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
1002
1269
|
}
|
|
1003
|
-
var PaintStrokeSession = class {
|
|
1270
|
+
var PaintStrokeSession = class _PaintStrokeSession {
|
|
1004
1271
|
constructor(host, init) {
|
|
1005
1272
|
this.host = host;
|
|
1006
1273
|
this.init = init;
|
|
@@ -1010,6 +1277,7 @@ var PaintStrokeSession = class {
|
|
|
1010
1277
|
this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
1011
1278
|
const brush = init.brush;
|
|
1012
1279
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
1280
|
+
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
1013
1281
|
const effect = init.effect?.pixels ?? null;
|
|
1014
1282
|
this.apply = {
|
|
1015
1283
|
mode: brush.engine === "eraser" ? "erase" : "paint",
|
|
@@ -1060,6 +1328,9 @@ var PaintStrokeSession = class {
|
|
|
1060
1328
|
wetDriver = null;
|
|
1061
1329
|
thickDriver = null;
|
|
1062
1330
|
stickerSpray = null;
|
|
1331
|
+
/** Light paint: the stroke's twin over the light channel, and what it left there. */
|
|
1332
|
+
lightTwin = null;
|
|
1333
|
+
lightResult = null;
|
|
1063
1334
|
/** A wet stroke that ended while the simulation was mid-batch: it commits when the driver catches up. */
|
|
1064
1335
|
wetEndPending = false;
|
|
1065
1336
|
apply;
|
|
@@ -1070,6 +1341,41 @@ var PaintStrokeSession = class {
|
|
|
1070
1341
|
result = null;
|
|
1071
1342
|
/** The mask stencils this stroke was painted with when they are not the editor's (replays). */
|
|
1072
1343
|
maskStencils = null;
|
|
1344
|
+
/** The stroke's twin over the light channel: the same marks in grey at the brush's light amount (see `light.ts`). */
|
|
1345
|
+
makeLightTwin(light) {
|
|
1346
|
+
const init = this.init;
|
|
1347
|
+
const grey = Math.round(init.brush.light.amount * 255);
|
|
1348
|
+
const g = grey / 255;
|
|
1349
|
+
const hex = "#" + grey.toString(16).padStart(2, "0").repeat(3);
|
|
1350
|
+
const host = {
|
|
1351
|
+
update: () => {
|
|
1352
|
+
},
|
|
1353
|
+
commit: (_s, surface) => {
|
|
1354
|
+
this.lightResult = surface ? shadowToLight(surface) : null;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
const effect = init.effect ? { source: init.effect.source, pixels: { ...init.effect.pixels, data: greyPixels(init.effect.pixels.data, grey) } } : null;
|
|
1358
|
+
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color: hex })) } : null;
|
|
1359
|
+
return new _PaintStrokeSession(host, {
|
|
1360
|
+
...init,
|
|
1361
|
+
surface: lightToShadow(light),
|
|
1362
|
+
lockTransparency: false,
|
|
1363
|
+
brush: lightTwinBrush(init.brush),
|
|
1364
|
+
color: [grey, grey, grey, 255],
|
|
1365
|
+
colorHex: hex,
|
|
1366
|
+
secondary: [grey, grey, grey, 255],
|
|
1367
|
+
inputFilter: void 0,
|
|
1368
|
+
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
1369
|
+
thick: init.thick,
|
|
1370
|
+
paint,
|
|
1371
|
+
effect,
|
|
1372
|
+
light: null
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
/** Light paint: the layer's light channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
1376
|
+
lightChannel() {
|
|
1377
|
+
return this.lightResult;
|
|
1378
|
+
}
|
|
1073
1379
|
tileTarget() {
|
|
1074
1380
|
return {
|
|
1075
1381
|
offset: this.init.offset,
|
|
@@ -1098,6 +1404,7 @@ var PaintStrokeSession = class {
|
|
|
1098
1404
|
if (filter) points = points.map(filter);
|
|
1099
1405
|
for (const p of points) this.points.push(p);
|
|
1100
1406
|
this.lastRaw = points[points.length - 1];
|
|
1407
|
+
this.lightTwin?.push(points);
|
|
1101
1408
|
if (this.wetDriver) {
|
|
1102
1409
|
this.wetUpdate(this.wetDriver.push(points));
|
|
1103
1410
|
return;
|
|
@@ -1179,6 +1486,7 @@ var PaintStrokeSession = class {
|
|
|
1179
1486
|
}
|
|
1180
1487
|
this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet.finish());
|
|
1181
1488
|
this.closed = true;
|
|
1489
|
+
if (this.lightTwin?.open) this.lightTwin.end();
|
|
1182
1490
|
const ch = this.base.channels;
|
|
1183
1491
|
const tiles = new Map(this.base.tiles);
|
|
1184
1492
|
let changed = false;
|
|
@@ -1202,9 +1510,19 @@ var PaintStrokeSession = class {
|
|
|
1202
1510
|
cancel() {
|
|
1203
1511
|
if (this.closed) return;
|
|
1204
1512
|
this.closed = true;
|
|
1513
|
+
if (this.lightTwin?.open) this.lightTwin.cancel();
|
|
1514
|
+
this.lightResult = null;
|
|
1205
1515
|
this.host.commit(this, null, true);
|
|
1206
1516
|
}
|
|
1207
1517
|
};
|
|
1518
|
+
function greyPixels(data, grey) {
|
|
1519
|
+
const out = new Uint8ClampedArray(data.length);
|
|
1520
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1521
|
+
out[i] = out[i + 1] = out[i + 2] = grey;
|
|
1522
|
+
out[i + 3] = data[i + 3];
|
|
1523
|
+
}
|
|
1524
|
+
return out;
|
|
1525
|
+
}
|
|
1208
1526
|
function mixerLoadForStroke(brush, current, color) {
|
|
1209
1527
|
if (!brush.loadsColor) return { reservoir: null, amount: 0, pickup: brush.mixer.cleanEachStroke ? null : current.pickup };
|
|
1210
1528
|
if (brush.mixer.loadEachStroke || !current.reservoir) return loadMixer(current, color);
|
|
@@ -1308,8 +1626,8 @@ function nearestOnStencil(s, p) {
|
|
|
1308
1626
|
if (s.kind === "arc") {
|
|
1309
1627
|
const c = s.center;
|
|
1310
1628
|
const ang = Math.atan2(p.y - c.y, p.x - c.x);
|
|
1311
|
-
const
|
|
1312
|
-
if (
|
|
1629
|
+
const full2 = Math.abs(s.endAngle - s.startAngle) >= 360;
|
|
1630
|
+
if (full2) {
|
|
1313
1631
|
const q = { x: c.x + Math.cos(ang) * s.radius, y: c.y + Math.sin(ang) * s.radius };
|
|
1314
1632
|
return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };
|
|
1315
1633
|
}
|
|
@@ -1896,6 +2214,7 @@ function downloadBytes(bytes, fileName, mimeType) {
|
|
|
1896
2214
|
// src/paint-editor/wet.ts
|
|
1897
2215
|
var STROKE_WATER = 0.9;
|
|
1898
2216
|
var STROKE_MASS = PIGMENT_UNIT * 2.5;
|
|
2217
|
+
var STROKE_LIGHT = 1.5;
|
|
1899
2218
|
var COVERAGE_STEPS = 4096;
|
|
1900
2219
|
function union2(a, b) {
|
|
1901
2220
|
if (!a) return { ...b };
|
|
@@ -1927,8 +2246,10 @@ var WetStrokeDriver = class {
|
|
|
1927
2246
|
this.medium = new WetMedium(init.world, init.laneId, {
|
|
1928
2247
|
water: STROKE_WATER * Math.min(1, wet.water * (1 + thin)) * perDab,
|
|
1929
2248
|
mass: STROKE_MASS * wet.pigment * (1 - 0.6 * thin) * perDab,
|
|
1930
|
-
body: wet.body * (1 - thin)
|
|
2249
|
+
body: wet.body * (1 - thin),
|
|
2250
|
+
light: 0
|
|
1931
2251
|
});
|
|
2252
|
+
this.lightBase = wet.tool === "paint" ? STROKE_LIGHT * brush.light.amount * (1 - 0.6 * thin) * perDab : 0;
|
|
1932
2253
|
const rates = exchangeRates(brush.behavior.pickup, brush.behavior.mix, wet.load, brush.behavior.mixing);
|
|
1933
2254
|
this.states = copies.map((c, i) => ({
|
|
1934
2255
|
walker: new PenDabWalker(brush, init.seed + i * 7919 | 0, c.linear),
|
|
@@ -1958,6 +2279,7 @@ var WetStrokeDriver = class {
|
|
|
1958
2279
|
paint = [0, 0, 0];
|
|
1959
2280
|
centre = [0, 0, 0];
|
|
1960
2281
|
depth;
|
|
2282
|
+
lightBase;
|
|
1961
2283
|
sampler;
|
|
1962
2284
|
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
1963
2285
|
copies;
|
|
@@ -2056,6 +2378,7 @@ var WetStrokeDriver = class {
|
|
|
2056
2378
|
switch (wet.tool) {
|
|
2057
2379
|
case "paint": {
|
|
2058
2380
|
const ex = st.exchange;
|
|
2381
|
+
this.medium.options.light = this.lightBase * (brush.light.pressure ? d.pressure : 1);
|
|
2059
2382
|
let flow = 1;
|
|
2060
2383
|
const effect = this.init.effect;
|
|
2061
2384
|
if (effect) {
|
|
@@ -2244,6 +2567,7 @@ var PaintWetLayers = class {
|
|
|
2244
2567
|
const flow = this.settings.get(l.id)?.flow;
|
|
2245
2568
|
return {
|
|
2246
2569
|
base: translateSurface(l.base, left, top),
|
|
2570
|
+
...l.light ? { light: translateSurface(l.light, left, top) } : {},
|
|
2247
2571
|
flow: l.flow && flow && flow.kind !== "none" ? flowField(flow, width, height) : l.flow
|
|
2248
2572
|
};
|
|
2249
2573
|
});
|
|
@@ -2283,7 +2607,7 @@ var PaintWetLayers = class {
|
|
|
2283
2607
|
if (!w.hasLane(layerId)) {
|
|
2284
2608
|
const base = this.host.layerSurface(layerId) ?? new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
|
|
2285
2609
|
const s = this.settingsOf(layerId, medium);
|
|
2286
|
-
w.ensureLane(layerId, { params: s.params, base });
|
|
2610
|
+
w.ensureLane(layerId, { params: s.params, base, light: this.host.layerLight(layerId) });
|
|
2287
2611
|
const { width, height } = this.host.canvas();
|
|
2288
2612
|
if (s.flow.kind !== "none") w.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);
|
|
2289
2613
|
}
|
|
@@ -2341,6 +2665,7 @@ var PaintWetLayers = class {
|
|
|
2341
2665
|
if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
|
|
2342
2666
|
rects.push(...d.changed);
|
|
2343
2667
|
}
|
|
2668
|
+
this.syncLights();
|
|
2344
2669
|
if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
|
|
2345
2670
|
return rects;
|
|
2346
2671
|
}
|
|
@@ -2356,9 +2681,21 @@ var PaintWetLayers = class {
|
|
|
2356
2681
|
if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
|
|
2357
2682
|
rects.push(...d.changed);
|
|
2358
2683
|
}
|
|
2684
|
+
this.syncLights();
|
|
2359
2685
|
if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
|
|
2360
2686
|
return rects;
|
|
2361
2687
|
}
|
|
2688
|
+
/** Shows each wet layer's live light paint (the light under it with the wet light shaded in). */
|
|
2689
|
+
syncLights() {
|
|
2690
|
+
const w = this.world;
|
|
2691
|
+
if (!w) return;
|
|
2692
|
+
const lights = /* @__PURE__ */ new Map();
|
|
2693
|
+
for (const id of w.laneIds()) {
|
|
2694
|
+
const l = w.lightDisplay(id);
|
|
2695
|
+
if (l && this.host.layerLight(id) !== l) lights.set(id, l);
|
|
2696
|
+
}
|
|
2697
|
+
if (lights.size) this.host.updateLights(lights);
|
|
2698
|
+
}
|
|
2362
2699
|
/** Drops the wet layers whose paint has all dried (they become ordinary pixel layers). */
|
|
2363
2700
|
retireDry() {
|
|
2364
2701
|
const w = this.world;
|
|
@@ -2366,7 +2703,9 @@ var PaintWetLayers = class {
|
|
|
2366
2703
|
for (const id of w.laneIds()) {
|
|
2367
2704
|
if (w.isActive(id) || this.driver?.init.laneId === id) continue;
|
|
2368
2705
|
const base = w.getBase(id);
|
|
2706
|
+
const light = w.isLit(id) ? w.getLight(id) : null;
|
|
2369
2707
|
w.removeLane(id);
|
|
2708
|
+
if (light && this.host.layerLight(id) !== light) this.host.updateLights(/* @__PURE__ */ new Map([[id, light]]));
|
|
2370
2709
|
this.host.updateSurfaces(/* @__PURE__ */ new Map([[id, base]]), []);
|
|
2371
2710
|
}
|
|
2372
2711
|
}
|
|
@@ -2485,6 +2824,12 @@ var PaintWetLayers = class {
|
|
|
2485
2824
|
this.retireDry();
|
|
2486
2825
|
return out;
|
|
2487
2826
|
}
|
|
2827
|
+
/** The layer's light paint with all its wet paint dried (`null` without a wet layer or light), without changing anything. */
|
|
2828
|
+
bakedLight(layerId) {
|
|
2829
|
+
const w = this.world;
|
|
2830
|
+
if (!w?.hasLane(layerId) || !w.isLit(layerId)) return null;
|
|
2831
|
+
return w.bakedLight(layerId);
|
|
2832
|
+
}
|
|
2488
2833
|
/** The layer's pixels with all its wet paint dried (deterministic), without changing anything. */
|
|
2489
2834
|
bakedSurface(layerId) {
|
|
2490
2835
|
const w = this.world;
|
|
@@ -2605,6 +2950,7 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
2605
2950
|
const [r, g, b] = hexToRgb8(color);
|
|
2606
2951
|
let result = surface;
|
|
2607
2952
|
let thick = null;
|
|
2953
|
+
let light = null;
|
|
2608
2954
|
const session = new PaintStrokeSession(
|
|
2609
2955
|
{
|
|
2610
2956
|
update: () => {
|
|
@@ -2612,6 +2958,7 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
2612
2958
|
commit: (sess, s) => {
|
|
2613
2959
|
result = s ?? result;
|
|
2614
2960
|
thick = sess.thickChannels();
|
|
2961
|
+
light = sess.lightChannel();
|
|
2615
2962
|
}
|
|
2616
2963
|
},
|
|
2617
2964
|
{
|
|
@@ -2630,12 +2977,14 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
2630
2977
|
copies: [IDENTITY_COPY],
|
|
2631
2978
|
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),
|
|
2632
2979
|
thick: brush.engine === "impasto" ? { channels: emptyThickChannels() } : void 0,
|
|
2633
|
-
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0
|
|
2980
|
+
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0,
|
|
2981
|
+
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null
|
|
2634
2982
|
}
|
|
2635
2983
|
);
|
|
2636
2984
|
session.push(thumbnailStroke(width, height));
|
|
2637
2985
|
session.end();
|
|
2638
|
-
|
|
2986
|
+
const lit = thick ? new LitCache().lit(result, thick, DEFAULT_LIGHTING) : result;
|
|
2987
|
+
return { surface: light || thick ? new LightCache().glow(lit, light ?? void 0, thick ?? void 0) : lit, light };
|
|
2639
2988
|
}
|
|
2640
2989
|
function renderBrushThumbnail(brush, options) {
|
|
2641
2990
|
const width = Math.max(16, Math.min(512, Math.round(options.width ?? 160)));
|
|
@@ -2652,9 +3001,15 @@ function renderBrushThumbnail(brush, options) {
|
|
|
2652
3001
|
surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);
|
|
2653
3002
|
surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
|
|
2654
3003
|
}
|
|
2655
|
-
|
|
3004
|
+
let light = null;
|
|
3005
|
+
if (b.engine === "wet") surface = paintWetTestStroke(b, options.paper, options.color, width, height);
|
|
3006
|
+
else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height));
|
|
2656
3007
|
const layer = { type: "pixel", id: "thumbnail", name: "thumbnail", visible: true, opacity: 1, blend: "normal", surface, offset: { x: 0, y: 0 } };
|
|
2657
|
-
const
|
|
3008
|
+
const paper = light ? { ...options.paper, color: "#141414" } : options.paper;
|
|
3009
|
+
const spill = light ? computeSpill(surface, light, { radius: Math.max(4, height / 6), strength: 1 }) : null;
|
|
3010
|
+
const layers = [paperNode(paper), layer];
|
|
3011
|
+
if (spill) layers.push({ type: "pixel", id: "thumbnail-light", name: "light", visible: true, opacity: 1, blend: "linear-dodge", surface: spill, offset: { x: 0, y: 0 } });
|
|
3012
|
+
const data = createPaintCompositor().composite(layers, { x: 0, y: 0, width, height }, { width, height });
|
|
2658
3013
|
return { width, height, data };
|
|
2659
3014
|
}
|
|
2660
3015
|
|
|
@@ -3127,6 +3482,7 @@ function liveStrokeRefusal(brush) {
|
|
|
3127
3482
|
case "smudge":
|
|
3128
3483
|
return `The ${brush.name} brush moves the paint under it, so it can't be redrawn on its own; rasterize the layer first (rasterize_stroke_layer) or blend on a pixel layer.`;
|
|
3129
3484
|
}
|
|
3485
|
+
if (brush.light.amount > 0) return `The ${brush.name} brush lays light, which a stroke layer doesn't redraw yet; paint light on a pixel layer (add_layer), or set light.amount to 0 (set_brush).`;
|
|
3130
3486
|
if (brush.behavior.pickup > 0) return `The ${brush.name} brush picks up the paint under it (pickup ${brush.behavior.pickup}), so it can't be redrawn on its own; set behavior.pickup to 0 (set_brush) or paint on a pixel layer.`;
|
|
3131
3487
|
return null;
|
|
3132
3488
|
}
|
|
@@ -3148,6 +3504,212 @@ function liveStrokeBounds(stroke) {
|
|
|
3148
3504
|
return { x: Math.floor(x0 - pad), y: Math.floor(y0 - pad), width: Math.ceil(x1 - x0 + 2 * pad), height: Math.ceil(y1 - y0 + 2 * pad) };
|
|
3149
3505
|
}
|
|
3150
3506
|
|
|
3507
|
+
// src/paint-editor/backlight.ts
|
|
3508
|
+
var DEFAULT_BACKLIGHT_COLOR = "#fff1d6";
|
|
3509
|
+
var BACKLIGHT_SPILL = { radius: 24, strength: 1.5 };
|
|
3510
|
+
var BLOCK_HEIGHT = 96;
|
|
3511
|
+
var VALLEY_GAIN = 0.35;
|
|
3512
|
+
function backlightOf(paper) {
|
|
3513
|
+
return paper.backlight ?? 0;
|
|
3514
|
+
}
|
|
3515
|
+
var LIN = new Float64Array(256);
|
|
3516
|
+
for (let i = 0; i < 256; i++) LIN[i] = srgbToLinear(i / 255);
|
|
3517
|
+
function needAlpha(e, p) {
|
|
3518
|
+
if (e < p) return 1 - e / p;
|
|
3519
|
+
if (e > p) return (e - p) / (255 - p);
|
|
3520
|
+
return 0;
|
|
3521
|
+
}
|
|
3522
|
+
function tilesUnder(surface, rect, offset, out) {
|
|
3523
|
+
if (!surface) {
|
|
3524
|
+
out.push(null);
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
const x0 = Math.floor((rect.x - offset.x) / TILE);
|
|
3528
|
+
const y0 = Math.floor((rect.y - offset.y) / TILE);
|
|
3529
|
+
const x1 = Math.floor((rect.x - offset.x + rect.width - 1) / TILE);
|
|
3530
|
+
const y1 = Math.floor((rect.y - offset.y + rect.height - 1) / TILE);
|
|
3531
|
+
for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) out.push(surface.tiles.get(tileKey(tx, ty)) ?? null);
|
|
3532
|
+
}
|
|
3533
|
+
var sameRefs = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
|
|
3534
|
+
var BacklightCache = class {
|
|
3535
|
+
tiles = /* @__PURE__ */ new Map();
|
|
3536
|
+
key = "";
|
|
3537
|
+
spillOf = null;
|
|
3538
|
+
out = null;
|
|
3539
|
+
/**
|
|
3540
|
+
* The backlight layer's pixels for `layers` (bottom first) on `paper` over a `width` × `height`
|
|
3541
|
+
* canvas, or `null` when the paper has no backlight. `spillFrom` is the committed painting (the
|
|
3542
|
+
* spill is drawn from it, so a stroke in progress does not redo the blur every frame).
|
|
3543
|
+
*/
|
|
3544
|
+
surface(paper, grain, width, height, layers, spillFrom) {
|
|
3545
|
+
const amount = backlightOf(paper);
|
|
3546
|
+
if (!(amount > 0)) return null;
|
|
3547
|
+
const key = JSON.stringify([amount, paper.backlightColor ?? DEFAULT_BACKLIGHT_COLOR, paper.color, paper.depth, grain.flat, width, height, layers.length, layers.map((l) => [l.opacity, l.offset.x, l.offset.y, l.surface.channels])]);
|
|
3548
|
+
if (key !== this.key) {
|
|
3549
|
+
this.tiles.clear();
|
|
3550
|
+
this.key = key;
|
|
3551
|
+
}
|
|
3552
|
+
const light = /* @__PURE__ */ new Map();
|
|
3553
|
+
const color = /* @__PURE__ */ new Map();
|
|
3554
|
+
const painted = /* @__PURE__ */ new Map();
|
|
3555
|
+
const tilesX = Math.ceil(width / TILE);
|
|
3556
|
+
const tilesY = Math.ceil(height / TILE);
|
|
3557
|
+
const refs = [];
|
|
3558
|
+
for (let ty = 0; ty < tilesY; ty++) {
|
|
3559
|
+
for (let tx = 0; tx < tilesX; tx++) {
|
|
3560
|
+
const rect = { x: tx * TILE, y: ty * TILE, width: Math.min(TILE, width - tx * TILE), height: Math.min(TILE, height - ty * TILE) };
|
|
3561
|
+
refs.length = 0;
|
|
3562
|
+
for (const l of layers) {
|
|
3563
|
+
tilesUnder(l.surface, rect, l.offset, refs);
|
|
3564
|
+
tilesUnder(l.thick?.height, rect, l.offset, refs);
|
|
3565
|
+
tilesUnder(l.thick?.material, rect, l.offset, refs);
|
|
3566
|
+
}
|
|
3567
|
+
const k = tileKey(tx, ty);
|
|
3568
|
+
let r = this.tiles.get(k);
|
|
3569
|
+
if (!r || !sameRefs(r.refs, refs)) {
|
|
3570
|
+
r = { refs: [...refs], ...computeTile(paper, grain, amount, rect, layers) };
|
|
3571
|
+
this.tiles.set(k, r);
|
|
3572
|
+
}
|
|
3573
|
+
if (r.light) light.set(k, r.light);
|
|
3574
|
+
if (r.color) color.set(k, r.color);
|
|
3575
|
+
if (r.painted) painted.set(k, r.painted);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return { light: new TiledSurface(4, light, 0), color: new TiledSurface(4, color, 0), painted: new TiledSurface(1, painted, 0) };
|
|
3579
|
+
}
|
|
3580
|
+
/**
|
|
3581
|
+
* The full Backlight layer: the light through the paint (`current`) plus the spill of the light
|
|
3582
|
+
* through the committed paint (`committed`), both Linear Dodge. Cached on their tiles.
|
|
3583
|
+
*/
|
|
3584
|
+
layer(current, committed, amount) {
|
|
3585
|
+
let spill = null;
|
|
3586
|
+
if (committed) {
|
|
3587
|
+
const s = this.spillOf;
|
|
3588
|
+
if (s && s.color.id === committed.color.id && s.painted.id === committed.painted.id) spill = s.out;
|
|
3589
|
+
else {
|
|
3590
|
+
spill = computeSpill(committed.color, committed.painted, { radius: BACKLIGHT_SPILL.radius, strength: BACKLIGHT_SPILL.strength * amount });
|
|
3591
|
+
this.spillOf = { color: committed.color, painted: committed.painted, out: spill };
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
if (!spill) return current.light;
|
|
3595
|
+
const parts = [...current.light.tiles.values(), ...spill.tiles.values()];
|
|
3596
|
+
if (this.out && sameRefs(this.out.parts, parts)) return this.out.surface;
|
|
3597
|
+
const keys = /* @__PURE__ */ new Set([...current.light.tiles.keys(), ...spill.tiles.keys()]);
|
|
3598
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
3599
|
+
for (const key of keys) {
|
|
3600
|
+
const a = current.light.tiles.get(key);
|
|
3601
|
+
const b = spill.tiles.get(key);
|
|
3602
|
+
const t = !a ? b : !b ? a : addTiles(a, b);
|
|
3603
|
+
if (t) tiles.set(key, t);
|
|
3604
|
+
}
|
|
3605
|
+
const surface = new TiledSurface(4, tiles, 0);
|
|
3606
|
+
this.out = { parts, surface };
|
|
3607
|
+
return surface;
|
|
3608
|
+
}
|
|
3609
|
+
};
|
|
3610
|
+
var full4 = (t) => {
|
|
3611
|
+
if (t.data.length === TILE * TILE * 4) return t.data;
|
|
3612
|
+
const out = new Uint8ClampedArray(TILE * TILE * 4);
|
|
3613
|
+
const u = t.uniform ?? 0;
|
|
3614
|
+
const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
|
|
3615
|
+
for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
|
|
3616
|
+
return out;
|
|
3617
|
+
};
|
|
3618
|
+
function addTiles(a, b) {
|
|
3619
|
+
const x = full4(a);
|
|
3620
|
+
const y = full4(b);
|
|
3621
|
+
const out = new Uint8ClampedArray(TILE * TILE * 4);
|
|
3622
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
3623
|
+
const ay = y[i + 3] / 255;
|
|
3624
|
+
const r = x[i] + y[i] * ay;
|
|
3625
|
+
const g = x[i + 1] + y[i + 1] * ay;
|
|
3626
|
+
const bl = x[i + 2] + y[i + 2] * ay;
|
|
3627
|
+
if (r < 0.5 && g < 0.5 && bl < 0.5) continue;
|
|
3628
|
+
out[i] = Math.round(r);
|
|
3629
|
+
out[i + 1] = Math.round(g);
|
|
3630
|
+
out[i + 2] = Math.round(bl);
|
|
3631
|
+
out[i + 3] = 255;
|
|
3632
|
+
}
|
|
3633
|
+
return finishTile(out, 4, 0);
|
|
3634
|
+
}
|
|
3635
|
+
function computeTile(paper, grain, amount, rect, layers) {
|
|
3636
|
+
const n = rect.width * rect.height;
|
|
3637
|
+
const [p0, p1, p2] = hexToRgb8(paper.color);
|
|
3638
|
+
const pb = [p0, p1, p2];
|
|
3639
|
+
const pl = [Math.max(1e-4, LIN[p0]), Math.max(1e-4, LIN[p1]), Math.max(1e-4, LIN[p2])];
|
|
3640
|
+
const [c0, c1, c2] = hexToRgb8(paper.backlightColor ?? DEFAULT_BACKLIGHT_COLOR);
|
|
3641
|
+
const t0 = new Float32Array(n).fill(amount * LIN[c0]);
|
|
3642
|
+
const t1 = new Float32Array(n).fill(amount * LIN[c1]);
|
|
3643
|
+
const t2 = new Float32Array(n).fill(amount * LIN[c2]);
|
|
3644
|
+
const depth = grain.flat ? 0 : paper.depth;
|
|
3645
|
+
if (depth > 0) {
|
|
3646
|
+
for (let y = 0, i = 0; y < rect.height; y++)
|
|
3647
|
+
for (let x = 0; x < rect.width; x++, i++) {
|
|
3648
|
+
const g = 1 + VALLEY_GAIN * depth * (0.5 - grain.at(rect.x + x + 0.5, rect.y + y + 0.5));
|
|
3649
|
+
t0[i] *= g;
|
|
3650
|
+
t1[i] *= g;
|
|
3651
|
+
t2[i] *= g;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
const clear = new Float32Array(n).fill(1);
|
|
3655
|
+
for (const l of layers) {
|
|
3656
|
+
const lr = { x: rect.x - l.offset.x, y: rect.y - l.offset.y, width: rect.width, height: rect.height };
|
|
3657
|
+
if (!l.surface.tiles.size) continue;
|
|
3658
|
+
const px = readRegion(l.surface, lr);
|
|
3659
|
+
const h = l.thick?.height.tiles.size ? readRegion(l.thick.height, lr) : null;
|
|
3660
|
+
const m = h && l.thick?.material.tiles.size ? readRegion(l.thick.material, lr) : null;
|
|
3661
|
+
const op = l.opacity;
|
|
3662
|
+
for (let i = 0; i < n; i++) {
|
|
3663
|
+
const a8 = px[i * 4 + 3];
|
|
3664
|
+
if (!a8 && !(h && h[i])) continue;
|
|
3665
|
+
const a = a8 / 255;
|
|
3666
|
+
let aMin = 0;
|
|
3667
|
+
const tr = [1, 1, 1];
|
|
3668
|
+
for (let c = 0; c < 3; c++) {
|
|
3669
|
+
const e = a * px[i * 4 + c] + (1 - a) * pb[c];
|
|
3670
|
+
aMin = Math.max(aMin, needAlpha(e, pb[c]));
|
|
3671
|
+
tr[c] = Math.min(1, LIN[Math.round(e)] / pl[c]);
|
|
3672
|
+
}
|
|
3673
|
+
const hide = aMin < 1 && a > aMin ? (a - aMin) / (1 - aMin) : 0;
|
|
3674
|
+
let k = 1 - hide;
|
|
3675
|
+
if (h && h[i]) {
|
|
3676
|
+
const ma = m ? m[i * 4 + 3] : 0;
|
|
3677
|
+
const clarity = ma ? (255 - ma) / 254 : 0;
|
|
3678
|
+
k *= 1 - Math.min(1, h[i] / BLOCK_HEIGHT) * (1 - clarity);
|
|
3679
|
+
}
|
|
3680
|
+
t0[i] *= 1 - op + op * tr[0] * k;
|
|
3681
|
+
t1[i] *= 1 - op + op * tr[1] * k;
|
|
3682
|
+
t2[i] *= 1 - op + op * tr[2] * k;
|
|
3683
|
+
clear[i] *= 1 - op * Math.max(a, h && h[i] ? 1 : 0);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
const light = new Uint8ClampedArray(TILE * TILE * 4);
|
|
3687
|
+
const color = new Uint8ClampedArray(TILE * TILE * 4);
|
|
3688
|
+
const painted = new Uint8ClampedArray(TILE * TILE);
|
|
3689
|
+
for (let y = 0, i = 0; y < rect.height; y++) {
|
|
3690
|
+
for (let x = 0; x < rect.width; x++, i++) {
|
|
3691
|
+
const o = (y * TILE + x) * 4;
|
|
3692
|
+
const add = [Math.min(1, t0[i]), Math.min(1, t1[i]), Math.min(1, t2[i])];
|
|
3693
|
+
const mx = Math.max(add[0], add[1], add[2]);
|
|
3694
|
+
if (mx > 0.5 / 255) {
|
|
3695
|
+
light[o] = Math.round(add[0] * 255);
|
|
3696
|
+
light[o + 1] = Math.round(add[1] * 255);
|
|
3697
|
+
light[o + 2] = Math.round(add[2] * 255);
|
|
3698
|
+
light[o + 3] = 255;
|
|
3699
|
+
}
|
|
3700
|
+
const pv = Math.round((1 - clear[i]) * 255);
|
|
3701
|
+
if (pv && mx > 0.5 / 255) {
|
|
3702
|
+
painted[y * TILE + x] = pv;
|
|
3703
|
+
color[o] = Math.round(linearToSrgb(add[0]) * 255);
|
|
3704
|
+
color[o + 1] = Math.round(linearToSrgb(add[1]) * 255);
|
|
3705
|
+
color[o + 2] = Math.round(linearToSrgb(add[2]) * 255);
|
|
3706
|
+
color[o + 3] = 255;
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
return { light: finishTile(light, 4, 0), color: finishTile(color, 4, 0), painted: finishTile(painted, 1, 0) };
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3151
3713
|
// src/paint-editor/replay.ts
|
|
3152
3714
|
function replayItems(doc) {
|
|
3153
3715
|
const items = [];
|
|
@@ -3535,6 +4097,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3535
4097
|
dirty = "all";
|
|
3536
4098
|
seedCounter = 1;
|
|
3537
4099
|
strokeCounter = 1;
|
|
4100
|
+
/** Light paint: each layer's spill settings (saved with the document, not undo steps; missing = the defaults). */
|
|
4101
|
+
lightSpills = {};
|
|
4102
|
+
lightCaches = /* @__PURE__ */ new Map();
|
|
4103
|
+
/** Backlit paper: the light through the paint as drawn (live strokes too), and as committed (its spill). */
|
|
4104
|
+
backlightView = new BacklightCache();
|
|
4105
|
+
backlightCommitted = new BacklightCache();
|
|
3538
4106
|
/** Live effect paint: playing and the frame rate cap (editor state), and the time the view shows. */
|
|
3539
4107
|
effectPlaybackState = { playing: true, fps: 30 };
|
|
3540
4108
|
effectTime = 0;
|
|
@@ -3596,6 +4164,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3596
4164
|
this.doc = { ...this.doc, layers };
|
|
3597
4165
|
this.emit(rects.length ? rects : "all");
|
|
3598
4166
|
},
|
|
4167
|
+
layerLight: (id) => this.doc.light?.[id] ?? null,
|
|
4168
|
+
updateLights: (lights) => {
|
|
4169
|
+
let light = this.doc.light;
|
|
4170
|
+
for (const [id, surface] of lights) if (findLayer(this.doc.layers, id)) light = withLayerLight(light, id, surface);
|
|
4171
|
+
this.doc = { ...this.doc, ...light ? { light } : {} };
|
|
4172
|
+
},
|
|
3599
4173
|
changed: () => {
|
|
3600
4174
|
}
|
|
3601
4175
|
},
|
|
@@ -4068,7 +4642,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4068
4642
|
const layers = mapLayer(this.doc.layers, id, () => null);
|
|
4069
4643
|
let active = this.doc.activeLayerId;
|
|
4070
4644
|
if (active && !findLayer(layers, active)) active = flattenLayers(layers).filter((x) => x.type === "pixel").pop()?.id ?? null;
|
|
4071
|
-
this.commit("Remove layer", { ...this.doc, layers, activeLayerId: active, ...this.thickWithout(flattenLayers([l]).map((x) => x.id)), ...this.stickersWithout(flattenLayers([l]).map((x) => x.id)), ...this.liveStrokesWithout(flattenLayers([l]).map((x) => x.id)) });
|
|
4645
|
+
this.commit("Remove layer", { ...this.doc, layers, activeLayerId: active, ...this.thickWithout(flattenLayers([l]).map((x) => x.id)), ...this.stickersWithout(flattenLayers([l]).map((x) => x.id)), ...this.liveStrokesWithout(flattenLayers([l]).map((x) => x.id)), ...this.lightWithout(flattenLayers([l]).map((x) => x.id)) });
|
|
4072
4646
|
return { removed: id, activeLayerId: active };
|
|
4073
4647
|
}
|
|
4074
4648
|
duplicateLayer(id) {
|
|
@@ -4082,8 +4656,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4082
4656
|
const thick = { ...this.doc.thick ?? {} };
|
|
4083
4657
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
4084
4658
|
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
4659
|
+
const light = { ...this.doc.light ?? {} };
|
|
4085
4660
|
const copy = (l) => {
|
|
4086
4661
|
const id2 = newPaintLayerId();
|
|
4662
|
+
if (light[l.id]) light[id2] = light[l.id];
|
|
4663
|
+
if (this.lightSpills[l.id]) this.lightSpills[id2] = { ...this.lightSpills[l.id] };
|
|
4087
4664
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
4088
4665
|
if (stickers[l.id]) stickers[id2] = stickers[l.id].map((x) => ({ ...x }));
|
|
4089
4666
|
if (liveStrokes[l.id]) liveStrokes[id2] = liveStrokes[l.id].map((x) => ({ ...x, id: `s${this.strokeCounter++}`, layerId: id2 }));
|
|
@@ -4091,7 +4668,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4091
4668
|
};
|
|
4092
4669
|
const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };
|
|
4093
4670
|
const layers = insertLayer(this.doc.layers, dup, loc.parent?.id ?? null, loc.index + 1);
|
|
4094
|
-
this.commit("Duplicate layer", this.withWetEvents({ ...this.doc, layers, activeLayerId: dup.id, ...this.doc.thick ? { thick } : {}, ...this.doc.stickers ? { stickers } : {}, ...this.doc.liveStrokes ? { liveStrokes } : {} }));
|
|
4671
|
+
this.commit("Duplicate layer", this.withWetEvents({ ...this.doc, layers, activeLayerId: dup.id, ...this.doc.thick ? { thick } : {}, ...this.doc.stickers ? { stickers } : {}, ...this.doc.liveStrokes ? { liveStrokes } : {}, ...this.doc.light ? { light } : {} }));
|
|
4095
4672
|
return this.describeLayer(dup);
|
|
4096
4673
|
}
|
|
4097
4674
|
/** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */
|
|
@@ -4137,7 +4714,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4137
4714
|
}
|
|
4138
4715
|
const merged = { ...below, surface, offset: { x: 0, y: 0 } };
|
|
4139
4716
|
const layers = mapLayer(mapLayer(this.doc.layers, upper.id, () => null), below.id, () => merged);
|
|
4140
|
-
|
|
4717
|
+
const toDoc = (t, l) => t ? translateSurface(t, l.offset.x, l.offset.y) : void 0;
|
|
4718
|
+
const mergedLight = this.doc.light?.[upper.id] || this.doc.light?.[below.id] ? mergeLight(toDoc(this.doc.light?.[below.id], below), upper.visible ? toDoc(this.doc.light?.[upper.id], upper) : void 0, translateSurface(upper.surface, upper.offset.x, upper.offset.y)) : void 0;
|
|
4719
|
+
const lightAfter = this.lightWithout([upper.id, below.id]).light ?? (this.doc.light ? { ...this.doc.light } : void 0);
|
|
4720
|
+
const light = mergedLight ? { ...lightAfter ?? {}, [below.id]: mergedLight } : lightAfter;
|
|
4721
|
+
this.commit("Merge down", this.withWetEvents({ ...this.doc, layers, activeLayerId: below.id, ...this.thickWithout([upper.id]), ...this.stickersWithout([upper.id, below.id]), ...this.liveStrokesWithout([upper.id, below.id]), ...light ? { light } : {} }));
|
|
4141
4722
|
return this.describeLayer(merged);
|
|
4142
4723
|
}
|
|
4143
4724
|
/** Removes every pixel of a layer. */
|
|
@@ -4149,7 +4730,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4149
4730
|
this.wet.world.removeLane(l.id);
|
|
4150
4731
|
}
|
|
4151
4732
|
const bounds = this.describeLayer(l).bounds;
|
|
4152
|
-
this.commit("Clear layer", this.withPaintEvent({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })), ...this.thickWithout([l.id]), ...this.stickersWithout([l.id]), ...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {} }, { kind: "clear", layerId: l.id }));
|
|
4733
|
+
this.commit("Clear layer", this.withPaintEvent({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })), ...this.thickWithout([l.id]), ...this.stickersWithout([l.id]), ...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {}, ...this.lightWithout([l.id]) }, { kind: "clear", layerId: l.id }));
|
|
4153
4734
|
return { layerId: l.id, bounds };
|
|
4154
4735
|
}
|
|
4155
4736
|
/**
|
|
@@ -4193,7 +4774,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4193
4774
|
offset: l.offset
|
|
4194
4775
|
};
|
|
4195
4776
|
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
4196
|
-
|
|
4777
|
+
const lit = this.doc.light?.[l.id];
|
|
4778
|
+
const light = lit ? withLayerLight(this.doc.light, l.id, scaleLight(lit, 1 - opacity)) : void 0;
|
|
4779
|
+
this.commit("Fill layer", this.withPaintEvent(this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), ...light ? { light } : {} }), { kind: "fill", layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }));
|
|
4197
4780
|
return { layerId: l.id, bounds: rect };
|
|
4198
4781
|
}
|
|
4199
4782
|
applyCoverage(l, coverage, rect, apply) {
|
|
@@ -4851,6 +5434,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4851
5434
|
})();
|
|
4852
5435
|
const brush = this.strokeBrush(init);
|
|
4853
5436
|
const effect = this.strokeEffect(init, brush);
|
|
5437
|
+
if (brush.light.amount > 0) {
|
|
5438
|
+
if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (list_brushes category light), or set light.amount to 0 (set_brush).`);
|
|
5439
|
+
}
|
|
4854
5440
|
if (doc.liveStrokes?.[layer.id]) {
|
|
4855
5441
|
const why = liveStrokeRefusal(brush);
|
|
4856
5442
|
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
@@ -4947,6 +5533,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4947
5533
|
secondary: rgbaOf(this.secondaryColor),
|
|
4948
5534
|
wet,
|
|
4949
5535
|
thick: brush.engine === "impasto" ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
5536
|
+
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
5537
|
+
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
4950
5538
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc) } : void 0,
|
|
4951
5539
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
4952
5540
|
effect
|
|
@@ -5029,7 +5617,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5029
5617
|
const thick = ch ? { ...this.doc.thick ?? {}, [l.id]: ch } : this.doc.thick;
|
|
5030
5618
|
const sprayed = s.stickers();
|
|
5031
5619
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
5032
|
-
|
|
5620
|
+
const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
|
|
5621
|
+
const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
|
|
5622
|
+
this.commit(`${brush.name} stroke`, this.withWetEvents(withLiveStroke({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {}, ...light ? { light } : {} }, stroke)), wetStroke ? before : this.doc);
|
|
5033
5623
|
}
|
|
5034
5624
|
};
|
|
5035
5625
|
let session;
|
|
@@ -5062,12 +5652,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5062
5652
|
const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;
|
|
5063
5653
|
const ch = s.thickChannels();
|
|
5064
5654
|
const sprayed = s.stickers();
|
|
5655
|
+
const lit = s.lightChannel() ?? (s.init.wet && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null);
|
|
5065
5656
|
out = withLiveStroke({
|
|
5066
5657
|
...out,
|
|
5067
5658
|
layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),
|
|
5068
5659
|
strokes: [...out.strokes, stroke],
|
|
5069
5660
|
...ch ? { thick: { ...out.thick ?? {}, [l.id]: ch } } : {},
|
|
5070
|
-
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {}
|
|
5661
|
+
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
|
|
5662
|
+
...lit ? { light: withLayerLight(out.light, l.id, lit) } : {}
|
|
5071
5663
|
}, stroke);
|
|
5072
5664
|
}
|
|
5073
5665
|
};
|
|
@@ -5182,6 +5774,46 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5182
5774
|
if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? "Replay stroke" : "Replay strokes", doc);
|
|
5183
5775
|
return { strokeIds: ids, bounds, layerIds: [...layerIds] };
|
|
5184
5776
|
}
|
|
5777
|
+
// --- Light paint --------------------------------------------------------------------
|
|
5778
|
+
/** The document's `light` without these layers (as a spread: `{}` when unchanged). */
|
|
5779
|
+
lightWithout(ids) {
|
|
5780
|
+
const t = this.doc.light;
|
|
5781
|
+
if (!t || !ids.some((id) => t[id])) return {};
|
|
5782
|
+
const out = { ...t };
|
|
5783
|
+
for (const id of ids) delete out[id];
|
|
5784
|
+
return { light: out };
|
|
5785
|
+
}
|
|
5786
|
+
lightSpillOf(layerId) {
|
|
5787
|
+
return this.lightSpills[layerId] ?? DEFAULT_LIGHT_SPILL;
|
|
5788
|
+
}
|
|
5789
|
+
/** A layer's light paint: whether it has any, where (document pixels), its brightest value 0..255, and its spill. */
|
|
5790
|
+
lightOf(layerId) {
|
|
5791
|
+
const l = this.wetLayer(layerId, "read the light of");
|
|
5792
|
+
const info = lightInfo(this.doc.light?.[l.id]);
|
|
5793
|
+
const bounds = info.bounds ? { ...info.bounds, x: info.bounds.x + l.offset.x, y: info.bounds.y + l.offset.y } : null;
|
|
5794
|
+
return { layerId: l.id, hasLight: info.peak > 0, bounds, peak: info.peak, spill: { ...this.lightSpillOf(l.id) } };
|
|
5795
|
+
}
|
|
5796
|
+
/** How far a layer's light spills (px; 0 turns it off) and how strongly (0..2). Saved with the document, not an undo step; nothing is repainted. */
|
|
5797
|
+
setLightSpill(layerId, update) {
|
|
5798
|
+
const l = this.wetLayer(layerId, "set the light spill of");
|
|
5799
|
+
try {
|
|
5800
|
+
this.lightSpills[l.id] = normalizeLightSpill(update, this.lightSpillOf(l.id));
|
|
5801
|
+
} catch (e) {
|
|
5802
|
+
throw new PaintEditorError(e.message);
|
|
5803
|
+
}
|
|
5804
|
+
this.emit("all");
|
|
5805
|
+
return { ...this.lightSpills[l.id] };
|
|
5806
|
+
}
|
|
5807
|
+
/** Takes a layer's light away; its paint keeps its colour. One undo step. Returns whether there was any. */
|
|
5808
|
+
clearLight(layerId) {
|
|
5809
|
+
this.requireIdle("clearing light");
|
|
5810
|
+
const l = this.paintTarget(layerId, "clear the light of");
|
|
5811
|
+
if (this.wet.bakedLight(l.id)) this.dryLayersFirst([l.id]);
|
|
5812
|
+
const t = this.lightWithout([l.id]);
|
|
5813
|
+
if (!t.light) return { layerId: l.id, cleared: false };
|
|
5814
|
+
this.commit("Clear light", { ...this.doc, light: t.light });
|
|
5815
|
+
return { layerId: l.id, cleared: true };
|
|
5816
|
+
}
|
|
5185
5817
|
// --- Thick paint and light -------------------------------------------------------
|
|
5186
5818
|
/** The document's `thick` without these layers (as a spread: `{}` when unchanged). */
|
|
5187
5819
|
thickWithout(ids) {
|
|
@@ -5530,8 +6162,62 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5530
6162
|
}
|
|
5531
6163
|
return out;
|
|
5532
6164
|
}
|
|
6165
|
+
/**
|
|
6166
|
+
* Light paint over the (lit) layers: glow, rim and clarity in each layer's pixels, and its spill
|
|
6167
|
+
* as a Linear Dodge (Add) layer directly above it (from the painting as committed, so a stroke in
|
|
6168
|
+
* progress does not redraw the blur every frame).
|
|
6169
|
+
*/
|
|
6170
|
+
lightLayers(layers) {
|
|
6171
|
+
const light = this.doc.light ?? {};
|
|
6172
|
+
const ids = /* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(this.doc.thick ?? {})]);
|
|
6173
|
+
if (!ids.size) return layers;
|
|
6174
|
+
let out = layers;
|
|
6175
|
+
for (const id of ids) {
|
|
6176
|
+
const l = findLayer(out, id)?.layer;
|
|
6177
|
+
if (!isPixelLayer(l)) continue;
|
|
6178
|
+
let cache = this.lightCaches.get(id);
|
|
6179
|
+
if (!cache) this.lightCaches.set(id, cache = new LightCache());
|
|
6180
|
+
const glowed = cache.glow(l.surface, light[id], this.liveThick?.layerId === id ? this.liveThick.channels : this.doc.thick?.[id]);
|
|
6181
|
+
if (glowed !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: glowed }));
|
|
6182
|
+
const committed = findLayer(this.doc.layers, id)?.layer;
|
|
6183
|
+
const spill = light[id] && isPixelLayer(committed) ? cache.spill(committed.surface, light[id], this.lightSpillOf(id)) : null;
|
|
6184
|
+
if (spill) out = insertAfter(out, id, { type: "pixel", id: `${id}${SPILL_SUFFIX}`, name: `${l.name} light`, visible: l.visible, opacity: l.opacity, blend: "linear-dodge", surface: spill, offset: { ...l.offset } });
|
|
6185
|
+
}
|
|
6186
|
+
return out;
|
|
6187
|
+
}
|
|
6188
|
+
/** Visible pixel layers bottom first, as the backlight reads them (group opacities multiplied in). */
|
|
6189
|
+
backlitLayers(layers, committed, opacity = 1, out = []) {
|
|
6190
|
+
for (const l of layers) {
|
|
6191
|
+
if (!l.visible) continue;
|
|
6192
|
+
if (l.type === "group") this.backlitLayers(l.children, committed, opacity * l.opacity, out);
|
|
6193
|
+
else if (isPixelLayer(l)) {
|
|
6194
|
+
const thick = !committed && this.liveThick?.layerId === l.id ? this.liveThick.channels : this.doc.thick?.[l.id];
|
|
6195
|
+
out.push({ surface: l.surface, offset: l.offset, opacity: opacity * l.opacity, ...thick ? { thick } : {} });
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
return out;
|
|
6199
|
+
}
|
|
6200
|
+
/**
|
|
6201
|
+
* Backlit paper: the light that makes it through `base` (the layers as drawn), with the spill of
|
|
6202
|
+
* the light through the committed painting, as a Linear Dodge (Add) layer for the top of the
|
|
6203
|
+
* stack; `null` when the paper has no backlight.
|
|
6204
|
+
*/
|
|
6205
|
+
backlightLayer(base) {
|
|
6206
|
+
const paper = this.doc.paper;
|
|
6207
|
+
const amount = backlightOf(paper);
|
|
6208
|
+
if (!(amount > 0)) return null;
|
|
6209
|
+
const { width, height } = this.doc.canvas;
|
|
6210
|
+
const grain = paperGrain(paper);
|
|
6211
|
+
const current = this.backlightView.surface(paper, grain, width, height, this.backlitLayers(base, false), null);
|
|
6212
|
+
const committed = base === this.doc.layers ? current : this.backlightCommitted.surface(paper, grain, width, height, this.backlitLayers(this.doc.layers, true), null);
|
|
6213
|
+
const surface = this.backlightView.layer(current, committed, amount);
|
|
6214
|
+
return { type: "pixel", id: BACKLIGHT_LAYER_ID, name: "Backlight", visible: true, opacity: 1, blend: "linear-dodge", surface, offset: { x: 0, y: 0 } };
|
|
6215
|
+
}
|
|
5533
6216
|
renderLayers(options, view = false) {
|
|
5534
|
-
|
|
6217
|
+
const base = (options.wet ?? "display") === "dried" ? this.driedLayers() : this.currentLayers();
|
|
6218
|
+
let layers = this.lightLayers(this.litLayers(base));
|
|
6219
|
+
const backlight = options.paper !== false && !options.layerIds ? this.backlightLayer(base) : null;
|
|
6220
|
+
if (backlight) layers = [...layers, backlight];
|
|
5535
6221
|
if (options.layerIds) {
|
|
5536
6222
|
const picked = [];
|
|
5537
6223
|
for (const id of options.layerIds) picked.push(findLayer(layers, id)?.layer ?? this.requireLayer(id));
|
|
@@ -5560,13 +6246,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5560
6246
|
}
|
|
5561
6247
|
/** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */
|
|
5562
6248
|
renderPreview(options = {}) {
|
|
5563
|
-
const
|
|
6249
|
+
const full2 = this.compositePixels({ ...options, wet: options.wet ?? "display" });
|
|
5564
6250
|
const side = Math.max(1, Math.round(options.maxSide ?? 512));
|
|
5565
|
-
const k = Math.min(1, side / Math.max(
|
|
5566
|
-
if (k >= 1) return
|
|
5567
|
-
const w = Math.max(1, Math.round(
|
|
5568
|
-
const h = Math.max(1, Math.round(
|
|
5569
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
6251
|
+
const k = Math.min(1, side / Math.max(full2.width, full2.height));
|
|
6252
|
+
if (k >= 1) return full2;
|
|
6253
|
+
const w = Math.max(1, Math.round(full2.width * k));
|
|
6254
|
+
const h = Math.max(1, Math.round(full2.height * k));
|
|
6255
|
+
return { width: w, height: h, data: downsampleRgba(full2.data, full2.width, full2.height, w, h) };
|
|
5570
6256
|
}
|
|
5571
6257
|
// --- Save, load and export -------------------------------------------------------
|
|
5572
6258
|
/** The document as JSON with inline PNG assets. */
|
|
@@ -5618,6 +6304,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5618
6304
|
for (const [id, list] of Object.entries(this.doc.liveStrokes ?? {})) if (findLayer(this.doc.layers, id)) liveStrokes[id] = list.map((x) => JSON.parse(JSON.stringify(x)));
|
|
5619
6305
|
if (Object.keys(liveStrokes).length) ext.liveStrokes = liveStrokes;
|
|
5620
6306
|
else delete ext.liveStrokes;
|
|
6307
|
+
const light = {};
|
|
6308
|
+
for (const [id, t] of Object.entries(this.doc.light ?? {})) {
|
|
6309
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
6310
|
+
const saved = lightToJSON(this.wet.bakedLight(id) ?? t);
|
|
6311
|
+
if (saved) light[id] = saved;
|
|
6312
|
+
}
|
|
6313
|
+
if (Object.keys(light).length) ext.light = light;
|
|
6314
|
+
else delete ext.light;
|
|
6315
|
+
const spills = Object.fromEntries(Object.entries(this.lightSpills).filter(([id]) => findLayer(this.doc.layers, id)));
|
|
6316
|
+
if (Object.keys(spills).length) ext.lightSpill = spills;
|
|
6317
|
+
else delete ext.lightSpill;
|
|
5621
6318
|
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
5622
6319
|
if (effectLayers) ext.effectLayers = effectLayers;
|
|
5623
6320
|
else delete ext.effectLayers;
|
|
@@ -5668,6 +6365,29 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5668
6365
|
problems.push(`The effect paper could not be read (${e.message}); the plain paper shows.`);
|
|
5669
6366
|
}
|
|
5670
6367
|
}
|
|
6368
|
+
this.lightSpills = {};
|
|
6369
|
+
this.lightCaches.clear();
|
|
6370
|
+
if (ext.lightSpill && typeof ext.lightSpill === "object") {
|
|
6371
|
+
for (const [id, v] of Object.entries(ext.lightSpill)) {
|
|
6372
|
+
try {
|
|
6373
|
+
if (findLayer(state.layers, id)) this.lightSpills[id] = normalizeLightSpill(v ?? {});
|
|
6374
|
+
} catch {
|
|
6375
|
+
problems.push(`The light spill of layer "${id}" could not be read; it takes the defaults.`);
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
if (ext.light && typeof ext.light === "object") {
|
|
6380
|
+
const light = {};
|
|
6381
|
+
for (const [id, v] of Object.entries(ext.light)) {
|
|
6382
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
6383
|
+
try {
|
|
6384
|
+
light[id] = lightFromJSON(v);
|
|
6385
|
+
} catch (e) {
|
|
6386
|
+
problems.push(`The light paint of layer "${id}" could not be read (${e.message}); it loads without light.`);
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
if (Object.keys(light).length) state = { ...state, light };
|
|
6390
|
+
}
|
|
5671
6391
|
const raw = ext.layerChannels;
|
|
5672
6392
|
if (!raw || typeof raw !== "object") return state;
|
|
5673
6393
|
const thick = {};
|
|
@@ -5709,6 +6429,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5709
6429
|
}
|
|
5710
6430
|
this.docRevision++;
|
|
5711
6431
|
delete this.extensions.layerChannels;
|
|
6432
|
+
delete this.extensions.light;
|
|
6433
|
+
delete this.extensions.lightSpill;
|
|
5712
6434
|
delete this.extensions.lighting;
|
|
5713
6435
|
delete this.extensions.stickers;
|
|
5714
6436
|
delete this.extensions.liveStrokes;
|
|
@@ -5742,13 +6464,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5742
6464
|
return encodePng(img.width, img.height, img.data);
|
|
5743
6465
|
}
|
|
5744
6466
|
scaled(options) {
|
|
5745
|
-
const
|
|
6467
|
+
const full2 = this.compositePixels(options);
|
|
5746
6468
|
const s = options.scale ?? 1;
|
|
5747
6469
|
if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);
|
|
5748
|
-
if (s === 1) return
|
|
5749
|
-
const w = Math.max(1, Math.round(
|
|
5750
|
-
const h = Math.max(1, Math.round(
|
|
5751
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
6470
|
+
if (s === 1) return full2;
|
|
6471
|
+
const w = Math.max(1, Math.round(full2.width * s));
|
|
6472
|
+
const h = Math.max(1, Math.round(full2.height * s));
|
|
6473
|
+
return { width: w, height: h, data: downsampleRgba(full2.data, full2.width, full2.height, w, h) };
|
|
5752
6474
|
}
|
|
5753
6475
|
/**
|
|
5754
6476
|
* The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.
|
|
@@ -5771,7 +6493,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5771
6493
|
* pixels), and the composite as the merged image.
|
|
5772
6494
|
*/
|
|
5773
6495
|
exportPsd() {
|
|
5774
|
-
const
|
|
6496
|
+
const driedBase = this.driedLayers(this.doc.layers);
|
|
6497
|
+
const backlight = this.backlightLayer(driedBase);
|
|
6498
|
+
const dried = [...this.lightLayers(this.litLayers(driedBase)), ...backlight ? [backlight] : []];
|
|
5775
6499
|
const { width, height, dpi } = this.doc.canvas;
|
|
5776
6500
|
const paperPixels = this.compositePixels({ layerIds: [], paper: true });
|
|
5777
6501
|
const paperLayer = {
|
|
@@ -5802,6 +6526,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5802
6526
|
const one = names.length === 1;
|
|
5803
6527
|
result.warnings.push(`${label} blending has no PSD equivalent; ${list} ${one ? "was" : "were"} written as ${std}, the nearest Photoshop mode, so ${one ? "it looks" : "they look"} different in Photoshop (the merged image is exact).`);
|
|
5804
6528
|
}
|
|
6529
|
+
if (backlight) {
|
|
6530
|
+
result.warnings.push('Backlit paper is written as pixels: the light that comes through the paint is a "Backlight" Linear Dodge (Add) layer at the top. Opening the PSD again gives a plain pixel layer, not a backlight.');
|
|
6531
|
+
}
|
|
6532
|
+
if (Object.values(this.doc.light ?? {}).some((l) => l.tiles.size)) {
|
|
6533
|
+
result.warnings.push('Light paint is written as pixels: each layer with its glow, and its spill as a "\u2039layer\u203A light" Linear Dodge (Add) layer above it. Opening the PSD again gives plain pixel layers, not editable light.');
|
|
6534
|
+
}
|
|
5805
6535
|
return result;
|
|
5806
6536
|
}
|
|
5807
6537
|
/** `render_preview` as a base64 PNG. */
|
|
@@ -6807,6 +7537,20 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6807
7537
|
var MAX_IMAGE_SIDE = 8192;
|
|
6808
7538
|
var movePoint = (p, dx, dy) => ({ x: p.x + dx, y: p.y + dy });
|
|
6809
7539
|
var moveRect = (r, dx, dy) => ({ ...r, x: r.x + dx, y: r.y + dy });
|
|
7540
|
+
var SPILL_SUFFIX = "::light";
|
|
7541
|
+
function withLayerLight(light, layerId, next) {
|
|
7542
|
+
if (!next) return light ? { ...light } : void 0;
|
|
7543
|
+
const out = { ...light ?? {} };
|
|
7544
|
+
if (next.tiles.size) out[layerId] = next;
|
|
7545
|
+
else delete out[layerId];
|
|
7546
|
+
return out;
|
|
7547
|
+
}
|
|
7548
|
+
var BACKLIGHT_LAYER_ID = "__backlight";
|
|
7549
|
+
function insertAfter(layers, id, node) {
|
|
7550
|
+
const i = layers.findIndex((l) => l.id === id);
|
|
7551
|
+
if (i >= 0) return [...layers.slice(0, i + 1), node, ...layers.slice(i + 1)];
|
|
7552
|
+
return layers.map((l) => l.type === "group" ? { ...l, children: insertAfter(l.children, id, node) } : l);
|
|
7553
|
+
}
|
|
6810
7554
|
function isLiveEffect(s) {
|
|
6811
7555
|
return s.paint?.type === "effect" && s.paint.live === true;
|
|
6812
7556
|
}
|
|
@@ -7796,7 +8540,7 @@ function createPaintWetTools(c) {
|
|
|
7796
8540
|
{
|
|
7797
8541
|
name: "advance_wet_paint",
|
|
7798
8542
|
title: "Let the wet paint flow",
|
|
7799
|
-
description: "Lets time pass for wet paint: the water spreads, runs downhill on a tilted canvas, granulates, blooms and evaporates, and pigment settles. Give steps (simulation steps; at the default 120 per second, 120 steps is one second) or seconds (default 1). Not an undo step, as time passing never is. Returns the steps run, the clock, stillWet and each wet layer\u2019s state. Paint dries by itself after a few thousand steps; dry_paint dries it at once.",
|
|
8543
|
+
description: "Lets time pass for wet paint: the water spreads, runs downhill on a tilted canvas, granulates, blooms and evaporates, and pigment settles. Give steps (simulation steps; at the default 120 per second, 120 steps is one second) or seconds (default 1). Not an undo step, as time passing never is. Returns the steps run, the clock, stillWet and each wet layer\u2019s state. Paint dries by itself after a few thousand steps; dry_paint dries it at once. Wet light paint (wet brushes with light.amount) flows with the pigment and settles where it dries into the layer\u2019s light (get_light).",
|
|
7800
8544
|
inputSchema: {
|
|
7801
8545
|
type: "object",
|
|
7802
8546
|
properties: {
|
|
@@ -7955,6 +8699,39 @@ var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: fals
|
|
|
7955
8699
|
var LAYER_ID3 = { type: "string", description: "Pixel layer id from list_layers (default the active layer)." };
|
|
7956
8700
|
function createPaintThickTools(c) {
|
|
7957
8701
|
return [
|
|
8702
|
+
{
|
|
8703
|
+
name: "get_light",
|
|
8704
|
+
title: "Read a layer\u2019s light paint",
|
|
8705
|
+
description: "Reads a layer\u2019s light paint (laid by brushes with light.amount above 0): whether it has any, where it is (bounds, document pixels), its brightest value (peak 0..255) and its spill (radius px, strength). Changes nothing.",
|
|
8706
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, additionalProperties: false },
|
|
8707
|
+
annotations: { title: "Read a layer\u2019s light paint", ...READ4 },
|
|
8708
|
+
coordinates: { out: ["bounds"] },
|
|
8709
|
+
handler: ({ layerId }) => c.lightOf(layerId)
|
|
8710
|
+
},
|
|
8711
|
+
{
|
|
8712
|
+
name: "set_light_spill",
|
|
8713
|
+
title: "Set how far a layer\u2019s light spills",
|
|
8714
|
+
description: "Sets how far a layer\u2019s light paint spills onto the paint and paper around it, in each stroke\u2019s own colour: radius (px, 0 turns spill off, default 24) and strength (0..2, default 1). Nothing is repainted; saved with the document, not an undo step. Returns the layer\u2019s spill.",
|
|
8715
|
+
inputSchema: {
|
|
8716
|
+
type: "object",
|
|
8717
|
+
properties: {
|
|
8718
|
+
layerId: LAYER_ID3,
|
|
8719
|
+
radius: { type: "number", minimum: 0, maximum: 512, description: "How far the light reaches, px (0 turns spill off; default 24)." },
|
|
8720
|
+
strength: { type: "number", minimum: 0, maximum: 2, description: "How strong the spill is, 0..2 (default 1)." }
|
|
8721
|
+
},
|
|
8722
|
+
additionalProperties: false
|
|
8723
|
+
},
|
|
8724
|
+
annotations: { title: "Set how far a layer\u2019s light spills", ...SET3 },
|
|
8725
|
+
handler: ({ layerId, radius, strength }) => c.setLightSpill(layerId, { radius, strength })
|
|
8726
|
+
},
|
|
8727
|
+
{
|
|
8728
|
+
name: "clear_light",
|
|
8729
|
+
title: "Clear a layer\u2019s light",
|
|
8730
|
+
description: "Takes a layer\u2019s light paint away; its paint keeps its colour (and relief). One undo step. Returns whether anything was cleared.",
|
|
8731
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, additionalProperties: false },
|
|
8732
|
+
annotations: { title: "Clear a layer\u2019s light", ...WRITE4 },
|
|
8733
|
+
handler: ({ layerId }) => c.clearLight(layerId)
|
|
8734
|
+
},
|
|
7958
8735
|
{
|
|
7959
8736
|
name: "set_lighting",
|
|
7960
8737
|
title: "Move the light",
|
|
@@ -8205,7 +8982,18 @@ var IMPASTO_BRUSH = {
|
|
|
8205
8982
|
metallic: { type: "number", minimum: 0, maximum: 1, description: "Metallic paint: highlights take the paint colour (gold, copper, silver)." },
|
|
8206
8983
|
load: { type: "number", minimum: 0, maximum: 1, description: "Paint loading: 1 never runs out; lower, the stroke thins and fades as the brush empties." },
|
|
8207
8984
|
thinner: { type: "number", minimum: 0, maximum: 1, description: "Thinner: flatter, more transparent paint." },
|
|
8208
|
-
drag: { type: "number", minimum: 0, maximum: 1, description: "Knife and brush: how much of the paint under the tool is dragged along." }
|
|
8985
|
+
drag: { type: "number", minimum: 0, maximum: 1, description: "Knife and brush: how much of the paint under the tool is dragged along." },
|
|
8986
|
+
rim: { type: "number", minimum: 0, maximum: 1, description: "Light paint (with light.amount): the bead's light is stronger where its surface turns away, as on backlit glass (default 0)." },
|
|
8987
|
+
clarity: { type: "number", minimum: 0, maximum: 1, description: "Glassy paint: the middle of a bead lets the layers below show through while its edges stay solid (default 0). flatten_relief clears it." }
|
|
8988
|
+
},
|
|
8989
|
+
additionalProperties: false
|
|
8990
|
+
};
|
|
8991
|
+
var LIGHT_BRUSH = {
|
|
8992
|
+
type: "object",
|
|
8993
|
+
description: "Light paint: the light the brush lays with its colour, glowing in the paint's own colour and spilling onto its surroundings (set_light_spill). Dab, particle, bristle, Mixer Brush, impasto and wet paint brushes take it (wet light flows, pools, granulates and blooms with the pigment until the paint dries, then is ordinary light paint); wet tools other than paint lay none, and sticker brushes refuse light above 0. Missing fields keep their current values.",
|
|
8994
|
+
properties: {
|
|
8995
|
+
amount: { type: "number", minimum: 0, maximum: 1, description: "Light each dab lays, scaled by its coverage and flow (0 none, default; 1 full)." },
|
|
8996
|
+
pressure: { type: "boolean", description: "Pressure scales the light (default false)." }
|
|
8209
8997
|
},
|
|
8210
8998
|
additionalProperties: false
|
|
8211
8999
|
};
|
|
@@ -8276,6 +9064,7 @@ var BRUSH_FIELDS = {
|
|
|
8276
9064
|
particle: PARTICLE_BRUSH,
|
|
8277
9065
|
bristle: BRISTLE_BRUSH,
|
|
8278
9066
|
sticker: STICKER_BRUSH,
|
|
9067
|
+
light: LIGHT_BRUSH,
|
|
8279
9068
|
paint: BRUSH_PAINT
|
|
8280
9069
|
};
|
|
8281
9070
|
var TIME = { type: "number", minimum: 0, description: "Draw live effect paint (strokes on stroke layers painted with live effect paint) this many ms into its animation (default: as painted, its own time)." };
|
|
@@ -8321,6 +9110,7 @@ function brushSummary(b) {
|
|
|
8321
9110
|
multicolor: b.behavior.multicolor.mode,
|
|
8322
9111
|
...b.engine === "wet" ? { wetTool: b.wet.tool, medium: b.wet.medium } : {},
|
|
8323
9112
|
...b.engine === "impasto" ? { impastoTool: b.impasto.tool } : {},
|
|
9113
|
+
...b.light.amount > 0 ? { light: b.light.amount } : {},
|
|
8324
9114
|
builtin: Boolean(b.builtin)
|
|
8325
9115
|
};
|
|
8326
9116
|
}
|
|
@@ -8431,7 +9221,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8431
9221
|
add({
|
|
8432
9222
|
name: "get_paper",
|
|
8433
9223
|
title: "Get the paper",
|
|
8434
|
-
description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness).",
|
|
9224
|
+
description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness, and metallic, backlight and backlightColor when set).",
|
|
8435
9225
|
inputSchema: EMPTY3,
|
|
8436
9226
|
annotations: { title: "Get the paper", ...READ6 },
|
|
8437
9227
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
@@ -8447,7 +9237,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8447
9237
|
add({
|
|
8448
9238
|
name: "set_paper",
|
|
8449
9239
|
title: "Set the paper",
|
|
8450
|
-
description: "Changes the paper under the whole painting: a preset from list_papers and / or its settings. Grain depth decides how much grain-responsive brushes (pencil, pastel, charcoal) skip the valleys; changing it re-renders the paper but not strokes already painted. One undo step. Returns the paper.",
|
|
9240
|
+
description: "Changes the paper under the whole painting: a preset from list_papers and / or its settings. Grain depth decides how much grain-responsive brushes (pencil, pastel, charcoal) skip the valleys; changing it re-renders the paper but not strokes already painted. backlight lights the paper from behind (the stained-glass preset has it on). One undo step. Returns the paper.",
|
|
8451
9241
|
inputSchema: {
|
|
8452
9242
|
type: "object",
|
|
8453
9243
|
properties: {
|
|
@@ -8465,6 +9255,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8465
9255
|
permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how freely water flows over the fibres (low: more ragged, slower spreading)." },
|
|
8466
9256
|
roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain pins wet edges and catches granulating pigment." },
|
|
8467
9257
|
metallic: { type: "number", minimum: 0, maximum: 1, description: "Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light (see set_lighting). 0 by default." },
|
|
9258
|
+
backlight: { type: "number", minimum: 0, maximum: 1, description: "Backlit paper (a lightbox, stained glass): light from behind the paper, 0 (none, the default) .. 1. Transparent glazes (watercolour, airbrush, thin marker) glow in their own colour; opaque paint (gouache, oil, solid ink) and thick paint block it; the light through paint spills softly around it. Nothing is repainted." },
|
|
9259
|
+
backlightColor: { ...COLOR3, description: "The backlight\u2019s colour #rrggbb (default #fff1d6, warm white)." },
|
|
8468
9260
|
name: { type: "string", minLength: 1, description: "Display name." }
|
|
8469
9261
|
},
|
|
8470
9262
|
additionalProperties: false
|
|
@@ -8639,7 +9431,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8639
9431
|
add({
|
|
8640
9432
|
name: "list_brushes",
|
|
8641
9433
|
title: "List brushes",
|
|
8642
|
-
description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
|
|
9434
|
+
description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, glow wash; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
|
|
8643
9435
|
inputSchema: EMPTY3,
|
|
8644
9436
|
annotations: { title: "List brushes", ...READ6 },
|
|
8645
9437
|
handler: () => {
|
|
@@ -8823,7 +9615,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8823
9615
|
add({
|
|
8824
9616
|
name: "paint_stroke",
|
|
8825
9617
|
title: "Paint a brush stroke",
|
|
8826
|
-
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer\u2019s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
|
|
9618
|
+
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer\u2019s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet; with light.amount, as glow-wash has, their light flows with the paint), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
|
|
8827
9619
|
inputSchema: {
|
|
8828
9620
|
type: "object",
|
|
8829
9621
|
properties: {
|
|
@@ -9545,7 +10337,8 @@ var PAPER_SLIDERS = [
|
|
|
9545
10337
|
{ path: "absorption", label: "Absorption", min: 0, max: 1, step: 0.01 },
|
|
9546
10338
|
{ path: "permeability", label: "Permeability", min: 0, max: 1, step: 0.01 },
|
|
9547
10339
|
{ path: "roughness", label: "Roughness", min: 0, max: 1, step: 0.01 },
|
|
9548
|
-
{ path: "metallic", label: "Metallic", min: 0, max: 1, step: 0.01 }
|
|
10340
|
+
{ path: "metallic", label: "Metallic", min: 0, max: 1, step: 0.01 },
|
|
10341
|
+
{ path: "backlight", label: "Backlight", min: 0, max: 1, step: 0.01 }
|
|
9549
10342
|
];
|
|
9550
10343
|
function readPath(obj, path) {
|
|
9551
10344
|
let v = obj;
|
|
@@ -9594,11 +10387,19 @@ var BLEND_MENU_GROUPS = [
|
|
|
9594
10387
|
{ label: "Standard", modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== "pigment") },
|
|
9595
10388
|
{ label: "Paint", modes: BLEND_MODES.filter((m) => m.painterly || m.id === "pigment") }
|
|
9596
10389
|
];
|
|
10390
|
+
var LIGHT_SLIDER = { path: "light.amount", label: "Light", min: 0, max: 1, step: 0.01 };
|
|
9597
10391
|
var PICKUP_SLIDERS = [
|
|
9598
10392
|
{ path: "behavior.pickup", label: "Pick-up", min: 0, max: 1, step: 0.01 },
|
|
9599
10393
|
{ path: "behavior.mix", label: "Mix", min: 0, max: 1, step: 0.01 }
|
|
9600
10394
|
];
|
|
9601
10395
|
function brushEngineControls(brush) {
|
|
10396
|
+
const c = engineControls(brush);
|
|
10397
|
+
const wetPaint = brush.engine === "wet" && brush.wet?.tool === "paint";
|
|
10398
|
+
if (!c || !(wetPaint || ["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine))) return c;
|
|
10399
|
+
const extra = brush.engine === "impasto" ? [{ path: "impasto.rim", label: "Rim", min: 0, max: 1, step: 0.01 }, { path: "impasto.clarity", label: "Clarity", min: 0, max: 1, step: 0.01 }] : [];
|
|
10400
|
+
return { ...c, sliders: [...c.sliders, LIGHT_SLIDER, ...extra], light: true };
|
|
10401
|
+
}
|
|
10402
|
+
function engineControls(brush) {
|
|
9602
10403
|
switch (brush.engine) {
|
|
9603
10404
|
case "particle":
|
|
9604
10405
|
return {
|
|
@@ -10128,6 +10929,7 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
10128
10929
|
});
|
|
10129
10930
|
}
|
|
10130
10931
|
export {
|
|
10932
|
+
BACKLIGHT_SPILL,
|
|
10131
10933
|
BLEND_MENU_GROUPS,
|
|
10132
10934
|
BLEND_MODES,
|
|
10133
10935
|
BRUSH_ID,
|
|
@@ -10135,6 +10937,7 @@ export {
|
|
|
10135
10937
|
BRUSH_SLIDERS,
|
|
10136
10938
|
BUILTIN_BRUSHES,
|
|
10137
10939
|
BristleStroke,
|
|
10940
|
+
DEFAULT_BACKLIGHT_COLOR,
|
|
10138
10941
|
DEFAULT_BEHAVIOR,
|
|
10139
10942
|
DEFAULT_BRISTLE,
|
|
10140
10943
|
DEFAULT_BRUSH_ID,
|
|
@@ -10142,6 +10945,7 @@ export {
|
|
|
10142
10945
|
DEFAULT_DYNAMICS,
|
|
10143
10946
|
DEFAULT_FLOW,
|
|
10144
10947
|
DEFAULT_IMPASTO,
|
|
10948
|
+
DEFAULT_LIGHT,
|
|
10145
10949
|
DEFAULT_LIGHTING,
|
|
10146
10950
|
DEFAULT_MULTI_COLOR,
|
|
10147
10951
|
DEFAULT_PAINT_GRADIENT,
|
|
@@ -10259,6 +11063,7 @@ export {
|
|
|
10259
11063
|
addPaintGradientStop,
|
|
10260
11064
|
affineCopy,
|
|
10261
11065
|
applyAffine,
|
|
11066
|
+
backlightOf,
|
|
10262
11067
|
brushEngineControls,
|
|
10263
11068
|
brushLoadColor,
|
|
10264
11069
|
brushLoadForStroke,
|
|
@@ -10340,6 +11145,7 @@ export {
|
|
|
10340
11145
|
normalizeFlow,
|
|
10341
11146
|
normalizeHexColor,
|
|
10342
11147
|
normalizeImpasto,
|
|
11148
|
+
normalizeLight,
|
|
10343
11149
|
normalizeLighting,
|
|
10344
11150
|
normalizeMultiColor,
|
|
10345
11151
|
normalizePaintBrush,
|