@sealpixel/ui 1.0.0-rc.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.
Files changed (196) hide show
  1. package/LICENSE.md +59 -0
  2. package/README.md +10 -0
  3. package/dist/components/lumbra-button.d.ts +28 -0
  4. package/dist/components/lumbra-button.d.ts.map +1 -0
  5. package/dist/components/lumbra-button.js +87 -0
  6. package/dist/components/lumbra-button.js.map +1 -0
  7. package/dist/components/lumbra-editor.d.ts +226 -0
  8. package/dist/components/lumbra-editor.d.ts.map +1 -0
  9. package/dist/components/lumbra-editor.js +873 -0
  10. package/dist/components/lumbra-editor.js.map +1 -0
  11. package/dist/components/lumbra-nav.d.ts +48 -0
  12. package/dist/components/lumbra-nav.d.ts.map +1 -0
  13. package/dist/components/lumbra-nav.js +159 -0
  14. package/dist/components/lumbra-nav.js.map +1 -0
  15. package/dist/components/lumbra-slider.d.ts +48 -0
  16. package/dist/components/lumbra-slider.d.ts.map +1 -0
  17. package/dist/components/lumbra-slider.js +168 -0
  18. package/dist/components/lumbra-slider.js.map +1 -0
  19. package/dist/components/lumbra-stage.d.ts +92 -0
  20. package/dist/components/lumbra-stage.d.ts.map +1 -0
  21. package/dist/components/lumbra-stage.js +344 -0
  22. package/dist/components/lumbra-stage.js.map +1 -0
  23. package/dist/components/lumbra-toolbar.d.ts +30 -0
  24. package/dist/components/lumbra-toolbar.d.ts.map +1 -0
  25. package/dist/components/lumbra-toolbar.js +127 -0
  26. package/dist/components/lumbra-toolbar.js.map +1 -0
  27. package/dist/components/pincel-button.d.ts +28 -0
  28. package/dist/components/pincel-button.d.ts.map +1 -0
  29. package/dist/components/pincel-button.js +87 -0
  30. package/dist/components/pincel-button.js.map +1 -0
  31. package/dist/components/pincel-editor.d.ts +195 -0
  32. package/dist/components/pincel-editor.d.ts.map +1 -0
  33. package/dist/components/pincel-editor.js +794 -0
  34. package/dist/components/pincel-editor.js.map +1 -0
  35. package/dist/components/pincel-nav.d.ts +33 -0
  36. package/dist/components/pincel-nav.d.ts.map +1 -0
  37. package/dist/components/pincel-nav.js +147 -0
  38. package/dist/components/pincel-nav.js.map +1 -0
  39. package/dist/components/pincel-slider.d.ts +48 -0
  40. package/dist/components/pincel-slider.d.ts.map +1 -0
  41. package/dist/components/pincel-slider.js +168 -0
  42. package/dist/components/pincel-slider.js.map +1 -0
  43. package/dist/components/pincel-stage.d.ts +66 -0
  44. package/dist/components/pincel-stage.d.ts.map +1 -0
  45. package/dist/components/pincel-stage.js +233 -0
  46. package/dist/components/pincel-stage.js.map +1 -0
  47. package/dist/components/pincel-toolbar.d.ts +30 -0
  48. package/dist/components/pincel-toolbar.d.ts.map +1 -0
  49. package/dist/components/pincel-toolbar.js +127 -0
  50. package/dist/components/pincel-toolbar.js.map +1 -0
  51. package/dist/components/sealpixel-button.d.ts +28 -0
  52. package/dist/components/sealpixel-button.d.ts.map +1 -0
  53. package/dist/components/sealpixel-button.js +117 -0
  54. package/dist/components/sealpixel-button.js.map +1 -0
  55. package/dist/components/sealpixel-editor.d.ts +226 -0
  56. package/dist/components/sealpixel-editor.d.ts.map +1 -0
  57. package/dist/components/sealpixel-editor.js +1245 -0
  58. package/dist/components/sealpixel-editor.js.map +1 -0
  59. package/dist/components/sealpixel-nav.d.ts +48 -0
  60. package/dist/components/sealpixel-nav.d.ts.map +1 -0
  61. package/dist/components/sealpixel-nav.js +159 -0
  62. package/dist/components/sealpixel-nav.js.map +1 -0
  63. package/dist/components/sealpixel-slider.d.ts +75 -0
  64. package/dist/components/sealpixel-slider.d.ts.map +1 -0
  65. package/dist/components/sealpixel-slider.js +269 -0
  66. package/dist/components/sealpixel-slider.js.map +1 -0
  67. package/dist/components/sealpixel-stage.d.ts +118 -0
  68. package/dist/components/sealpixel-stage.d.ts.map +1 -0
  69. package/dist/components/sealpixel-stage.js +409 -0
  70. package/dist/components/sealpixel-stage.js.map +1 -0
  71. package/dist/components/sealpixel-stepper.d.ts +69 -0
  72. package/dist/components/sealpixel-stepper.d.ts.map +1 -0
  73. package/dist/components/sealpixel-stepper.js +248 -0
  74. package/dist/components/sealpixel-stepper.js.map +1 -0
  75. package/dist/components/sealpixel-toolbar.d.ts +30 -0
  76. package/dist/components/sealpixel-toolbar.d.ts.map +1 -0
  77. package/dist/components/sealpixel-toolbar.js +149 -0
  78. package/dist/components/sealpixel-toolbar.js.map +1 -0
  79. package/dist/controllers/gesture-controller.d.ts +65 -0
  80. package/dist/controllers/gesture-controller.d.ts.map +1 -0
  81. package/dist/controllers/gesture-controller.js +125 -0
  82. package/dist/controllers/gesture-controller.js.map +1 -0
  83. package/dist/controllers/session-controller.d.ts +15 -0
  84. package/dist/controllers/session-controller.d.ts.map +1 -0
  85. package/dist/controllers/session-controller.js +37 -0
  86. package/dist/controllers/session-controller.js.map +1 -0
  87. package/dist/define.d.ts +21 -0
  88. package/dist/define.d.ts.map +1 -0
  89. package/dist/define.js +27 -0
  90. package/dist/define.js.map +1 -0
  91. package/dist/i18n.d.ts +11 -0
  92. package/dist/i18n.d.ts.map +1 -0
  93. package/dist/i18n.js +27 -0
  94. package/dist/i18n.js.map +1 -0
  95. package/dist/icons.d.ts +32 -0
  96. package/dist/icons.d.ts.map +1 -0
  97. package/dist/icons.js +45 -0
  98. package/dist/icons.js.map +1 -0
  99. package/dist/index.d.ts +18 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +17 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/locales/de.d.ts +3 -0
  104. package/dist/locales/de.d.ts.map +1 -0
  105. package/dist/locales/de.js +132 -0
  106. package/dist/locales/de.js.map +1 -0
  107. package/dist/locales/en.d.ts +3 -0
  108. package/dist/locales/en.d.ts.map +1 -0
  109. package/dist/locales/en.js +132 -0
  110. package/dist/locales/en.js.map +1 -0
  111. package/dist/locales/es.d.ts +3 -0
  112. package/dist/locales/es.d.ts.map +1 -0
  113. package/dist/locales/es.js +132 -0
  114. package/dist/locales/es.js.map +1 -0
  115. package/dist/locales/fr.d.ts +3 -0
  116. package/dist/locales/fr.d.ts.map +1 -0
  117. package/dist/locales/fr.js +132 -0
  118. package/dist/locales/fr.js.map +1 -0
  119. package/dist/locales/index.d.ts +6 -0
  120. package/dist/locales/index.d.ts.map +1 -0
  121. package/dist/locales/index.js +13 -0
  122. package/dist/locales/index.js.map +1 -0
  123. package/dist/locales/ja.d.ts +3 -0
  124. package/dist/locales/ja.d.ts.map +1 -0
  125. package/dist/locales/ja.js +132 -0
  126. package/dist/locales/ja.js.map +1 -0
  127. package/dist/locales/pt.d.ts +3 -0
  128. package/dist/locales/pt.d.ts.map +1 -0
  129. package/dist/locales/pt.js +132 -0
  130. package/dist/locales/pt.js.map +1 -0
  131. package/dist/theme.d.ts +19 -0
  132. package/dist/theme.d.ts.map +1 -0
  133. package/dist/theme.js +150 -0
  134. package/dist/theme.js.map +1 -0
  135. package/dist/tool.d.ts +70 -0
  136. package/dist/tool.d.ts.map +1 -0
  137. package/dist/tool.js +2 -0
  138. package/dist/tool.js.map +1 -0
  139. package/dist/tools/annotate.d.ts +3 -0
  140. package/dist/tools/annotate.d.ts.map +1 -0
  141. package/dist/tools/annotate.js +436 -0
  142. package/dist/tools/annotate.js.map +1 -0
  143. package/dist/tools/crop-mask.d.ts +29 -0
  144. package/dist/tools/crop-mask.d.ts.map +1 -0
  145. package/dist/tools/crop-mask.js +75 -0
  146. package/dist/tools/crop-mask.js.map +1 -0
  147. package/dist/tools/crop.d.ts +11 -0
  148. package/dist/tools/crop.d.ts.map +1 -0
  149. package/dist/tools/crop.js +361 -0
  150. package/dist/tools/crop.js.map +1 -0
  151. package/dist/tools/delete-handle.d.ts +18 -0
  152. package/dist/tools/delete-handle.d.ts.map +1 -0
  153. package/dist/tools/delete-handle.js +61 -0
  154. package/dist/tools/delete-handle.js.map +1 -0
  155. package/dist/tools/filter.d.ts +3 -0
  156. package/dist/tools/filter.d.ts.map +1 -0
  157. package/dist/tools/filter.js +174 -0
  158. package/dist/tools/filter.js.map +1 -0
  159. package/dist/tools/finetune.d.ts +11 -0
  160. package/dist/tools/finetune.d.ts.map +1 -0
  161. package/dist/tools/finetune.js +98 -0
  162. package/dist/tools/finetune.js.map +1 -0
  163. package/dist/tools/frame.d.ts +3 -0
  164. package/dist/tools/frame.d.ts.map +1 -0
  165. package/dist/tools/frame.js +57 -0
  166. package/dist/tools/frame.js.map +1 -0
  167. package/dist/tools/index.d.ts +14 -0
  168. package/dist/tools/index.d.ts.map +1 -0
  169. package/dist/tools/index.js +41 -0
  170. package/dist/tools/index.js.map +1 -0
  171. package/dist/tools/redact.d.ts +3 -0
  172. package/dist/tools/redact.d.ts.map +1 -0
  173. package/dist/tools/redact.js +252 -0
  174. package/dist/tools/redact.js.map +1 -0
  175. package/dist/tools/resize.d.ts +3 -0
  176. package/dist/tools/resize.d.ts.map +1 -0
  177. package/dist/tools/resize.js +91 -0
  178. package/dist/tools/resize.js.map +1 -0
  179. package/dist/tools/sticker.d.ts +8 -0
  180. package/dist/tools/sticker.d.ts.map +1 -0
  181. package/dist/tools/sticker.js +156 -0
  182. package/dist/tools/sticker.js.map +1 -0
  183. package/dist/tools/watermark.d.ts +3 -0
  184. package/dist/tools/watermark.d.ts.map +1 -0
  185. package/dist/tools/watermark.js +102 -0
  186. package/dist/tools/watermark.js.map +1 -0
  187. package/dist/view-mapping.d.ts +23 -0
  188. package/dist/view-mapping.d.ts.map +1 -0
  189. package/dist/view-mapping.js +72 -0
  190. package/dist/view-mapping.js.map +1 -0
  191. package/package.json +55 -0
  192. package/themes/default.css +23 -0
  193. package/themes/high-contrast.css +30 -0
  194. package/themes/minimal.css +32 -0
  195. package/themes/nocturne.css +21 -0
  196. package/themes/studio.css +23 -0
