@pixerate/editor 0.277.0 → 0.278.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/paint-editor/controller.d.cts +40 -8
- package/dist/paint-editor/controller.d.ts +40 -8
- package/dist/paint-editor/index.cjs +473 -254
- package/dist/paint-editor/index.js +265 -46
- package/dist/paint-editor/serialize.d.cts +3 -1
- package/dist/paint-editor/serialize.d.ts +3 -1
- package/dist/paint-editor/stroke.d.cts +3 -0
- package/dist/paint-editor/stroke.d.ts +3 -0
- package/dist/paint-editor/strokes.d.cts +11 -1
- package/dist/paint-editor/strokes.d.ts +11 -1
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +2 -2
|
@@ -368,7 +368,10 @@ import {
|
|
|
368
368
|
wheelToHue
|
|
369
369
|
} from "../chunk-2EUW2XZX.js";
|
|
370
370
|
import "../chunk-E2V5DA3W.js";
|
|
371
|
-
import
|
|
371
|
+
import {
|
|
372
|
+
isDualColor,
|
|
373
|
+
normalizeDualColor
|
|
374
|
+
} from "../chunk-VR6OGHU5.js";
|
|
372
375
|
import "../chunk-JC2SHA4A.js";
|
|
373
376
|
import {
|
|
374
377
|
linearToSrgb,
|
|
@@ -4172,6 +4175,8 @@ function normalizeBrushStroke(raw) {
|
|
|
4172
4175
|
points,
|
|
4173
4176
|
seed: finite2(r.seed) ? r.seed | 0 : 0
|
|
4174
4177
|
};
|
|
4178
|
+
const dual = r.dualColor === void 0 ? null : normalizeDualColor(r.dualColor);
|
|
4179
|
+
if (dual && /^#[0-9a-f]{6}$/.test(dual.light) && /^#[0-9a-f]{6}$/.test(dual.dark)) out.dualColor = dual;
|
|
4175
4180
|
const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m) => m !== null) : [];
|
|
4176
4181
|
if (copies.length) out.copies = copies;
|
|
4177
4182
|
const load = readLoad(r.load);
|
|
@@ -4380,9 +4385,77 @@ function serializePaintDocument(state, options = {}) {
|
|
|
4380
4385
|
const { layers, assets } = serializeLayers(state.layers);
|
|
4381
4386
|
return { ...header(state, options), layers, assets };
|
|
4382
4387
|
}
|
|
4388
|
+
var NO_EXTENSION_PNGS = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "wetEvents", "paintEvents", "origin"]);
|
|
4389
|
+
var EXTENSION_ASSET_REF = "asset:";
|
|
4390
|
+
var PNG_DATA_URL = "data:image/png;base64,";
|
|
4391
|
+
var storedPngs = /* @__PURE__ */ new WeakMap();
|
|
4392
|
+
function fnv1a(text) {
|
|
4393
|
+
let h = 2166136261;
|
|
4394
|
+
for (let i = 0; i < text.length; i++) {
|
|
4395
|
+
h ^= text.charCodeAt(i);
|
|
4396
|
+
h = Math.imul(h, 16777619);
|
|
4397
|
+
}
|
|
4398
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
4399
|
+
}
|
|
4400
|
+
async function externalizePngs(value, assets, store) {
|
|
4401
|
+
if (typeof value === "string") {
|
|
4402
|
+
if (!value.startsWith(PNG_DATA_URL)) return value;
|
|
4403
|
+
const id = `ext-${fnv1a(value)}-${value.length.toString(36)}`;
|
|
4404
|
+
if (!assets[id]) {
|
|
4405
|
+
let refs = storedPngs.get(store);
|
|
4406
|
+
if (!refs) storedPngs.set(store, refs = /* @__PURE__ */ new Map());
|
|
4407
|
+
let ref2 = refs.get(value);
|
|
4408
|
+
if (!ref2) {
|
|
4409
|
+
const parsed = parseDataUrl(value);
|
|
4410
|
+
if (!parsed) return value;
|
|
4411
|
+
const b = parsed.bytes;
|
|
4412
|
+
const u32 = (o) => (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
|
|
4413
|
+
ref2 = await store.put(new Blob([b], { type: "image/png" }), { id, width: b.length >= 24 ? u32(16) : 0, height: b.length >= 24 ? u32(20) : 0 });
|
|
4414
|
+
if (typeof ref2 !== "string" || !ref2) throw new Error(`The asset store returned no reference for "${id}".`);
|
|
4415
|
+
refs.set(value, ref2);
|
|
4416
|
+
}
|
|
4417
|
+
assets[id] = ref2;
|
|
4418
|
+
}
|
|
4419
|
+
return `${EXTENSION_ASSET_REF}${id}`;
|
|
4420
|
+
}
|
|
4421
|
+
if (Array.isArray(value)) {
|
|
4422
|
+
const out = [];
|
|
4423
|
+
for (const v of value) out.push(await externalizePngs(v, assets, store));
|
|
4424
|
+
return out;
|
|
4425
|
+
}
|
|
4426
|
+
if (value && typeof value === "object") {
|
|
4427
|
+
const out = {};
|
|
4428
|
+
for (const [k, v] of Object.entries(value)) out[k] = await externalizePngs(v, assets, store);
|
|
4429
|
+
return out;
|
|
4430
|
+
}
|
|
4431
|
+
return value;
|
|
4432
|
+
}
|
|
4433
|
+
function inlinePngs(value, assets, problems, field, used) {
|
|
4434
|
+
if (typeof value === "string") {
|
|
4435
|
+
if (!value.startsWith(EXTENSION_ASSET_REF)) return value;
|
|
4436
|
+
const id = value.slice(EXTENSION_ASSET_REF.length);
|
|
4437
|
+
const url = assets[id];
|
|
4438
|
+
if (typeof url === "string" && url.startsWith("data:")) {
|
|
4439
|
+
used.add(id);
|
|
4440
|
+
return url;
|
|
4441
|
+
}
|
|
4442
|
+
const msg = `"${field}" refers to asset "${id}", which could not be read; that part loads empty.`;
|
|
4443
|
+
if (!problems.includes(msg)) problems.push(msg);
|
|
4444
|
+
return value;
|
|
4445
|
+
}
|
|
4446
|
+
if (Array.isArray(value)) return value.map((v) => inlinePngs(v, assets, problems, field, used));
|
|
4447
|
+
if (value && typeof value === "object") {
|
|
4448
|
+
const out = {};
|
|
4449
|
+
for (const [k, v] of Object.entries(value)) out[k] = inlinePngs(v, assets, problems, field, used);
|
|
4450
|
+
return out;
|
|
4451
|
+
}
|
|
4452
|
+
return value;
|
|
4453
|
+
}
|
|
4383
4454
|
async function serializePaintDocumentExternal(state, store, options = {}) {
|
|
4384
4455
|
const { layers, assets } = await serializeLayersExternal(state.layers, store);
|
|
4385
|
-
|
|
4456
|
+
const out = { ...header(state, options), layers, assets };
|
|
4457
|
+
for (const key of Object.keys(out)) if (!NO_EXTENSION_PNGS.has(key)) out[key] = await externalizePngs(out[key], assets, store);
|
|
4458
|
+
return out;
|
|
4386
4459
|
}
|
|
4387
4460
|
function paintLayersOnly(layers, problems) {
|
|
4388
4461
|
const out = [];
|
|
@@ -4415,9 +4488,15 @@ function deserializePaintDocument(raw) {
|
|
|
4415
4488
|
throw new Error(`Paint document version ${JSON.stringify(r.version)} is newer than this editor reads (${PAINT_SCHEMA_VERSION}); update @pixerate/editor.`);
|
|
4416
4489
|
}
|
|
4417
4490
|
const problems = [];
|
|
4491
|
+
const rawAssets = r.assets && typeof r.assets === "object" ? r.assets : {};
|
|
4492
|
+
const used = /* @__PURE__ */ new Set();
|
|
4493
|
+
const inlined = { ...r };
|
|
4494
|
+
for (const key of Object.keys(r)) if (!NO_EXTENSION_PNGS.has(key)) inlined[key] = inlinePngs(r[key], rawAssets, problems, key, used);
|
|
4495
|
+
return readDocument(inlined, Object.fromEntries(Object.entries(rawAssets).filter(([id]) => !used.has(id))), problems);
|
|
4496
|
+
}
|
|
4497
|
+
function readDocument(r, assets, problems) {
|
|
4418
4498
|
const canvas = normalizeCanvas({ width: r.width, height: r.height, dpi: r.dpi ?? void 0 });
|
|
4419
4499
|
const paper = normalizePaper(r.paper ?? {});
|
|
4420
|
-
const assets = r.assets && typeof r.assets === "object" ? r.assets : {};
|
|
4421
4500
|
const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);
|
|
4422
4501
|
const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
|
|
4423
4502
|
const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : flattenLayers(layers).filter((l) => l.type === "pixel").pop()?.id ?? null;
|
|
@@ -9025,7 +9104,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9025
9104
|
brushColour = null;
|
|
9026
9105
|
/**
|
|
9027
9106
|
* The host's colour scheme: a paint colour that came from a brush's light / dark default
|
|
9028
|
-
* resolves to the new side
|
|
9107
|
+
* resolves to the new side, and stroke layers show their dual-colour strokes in it. Only the
|
|
9108
|
+
* view changes: the document, history and change listeners don't hear of it.
|
|
9029
9109
|
*/
|
|
9030
9110
|
setColorScheme(scheme) {
|
|
9031
9111
|
const next = scheme === "dark" ? "dark" : "light";
|
|
@@ -9033,9 +9113,77 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9033
9113
|
this.colorScheme = next;
|
|
9034
9114
|
const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
|
|
9035
9115
|
if (hex2) this.color = hex2;
|
|
9116
|
+
this.liveLayers = null;
|
|
9036
9117
|
const refs = brushStickerRefs(this.brush);
|
|
9037
9118
|
if (refs.some((r) => !editorStampOf(r))) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
9038
|
-
this.emit(
|
|
9119
|
+
this.emit();
|
|
9120
|
+
}
|
|
9121
|
+
/**
|
|
9122
|
+
* Stroke layers in the colour scheme: the document keeps every stroke layer drawn with each
|
|
9123
|
+
* stroke's `color` (a dual-colour stroke's light side), so saved and shared documents never
|
|
9124
|
+
* depend on who is looking. The view (display, image exports, the base a pointer stroke paints
|
|
9125
|
+
* on) draws dual-colour strokes in the scheme's side instead. One cached surface per layer,
|
|
9126
|
+
* brought up to date from the strokes that changed since it was drawn.
|
|
9127
|
+
*/
|
|
9128
|
+
schemeViews = /* @__PURE__ */ new Map();
|
|
9129
|
+
schemeCopies = /* @__PURE__ */ new WeakMap();
|
|
9130
|
+
/** Set while a stroke is painted for the document rather than the view (`runStroke` on a stroke layer): dual colours take this side. */
|
|
9131
|
+
paintScheme = null;
|
|
9132
|
+
/** A stroke as the scheme shows it: itself, or (a dual-colour stroke) a copy in the scheme's side. */
|
|
9133
|
+
schemeStroke(st, scheme) {
|
|
9134
|
+
const want = st.dualColor?.[scheme];
|
|
9135
|
+
if (!want || want === st.color) return st;
|
|
9136
|
+
const had = this.schemeCopies.get(st);
|
|
9137
|
+
if (had?.scheme === scheme) return had.stroke;
|
|
9138
|
+
const out = { ...st, color: want };
|
|
9139
|
+
if (st.load) {
|
|
9140
|
+
const load = brushLoadForStroke(st.brush, EMPTY_MIXER_LOAD, hexToRgb8(want).map((v) => v / 255));
|
|
9141
|
+
if (load.reservoir || load.pickup) out.load = load;
|
|
9142
|
+
else delete out.load;
|
|
9143
|
+
}
|
|
9144
|
+
this.schemeCopies.set(st, { scheme, stroke: out });
|
|
9145
|
+
return out;
|
|
9146
|
+
}
|
|
9147
|
+
/** A stroke layer's surface in the scheme, or `null` when the document's pixels already show it. */
|
|
9148
|
+
schemeSurface(layer, list) {
|
|
9149
|
+
const scheme = this.colorScheme;
|
|
9150
|
+
if (!list.some((st) => st.dualColor && st.dualColor[scheme] !== st.color)) {
|
|
9151
|
+
this.schemeViews.delete(layer.id);
|
|
9152
|
+
return null;
|
|
9153
|
+
}
|
|
9154
|
+
const had = this.schemeViews.get(layer.id);
|
|
9155
|
+
if (had && had.scheme === scheme && had.list === list && had.base === layer.surface) return had.surface;
|
|
9156
|
+
const view = list.map((st) => this.schemeStroke(st, scheme));
|
|
9157
|
+
let surface;
|
|
9158
|
+
if (had && had.scheme === scheme && had.list !== list && !layer.offset.x && !layer.offset.y) {
|
|
9159
|
+
const now = new Set(list);
|
|
9160
|
+
const before = new Set(had.list);
|
|
9161
|
+
const gone = had.list.filter((st) => !now.has(st));
|
|
9162
|
+
const kept = list.filter((st) => before.has(st));
|
|
9163
|
+
const sameOrder = kept.every((st, i) => st === had.list.filter((x) => now.has(x))[i]);
|
|
9164
|
+
surface = sameOrder ? this.drawLiveStrokes({ ...layer, surface: had.surface }, view, this.strokeAreas(gone.map((st) => this.schemeStroke(st, scheme)))) : this.drawLiveStrokes(layer, view);
|
|
9165
|
+
} else surface = this.drawLiveStrokes(layer, view);
|
|
9166
|
+
this.schemeViews.set(layer.id, { scheme, list, base: layer.surface, surface });
|
|
9167
|
+
return surface;
|
|
9168
|
+
}
|
|
9169
|
+
/** `layers` with each stroke layer that holds dual-colour strokes shown in the scheme. */
|
|
9170
|
+
schemeLayers(layers) {
|
|
9171
|
+
const live = this.doc.liveStrokes;
|
|
9172
|
+
if (!live) return layers;
|
|
9173
|
+
let out = layers;
|
|
9174
|
+
for (const [id, list] of Object.entries(live)) {
|
|
9175
|
+
const l = findLayer(out, id)?.layer;
|
|
9176
|
+
if (!isPixelLayer(l)) continue;
|
|
9177
|
+
const surface = this.schemeSurface(l, list);
|
|
9178
|
+
if (surface) out = mapLayer(out, id, () => ({ ...l, surface }));
|
|
9179
|
+
}
|
|
9180
|
+
return out;
|
|
9181
|
+
}
|
|
9182
|
+
/** The paint colour's light / dark pair for a stroke with `brushId`, when the colour came from one (a brush's own or default colour, or `setColor`). */
|
|
9183
|
+
dualPaint(brushId) {
|
|
9184
|
+
const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
|
|
9185
|
+
if (spec?.colour?.own && spec.colour.default !== void 0) return dualHex(spec.colour.default);
|
|
9186
|
+
return this.brushColour ? dualHex(this.brushColour) : null;
|
|
9039
9187
|
}
|
|
9040
9188
|
/** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
|
|
9041
9189
|
setColorLocked(locked) {
|
|
@@ -9065,10 +9213,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9065
9213
|
const d = spec?.colour?.own ? spec.colour.default : void 0;
|
|
9066
9214
|
return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
9067
9215
|
}
|
|
9068
|
-
/**
|
|
9216
|
+
/**
|
|
9217
|
+
* Sets the paint colour (and goes back to one colour from gradient paint). A light / dark pair
|
|
9218
|
+
* paints the colour scheme's side, follows `setColorScheme`, and is recorded with each stroke
|
|
9219
|
+
* painted in it. Returns the colour as #rrggbb.
|
|
9220
|
+
*/
|
|
9069
9221
|
setColor(color) {
|
|
9070
|
-
|
|
9071
|
-
|
|
9222
|
+
const dual = typeof color === "string" ? null : dualHex(color);
|
|
9223
|
+
if (typeof color !== "string" && !dual) throw new PaintEditorError(`color ${JSON.stringify(color)} is not a colour; use #rrggbb, or { light, dark } with a colour each.`);
|
|
9224
|
+
this.color = dual ? dual[this.colorScheme] : parseColor(color);
|
|
9225
|
+
this.brushColour = dual;
|
|
9072
9226
|
this.gradient = null;
|
|
9073
9227
|
this.effectPaint = this.effectPaintPending = null;
|
|
9074
9228
|
this.paintEpoch++;
|
|
@@ -9197,11 +9351,20 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9197
9351
|
strokeCopies() {
|
|
9198
9352
|
return symmetryCopies(this.guides.symmetry);
|
|
9199
9353
|
}
|
|
9200
|
-
sessionFor(doc,
|
|
9354
|
+
sessionFor(doc, given, host, points) {
|
|
9355
|
+
let dualColor = null;
|
|
9356
|
+
if (given.color !== void 0 && typeof given.color !== "string") {
|
|
9357
|
+
dualColor = dualHex(given.color);
|
|
9358
|
+
if (!dualColor) throw new PaintEditorError(`color ${JSON.stringify(given.color)} is not a colour; use #rrggbb, or { light, dark } with a colour each.`);
|
|
9359
|
+
}
|
|
9360
|
+
const scheme = this.paintScheme ?? this.colorScheme;
|
|
9361
|
+
let init = dualColor ? { ...given, color: dualColor[scheme] } : given;
|
|
9201
9362
|
if (init.paint !== void 0 && !isPaintEffectPaint(init.paint)) {
|
|
9202
9363
|
const p = this.readStrokePaint(init.paint);
|
|
9203
|
-
if (p.type === "color")
|
|
9204
|
-
|
|
9364
|
+
if (p.type === "color") {
|
|
9365
|
+
init = { ...init, color: p.color, paint: void 0 };
|
|
9366
|
+
dualColor = null;
|
|
9367
|
+
} else init = { ...init, paint: p };
|
|
9205
9368
|
}
|
|
9206
9369
|
let layer = (() => {
|
|
9207
9370
|
const id = init.layerId ?? doc.activeLayerId;
|
|
@@ -9341,7 +9504,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9341
9504
|
}
|
|
9342
9505
|
let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
9343
9506
|
const tracing = this.guides.tracing;
|
|
9344
|
-
|
|
9507
|
+
const traced = init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors ? this.sampleTracingColor(init.start.x, init.start.y) : null;
|
|
9508
|
+
if (traced) color = traced;
|
|
9509
|
+
else if (init.color === void 0) {
|
|
9510
|
+
dualColor = this.dualPaint(init.brushId);
|
|
9511
|
+
if (dualColor) color = dualColor[scheme];
|
|
9512
|
+
}
|
|
9345
9513
|
const perspective = this.guides.perspective;
|
|
9346
9514
|
const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;
|
|
9347
9515
|
const useStencils = init.stencils !== false;
|
|
@@ -9357,6 +9525,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9357
9525
|
const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
|
|
9358
9526
|
const hasThick = Boolean(doc.thick?.[layer.id]);
|
|
9359
9527
|
const needThick = brush.engine === "impasto" || Boolean(brush.iridescence) || brush.engine === "eraser" && hasThick || Boolean(wet?.driver.init.film);
|
|
9528
|
+
const gradient = effect ? null : this.strokeGradient(init, brush, color);
|
|
9360
9529
|
const session = new PaintStrokeSession(host, {
|
|
9361
9530
|
layerId: layer.id,
|
|
9362
9531
|
surface: layer.surface,
|
|
@@ -9369,6 +9538,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9369
9538
|
paperDepth: doc.paper.depth,
|
|
9370
9539
|
seed,
|
|
9371
9540
|
colorHex: color,
|
|
9541
|
+
// Only a stroke that paints its one colour keeps the pair (gradient and effect paint have their own colours).
|
|
9542
|
+
...dualColor && !gradient && !effect && dualColor[scheme] === color ? { dualColor } : {},
|
|
9372
9543
|
affineCopies: copies,
|
|
9373
9544
|
copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
|
|
9374
9545
|
inputFilter,
|
|
@@ -9385,7 +9556,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9385
9556
|
thermo: brush.engine !== "sticker" && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
|
|
9386
9557
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
9387
9558
|
stampArt: this.stampArt,
|
|
9388
|
-
paint:
|
|
9559
|
+
paint: gradient,
|
|
9389
9560
|
effect
|
|
9390
9561
|
});
|
|
9391
9562
|
if (wet) this.wet.driver = wet.driver;
|
|
@@ -9402,6 +9573,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9402
9573
|
points: session.points.map((p) => ({ ...p })),
|
|
9403
9574
|
seed: init.seed
|
|
9404
9575
|
};
|
|
9576
|
+
if (init.dualColor) {
|
|
9577
|
+
stroke.dualColor = { ...init.dualColor };
|
|
9578
|
+
if (this.doc.liveStrokes?.[session.layerId]) stroke.color = init.dualColor.light;
|
|
9579
|
+
}
|
|
9405
9580
|
if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m]);
|
|
9406
9581
|
if (init.effect) stroke.paint = JSON.parse(JSON.stringify(init.effect.source));
|
|
9407
9582
|
else if (init.paint) stroke.paint = JSON.parse(JSON.stringify(init.paint));
|
|
@@ -9510,34 +9685,43 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9510
9685
|
const ferroSettings = fc ? withLayerFerroSettings(this.doc.ferroSettings, ferro, l.id, brush.ferro.amount > 0 ? brush.ferro : this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO) : this.doc.ferroSettings;
|
|
9511
9686
|
const fluor = withLayerLight(this.doc.fluor, l.id, s.fluorChannel());
|
|
9512
9687
|
const thermo = withLayerLight(this.doc.thermo, l.id, s.thermoChannel());
|
|
9513
|
-
this.
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
{
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
stroke
|
|
9530
|
-
)
|
|
9531
|
-
),
|
|
9532
|
-
before
|
|
9688
|
+
let next = this.withWetEvents(
|
|
9689
|
+
withLiveStroke(
|
|
9690
|
+
{
|
|
9691
|
+
...this.doc,
|
|
9692
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
9693
|
+
strokes: [...this.doc.strokes, stroke],
|
|
9694
|
+
...thick ? { thick } : {},
|
|
9695
|
+
...stickers ? { stickers } : {},
|
|
9696
|
+
...light ? { light } : {},
|
|
9697
|
+
...fluor ? { fluor } : {},
|
|
9698
|
+
...thermo ? { thermo } : {},
|
|
9699
|
+
...ferro ? { ferro } : { ferro: void 0 },
|
|
9700
|
+
...ferroSettings ? { ferroSettings } : { ferroSettings: void 0 }
|
|
9701
|
+
},
|
|
9702
|
+
stroke
|
|
9703
|
+
)
|
|
9533
9704
|
);
|
|
9705
|
+
const list = next.liveStrokes?.[l.id];
|
|
9706
|
+
if (list && (viewBase || stroke.color !== s.init.colorHex)) {
|
|
9707
|
+
const canonical = this.withLiveStrokes({ ...next, layers: mapLayer(next.layers, l.id, () => l) }, l, list, s.result?.bounds ? [s.result.bounds] : null, { record: false });
|
|
9708
|
+
const drawn = findLayer(canonical.layers, l.id)?.layer;
|
|
9709
|
+
this.schemeViews.set(l.id, { scheme: viewScheme, list, base: drawn.surface, surface });
|
|
9710
|
+
next = canonical;
|
|
9711
|
+
}
|
|
9712
|
+
this.commit(`${brush.name} stroke`, next, before);
|
|
9534
9713
|
this.recordMutationStroke(stroke, s.result?.bounds ?? null);
|
|
9535
9714
|
this.notifySample("end", s, [], stroke.id);
|
|
9536
9715
|
}
|
|
9537
9716
|
};
|
|
9717
|
+
const targetId = init.layerId ?? this.doc.activeLayerId;
|
|
9718
|
+
const target = targetId ? findLayer(this.doc.layers, targetId)?.layer : void 0;
|
|
9719
|
+
const targetStrokes = isPixelLayer(target) ? this.doc.liveStrokes?.[target.id] : void 0;
|
|
9720
|
+
const viewBase = targetStrokes && isPixelLayer(target) ? this.schemeSurface(target, targetStrokes) : null;
|
|
9721
|
+
const viewScheme = this.colorScheme;
|
|
9538
9722
|
let session;
|
|
9539
9723
|
try {
|
|
9540
|
-
session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);
|
|
9724
|
+
session = this.sessionFor(viewBase && target ? { ...this.doc, layers: mapLayer(this.doc.layers, target.id, (x) => ({ ...x, surface: viewBase })) } : this.doc, { snapToPerspective: true, ...init }, host);
|
|
9541
9725
|
} catch (e) {
|
|
9542
9726
|
this.wetOp = null;
|
|
9543
9727
|
throw e;
|
|
@@ -9614,7 +9798,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9614
9798
|
this.liveThermo = null;
|
|
9615
9799
|
const masks = s.init.selection ? (s.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask") : [];
|
|
9616
9800
|
const load = s.init.mixerLoad;
|
|
9617
|
-
const result = this.paintStroke({ layerId: s.layerId, points: s.points, color: s.init.colorHex, brush: brushToJSON(s.init.brush), seed: s.init.seed, copies: s.init.affineCopies, ...load.reservoir || load.pickup ? { load } : {}, paint, snapToPerspective: false, stencils: false, maskStencils: masks });
|
|
9801
|
+
const result = this.paintStroke({ layerId: s.layerId, points: s.points, color: s.init.dualColor ?? s.init.colorHex, brush: brushToJSON(s.init.brush), seed: s.init.seed, copies: s.init.affineCopies, ...load.reservoir || load.pickup ? { load } : {}, paint, snapToPerspective: false, stencils: false, maskStencils: masks });
|
|
9618
9802
|
Object.assign(s.result, result);
|
|
9619
9803
|
this.notifySample("end", s, [], result.strokeId);
|
|
9620
9804
|
};
|
|
@@ -9698,7 +9882,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9698
9882
|
}, stroke);
|
|
9699
9883
|
}
|
|
9700
9884
|
};
|
|
9701
|
-
const
|
|
9885
|
+
const target = init.layerId ?? doc.activeLayerId;
|
|
9886
|
+
this.paintScheme = target && doc.liveStrokes?.[target] ? "light" : null;
|
|
9887
|
+
let session;
|
|
9888
|
+
try {
|
|
9889
|
+
session = this.sessionFor(doc, init, host, points);
|
|
9890
|
+
} finally {
|
|
9891
|
+
this.paintScheme = null;
|
|
9892
|
+
}
|
|
9702
9893
|
session.push(points);
|
|
9703
9894
|
const result = session.end();
|
|
9704
9895
|
if (session.init.wet) {
|
|
@@ -10669,9 +10860,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10669
10860
|
const override = this.frameOverride;
|
|
10670
10861
|
if (override) return Array.from(override).reduce((ls, [id, surface]) => mapLayer(ls, id, (l) => ({ ...l, surface })), this.doc.layers);
|
|
10671
10862
|
const live = this.live;
|
|
10672
|
-
if (!live) return this.animatedView();
|
|
10863
|
+
if (!live) return this.schemeLayers(this.animatedView());
|
|
10673
10864
|
if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;
|
|
10674
|
-
const layers = mapLayer(this.doc.layers, live.layerId, (l) => ({ ...l, surface: live.surface }));
|
|
10865
|
+
const layers = mapLayer(this.schemeLayers(this.doc.layers), live.layerId, (l) => ({ ...l, surface: live.surface }));
|
|
10675
10866
|
this.liveLayers = { key: live.surface, layers };
|
|
10676
10867
|
return layers;
|
|
10677
10868
|
}
|
|
@@ -11226,6 +11417,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11226
11417
|
delete this.extensions.heat;
|
|
11227
11418
|
delete this.extensions.uvLight;
|
|
11228
11419
|
this.viewCompositor.clear();
|
|
11420
|
+
this.schemeViews.clear();
|
|
11229
11421
|
this.emit();
|
|
11230
11422
|
const refs = Object.values(this.doc.stickers ?? {}).flatMap((list) => list.map((x) => x.ref));
|
|
11231
11423
|
if (refs.some((r) => !editorStampOf(r))) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
@@ -11284,7 +11476,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11284
11476
|
* pixels), and the composite as the merged image.
|
|
11285
11477
|
*/
|
|
11286
11478
|
exportPsd() {
|
|
11287
|
-
const driedBase = this.driedLayers(this.doc.layers);
|
|
11479
|
+
const driedBase = this.driedLayers(this.schemeLayers(this.doc.layers));
|
|
11288
11480
|
const backlight = this.backlightLayer(driedBase);
|
|
11289
11481
|
const dried = [...this.lightLayers(this.uvLayers(this.litLayers(this.thermoLayers(driedBase)))), ...backlight ? [backlight] : []];
|
|
11290
11482
|
const { width, height, dpi } = this.doc.canvas;
|
|
@@ -11838,7 +12030,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11838
12030
|
next.seed = update.seed;
|
|
11839
12031
|
}
|
|
11840
12032
|
if (update.color !== void 0) {
|
|
11841
|
-
|
|
12033
|
+
const dual = typeof update.color === "string" ? null : dualHex(update.color);
|
|
12034
|
+
if (typeof update.color !== "string" && !dual) throw new PaintEditorError(`color ${JSON.stringify(update.color)} is not a colour; use #rrggbb, or { light, dark } with a colour each.`);
|
|
12035
|
+
next.color = dual ? dual.light : parseColor(update.color);
|
|
12036
|
+
if (dual) next.dualColor = dual;
|
|
12037
|
+
else delete next.dualColor;
|
|
11842
12038
|
delete next.paint;
|
|
11843
12039
|
}
|
|
11844
12040
|
if (update.paint !== void 0) {
|
|
@@ -11851,6 +12047,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11851
12047
|
const p = this.readStrokePaint(update.paint);
|
|
11852
12048
|
if (p.type === "color") {
|
|
11853
12049
|
next.color = p.color;
|
|
12050
|
+
delete next.dualColor;
|
|
11854
12051
|
delete next.paint;
|
|
11855
12052
|
} else next.paint = JSON.parse(JSON.stringify(p));
|
|
11856
12053
|
}
|
|
@@ -11966,8 +12163,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11966
12163
|
commitLiveStrokes(label, layer, list, dirty) {
|
|
11967
12164
|
this.commit(label, this.withLiveStrokes(this.doc, layer, list, dirty));
|
|
11968
12165
|
}
|
|
11969
|
-
|
|
11970
|
-
|
|
12166
|
+
/**
|
|
12167
|
+
* The document with a layer's strokes set and its pixels redrawn. With `record` false (the same
|
|
12168
|
+
* strokes drawn for the colour scheme) the pixel lock doesn't apply and no event is recorded for replays.
|
|
12169
|
+
*/
|
|
12170
|
+
withLiveStrokes(doc, layer, list, dirty, options = {}) {
|
|
12171
|
+
const record = options.record !== false;
|
|
12172
|
+
if (record && isLayerLocked(layer, "pixels")) throw new PaintEditorError(`Layer "${layer.name}" has its pixels locked; unlock it (update_layer locks) first.`);
|
|
11971
12173
|
const current = findLayer(doc.layers, layer.id)?.layer;
|
|
11972
12174
|
const hasFilm = list.some((s) => Boolean(s.brush.iridescence)) || Boolean(doc.thick?.[layer.id]);
|
|
11973
12175
|
const surface = this.drawLiveStrokes(current ?? layer, list, hasFilm ? null : dirty);
|
|
@@ -11987,6 +12189,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11987
12189
|
thick = withLayerThick(doc.thick, layer.id, ch);
|
|
11988
12190
|
}
|
|
11989
12191
|
const next = { ...doc, layers: mapLayer(doc.layers, layer.id, (l) => ({ ...l, surface, offset: { x: 0, y: 0 } })), liveStrokes: { ...doc.liveStrokes ?? {}, [layer.id]: [...list] }, ...thick ? { thick } : {} };
|
|
12192
|
+
if (!record) return next;
|
|
11990
12193
|
return this.withPaintEvent(next, { kind: "live-strokes", layerId: layer.id, data: { strokes: list.map((x) => JSON.parse(JSON.stringify(x))) } });
|
|
11991
12194
|
}
|
|
11992
12195
|
/** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
|
|
@@ -13276,6 +13479,12 @@ function readWetGhost(raw) {
|
|
|
13276
13479
|
const linger = typeof g.linger === "number" && Number.isFinite(g.linger) ? Math.max(0.5, Math.min(60, g.linger)) : 4;
|
|
13277
13480
|
return { pigment: [g.pigment[0], g.pigment[1], g.pigment[2], g.pigment[3]], remaining, total, linger };
|
|
13278
13481
|
}
|
|
13482
|
+
function dualHex(value) {
|
|
13483
|
+
if (!isDualColor(value)) return null;
|
|
13484
|
+
const light = brushColourHex(value.light, "light");
|
|
13485
|
+
const dark = brushColourHex(value.dark, "dark");
|
|
13486
|
+
return light && dark ? { light, dark } : null;
|
|
13487
|
+
}
|
|
13279
13488
|
|
|
13280
13489
|
// src/paint-editor/presence.ts
|
|
13281
13490
|
var PaintPresence = class extends PresenceStore {
|
|
@@ -15687,24 +15896,33 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15687
15896
|
add({
|
|
15688
15897
|
name: "set_color",
|
|
15689
15898
|
title: "Set the paint colour",
|
|
15690
|
-
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default, and whether it is locked. Choosing a brush with a default colour (set_brush id, e.g. the highlighter's yellow) makes that the paint colour unless the colour is locked; a brush with colour.own always paints its own. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours and locked.",
|
|
15899
|
+
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default, and whether it is locked. Choosing a brush with a default colour (set_brush id, e.g. the highlighter's yellow) makes that the paint colour unless the colour is locked; a brush with colour.own always paints its own. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). dualColor sets a light / dark pair instead: strokes record the pair, and on a stroke layer each viewer sees them in the side of their own colour scheme (a pixel layer keeps the side painted). Editor state, not an undo step. Returns both colours and locked (and dualColor when given).",
|
|
15691
15900
|
inputSchema: {
|
|
15692
15901
|
type: "object",
|
|
15693
15902
|
properties: {
|
|
15694
15903
|
color: { ...COLOR3, description: "Paint colour #rrggbb." },
|
|
15904
|
+
dualColor: {
|
|
15905
|
+
type: "object",
|
|
15906
|
+
description: "A light / dark pair as the paint colour, instead of color: each side a colour #rrggbb.",
|
|
15907
|
+
properties: { light: { ...COLOR3, description: "The colour in the light scheme, #rrggbb." }, dark: { ...COLOR3, description: "The colour in the dark scheme, #rrggbb." } },
|
|
15908
|
+
required: ["light", "dark"],
|
|
15909
|
+
additionalProperties: false
|
|
15910
|
+
},
|
|
15695
15911
|
secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
|
|
15696
15912
|
locked: { type: "boolean", description: "Lock the paint colour: brushes chosen later keep it instead of taking their default colour (default: unchanged; the editor starts unlocked)." }
|
|
15697
15913
|
},
|
|
15698
15914
|
additionalProperties: false
|
|
15699
15915
|
},
|
|
15700
15916
|
annotations: { title: "Set the paint colour", ...SET6 },
|
|
15701
|
-
handler: ({ color, secondaryColor, locked }) => {
|
|
15702
|
-
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
15917
|
+
handler: ({ color, dualColor, secondaryColor, locked }) => {
|
|
15918
|
+
if (color === void 0 && dualColor === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, dualColor, secondaryColor or locked.");
|
|
15919
|
+
if (color !== void 0 && dualColor !== void 0) throw new Error("Pass color or dualColor, not both.");
|
|
15703
15920
|
if (color !== void 0) c.setColor(color);
|
|
15921
|
+
if (dualColor !== void 0) c.setColor(dualColor);
|
|
15704
15922
|
if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
|
|
15705
15923
|
if (locked !== void 0) c.setColorLocked(locked);
|
|
15706
15924
|
const s = c.getState();
|
|
15707
|
-
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
|
|
15925
|
+
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked, ...dualColor !== void 0 ? { dualColor: { light: dualColor.light.toLowerCase(), dark: dualColor.dark.toLowerCase() } } : {} };
|
|
15708
15926
|
}
|
|
15709
15927
|
});
|
|
15710
15928
|
add({
|
|
@@ -17575,6 +17793,7 @@ export {
|
|
|
17575
17793
|
EVOLUTION_BRUSH_FACETS,
|
|
17576
17794
|
EVOLUTION_MUTATION_SCHEMA,
|
|
17577
17795
|
EVOLUTION_SLIDERS,
|
|
17796
|
+
EXTENSION_ASSET_REF,
|
|
17578
17797
|
FERRO_CARRIER_RGB,
|
|
17579
17798
|
FERRO_DEFAULT_GLOSS,
|
|
17580
17799
|
FERRO_DEFAULT_METALLIC,
|
|
@@ -59,7 +59,9 @@ export interface SerializeOptions {
|
|
|
59
59
|
}
|
|
60
60
|
/** The document as JSON with inline PNG data URLs. */
|
|
61
61
|
export declare function serializePaintDocument(state: PaintDocumentState, options?: SerializeOptions): SerializedPaintDocument;
|
|
62
|
-
/**
|
|
62
|
+
/** How an extension field refers to a PNG in `assets`: `asset:<asset id>`. */
|
|
63
|
+
export declare const EXTENSION_ASSET_REF = "asset:";
|
|
64
|
+
/** The document as JSON with each PNG (layers and extension fields alike) uploaded to `store` once (by content). */
|
|
63
65
|
export declare function serializePaintDocumentExternal(state: PaintDocumentState, store: PaintAssetStore, options?: SerializeOptions): Promise<SerializedPaintDocument>;
|
|
64
66
|
export interface LoadedPaintDocument {
|
|
65
67
|
state: PaintDocumentState;
|
|
@@ -59,7 +59,9 @@ export interface SerializeOptions {
|
|
|
59
59
|
}
|
|
60
60
|
/** The document as JSON with inline PNG data URLs. */
|
|
61
61
|
export declare function serializePaintDocument(state: PaintDocumentState, options?: SerializeOptions): SerializedPaintDocument;
|
|
62
|
-
/**
|
|
62
|
+
/** How an extension field refers to a PNG in `assets`: `asset:<asset id>`. */
|
|
63
|
+
export declare const EXTENSION_ASSET_REF = "asset:";
|
|
64
|
+
/** The document as JSON with each PNG (layers and extension fields alike) uploaded to `store` once (by content). */
|
|
63
65
|
export declare function serializePaintDocumentExternal(state: PaintDocumentState, store: PaintAssetStore, options?: SerializeOptions): Promise<SerializedPaintDocument>;
|
|
64
66
|
export interface LoadedPaintDocument {
|
|
65
67
|
state: PaintDocumentState;
|
|
@@ -22,6 +22,7 @@ import { type PaperGrain } from './paper.cjs';
|
|
|
22
22
|
import { type Sticker } from './stickers.cjs';
|
|
23
23
|
import type { StickerArtLookup } from './sticker-art.cjs';
|
|
24
24
|
import type { StickerRef, StickerVariant } from '../stickers/types.cjs';
|
|
25
|
+
import type { DualColor } from '../color/dual.cjs';
|
|
25
26
|
export interface StrokeSessionHost {
|
|
26
27
|
/** Shows the layer's live surface (no history); `rects` are where it changed. */
|
|
27
28
|
update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;
|
|
@@ -48,6 +49,8 @@ export interface StrokeSessionInit {
|
|
|
48
49
|
seed: number;
|
|
49
50
|
/** The paint colour as #rrggbb (recorded with the stroke). */
|
|
50
51
|
colorHex: string;
|
|
52
|
+
/** The light / dark pair `colorHex` is one side of, when the colour came from one (recorded with the stroke). */
|
|
53
|
+
dualColor?: DualColor;
|
|
51
54
|
/** The stroke's extra copies as affine maps (recorded with the stroke); `copies` holds all of them as engine copies. */
|
|
52
55
|
affineCopies: readonly Affine[];
|
|
53
56
|
copies: readonly StrokeCopy[];
|
|
@@ -22,6 +22,7 @@ import { type PaperGrain } from './paper.js';
|
|
|
22
22
|
import { type Sticker } from './stickers.js';
|
|
23
23
|
import type { StickerArtLookup } from './sticker-art.js';
|
|
24
24
|
import type { StickerRef, StickerVariant } from '../stickers/types.js';
|
|
25
|
+
import type { DualColor } from '../color/dual.js';
|
|
25
26
|
export interface StrokeSessionHost {
|
|
26
27
|
/** Shows the layer's live surface (no history); `rects` are where it changed. */
|
|
27
28
|
update(layerId: string, surface: TiledSurface, rects: RasterRect[]): void;
|
|
@@ -48,6 +49,8 @@ export interface StrokeSessionInit {
|
|
|
48
49
|
seed: number;
|
|
49
50
|
/** The paint colour as #rrggbb (recorded with the stroke). */
|
|
50
51
|
colorHex: string;
|
|
52
|
+
/** The light / dark pair `colorHex` is one side of, when the colour came from one (recorded with the stroke). */
|
|
53
|
+
dualColor?: DualColor;
|
|
51
54
|
/** The stroke's extra copies as affine maps (recorded with the stroke); `copies` holds all of them as engine copies. */
|
|
52
55
|
affineCopies: readonly Affine[];
|
|
53
56
|
copies: readonly StrokeCopy[];
|
|
@@ -14,6 +14,7 @@ import type { MixerLoad } from '../paint/exchange.cjs';
|
|
|
14
14
|
import { type Stencil } from './stencils.cjs';
|
|
15
15
|
import { type PaintGradientSource } from '../paint/gradient-paint.cjs';
|
|
16
16
|
import { type PaintEffectPaint } from './effect-paint.cjs';
|
|
17
|
+
import { type DualColor } from '../color/dual.cjs';
|
|
17
18
|
/** An affine map [a, b, c, d, e, f]: (x, y) → (a·x + b·y + e, c·x + d·y + f). */
|
|
18
19
|
export type Affine = readonly [number, number, number, number, number, number];
|
|
19
20
|
export declare const IDENTITY_AFFINE: Affine;
|
|
@@ -21,8 +22,17 @@ export interface RecordedStroke {
|
|
|
21
22
|
id: string;
|
|
22
23
|
layerId: string;
|
|
23
24
|
brush: PaintBrush;
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* #rrggbb: the colour the document draws the stroke with. A dual-colour stroke on a stroke layer
|
|
27
|
+
* keeps its light side here, whoever painted it; on a pixel layer, the side it was painted in.
|
|
28
|
+
*/
|
|
25
29
|
color: string;
|
|
30
|
+
/**
|
|
31
|
+
* The light / dark pair the colour came from (both #rrggbb), when it came from one. A stroke
|
|
32
|
+
* layer shows it in the viewer's side (`PaintEditorController.setColorScheme`) without changing
|
|
33
|
+
* the document; a pixel layer's pixels stay as painted.
|
|
34
|
+
*/
|
|
35
|
+
dualColor?: DualColor;
|
|
26
36
|
/** Raw pen samples in document pixels, before smoothing. */
|
|
27
37
|
points: PenPoint[];
|
|
28
38
|
seed: number;
|
|
@@ -14,6 +14,7 @@ import type { MixerLoad } from '../paint/exchange.js';
|
|
|
14
14
|
import { type Stencil } from './stencils.js';
|
|
15
15
|
import { type PaintGradientSource } from '../paint/gradient-paint.js';
|
|
16
16
|
import { type PaintEffectPaint } from './effect-paint.js';
|
|
17
|
+
import { type DualColor } from '../color/dual.js';
|
|
17
18
|
/** An affine map [a, b, c, d, e, f]: (x, y) → (a·x + b·y + e, c·x + d·y + f). */
|
|
18
19
|
export type Affine = readonly [number, number, number, number, number, number];
|
|
19
20
|
export declare const IDENTITY_AFFINE: Affine;
|
|
@@ -21,8 +22,17 @@ export interface RecordedStroke {
|
|
|
21
22
|
id: string;
|
|
22
23
|
layerId: string;
|
|
23
24
|
brush: PaintBrush;
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* #rrggbb: the colour the document draws the stroke with. A dual-colour stroke on a stroke layer
|
|
27
|
+
* keeps its light side here, whoever painted it; on a pixel layer, the side it was painted in.
|
|
28
|
+
*/
|
|
25
29
|
color: string;
|
|
30
|
+
/**
|
|
31
|
+
* The light / dark pair the colour came from (both #rrggbb), when it came from one. A stroke
|
|
32
|
+
* layer shows it in the viewer's side (`PaintEditorController.setColorScheme`) without changing
|
|
33
|
+
* the document; a pixel layer's pixels stay as painted.
|
|
34
|
+
*/
|
|
35
|
+
dualColor?: DualColor;
|
|
26
36
|
/** Raw pen samples in document pixels, before smoothing. */
|
|
27
37
|
points: PenPoint[];
|
|
28
38
|
seed: number;
|