@pixerate/editor 0.242.0 → 0.243.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-274HGNZR.js} +3 -3
- package/dist/{brushstrokeRender-UKADOV5D.cjs → brushstrokeRender-C7RJZPMC.cjs} +5 -5
- package/dist/canvas/index.cjs +8 -8
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-KCMV2534.cjs → chunk-3WOZEBXZ.cjs} +2 -2
- package/dist/{chunk-ZDPLZDTT.js → chunk-4ZHXLFXS.js} +1 -1
- package/dist/{chunk-5BR7LC24.cjs → chunk-5FZS7J7C.cjs} +50 -4
- package/dist/{chunk-ILM6GYAX.js → chunk-6L3WWIDW.js} +1 -1
- package/dist/{chunk-UDBDYZET.js → chunk-H4Z72Z4Z.js} +1 -1
- package/dist/{chunk-XBZK47LV.js → chunk-HQZHOPNE.js} +49 -3
- package/dist/{chunk-QP2B3MID.js → chunk-IXI5KATU.js} +1 -1
- package/dist/{chunk-HGGJ36HR.cjs → chunk-KK2FWMA2.cjs} +1 -1
- package/dist/{chunk-AFLMXITL.cjs → chunk-OONFIFEU.cjs} +23 -23
- package/dist/{chunk-HW72ABGR.cjs → chunk-R2FVAKFH.cjs} +86 -86
- package/dist/{chunk-NE6OOKWJ.js → chunk-SRXWDOUI.js} +1 -1
- package/dist/{chunk-7F4G74UQ.cjs → chunk-UV7OOZJQ.cjs} +35 -35
- package/dist/image-editor/index.cjs +775 -775
- package/dist/image-editor/index.js +4 -4
- 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 +28 -0
- package/dist/paint-editor/controller.d.ts +28 -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 +1011 -493
- package/dist/paint-editor/index.js +560 -42
- package/dist/paint-editor/light.d.cts +74 -0
- package/dist/paint-editor/light.d.ts +74 -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/vector/index.cjs +962 -962
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +9 -9
- package/skills/pixerate-paint-editor/references/canvas-hosted.md +8 -0
- package/skills/pixerate-paint-editor/references/light-paint.md +31 -0
- package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveExternalAssets,
|
|
8
8
|
serializeLayers,
|
|
9
9
|
serializeLayersExternal
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-SRXWDOUI.js";
|
|
11
11
|
import "../chunk-3CI7FRMK.js";
|
|
12
12
|
import "../chunk-ZNA3IQZF.js";
|
|
13
13
|
import {
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
realPresenceClock,
|
|
56
56
|
tweenPresenceCursor
|
|
57
57
|
} from "../chunk-IVSWRVB4.js";
|
|
58
|
-
import "../chunk-
|
|
58
|
+
import "../chunk-6L3WWIDW.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-HQZHOPNE.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
|
}
|
|
@@ -2605,6 +2923,7 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
2605
2923
|
const [r, g, b] = hexToRgb8(color);
|
|
2606
2924
|
let result = surface;
|
|
2607
2925
|
let thick = null;
|
|
2926
|
+
let light = null;
|
|
2608
2927
|
const session = new PaintStrokeSession(
|
|
2609
2928
|
{
|
|
2610
2929
|
update: () => {
|
|
@@ -2612,6 +2931,7 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
2612
2931
|
commit: (sess, s) => {
|
|
2613
2932
|
result = s ?? result;
|
|
2614
2933
|
thick = sess.thickChannels();
|
|
2934
|
+
light = sess.lightChannel();
|
|
2615
2935
|
}
|
|
2616
2936
|
},
|
|
2617
2937
|
{
|
|
@@ -2630,12 +2950,14 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
2630
2950
|
copies: [IDENTITY_COPY],
|
|
2631
2951
|
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),
|
|
2632
2952
|
thick: brush.engine === "impasto" ? { channels: emptyThickChannels() } : void 0,
|
|
2633
|
-
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0
|
|
2953
|
+
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0,
|
|
2954
|
+
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null
|
|
2634
2955
|
}
|
|
2635
2956
|
);
|
|
2636
2957
|
session.push(thumbnailStroke(width, height));
|
|
2637
2958
|
session.end();
|
|
2638
|
-
|
|
2959
|
+
const lit = thick ? new LitCache().lit(result, thick, DEFAULT_LIGHTING) : result;
|
|
2960
|
+
return { surface: light || thick ? new LightCache().glow(lit, light ?? void 0, thick ?? void 0) : lit, light };
|
|
2639
2961
|
}
|
|
2640
2962
|
function renderBrushThumbnail(brush, options) {
|
|
2641
2963
|
const width = Math.max(16, Math.min(512, Math.round(options.width ?? 160)));
|
|
@@ -2652,9 +2974,15 @@ function renderBrushThumbnail(brush, options) {
|
|
|
2652
2974
|
surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);
|
|
2653
2975
|
surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
|
|
2654
2976
|
}
|
|
2655
|
-
|
|
2977
|
+
let light = null;
|
|
2978
|
+
if (b.engine === "wet") surface = paintWetTestStroke(b, options.paper, options.color, width, height);
|
|
2979
|
+
else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height));
|
|
2656
2980
|
const layer = { type: "pixel", id: "thumbnail", name: "thumbnail", visible: true, opacity: 1, blend: "normal", surface, offset: { x: 0, y: 0 } };
|
|
2657
|
-
const
|
|
2981
|
+
const paper = light ? { ...options.paper, color: "#141414" } : options.paper;
|
|
2982
|
+
const spill = light ? computeSpill(surface, light, { radius: Math.max(4, height / 6), strength: 1 }) : null;
|
|
2983
|
+
const layers = [paperNode(paper), layer];
|
|
2984
|
+
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 } });
|
|
2985
|
+
const data = createPaintCompositor().composite(layers, { x: 0, y: 0, width, height }, { width, height });
|
|
2658
2986
|
return { width, height, data };
|
|
2659
2987
|
}
|
|
2660
2988
|
|
|
@@ -3127,6 +3455,7 @@ function liveStrokeRefusal(brush) {
|
|
|
3127
3455
|
case "smudge":
|
|
3128
3456
|
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
3457
|
}
|
|
3458
|
+
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
3459
|
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
3460
|
return null;
|
|
3132
3461
|
}
|
|
@@ -3535,6 +3864,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3535
3864
|
dirty = "all";
|
|
3536
3865
|
seedCounter = 1;
|
|
3537
3866
|
strokeCounter = 1;
|
|
3867
|
+
/** Light paint: each layer's spill settings (saved with the document, not undo steps; missing = the defaults). */
|
|
3868
|
+
lightSpills = {};
|
|
3869
|
+
lightCaches = /* @__PURE__ */ new Map();
|
|
3538
3870
|
/** Live effect paint: playing and the frame rate cap (editor state), and the time the view shows. */
|
|
3539
3871
|
effectPlaybackState = { playing: true, fps: 30 };
|
|
3540
3872
|
effectTime = 0;
|
|
@@ -4068,7 +4400,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4068
4400
|
const layers = mapLayer(this.doc.layers, id, () => null);
|
|
4069
4401
|
let active = this.doc.activeLayerId;
|
|
4070
4402
|
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)) });
|
|
4403
|
+
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
4404
|
return { removed: id, activeLayerId: active };
|
|
4073
4405
|
}
|
|
4074
4406
|
duplicateLayer(id) {
|
|
@@ -4082,8 +4414,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4082
4414
|
const thick = { ...this.doc.thick ?? {} };
|
|
4083
4415
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
4084
4416
|
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
4417
|
+
const light = { ...this.doc.light ?? {} };
|
|
4085
4418
|
const copy = (l) => {
|
|
4086
4419
|
const id2 = newPaintLayerId();
|
|
4420
|
+
if (light[l.id]) light[id2] = light[l.id];
|
|
4421
|
+
if (this.lightSpills[l.id]) this.lightSpills[id2] = { ...this.lightSpills[l.id] };
|
|
4087
4422
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
4088
4423
|
if (stickers[l.id]) stickers[id2] = stickers[l.id].map((x) => ({ ...x }));
|
|
4089
4424
|
if (liveStrokes[l.id]) liveStrokes[id2] = liveStrokes[l.id].map((x) => ({ ...x, id: `s${this.strokeCounter++}`, layerId: id2 }));
|
|
@@ -4091,7 +4426,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4091
4426
|
};
|
|
4092
4427
|
const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };
|
|
4093
4428
|
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 } : {} }));
|
|
4429
|
+
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
4430
|
return this.describeLayer(dup);
|
|
4096
4431
|
}
|
|
4097
4432
|
/** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */
|
|
@@ -4137,7 +4472,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4137
4472
|
}
|
|
4138
4473
|
const merged = { ...below, surface, offset: { x: 0, y: 0 } };
|
|
4139
4474
|
const layers = mapLayer(mapLayer(this.doc.layers, upper.id, () => null), below.id, () => merged);
|
|
4140
|
-
|
|
4475
|
+
const toDoc = (t, l) => t ? translateSurface(t, l.offset.x, l.offset.y) : void 0;
|
|
4476
|
+
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;
|
|
4477
|
+
const lightAfter = this.lightWithout([upper.id, below.id]).light ?? (this.doc.light ? { ...this.doc.light } : void 0);
|
|
4478
|
+
const light = mergedLight ? { ...lightAfter ?? {}, [below.id]: mergedLight } : lightAfter;
|
|
4479
|
+
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
4480
|
return this.describeLayer(merged);
|
|
4142
4481
|
}
|
|
4143
4482
|
/** Removes every pixel of a layer. */
|
|
@@ -4149,7 +4488,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4149
4488
|
this.wet.world.removeLane(l.id);
|
|
4150
4489
|
}
|
|
4151
4490
|
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 }));
|
|
4491
|
+
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
4492
|
return { layerId: l.id, bounds };
|
|
4154
4493
|
}
|
|
4155
4494
|
/**
|
|
@@ -4193,7 +4532,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4193
4532
|
offset: l.offset
|
|
4194
4533
|
};
|
|
4195
4534
|
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
4196
|
-
|
|
4535
|
+
const lit = this.doc.light?.[l.id];
|
|
4536
|
+
const light = lit ? withLayerLight(this.doc.light, l.id, scaleLight(lit, 1 - opacity)) : void 0;
|
|
4537
|
+
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
4538
|
return { layerId: l.id, bounds: rect };
|
|
4198
4539
|
}
|
|
4199
4540
|
applyCoverage(l, coverage, rect, apply) {
|
|
@@ -4851,6 +5192,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4851
5192
|
})();
|
|
4852
5193
|
const brush = this.strokeBrush(init);
|
|
4853
5194
|
const effect = this.strokeEffect(init, brush);
|
|
5195
|
+
if (brush.light.amount > 0) {
|
|
5196
|
+
if (brush.engine === "wet") throw new PaintEditorError(`The ${brush.name} brush paints wet media, which doesn't take light yet (glowing watercolour is planned in #494); paint light with a dab or impasto light brush (list_brushes category light), or set light.amount to 0 (set_brush).`);
|
|
5197
|
+
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).`);
|
|
5198
|
+
}
|
|
4854
5199
|
if (doc.liveStrokes?.[layer.id]) {
|
|
4855
5200
|
const why = liveStrokeRefusal(brush);
|
|
4856
5201
|
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
@@ -4947,6 +5292,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4947
5292
|
secondary: rgbaOf(this.secondaryColor),
|
|
4948
5293
|
wet,
|
|
4949
5294
|
thick: brush.engine === "impasto" ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
5295
|
+
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
5296
|
+
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
4950
5297
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc) } : void 0,
|
|
4951
5298
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
4952
5299
|
effect
|
|
@@ -5029,7 +5376,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5029
5376
|
const thick = ch ? { ...this.doc.thick ?? {}, [l.id]: ch } : this.doc.thick;
|
|
5030
5377
|
const sprayed = s.stickers();
|
|
5031
5378
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
5032
|
-
|
|
5379
|
+
const light = withLayerLight(this.doc.light, l.id, s.lightChannel());
|
|
5380
|
+
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
5381
|
}
|
|
5034
5382
|
};
|
|
5035
5383
|
let session;
|
|
@@ -5067,7 +5415,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5067
5415
|
layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),
|
|
5068
5416
|
strokes: [...out.strokes, stroke],
|
|
5069
5417
|
...ch ? { thick: { ...out.thick ?? {}, [l.id]: ch } } : {},
|
|
5070
|
-
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {}
|
|
5418
|
+
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
|
|
5419
|
+
...s.lightChannel() ? { light: withLayerLight(out.light, l.id, s.lightChannel()) } : {}
|
|
5071
5420
|
}, stroke);
|
|
5072
5421
|
}
|
|
5073
5422
|
};
|
|
@@ -5182,6 +5531,45 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5182
5531
|
if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? "Replay stroke" : "Replay strokes", doc);
|
|
5183
5532
|
return { strokeIds: ids, bounds, layerIds: [...layerIds] };
|
|
5184
5533
|
}
|
|
5534
|
+
// --- Light paint --------------------------------------------------------------------
|
|
5535
|
+
/** The document's `light` without these layers (as a spread: `{}` when unchanged). */
|
|
5536
|
+
lightWithout(ids) {
|
|
5537
|
+
const t = this.doc.light;
|
|
5538
|
+
if (!t || !ids.some((id) => t[id])) return {};
|
|
5539
|
+
const out = { ...t };
|
|
5540
|
+
for (const id of ids) delete out[id];
|
|
5541
|
+
return { light: out };
|
|
5542
|
+
}
|
|
5543
|
+
lightSpillOf(layerId) {
|
|
5544
|
+
return this.lightSpills[layerId] ?? DEFAULT_LIGHT_SPILL;
|
|
5545
|
+
}
|
|
5546
|
+
/** A layer's light paint: whether it has any, where (document pixels), its brightest value 0..255, and its spill. */
|
|
5547
|
+
lightOf(layerId) {
|
|
5548
|
+
const l = this.wetLayer(layerId, "read the light of");
|
|
5549
|
+
const info = lightInfo(this.doc.light?.[l.id]);
|
|
5550
|
+
const bounds = info.bounds ? { ...info.bounds, x: info.bounds.x + l.offset.x, y: info.bounds.y + l.offset.y } : null;
|
|
5551
|
+
return { layerId: l.id, hasLight: info.peak > 0, bounds, peak: info.peak, spill: { ...this.lightSpillOf(l.id) } };
|
|
5552
|
+
}
|
|
5553
|
+
/** 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. */
|
|
5554
|
+
setLightSpill(layerId, update) {
|
|
5555
|
+
const l = this.wetLayer(layerId, "set the light spill of");
|
|
5556
|
+
try {
|
|
5557
|
+
this.lightSpills[l.id] = normalizeLightSpill(update, this.lightSpillOf(l.id));
|
|
5558
|
+
} catch (e) {
|
|
5559
|
+
throw new PaintEditorError(e.message);
|
|
5560
|
+
}
|
|
5561
|
+
this.emit("all");
|
|
5562
|
+
return { ...this.lightSpills[l.id] };
|
|
5563
|
+
}
|
|
5564
|
+
/** Takes a layer's light away; its paint keeps its colour. One undo step. Returns whether there was any. */
|
|
5565
|
+
clearLight(layerId) {
|
|
5566
|
+
this.requireIdle("clearing light");
|
|
5567
|
+
const l = this.paintTarget(layerId, "clear the light of");
|
|
5568
|
+
const t = this.lightWithout([l.id]);
|
|
5569
|
+
if (!t.light) return { layerId: l.id, cleared: false };
|
|
5570
|
+
this.commit("Clear light", { ...this.doc, light: t.light });
|
|
5571
|
+
return { layerId: l.id, cleared: true };
|
|
5572
|
+
}
|
|
5185
5573
|
// --- Thick paint and light -------------------------------------------------------
|
|
5186
5574
|
/** The document's `thick` without these layers (as a spread: `{}` when unchanged). */
|
|
5187
5575
|
thickWithout(ids) {
|
|
@@ -5530,8 +5918,31 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5530
5918
|
}
|
|
5531
5919
|
return out;
|
|
5532
5920
|
}
|
|
5921
|
+
/**
|
|
5922
|
+
* Light paint over the (lit) layers: glow, rim and clarity in each layer's pixels, and its spill
|
|
5923
|
+
* as a Linear Dodge (Add) layer directly above it (from the painting as committed, so a stroke in
|
|
5924
|
+
* progress does not redraw the blur every frame).
|
|
5925
|
+
*/
|
|
5926
|
+
lightLayers(layers) {
|
|
5927
|
+
const light = this.doc.light ?? {};
|
|
5928
|
+
const ids = /* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(this.doc.thick ?? {})]);
|
|
5929
|
+
if (!ids.size) return layers;
|
|
5930
|
+
let out = layers;
|
|
5931
|
+
for (const id of ids) {
|
|
5932
|
+
const l = findLayer(out, id)?.layer;
|
|
5933
|
+
if (!isPixelLayer(l)) continue;
|
|
5934
|
+
let cache = this.lightCaches.get(id);
|
|
5935
|
+
if (!cache) this.lightCaches.set(id, cache = new LightCache());
|
|
5936
|
+
const glowed = cache.glow(l.surface, light[id], this.liveThick?.layerId === id ? this.liveThick.channels : this.doc.thick?.[id]);
|
|
5937
|
+
if (glowed !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: glowed }));
|
|
5938
|
+
const committed = findLayer(this.doc.layers, id)?.layer;
|
|
5939
|
+
const spill = light[id] && isPixelLayer(committed) ? cache.spill(committed.surface, light[id], this.lightSpillOf(id)) : null;
|
|
5940
|
+
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 } });
|
|
5941
|
+
}
|
|
5942
|
+
return out;
|
|
5943
|
+
}
|
|
5533
5944
|
renderLayers(options, view = false) {
|
|
5534
|
-
let layers = this.litLayers((options.wet ?? "display") === "dried" ? this.driedLayers() : this.currentLayers());
|
|
5945
|
+
let layers = this.lightLayers(this.litLayers((options.wet ?? "display") === "dried" ? this.driedLayers() : this.currentLayers()));
|
|
5535
5946
|
if (options.layerIds) {
|
|
5536
5947
|
const picked = [];
|
|
5537
5948
|
for (const id of options.layerIds) picked.push(findLayer(layers, id)?.layer ?? this.requireLayer(id));
|
|
@@ -5560,13 +5971,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5560
5971
|
}
|
|
5561
5972
|
/** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */
|
|
5562
5973
|
renderPreview(options = {}) {
|
|
5563
|
-
const
|
|
5974
|
+
const full2 = this.compositePixels({ ...options, wet: options.wet ?? "display" });
|
|
5564
5975
|
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(
|
|
5976
|
+
const k = Math.min(1, side / Math.max(full2.width, full2.height));
|
|
5977
|
+
if (k >= 1) return full2;
|
|
5978
|
+
const w = Math.max(1, Math.round(full2.width * k));
|
|
5979
|
+
const h = Math.max(1, Math.round(full2.height * k));
|
|
5980
|
+
return { width: w, height: h, data: downsampleRgba(full2.data, full2.width, full2.height, w, h) };
|
|
5570
5981
|
}
|
|
5571
5982
|
// --- Save, load and export -------------------------------------------------------
|
|
5572
5983
|
/** The document as JSON with inline PNG assets. */
|
|
@@ -5618,6 +6029,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5618
6029
|
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
6030
|
if (Object.keys(liveStrokes).length) ext.liveStrokes = liveStrokes;
|
|
5620
6031
|
else delete ext.liveStrokes;
|
|
6032
|
+
const light = {};
|
|
6033
|
+
for (const [id, t] of Object.entries(this.doc.light ?? {})) {
|
|
6034
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
6035
|
+
const saved = lightToJSON(t);
|
|
6036
|
+
if (saved) light[id] = saved;
|
|
6037
|
+
}
|
|
6038
|
+
if (Object.keys(light).length) ext.light = light;
|
|
6039
|
+
else delete ext.light;
|
|
6040
|
+
const spills = Object.fromEntries(Object.entries(this.lightSpills).filter(([id]) => findLayer(this.doc.layers, id)));
|
|
6041
|
+
if (Object.keys(spills).length) ext.lightSpill = spills;
|
|
6042
|
+
else delete ext.lightSpill;
|
|
5621
6043
|
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
5622
6044
|
if (effectLayers) ext.effectLayers = effectLayers;
|
|
5623
6045
|
else delete ext.effectLayers;
|
|
@@ -5668,6 +6090,29 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5668
6090
|
problems.push(`The effect paper could not be read (${e.message}); the plain paper shows.`);
|
|
5669
6091
|
}
|
|
5670
6092
|
}
|
|
6093
|
+
this.lightSpills = {};
|
|
6094
|
+
this.lightCaches.clear();
|
|
6095
|
+
if (ext.lightSpill && typeof ext.lightSpill === "object") {
|
|
6096
|
+
for (const [id, v] of Object.entries(ext.lightSpill)) {
|
|
6097
|
+
try {
|
|
6098
|
+
if (findLayer(state.layers, id)) this.lightSpills[id] = normalizeLightSpill(v ?? {});
|
|
6099
|
+
} catch {
|
|
6100
|
+
problems.push(`The light spill of layer "${id}" could not be read; it takes the defaults.`);
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
if (ext.light && typeof ext.light === "object") {
|
|
6105
|
+
const light = {};
|
|
6106
|
+
for (const [id, v] of Object.entries(ext.light)) {
|
|
6107
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
6108
|
+
try {
|
|
6109
|
+
light[id] = lightFromJSON(v);
|
|
6110
|
+
} catch (e) {
|
|
6111
|
+
problems.push(`The light paint of layer "${id}" could not be read (${e.message}); it loads without light.`);
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
if (Object.keys(light).length) state = { ...state, light };
|
|
6115
|
+
}
|
|
5671
6116
|
const raw = ext.layerChannels;
|
|
5672
6117
|
if (!raw || typeof raw !== "object") return state;
|
|
5673
6118
|
const thick = {};
|
|
@@ -5709,6 +6154,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5709
6154
|
}
|
|
5710
6155
|
this.docRevision++;
|
|
5711
6156
|
delete this.extensions.layerChannels;
|
|
6157
|
+
delete this.extensions.light;
|
|
6158
|
+
delete this.extensions.lightSpill;
|
|
5712
6159
|
delete this.extensions.lighting;
|
|
5713
6160
|
delete this.extensions.stickers;
|
|
5714
6161
|
delete this.extensions.liveStrokes;
|
|
@@ -5742,13 +6189,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5742
6189
|
return encodePng(img.width, img.height, img.data);
|
|
5743
6190
|
}
|
|
5744
6191
|
scaled(options) {
|
|
5745
|
-
const
|
|
6192
|
+
const full2 = this.compositePixels(options);
|
|
5746
6193
|
const s = options.scale ?? 1;
|
|
5747
6194
|
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(
|
|
6195
|
+
if (s === 1) return full2;
|
|
6196
|
+
const w = Math.max(1, Math.round(full2.width * s));
|
|
6197
|
+
const h = Math.max(1, Math.round(full2.height * s));
|
|
6198
|
+
return { width: w, height: h, data: downsampleRgba(full2.data, full2.width, full2.height, w, h) };
|
|
5752
6199
|
}
|
|
5753
6200
|
/**
|
|
5754
6201
|
* The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.
|
|
@@ -5771,7 +6218,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5771
6218
|
* pixels), and the composite as the merged image.
|
|
5772
6219
|
*/
|
|
5773
6220
|
exportPsd() {
|
|
5774
|
-
const dried = this.litLayers(this.driedLayers(this.doc.layers));
|
|
6221
|
+
const dried = this.lightLayers(this.litLayers(this.driedLayers(this.doc.layers)));
|
|
5775
6222
|
const { width, height, dpi } = this.doc.canvas;
|
|
5776
6223
|
const paperPixels = this.compositePixels({ layerIds: [], paper: true });
|
|
5777
6224
|
const paperLayer = {
|
|
@@ -5802,6 +6249,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5802
6249
|
const one = names.length === 1;
|
|
5803
6250
|
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
6251
|
}
|
|
6252
|
+
if (Object.values(this.doc.light ?? {}).some((l) => l.tiles.size)) {
|
|
6253
|
+
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.');
|
|
6254
|
+
}
|
|
5805
6255
|
return result;
|
|
5806
6256
|
}
|
|
5807
6257
|
/** `render_preview` as a base64 PNG. */
|
|
@@ -6807,6 +7257,19 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6807
7257
|
var MAX_IMAGE_SIDE = 8192;
|
|
6808
7258
|
var movePoint = (p, dx, dy) => ({ x: p.x + dx, y: p.y + dy });
|
|
6809
7259
|
var moveRect = (r, dx, dy) => ({ ...r, x: r.x + dx, y: r.y + dy });
|
|
7260
|
+
var SPILL_SUFFIX = "::light";
|
|
7261
|
+
function withLayerLight(light, layerId, next) {
|
|
7262
|
+
if (!next) return light ? { ...light } : void 0;
|
|
7263
|
+
const out = { ...light ?? {} };
|
|
7264
|
+
if (next.tiles.size) out[layerId] = next;
|
|
7265
|
+
else delete out[layerId];
|
|
7266
|
+
return out;
|
|
7267
|
+
}
|
|
7268
|
+
function insertAfter(layers, id, node) {
|
|
7269
|
+
const i = layers.findIndex((l) => l.id === id);
|
|
7270
|
+
if (i >= 0) return [...layers.slice(0, i + 1), node, ...layers.slice(i + 1)];
|
|
7271
|
+
return layers.map((l) => l.type === "group" ? { ...l, children: insertAfter(l.children, id, node) } : l);
|
|
7272
|
+
}
|
|
6810
7273
|
function isLiveEffect(s) {
|
|
6811
7274
|
return s.paint?.type === "effect" && s.paint.live === true;
|
|
6812
7275
|
}
|
|
@@ -7955,6 +8418,39 @@ var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: fals
|
|
|
7955
8418
|
var LAYER_ID3 = { type: "string", description: "Pixel layer id from list_layers (default the active layer)." };
|
|
7956
8419
|
function createPaintThickTools(c) {
|
|
7957
8420
|
return [
|
|
8421
|
+
{
|
|
8422
|
+
name: "get_light",
|
|
8423
|
+
title: "Read a layer\u2019s light paint",
|
|
8424
|
+
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.",
|
|
8425
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, additionalProperties: false },
|
|
8426
|
+
annotations: { title: "Read a layer\u2019s light paint", ...READ4 },
|
|
8427
|
+
coordinates: { out: ["bounds"] },
|
|
8428
|
+
handler: ({ layerId }) => c.lightOf(layerId)
|
|
8429
|
+
},
|
|
8430
|
+
{
|
|
8431
|
+
name: "set_light_spill",
|
|
8432
|
+
title: "Set how far a layer\u2019s light spills",
|
|
8433
|
+
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.",
|
|
8434
|
+
inputSchema: {
|
|
8435
|
+
type: "object",
|
|
8436
|
+
properties: {
|
|
8437
|
+
layerId: LAYER_ID3,
|
|
8438
|
+
radius: { type: "number", minimum: 0, maximum: 512, description: "How far the light reaches, px (0 turns spill off; default 24)." },
|
|
8439
|
+
strength: { type: "number", minimum: 0, maximum: 2, description: "How strong the spill is, 0..2 (default 1)." }
|
|
8440
|
+
},
|
|
8441
|
+
additionalProperties: false
|
|
8442
|
+
},
|
|
8443
|
+
annotations: { title: "Set how far a layer\u2019s light spills", ...SET3 },
|
|
8444
|
+
handler: ({ layerId, radius, strength }) => c.setLightSpill(layerId, { radius, strength })
|
|
8445
|
+
},
|
|
8446
|
+
{
|
|
8447
|
+
name: "clear_light",
|
|
8448
|
+
title: "Clear a layer\u2019s light",
|
|
8449
|
+
description: "Takes a layer\u2019s light paint away; its paint keeps its colour (and relief). One undo step. Returns whether anything was cleared.",
|
|
8450
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, additionalProperties: false },
|
|
8451
|
+
annotations: { title: "Clear a layer\u2019s light", ...WRITE4 },
|
|
8452
|
+
handler: ({ layerId }) => c.clearLight(layerId)
|
|
8453
|
+
},
|
|
7958
8454
|
{
|
|
7959
8455
|
name: "set_lighting",
|
|
7960
8456
|
title: "Move the light",
|
|
@@ -8205,7 +8701,18 @@ var IMPASTO_BRUSH = {
|
|
|
8205
8701
|
metallic: { type: "number", minimum: 0, maximum: 1, description: "Metallic paint: highlights take the paint colour (gold, copper, silver)." },
|
|
8206
8702
|
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
8703
|
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." }
|
|
8704
|
+
drag: { type: "number", minimum: 0, maximum: 1, description: "Knife and brush: how much of the paint under the tool is dragged along." },
|
|
8705
|
+
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)." },
|
|
8706
|
+
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." }
|
|
8707
|
+
},
|
|
8708
|
+
additionalProperties: false
|
|
8709
|
+
};
|
|
8710
|
+
var LIGHT_BRUSH = {
|
|
8711
|
+
type: "object",
|
|
8712
|
+
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 and impasto brushes take it; wet and sticker brushes refuse light above 0. Missing fields keep their current values.",
|
|
8713
|
+
properties: {
|
|
8714
|
+
amount: { type: "number", minimum: 0, maximum: 1, description: "Light each dab lays, scaled by its coverage and flow (0 none, default; 1 full)." },
|
|
8715
|
+
pressure: { type: "boolean", description: "Pressure scales the light (default false)." }
|
|
8209
8716
|
},
|
|
8210
8717
|
additionalProperties: false
|
|
8211
8718
|
};
|
|
@@ -8276,6 +8783,7 @@ var BRUSH_FIELDS = {
|
|
|
8276
8783
|
particle: PARTICLE_BRUSH,
|
|
8277
8784
|
bristle: BRISTLE_BRUSH,
|
|
8278
8785
|
sticker: STICKER_BRUSH,
|
|
8786
|
+
light: LIGHT_BRUSH,
|
|
8279
8787
|
paint: BRUSH_PAINT
|
|
8280
8788
|
};
|
|
8281
8789
|
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 +8829,7 @@ function brushSummary(b) {
|
|
|
8321
8829
|
multicolor: b.behavior.multicolor.mode,
|
|
8322
8830
|
...b.engine === "wet" ? { wetTool: b.wet.tool, medium: b.wet.medium } : {},
|
|
8323
8831
|
...b.engine === "impasto" ? { impastoTool: b.impasto.tool } : {},
|
|
8832
|
+
...b.light.amount > 0 ? { light: b.light.amount } : {},
|
|
8324
8833
|
builtin: Boolean(b.builtin)
|
|
8325
8834
|
};
|
|
8326
8835
|
}
|
|
@@ -8639,7 +9148,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8639
9148
|
add({
|
|
8640
9149
|
name: "list_brushes",
|
|
8641
9150
|
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.",
|
|
9151
|
+
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; 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
9152
|
inputSchema: EMPTY3,
|
|
8644
9153
|
annotations: { title: "List brushes", ...READ6 },
|
|
8645
9154
|
handler: () => {
|
|
@@ -9594,11 +10103,18 @@ var BLEND_MENU_GROUPS = [
|
|
|
9594
10103
|
{ label: "Standard", modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== "pigment") },
|
|
9595
10104
|
{ label: "Paint", modes: BLEND_MODES.filter((m) => m.painterly || m.id === "pigment") }
|
|
9596
10105
|
];
|
|
10106
|
+
var LIGHT_SLIDER = { path: "light.amount", label: "Light", min: 0, max: 1, step: 0.01 };
|
|
9597
10107
|
var PICKUP_SLIDERS = [
|
|
9598
10108
|
{ path: "behavior.pickup", label: "Pick-up", min: 0, max: 1, step: 0.01 },
|
|
9599
10109
|
{ path: "behavior.mix", label: "Mix", min: 0, max: 1, step: 0.01 }
|
|
9600
10110
|
];
|
|
9601
10111
|
function brushEngineControls(brush) {
|
|
10112
|
+
const c = engineControls(brush);
|
|
10113
|
+
if (!c || !["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine)) return c;
|
|
10114
|
+
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 }] : [];
|
|
10115
|
+
return { ...c, sliders: [...c.sliders, LIGHT_SLIDER, ...extra], light: true };
|
|
10116
|
+
}
|
|
10117
|
+
function engineControls(brush) {
|
|
9602
10118
|
switch (brush.engine) {
|
|
9603
10119
|
case "particle":
|
|
9604
10120
|
return {
|
|
@@ -10142,6 +10658,7 @@ export {
|
|
|
10142
10658
|
DEFAULT_DYNAMICS,
|
|
10143
10659
|
DEFAULT_FLOW,
|
|
10144
10660
|
DEFAULT_IMPASTO,
|
|
10661
|
+
DEFAULT_LIGHT,
|
|
10145
10662
|
DEFAULT_LIGHTING,
|
|
10146
10663
|
DEFAULT_MULTI_COLOR,
|
|
10147
10664
|
DEFAULT_PAINT_GRADIENT,
|
|
@@ -10340,6 +10857,7 @@ export {
|
|
|
10340
10857
|
normalizeFlow,
|
|
10341
10858
|
normalizeHexColor,
|
|
10342
10859
|
normalizeImpasto,
|
|
10860
|
+
normalizeLight,
|
|
10343
10861
|
normalizeLighting,
|
|
10344
10862
|
normalizeMultiColor,
|
|
10345
10863
|
normalizePaintBrush,
|