@1agh/maude 0.42.0 → 0.43.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -22
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +451 -34
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +42 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +196 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +173 -14
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +5 -1
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +53 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file photo/filters.ts — pure photo-pipeline PLANNER (Stage B, Task 4)
|
|
3
|
+
* @scope apps/studio/photo/filters.ts
|
|
4
|
+
* @purpose Turn a `PhotoEdit` into an ordered, plain-data description of the
|
|
5
|
+
* compositing stack — `planPhotoPipeline(edit) → PhotoPipelineStep[]`.
|
|
6
|
+
* This is the deterministic, testable core: it decides WHAT filters/
|
|
7
|
+
* overlays/masks apply and in WHAT order, without ever touching a GPU.
|
|
8
|
+
*
|
|
9
|
+
* @invariant DEPENDENCY-FREE — like `schema.ts`, this file MUST NOT import
|
|
10
|
+
* `pixi.js`. The pixi realization (constructing real
|
|
11
|
+
* `PIXI.ColorMatrixFilter` / `NoiseFilter` / the duotone shader /
|
|
12
|
+
* `TilingSprite` / `Graphics` mask objects) lives in `pipeline.ts`,
|
|
13
|
+
* which is browser-only.
|
|
14
|
+
*
|
|
15
|
+
* WHY the split (deviation from the plan's single
|
|
16
|
+
* `buildFilterGraph(): PIXI.Filter[]`): pixi v8 eagerly compiles a
|
|
17
|
+
* `GlProgram` in every filter's constructor, which calls
|
|
18
|
+
* `document.createElement('canvas')` — so constructing a filter in a
|
|
19
|
+
* headless `bun test` throws `ReferenceError: document is not
|
|
20
|
+
* defined`. Returning plain-data STEP specs keeps the plan's stated
|
|
21
|
+
* Task-4 validation ("assert filter count/order, not pixel output")
|
|
22
|
+
* genuinely runnable, and mirrors how `draw/serialize.ts` produces a
|
|
23
|
+
* browser-free description of a render. `pipeline.ts` re-exports a
|
|
24
|
+
* `buildFilterGraph(edit)` that realizes these steps into pixi objects.
|
|
25
|
+
*
|
|
26
|
+
* @order Fixed, load-bearing pipeline order (filters are NOT commutative) —
|
|
27
|
+
* `PHOTO_PIPELINE_ORDER` from schema.ts:
|
|
28
|
+
* adjustments → duotone → grain → pattern → mask
|
|
29
|
+
* The adjustment SUB-order (within the single color-matrix step) is
|
|
30
|
+
* also fixed: brightness → exposure → contrast → saturation → hue →
|
|
31
|
+
* sepia → grayscale → invert (color-matrix ops compose, so order
|
|
32
|
+
* changes the result). Both orders are asserted by the unit tests.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type {
|
|
36
|
+
MaskPreset,
|
|
37
|
+
PatternBlend,
|
|
38
|
+
PatternType,
|
|
39
|
+
PhotoAdjustments,
|
|
40
|
+
PhotoEdit,
|
|
41
|
+
} from './schema.ts';
|
|
42
|
+
import { PHOTO_PIPELINE_ORDER } from './schema.ts';
|
|
43
|
+
|
|
44
|
+
// ── Step vocabulary (plain data — the realizer in pipeline.ts consumes these) ──
|
|
45
|
+
|
|
46
|
+
/** One color-matrix operation, with a NORMALIZED value (see schema.ts ranges).
|
|
47
|
+
* The realizer maps each op onto the corresponding pixi ColorMatrixFilter call. */
|
|
48
|
+
export type AdjustOpKind =
|
|
49
|
+
| 'brightness'
|
|
50
|
+
| 'exposure'
|
|
51
|
+
| 'contrast'
|
|
52
|
+
| 'saturation'
|
|
53
|
+
| 'hue'
|
|
54
|
+
| 'sepia'
|
|
55
|
+
| 'grayscale'
|
|
56
|
+
| 'invert';
|
|
57
|
+
|
|
58
|
+
export interface AdjustOp {
|
|
59
|
+
op: AdjustOpKind;
|
|
60
|
+
/** Normalized value straight from PhotoEdit.adjustments (−1…1, deg, or 0…1). */
|
|
61
|
+
value: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Adjustments compile to ONE color-matrix filter carrying an ordered op list. */
|
|
65
|
+
export interface AdjustmentsStep {
|
|
66
|
+
stage: 'adjustments';
|
|
67
|
+
kind: 'colorMatrix';
|
|
68
|
+
ops: AdjustOp[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DuotoneStep {
|
|
72
|
+
stage: 'duotone';
|
|
73
|
+
kind: 'duotone';
|
|
74
|
+
/** `#rrggbb` shadow color. */
|
|
75
|
+
colorA: string;
|
|
76
|
+
/** `#rrggbb` highlight color. */
|
|
77
|
+
colorB: string;
|
|
78
|
+
/** 0…1 crossfade between original and full duotone. */
|
|
79
|
+
intensity: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GrainStep {
|
|
83
|
+
stage: 'grain';
|
|
84
|
+
kind: 'noise';
|
|
85
|
+
amount: number;
|
|
86
|
+
size: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PatternStep {
|
|
90
|
+
stage: 'pattern';
|
|
91
|
+
kind: 'pattern';
|
|
92
|
+
type: PatternType;
|
|
93
|
+
scale: number;
|
|
94
|
+
opacity: number;
|
|
95
|
+
blend: PatternBlend;
|
|
96
|
+
/** `#rrggbb` ink color for the pattern tile. */
|
|
97
|
+
color: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MaskStep {
|
|
101
|
+
stage: 'mask';
|
|
102
|
+
kind: 'mask';
|
|
103
|
+
/** 'none' never reaches here — an unmasked edit omits the step entirely. */
|
|
104
|
+
preset: Exclude<MaskPreset, 'none'>;
|
|
105
|
+
strength: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type PhotoPipelineStep = AdjustmentsStep | DuotoneStep | GrainStep | PatternStep | MaskStep;
|
|
109
|
+
|
|
110
|
+
// ── Sensible visual defaults (used when a section is enabled but a field unset) ─
|
|
111
|
+
|
|
112
|
+
const DUOTONE_DEFAULT_A = '#111111';
|
|
113
|
+
const DUOTONE_DEFAULT_B = '#ffffff';
|
|
114
|
+
const DUOTONE_DEFAULT_INTENSITY = 1;
|
|
115
|
+
const GRAIN_DEFAULT_AMOUNT = 0.4;
|
|
116
|
+
const GRAIN_DEFAULT_SIZE = 1;
|
|
117
|
+
const PATTERN_DEFAULT_TYPE: PatternType = 'dots';
|
|
118
|
+
const PATTERN_DEFAULT_SCALE = 1;
|
|
119
|
+
const PATTERN_DEFAULT_OPACITY = 0.5;
|
|
120
|
+
const PATTERN_DEFAULT_BLEND: PatternBlend = 'normal';
|
|
121
|
+
const PATTERN_DEFAULT_COLOR = '#ffffff';
|
|
122
|
+
const MASK_DEFAULT_STRENGTH = 0.6;
|
|
123
|
+
|
|
124
|
+
const EPS = 1e-6;
|
|
125
|
+
const isSet = (v: number | undefined, origin = 0): boolean =>
|
|
126
|
+
v != null && Math.abs(v - origin) > EPS;
|
|
127
|
+
|
|
128
|
+
// Fixed adjustment sub-order. See @order above.
|
|
129
|
+
const ADJUST_FIELD_ORDER: readonly AdjustOpKind[] = [
|
|
130
|
+
'brightness',
|
|
131
|
+
'exposure',
|
|
132
|
+
'contrast',
|
|
133
|
+
'saturation',
|
|
134
|
+
'hue',
|
|
135
|
+
'sepia',
|
|
136
|
+
'grayscale',
|
|
137
|
+
'invert',
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function planAdjustments(a: PhotoAdjustments | undefined): AdjustmentsStep | null {
|
|
141
|
+
if (!a) return null;
|
|
142
|
+
const ops: AdjustOp[] = [];
|
|
143
|
+
for (const field of ADJUST_FIELD_ORDER) {
|
|
144
|
+
const v = a[field];
|
|
145
|
+
if (isSet(v)) ops.push({ op: field, value: v as number });
|
|
146
|
+
}
|
|
147
|
+
return ops.length ? { stage: 'adjustments', kind: 'colorMatrix', ops } : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The single source of truth for "what does this edit do." Returns the ordered
|
|
152
|
+
* list of pipeline steps — a neutral/empty edit returns `[]` (the compositor
|
|
153
|
+
* then skips pixi entirely, preserving the lazy-bundle guarantee). Steps always
|
|
154
|
+
* appear in `PHOTO_PIPELINE_ORDER`.
|
|
155
|
+
*/
|
|
156
|
+
export function planPhotoPipeline(edit: PhotoEdit | null | undefined): PhotoPipelineStep[] {
|
|
157
|
+
if (!edit) return [];
|
|
158
|
+
const byStage: Record<string, PhotoPipelineStep | null> = {
|
|
159
|
+
adjustments: planAdjustments(edit.adjustments),
|
|
160
|
+
duotone: null,
|
|
161
|
+
grain: null,
|
|
162
|
+
pattern: null,
|
|
163
|
+
mask: null,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const d = edit.duotone;
|
|
167
|
+
if (d?.enabled && (d.intensity ?? DUOTONE_DEFAULT_INTENSITY) > EPS) {
|
|
168
|
+
byStage.duotone = {
|
|
169
|
+
stage: 'duotone',
|
|
170
|
+
kind: 'duotone',
|
|
171
|
+
colorA: d.colorA ?? DUOTONE_DEFAULT_A,
|
|
172
|
+
colorB: d.colorB ?? DUOTONE_DEFAULT_B,
|
|
173
|
+
intensity: d.intensity ?? DUOTONE_DEFAULT_INTENSITY,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const g = edit.grain;
|
|
178
|
+
if (g?.enabled && (g.amount ?? GRAIN_DEFAULT_AMOUNT) > EPS) {
|
|
179
|
+
byStage.grain = {
|
|
180
|
+
stage: 'grain',
|
|
181
|
+
kind: 'noise',
|
|
182
|
+
amount: g.amount ?? GRAIN_DEFAULT_AMOUNT,
|
|
183
|
+
size: g.size ?? GRAIN_DEFAULT_SIZE,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const p = edit.pattern;
|
|
188
|
+
if (p?.enabled && (p.opacity ?? PATTERN_DEFAULT_OPACITY) > EPS) {
|
|
189
|
+
byStage.pattern = {
|
|
190
|
+
stage: 'pattern',
|
|
191
|
+
kind: 'pattern',
|
|
192
|
+
type: p.type ?? PATTERN_DEFAULT_TYPE,
|
|
193
|
+
scale: p.scale ?? PATTERN_DEFAULT_SCALE,
|
|
194
|
+
opacity: p.opacity ?? PATTERN_DEFAULT_OPACITY,
|
|
195
|
+
blend: p.blend ?? PATTERN_DEFAULT_BLEND,
|
|
196
|
+
color: p.color ?? PATTERN_DEFAULT_COLOR,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const m = edit.mask;
|
|
201
|
+
if (m?.preset && m.preset !== 'none' && (m.strength ?? MASK_DEFAULT_STRENGTH) > EPS) {
|
|
202
|
+
byStage.mask = {
|
|
203
|
+
stage: 'mask',
|
|
204
|
+
kind: 'mask',
|
|
205
|
+
preset: m.preset,
|
|
206
|
+
strength: m.strength ?? MASK_DEFAULT_STRENGTH,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Emit in the canonical, fixed order.
|
|
211
|
+
const out: PhotoPipelineStep[] = [];
|
|
212
|
+
for (const stage of PHOTO_PIPELINE_ORDER) {
|
|
213
|
+
const step = byStage[stage];
|
|
214
|
+
if (step) out.push(step);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Pure color helper (shared by the realizer + tests) ───────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `#rrggbb` → normalized `[r, g, b]` in 0…1. Throws on malformed input (the
|
|
223
|
+
* validator in schema.ts already rejects bad colors before persistence, so this
|
|
224
|
+
* is a belt-and-braces guard for the realizer). Kept here (pure, no pixi) so the
|
|
225
|
+
* duotone realization and its tests share one parser.
|
|
226
|
+
*/
|
|
227
|
+
export function hexToRgb01(hex: string): [number, number, number] {
|
|
228
|
+
const m = /^#([0-9a-fA-F]{6})$/.exec(hex);
|
|
229
|
+
if (!m) throw new Error(`hexToRgb01: not a #rrggbb color: ${hex}`);
|
|
230
|
+
const int = Number.parseInt(m[1], 16);
|
|
231
|
+
return [((int >> 16) & 0xff) / 255, ((int >> 8) & 0xff) / 255, (int & 0xff) / 255];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── GLSL for the custom duotone filter (a string — no pixi dependency) ────────
|
|
235
|
+
// Duotone is a per-pixel luminance → two-color gradient-map lerp, which a linear
|
|
236
|
+
// `ColorMatrixFilter` affine transform cannot express, so it needs its own
|
|
237
|
+
// fragment shader. `pipeline.ts` compiles these into a `PIXI.Filter` via
|
|
238
|
+
// `GlProgram.from({ vertex, fragment })`. WebGL (GLSL ES 1.00-ish, pixi v8
|
|
239
|
+
// high-precision) only — the Application is pinned to `preference: 'webgl'` so
|
|
240
|
+
// the WebGPU program is unnecessary (and WKWebView WebGPU is partial — Task 25).
|
|
241
|
+
|
|
242
|
+
export const DUOTONE_VERT_SOURCE = /* glsl */ `
|
|
243
|
+
in vec2 aPosition;
|
|
244
|
+
out vec2 vTextureCoord;
|
|
245
|
+
|
|
246
|
+
uniform vec4 uInputSize;
|
|
247
|
+
uniform vec4 uOutputFrame;
|
|
248
|
+
uniform vec4 uOutputTexture;
|
|
249
|
+
|
|
250
|
+
vec4 filterVertexPosition(void) {
|
|
251
|
+
vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
|
|
252
|
+
position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
|
|
253
|
+
position.y = position.y * (2.0 * uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
|
|
254
|
+
return vec4(position, 0.0, 1.0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
vec2 filterTextureCoord(void) {
|
|
258
|
+
return aPosition * (uOutputFrame.zw * uInputSize.zw);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
void main(void) {
|
|
262
|
+
gl_Position = filterVertexPosition();
|
|
263
|
+
vTextureCoord = filterTextureCoord();
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
export const DUOTONE_FRAG_SOURCE = /* glsl */ `
|
|
268
|
+
in vec2 vTextureCoord;
|
|
269
|
+
out vec4 finalColor;
|
|
270
|
+
|
|
271
|
+
uniform sampler2D uTexture;
|
|
272
|
+
uniform vec3 uColorA; // shadow color (0..1)
|
|
273
|
+
uniform vec3 uColorB; // highlight color (0..1)
|
|
274
|
+
uniform float uIntensity; // 0..1 crossfade with the original
|
|
275
|
+
|
|
276
|
+
void main(void) {
|
|
277
|
+
vec4 src = texture(uTexture, vTextureCoord);
|
|
278
|
+
// Rec. 601 luma over UN-premultiplied color.
|
|
279
|
+
vec3 rgb = src.a > 0.0 ? src.rgb / src.a : src.rgb;
|
|
280
|
+
float luma = dot(rgb, vec3(0.299, 0.587, 0.114));
|
|
281
|
+
vec3 duo = mix(uColorA, uColorB, luma);
|
|
282
|
+
vec3 outRgb = mix(rgb, duo, uIntensity);
|
|
283
|
+
finalColor = vec4(outRgb * src.a, src.a); // re-premultiply
|
|
284
|
+
}
|
|
285
|
+
`;
|