@@ -0,0 +1,361 @@
1
+ import { CROP_DEFAULT_OPTIONS, ORIGINAL_ASPECT } from '@sealpixel/plugins';
2
+ import { html, nothing, svg } from 'lit';
3
+ import { icons } from '../icons.js';
4
+ import { opId } from './finetune.js';
5
+ /**
6
+ * While the crop tool is active the preview shows the whole rotated image
7
+ * (rect forced to full), and the gizmo draws the crop rectangle on top.
8
+ */
9
+ function showFullRect(log) {
10
+ return {
11
+ ...log,
12
+ ops: [
13
+ ...log.ops,
14
+ {
15
+ type: 'crop.set',
16
+ id: '__sealpixel_preview_rect__',
17
+ rect: { x: 0, y: 0, width: 1, height: 1 },
18
+ // The gizmo draws the round mask itself; the preview shows the unmasked pixels.
19
+ mask: 'none',
20
+ },
21
+ ],
22
+ };
23
+ }
24
+ const drag = {
25
+ handle: 'move',
26
+ start: { x: 0, y: 0, width: 1, height: 1 },
27
+ sx: 0,
28
+ sy: 0,
29
+ live: null,
30
+ };
31
+ /**
32
+ * How long the pointer has to be still after a change before the stage shows
33
+ * the result. Long enough never to fire between two drags, short enough that
34
+ * nobody waits for it.
35
+ */
36
+ const SETTLE_MS = 500;
37
+ /**
38
+ * The crop is written to the log the moment a handle is released, but the
39
+ * stage keeps showing the whole image, because a corner cannot be pulled
40
+ * outwards through an edge that is off screen. Letting go therefore looks
41
+ * exactly like doing nothing. After the pointer rests, the scrim closes over
42
+ * what the crop removes and the gizmo fades, so what is left on the stage is
43
+ * the picture that will come out. Touching the gizmo brings it all back.
44
+ */
45
+ const settle = {
46
+ at: null,
47
+ timer: null,
48
+ touched: false,
49
+ };
50
+ /** The preview clipped to the crop, in percentages of the canvas box. */
51
+ function clipFor(rect, circle) {
52
+ const l = rect.x * 100;
53
+ const t = rect.y * 100;
54
+ const r = (rect.x + rect.width) * 100;
55
+ const b = (rect.y + rect.height) * 100;
56
+ if (circle) {
57
+ return `ellipse(${(rect.width / 2) * 100}% ${(rect.height / 2) * 100}% at ${(rect.x + rect.width / 2) * 100}% ${(rect.y + rect.height / 2) * 100}%)`;
58
+ }
59
+ // A polygon at both ends, so the clip animates; `inset(0)` would not
60
+ // interpolate with it and the reveal would snap.
61
+ return `polygon(${l}% ${t}%, ${r}% ${t}%, ${r}% ${b}%, ${l}% ${b}%)`;
62
+ }
63
+ const FULL_CLIP = 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)';
64
+ function unsettle(ctx) {
65
+ if (settle.timer !== null)
66
+ clearTimeout(settle.timer);
67
+ settle.timer = null;
68
+ if (settle.at !== null) {
69
+ settle.at = null;
70
+ ctx.clipPreview(FULL_CLIP);
71
+ ctx.refresh();
72
+ }
73
+ }
74
+ function scheduleSettle(ctx) {
75
+ if (settle.timer !== null)
76
+ clearTimeout(settle.timer);
77
+ settle.at = null;
78
+ settle.touched = true;
79
+ settle.timer = setTimeout(() => {
80
+ settle.timer = null;
81
+ settle.at = Date.now();
82
+ // Fresh from the session: `ctx.document` is the snapshot from the render
83
+ // that scheduled this, which is the state from before the commit.
84
+ const doc = ctx.session.getDocument().crop;
85
+ const circle = (ctx.options('crop') ?? CROP_DEFAULT_OPTIONS).shape === 'circle' ||
86
+ doc.mask === 'circle';
87
+ ctx.clipPreview(clipFor(doc.rect, circle));
88
+ ctx.refresh();
89
+ }, SETTLE_MS);
90
+ }
91
+ export const cropTool = {
92
+ id: 'crop',
93
+ icon: icons.crop,
94
+ label: 'tool.crop',
95
+ activate: (ctx) => {
96
+ unsettle(ctx);
97
+ settle.touched = false;
98
+ ctx.clipPreview(FULL_CLIP);
99
+ ctx.session.setPreviewTransform(showFullRect);
100
+ },
101
+ deactivate: (ctx) => {
102
+ drag.live = null;
103
+ unsettle(ctx);
104
+ // The next preview is the real crop; nothing left to clip.
105
+ ctx.clipPreview(null);
106
+ // The image becomes the crop now, so ask for it to be seen happening — but
107
+ // only if this visit changed it. Crop is often the tool an editor opens on,
108
+ // and leaving one the user never touched should move nothing.
109
+ if (settle.touched)
110
+ ctx.animateNextView();
111
+ settle.touched = false;
112
+ ctx.session.setPreviewTransform(null);
113
+ },
114
+ panel: (ctx) => {
115
+ const doc = ctx.document.crop;
116
+ const deg = (doc.rotation * 180) / Math.PI;
117
+ // A rotation, a flip or an aspect ratio is a change like a drag is, so it
118
+ // settles into the result the same way.
119
+ const set = (patch) => {
120
+ ctx.dispatch({ type: 'crop.set', id: opId(), ...patch });
121
+ scheduleSettle(ctx);
122
+ };
123
+ const src = ctx.state.source;
124
+ const original = src ? src.width / src.height : 1;
125
+ const options = ctx.options('crop') ?? CROP_DEFAULT_OPTIONS;
126
+ const circle = options.shape === 'circle';
127
+ // In circle mode a reset restores a full round crop instead of dropping the mask.
128
+ const reset = () => {
129
+ unsettle(ctx);
130
+ return ctx.dispatch(circle
131
+ ? {
132
+ type: 'crop.set',
133
+ id: opId(),
134
+ rotation: 0,
135
+ flipX: false,
136
+ flipY: false,
137
+ rect: { x: 0, y: 0, width: 1, height: 1 },
138
+ aspect: 1,
139
+ mask: 'circle',
140
+ }
141
+ : { type: 'crop.reset', id: opId() });
142
+ };
143
+ const aspects = options.aspects.map((a) => ({
144
+ key: a.label,
145
+ value: a.value === ORIGINAL_ASPECT ? original : a.value,
146
+ }));
147
+ const aspectSelect = html `<select
148
+ data-field="aspect"
149
+ aria-label=${ctx.t('crop.aspect')}
150
+ @change=${(e) => {
151
+ const v = e.target.value;
152
+ const aspect = v === '' ? null : Number(v);
153
+ set({ aspect, ...(aspect === null ? {} : { rect: fitAspect(doc.rect, aspect, ctx) }) });
154
+ }}
155
+ >
156
+ ${aspects.map((a) => html `<option value=${a.value === null ? '' : String(a.value)} ?selected=${a.value === doc.aspect || (a.value !== null && doc.aspect !== null && Math.abs(a.value - doc.aspect) < 1e-6)}>
157
+ ${a.key.startsWith('crop.') ? ctx.t(a.key) : a.key}
158
+ </option>`)}
159
+ </select>`;
160
+ return html `
161
+ ${options.rotation
162
+ ? html `<sealpixel-slider
163
+ data-param="rotation"
164
+ .label=${ctx.t('crop.rotation')}
165
+ .value=${Number(deg.toFixed(2))}
166
+ .min=${-180}
167
+ .max=${180}
168
+ .step=${options.rotationStep}
169
+ .format=${(v) => `${v.toFixed(1)}°`}
170
+ @slide=${(e) => ctx.hint({ transform: `rotate(${e.detail - deg}deg)` })}
171
+ @commit=${(e) => {
172
+ ctx.hint(null);
173
+ set({ rotation: (e.detail * Math.PI) / 180 });
174
+ }}
175
+ ></sealpixel-slider>`
176
+ : nothing}
177
+ <div class="row compact choice">
178
+ <div class="group">
179
+ ${options.rotation
180
+ ? html `<sealpixel-button data-action="rotate-left" .label=${'-90°'} @click=${() => set({ rotation: doc.rotation - Math.PI / 2 })}>⟲ 90°</sealpixel-button>
181
+ <sealpixel-button data-action="rotate-right" .label=${'+90°'} @click=${() => set({ rotation: doc.rotation + Math.PI / 2 })}>⟳ 90°</sealpixel-button>`
182
+ : nothing}
183
+ ${options.flip
184
+ ? html `<sealpixel-button data-action="flip-x" ?pressed=${doc.flipX} .label=${ctx.t('crop.flipX')} @click=${() => set({ flipX: !doc.flipX })}>⇋</sealpixel-button>
185
+ <sealpixel-button data-action="flip-y" ?pressed=${doc.flipY} .label=${ctx.t('crop.flipY')} @click=${() => set({ flipY: !doc.flipY })}>⇅</sealpixel-button>`
186
+ : nothing}
187
+ </div>
188
+ ${circle ? nothing : aspectSelect}
189
+ ${options.reset
190
+ ? html `<sealpixel-button variant="ghost" data-action="reset" @click=${reset}>
191
+ ${ctx.t('crop.reset')}
192
+ </sealpixel-button>`
193
+ : nothing}
194
+ </div>
195
+ `;
196
+ },
197
+ overlay: (ctx) => {
198
+ const doc = ctx.document.crop;
199
+ const { width: W, height: H, zoom } = ctx.stage;
200
+ if (!W || !H)
201
+ return svg ``;
202
+ const r = drag.live ?? doc.rect;
203
+ const x = r.x * W;
204
+ const y = r.y * H;
205
+ const w = r.width * W;
206
+ const h = r.height * H;
207
+ const hr = 7 / zoom;
208
+ const sw = 1.5 / zoom;
209
+ const handles = [
210
+ { id: 'nw', cx: x, cy: y },
211
+ { id: 'n', cx: x + w / 2, cy: y },
212
+ { id: 'ne', cx: x + w, cy: y },
213
+ { id: 'e', cx: x + w, cy: y + h / 2 },
214
+ { id: 'se', cx: x + w, cy: y + h },
215
+ { id: 's', cx: x + w / 2, cy: y + h },
216
+ { id: 'sw', cx: x, cy: y + h },
217
+ { id: 'w', cx: x, cy: y + h / 2 },
218
+ ];
219
+ const start = (handle) => (e) => {
220
+ e.stopPropagation();
221
+ e.preventDefault();
222
+ unsettle(ctx);
223
+ const p = ctx.stage.toImage(e.clientX, e.clientY);
224
+ drag.handle = handle;
225
+ drag.start = doc.rect;
226
+ drag.sx = p.x / W;
227
+ drag.sy = p.y / H;
228
+ drag.live = doc.rect;
229
+ const move = (ev) => {
230
+ const q = ctx.stage.toImage(ev.clientX, ev.clientY);
231
+ drag.live = applyDrag(drag.start, handle, q.x / W - drag.sx, q.y / H - drag.sy, doc.aspect, W / H);
232
+ ctx.refresh();
233
+ };
234
+ const up = () => {
235
+ window.removeEventListener('pointermove', move);
236
+ window.removeEventListener('pointerup', up);
237
+ window.removeEventListener('pointercancel', up);
238
+ const final = drag.live;
239
+ drag.live = null;
240
+ if (final && !sameRect(final, doc.rect))
241
+ ctx.dispatch({ type: 'crop.set', id: opId(), rect: final });
242
+ scheduleSettle(ctx);
243
+ ctx.refresh();
244
+ };
245
+ window.addEventListener('pointermove', move);
246
+ window.addEventListener('pointerup', up);
247
+ window.addEventListener('pointercancel', up);
248
+ };
249
+ const circleShape = (ctx.options('crop') ?? CROP_DEFAULT_OPTIONS).shape === 'circle' ||
250
+ doc.mask === 'circle';
251
+ const cx = x + w / 2;
252
+ const cy = y + h / 2;
253
+ // Ellipse hole as two arcs so the evenodd path can carve it out.
254
+ const hole = circleShape
255
+ ? `M${cx - w / 2} ${cy}A${w / 2} ${h / 2} 0 1 0 ${cx + w / 2} ${cy}A${w / 2} ${h / 2} 0 1 0 ${cx - w / 2} ${cy}Z`
256
+ : `M${x} ${y}H${x + w}V${y + h}H${x}Z`;
257
+ // Settled: the preview is clipped to the crop and the gizmo goes, so what is
258
+ // left on the stage is the picture that comes out, with the stage's own
259
+ // backdrop around it rather than a painted stand-in. The scrim is only the
260
+ // dim for adjusting and fades out with the rest.
261
+ //
262
+ // The transitions are inline, and the reduced-motion rule in the shared
263
+ // base still overrides them, which leaves the same state change unanimated.
264
+ const shown = settle.at !== null;
265
+ const ease = 'var(--sealpixel-duration-slow, 320ms) var(--sealpixel-ease-out, ease-out)';
266
+ const gizmo = `transition:opacity ${ease};opacity:${shown ? 0 : 1}`;
267
+ return svg `
268
+ <path
269
+ data-crop-scrim
270
+ d=${`M0 0H${W}V${H}H0Z ${hole}`}
271
+ fill="var(--sealpixel-color-bg, #000)"
272
+ fill-opacity=${shown ? 0 : 0.5}
273
+ fill-rule="evenodd"
274
+ style=${`transition:fill-opacity ${ease}`}
275
+ pointer-events="none"
276
+ />
277
+ ${circleShape ? svg `<ellipse data-crop-shape="circle" cx=${cx} cy=${cy} rx=${w / 2} ry=${h / 2} fill="none" stroke="white" stroke-width=${sw} style=${gizmo} pointer-events="none" />` : nothing}
278
+ <rect data-interactive data-handle="move" x=${x} y=${y} width=${w} height=${h} fill="transparent" stroke="white" stroke-width=${sw} style=${`cursor:move;transition:stroke-opacity ${ease};stroke-opacity:${shown ? 0 : 1}`} @pointerdown=${start('move')} />
279
+ <path d=${`M${x + w / 3} ${y}V${y + h}M${x + (2 * w) / 3} ${y}V${y + h}M${x} ${y + h / 3}H${x + w}M${x} ${y + (2 * h) / 3}H${x + w}`} stroke="rgba(255,255,255,0.35)" stroke-width=${sw / 1.5} fill="none" style=${gizmo} pointer-events="none" />
280
+ ${handles.map((hd) => svg `<circle data-interactive data-handle=${hd.id} cx=${hd.cx} cy=${hd.cy} r=${hr} fill="white" stroke="var(--sealpixel-color-accent)" stroke-width=${sw} style=${`cursor:${cursorFor(hd.id)};transition:opacity ${ease};opacity:${shown ? 0 : 1}`} @pointerdown=${start(hd.id)} />`)}
281
+ `;
282
+ },
283
+ };
284
+ function cursorFor(h) {
285
+ return {
286
+ nw: 'nwse-resize',
287
+ se: 'nwse-resize',
288
+ ne: 'nesw-resize',
289
+ sw: 'nesw-resize',
290
+ n: 'ns-resize',
291
+ s: 'ns-resize',
292
+ e: 'ew-resize',
293
+ w: 'ew-resize',
294
+ move: 'move',
295
+ }[h];
296
+ }
297
+ function sameRect(a, b) {
298
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
299
+ }
300
+ const MIN = 0.02;
301
+ /**
302
+ * Applies a normalised drag delta to `start`. `aspect` is width/height in
303
+ * output pixels; `stageAspect` converts it into normalised units.
304
+ */
305
+ export function applyDrag(start, handle, dx, dy, aspect, stageAspect) {
306
+ if (handle === 'move') {
307
+ return {
308
+ ...start,
309
+ x: clamp(start.x + dx, 0, 1 - start.width),
310
+ y: clamp(start.y + dy, 0, 1 - start.height),
311
+ };
312
+ }
313
+ let x0 = start.x;
314
+ let y0 = start.y;
315
+ let x1 = start.x + start.width;
316
+ let y1 = start.y + start.height;
317
+ if (handle.includes('w'))
318
+ x0 = clamp(x0 + dx, 0, x1 - MIN);
319
+ if (handle.includes('e'))
320
+ x1 = clamp(x1 + dx, x0 + MIN, 1);
321
+ if (handle.includes('n'))
322
+ y0 = clamp(y0 + dy, 0, y1 - MIN);
323
+ if (handle.includes('s'))
324
+ y1 = clamp(y1 + dy, y0 + MIN, 1);
325
+ let rect = { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
326
+ if (aspect !== null) {
327
+ // normalised height for a given normalised width under this aspect
328
+ const ratio = aspect / stageAspect; // width_n / height_n
329
+ const anchorRight = handle.includes('w');
330
+ const anchorBottom = handle.includes('n');
331
+ const horizontal = handle === 'e' || handle === 'w';
332
+ let w = rect.width;
333
+ let h = rect.height;
334
+ if (horizontal || (!['n', 's'].includes(handle) && Math.abs(dx) >= Math.abs(dy)))
335
+ h = w / ratio;
336
+ else
337
+ w = h * ratio;
338
+ // keep inside bounds
339
+ if (w > 1) {
340
+ w = 1;
341
+ h = w / ratio;
342
+ }
343
+ if (h > 1) {
344
+ h = 1;
345
+ w = h * ratio;
346
+ }
347
+ const nx = anchorRight ? x1 - w : x0;
348
+ const ny = anchorBottom ? y1 - h : y0;
349
+ rect = { x: clamp(nx, 0, 1 - w), y: clamp(ny, 0, 1 - h), width: w, height: h };
350
+ }
351
+ return rect;
352
+ }
353
+ function fitAspect(rect, aspect, ctx) {
354
+ const { width: W, height: H } = ctx.stage;
355
+ const stageAspect = W && H ? W / H : 1;
356
+ return applyDrag({ ...rect }, 'se', 0, 0, aspect, stageAspect);
357
+ }
358
+ function clamp(v, lo, hi) {
359
+ return v < lo ? lo : v > hi ? hi : v;
360
+ }
361
+ //# sourceMappingURL=crop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop.js","sourceRoot":"","sources":["../../src/tools/crop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Rect } from '@sealpixel/core';
2
+ import { type SVGTemplateResult } from 'lit';
3
+ /**
4
+ * A trash button floating above a selected element, rendered inside a tool
5
+ * overlay in image pixel coordinates. It is sized through `zoom`, so it stays
6
+ * 32 px on screen at every zoom level, and moves below the element when the
7
+ * image edge leaves no room above. Enter and Space trigger it from the keyboard.
8
+ */
9
+ export declare function deleteHandle(box: Rect, stage: {
10
+ width: number;
11
+ height: number;
12
+ zoom: number;
13
+ }, label: string, onDelete: () => void): SVGTemplateResult;
14
+ /** Axis-aligned box around normalized points. */
15
+ export declare function pointsBox(points: readonly (readonly [number, number])[]): Rect;
16
+ /** Normalized rect to image pixels. */
17
+ export declare function toPx(r: Rect, W: number, H: number): Rect;
18
+ //# sourceMappingURL=delete-handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-handle.d.ts","sourceRoot":"","sources":["../../src/tools/delete-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,KAAK,iBAAiB,EAAO,MAAM,KAAK,CAAC;AAGlD;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,IAAI,EACT,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACtD,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,IAAI,GACnB,iBAAiB,CAwBnB;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAY9E;AAED,uCAAuC;AACvC,wBAAgB,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAExD"}
@@ -0,0 +1,61 @@
1
+ import { svg } from 'lit';
2
+ import { TRASH_PATH } from '../icons.js';
3
+ /**
4
+ * A trash button floating above a selected element, rendered inside a tool
5
+ * overlay in image pixel coordinates. It is sized through `zoom`, so it stays
6
+ * 32 px on screen at every zoom level, and moves below the element when the
7
+ * image edge leaves no room above. Enter and Space trigger it from the keyboard.
8
+ */
9
+ export function deleteHandle(box, stage, label, onDelete) {
10
+ const { zoom, width: W, height: H } = stage;
11
+ const r = 16 / zoom;
12
+ const gap = 10 / zoom;
13
+ const cx = clamp(box.x + box.width / 2, r, W - r);
14
+ let cy = box.y - gap - r;
15
+ if (cy - r < 0)
16
+ cy = Math.min(box.y + box.height + gap + r, H - r);
17
+ const stop = (e) => {
18
+ e.stopPropagation();
19
+ e.preventDefault();
20
+ };
21
+ const act = (e) => {
22
+ e.stopPropagation();
23
+ onDelete();
24
+ };
25
+ return svg `<g data-interactive data-action="delete" role="button" tabindex="0" aria-label=${label}
26
+ transform=${`translate(${cx} ${cy})`} style="cursor:pointer"
27
+ @pointerdown=${stop} @click=${act}
28
+ @keydown=${(e) => {
29
+ if (e.key === 'Enter' || e.key === ' ')
30
+ act(e);
31
+ }}>
32
+ <circle r=${r} fill="var(--sealpixel-color-surface)" stroke="var(--sealpixel-color-border)" stroke-width=${1 / zoom} />
33
+ <g transform=${`scale(${1 / zoom}) translate(-9 -9) scale(0.75)`} fill="none" stroke="var(--sealpixel-color-danger)" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d=${TRASH_PATH} /></g>
34
+ </g>`;
35
+ }
36
+ /** Axis-aligned box around normalized points. */
37
+ export function pointsBox(points) {
38
+ let x0 = 1;
39
+ let y0 = 1;
40
+ let x1 = 0;
41
+ let y1 = 0;
42
+ for (const [x, y] of points) {
43
+ if (x < x0)
44
+ x0 = x;
45
+ if (y < y0)
46
+ y0 = y;
47
+ if (x > x1)
48
+ x1 = x;
49
+ if (y > y1)
50
+ y1 = y;
51
+ }
52
+ return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
53
+ }
54
+ /** Normalized rect to image pixels. */
55
+ export function toPx(r, W, H) {
56
+ return { x: r.x * W, y: r.y * H, width: r.width * W, height: r.height * H };
57
+ }
58
+ function clamp(v, lo, hi) {
59
+ return v < lo ? lo : v > hi ? hi : v;
60
+ }
61
+ //# sourceMappingURL=delete-handle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-handle.js","sourceRoot":"","sources":["../../src/tools/delete-handle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { UiTool } from '../tool.js';
2
+ export declare const filterTool: UiTool;
3
+ //# sourceMappingURL=filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/tools/filter.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,YAAY,CAAC;AA6GxD,eAAO,MAAM,UAAU,EAAE,MAmFxB,CAAC"}
@@ -0,0 +1,174 @@
1
+ import { canonicalJson } from '@sealpixel/core';
2
+ import { FILTER_DEFAULT_OPTIONS, presetLabelKey, presetMatrix, presetName, } from '@sealpixel/plugins';
3
+ import { html, nothing } from 'lit';
4
+ import { icons } from '../icons.js';
5
+ import { opId } from './finetune.js';
6
+ /** Leaves the image as it is, so the "none" tile shows the real source. */
7
+ const IDENTITY = [
8
+ [1, 0, 0, 0, 0],
9
+ [0, 1, 0, 0, 0],
10
+ [0, 0, 1, 0, 0],
11
+ [0, 0, 0, 1, 0],
12
+ ];
13
+ const sameMatrix = (a, b) => a !== null &&
14
+ a.every((row, i) => row.every((v, j) => Math.abs(v - b[i][j]) < 1e-9));
15
+ /**
16
+ * The image every thumbnail is drawn from: the current log with the filter
17
+ * taken out. Rendering the tiles from the stage preview instead would push
18
+ * each preset through whichever preset is already chosen, so picking Sepia
19
+ * turned every other tile sepia too and "None" stopped meaning none.
20
+ *
21
+ * Keyed by the rest of the log, so a crop or a finetune invalidates it while
22
+ * changing the filter does not.
23
+ */
24
+ const withoutFilter = (log) => ({
25
+ ...log,
26
+ ops: log.ops.filter((op) => !op.type.startsWith('filter.')),
27
+ });
28
+ const baseKey = (log) => canonicalJson(withoutFilter(log).ops);
29
+ let base = null;
30
+ let basePending = null;
31
+ /**
32
+ * Returns the unfiltered image for this log, rendering it once per key. While
33
+ * a render is in flight the caller gets nothing and the tiles stay flat; the
34
+ * refresh lands a moment later.
35
+ */
36
+ function baseImage(ctx) {
37
+ const { log } = ctx.state.history;
38
+ const key = baseKey(log);
39
+ if (base?.key === key)
40
+ return base.image;
41
+ // No filter in the log means the stage already shows the unfiltered image.
42
+ if (log.ops.every((op) => !op.type.startsWith('filter.'))) {
43
+ const image = ctx.state.preview?.image ?? null;
44
+ if (image)
45
+ base = { key, image };
46
+ return image;
47
+ }
48
+ if (basePending === key)
49
+ return null;
50
+ basePending = key;
51
+ ctx.session
52
+ .renderDetached(withoutFilter)
53
+ .then((image) => {
54
+ base = { key, image };
55
+ ctx.refresh();
56
+ })
57
+ .catch(() => { })
58
+ .finally(() => {
59
+ if (basePending === key)
60
+ basePending = null;
61
+ });
62
+ return null;
63
+ }
64
+ /** Thumbnails: the base image downsampled and pushed through a matrix. */
65
+ const thumbCache = new Map();
66
+ function thumbnail(image, version, name, matrix) {
67
+ const key = `${version}:${name}`;
68
+ const hit = thumbCache.get(key);
69
+ if (hit)
70
+ return hit;
71
+ if (thumbCache.size > 64)
72
+ thumbCache.clear();
73
+ // Rendered wider than the tile so an 84 px thumbnail is not an upscale.
74
+ const w = 96;
75
+ const h = Math.max(1, Math.round((image.height / image.width) * w));
76
+ const canvas = document.createElement('canvas');
77
+ canvas.width = w;
78
+ canvas.height = h;
79
+ const ctx = canvas.getContext('2d');
80
+ if (!ctx)
81
+ return '';
82
+ const out = ctx.createImageData(w, h);
83
+ for (let y = 0; y < h; y++) {
84
+ const sy = Math.floor((y / h) * image.height);
85
+ for (let x = 0; x < w; x++) {
86
+ const sx = Math.floor((x / w) * image.width);
87
+ const si = (sy * image.width + sx) * 4;
88
+ const r = image.data[si] / 255;
89
+ const g = image.data[si + 1] / 255;
90
+ const b = image.data[si + 2] / 255;
91
+ const a = image.data[si + 3] / 255;
92
+ const di = (y * w + x) * 4;
93
+ for (let c = 0; c < 4; c++) {
94
+ const m = matrix[c];
95
+ const v = m[0] * r + m[1] * g + m[2] * b + m[3] * a + m[4];
96
+ out.data[di + c] = Math.max(0, Math.min(255, Math.round(v * 255)));
97
+ }
98
+ }
99
+ }
100
+ ctx.putImageData(out, 0, 0);
101
+ const url = canvas.toDataURL('image/png');
102
+ thumbCache.set(key, url);
103
+ return url;
104
+ }
105
+ export const filterTool = {
106
+ id: 'filter',
107
+ icon: icons.filter,
108
+ label: 'tool.filter',
109
+ panel: (ctx) => {
110
+ const doc = ctx.document.filter;
111
+ const options = ctx.options('filter') ?? FILTER_DEFAULT_OPTIONS;
112
+ const strength = doc.preset === null ? options.defaultStrength : doc.strength;
113
+ const select = (preset) => {
114
+ if (preset === null) {
115
+ ctx.dispatch({ type: 'filter.set', id: opId(), preset: null });
116
+ return;
117
+ }
118
+ const option = options.presets.find((p) => presetName(p) === preset);
119
+ if (!option)
120
+ return;
121
+ ctx.dispatch(typeof option === 'string'
122
+ ? { type: 'filter.set', id: opId(), preset, strength }
123
+ : { type: 'filter.set', id: opId(), preset: 'custom', matrix: option.matrix, strength });
124
+ };
125
+ const pressed = (p) => typeof p === 'string'
126
+ ? doc.preset === p
127
+ : doc.preset === 'custom' && sameMatrix(doc.matrix, p.matrix);
128
+ const label = (p) => {
129
+ const { key, literal } = presetLabelKey(p);
130
+ return literal ?? ctx.t(key);
131
+ };
132
+ const source = options.thumbnails ? baseImage(ctx) : null;
133
+ const sourceKey = source ? baseKey(ctx.state.history.log) : '';
134
+ // The untouched image sits in its own group. It is not one preset among
135
+ // the others; it is what every preset is being compared against, and a gap
136
+ // says that faster than a label can.
137
+ const tile = (name, text, isPressed, matrix, onClick) => html `<sealpixel-button
138
+ variant="ghost"
139
+ data-preset=${name}
140
+ ?pressed=${isPressed}
141
+ .label=${text}
142
+ @click=${onClick}
143
+ >${source && matrix
144
+ ? html `<img class="thumb" alt="" src=${thumbnail(source, sourceKey, name, matrix)} /><span>${text}</span>`
145
+ : text}</sealpixel-button
146
+ >`;
147
+ return html `
148
+ <div class=${source ? 'strip tiles choice' : 'strip choice'} role="group" aria-label=${ctx.t('tool.filter')}>
149
+ ${tile('none', ctx.t('filter.none'), doc.preset === null, IDENTITY, () => select(null))}
150
+ <span class="divider" aria-hidden="true"></span>
151
+ ${options.presets.map((p) => tile(presetName(p), label(p), pressed(p), presetMatrix(p), () => select(presetName(p))))}
152
+ </div>
153
+ ${options.strength
154
+ ? html `<sealpixel-slider
155
+ data-param="strength"
156
+ .label=${ctx.t('filter.strength')}
157
+ .value=${doc.strength}
158
+ .min=${options.strengthRange.min}
159
+ .max=${options.strengthRange.max}
160
+ .step=${options.strengthRange.step}
161
+ ?disabled=${doc.preset === null}
162
+ @commit=${(e) => ctx.dispatch({
163
+ type: 'filter.set',
164
+ id: opId(),
165
+ preset: doc.preset,
166
+ ...(doc.preset === 'custom' && doc.matrix ? { matrix: doc.matrix } : {}),
167
+ strength: e.detail,
168
+ })}
169
+ ></sealpixel-slider>`
170
+ : nothing}
171
+ `;
172
+ },
173
+ };
174
+ //# sourceMappingURL=filter.js.map