@we-are-singular/svelte-chop-chop 0.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/components/.gitkeep +0 -0
  4. package/dist/components/CircleStencil.svelte +126 -0
  5. package/dist/components/CircleStencil.svelte.d.ts +10 -0
  6. package/dist/components/CropOverlay.svelte +51 -0
  7. package/dist/components/CropOverlay.svelte.d.ts +8 -0
  8. package/dist/components/CropStencil.svelte +84 -0
  9. package/dist/components/CropStencil.svelte.d.ts +10 -0
  10. package/dist/components/Cropper.svelte +242 -0
  11. package/dist/components/Cropper.svelte.d.ts +32 -0
  12. package/dist/components/DragHandle.svelte +129 -0
  13. package/dist/components/DragHandle.svelte.d.ts +13 -0
  14. package/dist/components/FilterStrip.svelte +58 -0
  15. package/dist/components/FilterStrip.svelte.d.ts +9 -0
  16. package/dist/components/GridOverlay.svelte +85 -0
  17. package/dist/components/GridOverlay.svelte.d.ts +9 -0
  18. package/dist/components/ImageEditor.svelte +1087 -0
  19. package/dist/components/ImageEditor.svelte.d.ts +16 -0
  20. package/dist/components/Toolbar.svelte +103 -0
  21. package/dist/components/Toolbar.svelte.d.ts +7 -0
  22. package/dist/composables/.gitkeep +0 -0
  23. package/dist/composables/create-cropper.svelte.d.ts +49 -0
  24. package/dist/composables/create-cropper.svelte.js +257 -0
  25. package/dist/composables/create-image-editor.svelte.d.ts +20 -0
  26. package/dist/composables/create-image-editor.svelte.js +596 -0
  27. package/dist/composables/create-transform.svelte.d.ts +28 -0
  28. package/dist/composables/create-transform.svelte.js +26 -0
  29. package/dist/core/.gitkeep +0 -0
  30. package/dist/core/color-matrix.d.ts +39 -0
  31. package/dist/core/color-matrix.js +137 -0
  32. package/dist/core/constraints.d.ts +46 -0
  33. package/dist/core/constraints.js +107 -0
  34. package/dist/core/coordinate-system.d.ts +65 -0
  35. package/dist/core/coordinate-system.js +185 -0
  36. package/dist/core/crop-engine.svelte.d.ts +33 -0
  37. package/dist/core/crop-engine.svelte.js +192 -0
  38. package/dist/core/export.d.ts +14 -0
  39. package/dist/core/export.js +99 -0
  40. package/dist/core/history-manager.svelte.d.ts +22 -0
  41. package/dist/core/history-manager.svelte.js +72 -0
  42. package/dist/core/image-loader.svelte.d.ts +17 -0
  43. package/dist/core/image-loader.svelte.js +126 -0
  44. package/dist/core/interactions.d.ts +52 -0
  45. package/dist/core/interactions.js +118 -0
  46. package/dist/core/keyboard.d.ts +11 -0
  47. package/dist/core/keyboard.js +23 -0
  48. package/dist/core/transform-engine.svelte.d.ts +27 -0
  49. package/dist/core/transform-engine.svelte.js +79 -0
  50. package/dist/core/types.d.ts +265 -0
  51. package/dist/core/types.js +5 -0
  52. package/dist/index.d.ts +30 -0
  53. package/dist/index.js +35 -0
  54. package/dist/plugins/.gitkeep +0 -0
  55. package/dist/plugins/index.d.ts +8 -0
  56. package/dist/plugins/index.js +8 -0
  57. package/dist/plugins/plugin-filters.d.ts +14 -0
  58. package/dist/plugins/plugin-filters.js +100 -0
  59. package/dist/plugins/plugin-finetune.d.ts +10 -0
  60. package/dist/plugins/plugin-finetune.js +23 -0
  61. package/dist/plugins/plugin-frame.d.ts +11 -0
  62. package/dist/plugins/plugin-frame.js +81 -0
  63. package/dist/plugins/plugin-resize.d.ts +10 -0
  64. package/dist/plugins/plugin-resize.js +23 -0
  65. package/dist/plugins/plugin-watermark.d.ts +10 -0
  66. package/dist/plugins/plugin-watermark.js +86 -0
  67. package/dist/presets/.gitkeep +0 -0
  68. package/dist/presets/cover-photo.d.ts +14 -0
  69. package/dist/presets/cover-photo.js +14 -0
  70. package/dist/presets/index.d.ts +6 -0
  71. package/dist/presets/index.js +6 -0
  72. package/dist/presets/product-image.d.ts +11 -0
  73. package/dist/presets/product-image.js +11 -0
  74. package/dist/presets/profile-picture.d.ts +17 -0
  75. package/dist/presets/profile-picture.js +17 -0
  76. package/dist/themes/.gitkeep +0 -0
  77. package/dist/themes/dark.css +17 -0
  78. package/dist/themes/default.css +23 -0
  79. package/dist/themes/minimal.css +17 -0
  80. package/package.json +118 -0
@@ -0,0 +1,596 @@
1
+ /**
2
+ * svelte-chop-chop — Full image editor orchestrator
3
+ * Combines crop, transform, filters, history, plugins.
4
+ */
5
+ import { createImageLoader } from "../core/image-loader.svelte.js";
6
+ import { createCropEngine } from "../core/crop-engine.svelte.js";
7
+ import { createTransformEngine } from "../core/transform-engine.svelte.js";
8
+ import { createHistoryManager } from "../core/history-manager.svelte.js";
9
+ import { createPinchHandler, createWheelHandler, } from "../core/interactions.js";
10
+ import { exportImage } from "../core/export.js";
11
+ import { applyColorMatrix, applyGamma, brightnessMatrix, clarityMatrix, contrastMatrix, exposureMatrix, multiplyMatrices, saturationMatrix, temperatureMatrix, } from "../core/color-matrix.js";
12
+ import { createKeyboardHandler } from "../core/keyboard.js";
13
+ const DEFAULT_FILTERS = {
14
+ preset: null,
15
+ brightness: 0,
16
+ contrast: 0,
17
+ saturation: 0,
18
+ exposure: 0,
19
+ temperature: 0,
20
+ clarity: 0,
21
+ gamma: 1,
22
+ };
23
+ const DEFAULT_FRAME = {
24
+ type: "none",
25
+ color: "#000000",
26
+ width: 20,
27
+ };
28
+ const DEFAULT_WATERMARK = {
29
+ text: "",
30
+ opacity: 0.7,
31
+ position: "bottom-right",
32
+ color: "#ffffff",
33
+ fontSize: 24,
34
+ };
35
+ /**
36
+ * Create full image editor composable.
37
+ */
38
+ export function createImageEditor(options = {}) {
39
+ const loader = createImageLoader();
40
+ const engine = createCropEngine({
41
+ aspectRatio: options.aspectRatio ?? null,
42
+ sizeConstraints: options.sizeConstraints,
43
+ initialCrop: options.initialCrop,
44
+ });
45
+ const transform = createTransformEngine({
46
+ initialTransforms: options.initialTransforms,
47
+ });
48
+ const history = createHistoryManager({
49
+ maxEntries: options.maxHistory ?? 50,
50
+ });
51
+ let filters = $state({ ...DEFAULT_FILTERS });
52
+ let frame = $state({ ...DEFAULT_FRAME });
53
+ let watermark = $state({ ...DEFAULT_WATERMARK });
54
+ let interacting = $state(false);
55
+ let activeTab = $state(null);
56
+ let actions = $state([]);
57
+ let filterPresets = $state([]);
58
+ let postProcessors = $state([]);
59
+ let canvasEl = null;
60
+ let containerEl = null;
61
+ let resizeObserver = null;
62
+ let animationFrame = null;
63
+ let pinchCleanup = null;
64
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
+ const eventHandlers = new Map();
66
+ const ready = $derived(!!loader.image && !loader.loading);
67
+ const crop = $derived(engine.coordinates);
68
+ const dirty = $derived(true);
69
+ function emit(event, ...args) {
70
+ const handlers = eventHandlers.get(event);
71
+ if (handlers) {
72
+ handlers.forEach((fn) => fn(...args));
73
+ }
74
+ }
75
+ function on(event, handler) {
76
+ if (!eventHandlers.has(event)) {
77
+ eventHandlers.set(event, new Set());
78
+ }
79
+ const set = eventHandlers.get(event);
80
+ set.add(handler);
81
+ return () => set.delete(handler);
82
+ }
83
+ async function loadImage(src) {
84
+ await loader.load(src);
85
+ if (loader.image) {
86
+ engine.setImageSize({
87
+ width: loader.image.naturalWidth,
88
+ height: loader.image.naturalHeight,
89
+ });
90
+ if (containerEl) {
91
+ const w = Math.max(1, containerEl.clientWidth);
92
+ const h = Math.max(1, containerEl.clientHeight);
93
+ engine.setContainerSize({ width: w, height: h });
94
+ }
95
+ // Save initial state into history
96
+ pushHistory("Initial");
97
+ emit("image:load", loader.image);
98
+ emit("ready");
99
+ }
100
+ if (loader.error) {
101
+ emit("image:error", loader.error);
102
+ }
103
+ }
104
+ function bindContainer(container) {
105
+ // Clean up previous pinch listener
106
+ pinchCleanup?.();
107
+ pinchCleanup = null;
108
+ if (resizeObserver && containerEl) {
109
+ resizeObserver.unobserve(containerEl);
110
+ }
111
+ containerEl = container;
112
+ if (container) {
113
+ const w = Math.max(1, container.clientWidth);
114
+ const h = Math.max(1, container.clientHeight);
115
+ engine.setContainerSize({ width: w, height: h });
116
+ resizeObserver = new ResizeObserver((entries) => {
117
+ for (const entry of entries) {
118
+ const { width, height } = entry.contentRect;
119
+ engine.setContainerSize({
120
+ width: Math.max(1, width),
121
+ height: Math.max(1, height),
122
+ });
123
+ }
124
+ });
125
+ resizeObserver.observe(container);
126
+ // Attach pinch zoom via capture-phase listeners so they fire even when
127
+ // child elements have captured pointers.
128
+ const pinch = createPinchHandler({
129
+ onPinch: (scale) => {
130
+ if (!options.readOnly)
131
+ transform.zoomBy(scale - 1);
132
+ },
133
+ });
134
+ container.addEventListener("pointerdown", pinch.onpointerdown, true);
135
+ container.addEventListener("pointermove", pinch.onpointermove, true);
136
+ container.addEventListener("pointerup", pinch.onpointerup, true);
137
+ pinchCleanup = () => {
138
+ container.removeEventListener("pointerdown", pinch.onpointerdown, true);
139
+ container.removeEventListener("pointermove", pinch.onpointermove, true);
140
+ container.removeEventListener("pointerup", pinch.onpointerup, true);
141
+ };
142
+ }
143
+ }
144
+ function bindCanvas(canvas) {
145
+ canvasEl = canvas;
146
+ render();
147
+ }
148
+ function render() {
149
+ if (!canvasEl || !loader.image || !containerEl)
150
+ return;
151
+ const dpr = window.devicePixelRatio ?? 1;
152
+ const w = containerEl.clientWidth;
153
+ const h = containerEl.clientHeight;
154
+ canvasEl.width = w * dpr;
155
+ canvasEl.height = h * dpr;
156
+ canvasEl.style.width = `${w}px`;
157
+ canvasEl.style.height = `${h}px`;
158
+ const ctx = canvasEl.getContext("2d");
159
+ if (!ctx)
160
+ return;
161
+ // Pan/zoom/rotate/flip are handled by CSS transform on the DOM layer,
162
+ // so we only draw the image at its natural imageRect (fitted to container).
163
+ const imgRect = engine.imageRect;
164
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
165
+ ctx.save();
166
+ ctx.scale(dpr, dpr);
167
+ ctx.drawImage(loader.image.element, imgRect.x, imgRect.y, imgRect.width, imgRect.height);
168
+ ctx.restore();
169
+ // Apply filters for live preview (same logic as export)
170
+ if (hasActiveFilters()) {
171
+ const imageData = ctx.getImageData(0, 0, canvasEl.width, canvasEl.height);
172
+ applyColorMatrix(imageData, buildFilterMatrix());
173
+ if (filters.gamma !== 1)
174
+ applyGamma(imageData, filters.gamma);
175
+ ctx.putImageData(imageData, 0, 0);
176
+ }
177
+ }
178
+ // Re-render whenever the image, image rect, or filters change.
179
+ // Transforms (pan/zoom/rotate/flip) are handled by CSS on the DOM layer — no canvas re-render needed.
180
+ $effect(() => {
181
+ // Access reactive state to establish dependencies
182
+ const _ready = ready;
183
+ const _imgRect = engine.imageRect;
184
+ const _filters = filters; // re-render canvas when filters change for live preview
185
+ if (!_ready || !canvasEl || !containerEl)
186
+ return;
187
+ if (animationFrame)
188
+ cancelAnimationFrame(animationFrame);
189
+ animationFrame = requestAnimationFrame(() => {
190
+ render();
191
+ animationFrame = null;
192
+ });
193
+ return () => {
194
+ if (animationFrame)
195
+ cancelAnimationFrame(animationFrame);
196
+ };
197
+ });
198
+ // ─── Filter helpers ──────────────────────────────────────
199
+ function buildFilterMatrix() {
200
+ const IDENTITY = [
201
+ 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0,
202
+ ];
203
+ let matrix = IDENTITY;
204
+ // Apply selected preset first
205
+ if (filters.preset && filters.preset !== "none") {
206
+ const preset = filterPresets.find((p) => p.name === filters.preset);
207
+ if (preset)
208
+ matrix = multiplyMatrices(matrix, preset.matrix);
209
+ }
210
+ // Layer finetune adjustments on top
211
+ if (filters.brightness !== 0)
212
+ matrix = multiplyMatrices(matrix, brightnessMatrix(filters.brightness));
213
+ if (filters.contrast !== 0)
214
+ matrix = multiplyMatrices(matrix, contrastMatrix(filters.contrast));
215
+ if (filters.saturation !== 0)
216
+ matrix = multiplyMatrices(matrix, saturationMatrix(filters.saturation));
217
+ if (filters.exposure !== 0)
218
+ matrix = multiplyMatrices(matrix, exposureMatrix(filters.exposure));
219
+ if (filters.temperature !== 0)
220
+ matrix = multiplyMatrices(matrix, temperatureMatrix(filters.temperature));
221
+ if (filters.clarity !== 0)
222
+ matrix = multiplyMatrices(matrix, clarityMatrix(filters.clarity));
223
+ return matrix;
224
+ }
225
+ function hasActiveFilters() {
226
+ return ((filters.preset !== null && filters.preset !== "none") ||
227
+ filters.brightness !== 0 ||
228
+ filters.contrast !== 0 ||
229
+ filters.saturation !== 0 ||
230
+ filters.exposure !== 0 ||
231
+ filters.temperature !== 0 ||
232
+ filters.clarity !== 0 ||
233
+ filters.gamma !== 1);
234
+ }
235
+ // ─── Export ──────────────────────────────────────────────
236
+ async function doExport(opts) {
237
+ if (!loader.image)
238
+ throw new Error("No image loaded");
239
+ emit("export:start");
240
+ try {
241
+ const merged = {
242
+ ...options.exportDefaults,
243
+ ...opts,
244
+ postProcess: async (drawCtx, canvas) => {
245
+ // 1. Apply colour filters
246
+ if (hasActiveFilters()) {
247
+ const imageData = drawCtx.getImageData(0, 0, canvas.width, canvas.height);
248
+ applyColorMatrix(imageData, buildFilterMatrix());
249
+ if (filters.gamma !== 1)
250
+ applyGamma(imageData, filters.gamma);
251
+ drawCtx.putImageData(imageData, 0, 0);
252
+ }
253
+ // 2. Run custom postProcess passed by caller
254
+ await opts?.postProcess?.(drawCtx, canvas);
255
+ // 3. Run plugin-registered post-processors (frame, watermark, etc.)
256
+ for (const pp of postProcessors) {
257
+ await pp(drawCtx, canvas);
258
+ }
259
+ },
260
+ };
261
+ const result = await exportImage(loader.image, engine.coordinates, transform.transforms, merged);
262
+ emit("export:complete", result);
263
+ return result;
264
+ }
265
+ catch (err) {
266
+ emit("export:error", err instanceof Error ? err : new Error(String(err)));
267
+ throw err;
268
+ }
269
+ }
270
+ function getResult() {
271
+ if (!loader.image)
272
+ throw new Error("No image loaded");
273
+ return {
274
+ coordinates: engine.coordinates,
275
+ transforms: transform.transforms,
276
+ originalSize: {
277
+ width: loader.image.naturalWidth,
278
+ height: loader.image.naturalHeight,
279
+ },
280
+ };
281
+ }
282
+ async function download(filename, opts) {
283
+ const result = await doExport(opts);
284
+ if (!result.blob)
285
+ return;
286
+ const a = document.createElement("a");
287
+ a.href = URL.createObjectURL(result.blob);
288
+ a.download = filename ?? "export.png";
289
+ a.click();
290
+ URL.revokeObjectURL(a.href);
291
+ }
292
+ // ─── History ─────────────────────────────────────────────
293
+ function pushHistory(label) {
294
+ const cropVal = engine.crop;
295
+ const transformsVal = transform.transforms;
296
+ // Explicitly extract plain values — Svelte $state proxies can't be JSON-serialized via spread alone
297
+ history.push(label, {
298
+ crop: { x: cropVal.x, y: cropVal.y, width: cropVal.width, height: cropVal.height },
299
+ transforms: {
300
+ rotation: transformsVal.rotation,
301
+ flipX: transformsVal.flipX,
302
+ flipY: transformsVal.flipY,
303
+ zoom: transformsVal.zoom,
304
+ pan: { x: transformsVal.pan.x, y: transformsVal.pan.y },
305
+ },
306
+ filters: { ...filters },
307
+ });
308
+ }
309
+ function restoreState(state) {
310
+ engine.setCrop(state.crop);
311
+ transform.setRotation(state.transforms.rotation);
312
+ // Conditionally flip only if state differs from current
313
+ if (state.transforms.flipX !== transform.transforms.flipX)
314
+ transform.flipX();
315
+ if (state.transforms.flipY !== transform.transforms.flipY)
316
+ transform.flipY();
317
+ transform.setZoom(state.transforms.zoom);
318
+ transform.setPan(state.transforms.pan);
319
+ filters = { ...state.filters };
320
+ }
321
+ // ─── Keyboard shortcuts ───────────────────────────────────
322
+ const keyboardHandler = createKeyboardHandler([
323
+ {
324
+ key: "r",
325
+ action: () => {
326
+ if (options.readOnly)
327
+ return;
328
+ pushHistory("Rotate right");
329
+ transform.rotate(90);
330
+ },
331
+ },
332
+ {
333
+ key: "r",
334
+ shift: true,
335
+ action: () => {
336
+ if (options.readOnly)
337
+ return;
338
+ pushHistory("Rotate left");
339
+ transform.rotate(-90);
340
+ },
341
+ },
342
+ {
343
+ key: "h",
344
+ action: () => {
345
+ if (options.readOnly)
346
+ return;
347
+ pushHistory("Flip H");
348
+ transform.flipX();
349
+ },
350
+ },
351
+ {
352
+ key: "v",
353
+ action: () => {
354
+ if (options.readOnly)
355
+ return;
356
+ pushHistory("Flip V");
357
+ transform.flipY();
358
+ },
359
+ },
360
+ {
361
+ key: "z",
362
+ ctrl: true,
363
+ action: () => {
364
+ const s = history.undo();
365
+ if (s)
366
+ restoreState(s);
367
+ },
368
+ },
369
+ {
370
+ key: "z",
371
+ ctrl: true,
372
+ shift: true,
373
+ action: () => {
374
+ const s = history.redo();
375
+ if (s)
376
+ restoreState(s);
377
+ },
378
+ },
379
+ {
380
+ key: "=",
381
+ action: () => {
382
+ if (!options.readOnly)
383
+ transform.zoomBy(0.1);
384
+ },
385
+ },
386
+ {
387
+ key: "+",
388
+ action: () => {
389
+ if (!options.readOnly)
390
+ transform.zoomBy(0.1);
391
+ },
392
+ },
393
+ {
394
+ key: "-",
395
+ action: () => {
396
+ if (!options.readOnly)
397
+ transform.zoomBy(-0.1);
398
+ },
399
+ },
400
+ { key: "0", action: () => engine.fitToImage() },
401
+ {
402
+ key: "Escape",
403
+ action: () => {
404
+ engine.fitToImage();
405
+ transform.reset();
406
+ },
407
+ },
408
+ { key: "g", action: () => { } }, // grid toggle — handled in ImageEditor component
409
+ ]);
410
+ function handleKeyboard(event) {
411
+ keyboardHandler(event);
412
+ }
413
+ const wheelHandler = createWheelHandler({
414
+ onZoom: (factor) => {
415
+ if (!options.readOnly)
416
+ transform.zoomBy(factor - 1);
417
+ },
418
+ });
419
+ function handleWheel(event) {
420
+ wheelHandler.onwheel(event);
421
+ }
422
+ // ─── Mutations ────────────────────────────────────────────
423
+ function applyFilter(name) {
424
+ pushHistory(`Filter: ${name}`);
425
+ filters = { ...filters, preset: name };
426
+ emit("filter:change", filters);
427
+ }
428
+ function setFinetune(key, value) {
429
+ if (key === "preset")
430
+ return;
431
+ filters = { ...filters, [key]: value };
432
+ emit("filter:change", filters);
433
+ }
434
+ function resetFilters() {
435
+ pushHistory("Reset filters");
436
+ filters = { ...DEFAULT_FILTERS };
437
+ emit("filter:change", filters);
438
+ }
439
+ function setFrame(settings) {
440
+ frame = { ...frame, ...settings };
441
+ }
442
+ function setWatermark(settings) {
443
+ watermark = { ...watermark, ...settings };
444
+ }
445
+ // ─── Editor object ────────────────────────────────────────
446
+ const editor = {
447
+ get image() {
448
+ return loader.image;
449
+ },
450
+ get loading() {
451
+ return loader.loading;
452
+ },
453
+ get error() {
454
+ return loader.error;
455
+ },
456
+ get ready() {
457
+ return ready;
458
+ },
459
+ get crop() {
460
+ return crop;
461
+ },
462
+ get transforms() {
463
+ return transform.transforms;
464
+ },
465
+ get filters() {
466
+ return filters;
467
+ },
468
+ get dirty() {
469
+ return dirty;
470
+ },
471
+ get interacting() {
472
+ return interacting;
473
+ },
474
+ get canUndo() {
475
+ return history.canUndo;
476
+ },
477
+ get canRedo() {
478
+ return history.canRedo;
479
+ },
480
+ get history() {
481
+ return history.entries;
482
+ },
483
+ get actions() {
484
+ return actions;
485
+ },
486
+ get activeTab() {
487
+ return activeTab;
488
+ },
489
+ get frameSettings() {
490
+ return frame;
491
+ },
492
+ get watermarkSettings() {
493
+ return watermark;
494
+ },
495
+ get filterPresets() {
496
+ return filterPresets;
497
+ },
498
+ setActiveTab: (tab) => {
499
+ activeTab = tab;
500
+ },
501
+ setFrame,
502
+ setWatermark,
503
+ handleKeyboard,
504
+ handleWheel,
505
+ get imageRect() {
506
+ return engine.imageRect;
507
+ },
508
+ moveBy: engine.moveBy,
509
+ resizeBy: engine.resizeBy,
510
+ setInteracting: engine.setInteracting,
511
+ loadImage,
512
+ setCrop: engine.setCrop,
513
+ setAspectRatio: engine.setAspectRatio,
514
+ fitToImage: engine.fitToImage,
515
+ rotate: (degrees) => {
516
+ pushHistory(`Rotate ${degrees}°`);
517
+ transform.rotate(degrees);
518
+ },
519
+ setRotation: (degrees) => {
520
+ pushHistory(`Set rotation ${degrees}°`);
521
+ transform.setRotation(degrees);
522
+ },
523
+ flipX: () => {
524
+ pushHistory("Flip horizontal");
525
+ transform.flipX();
526
+ },
527
+ flipY: () => {
528
+ pushHistory("Flip vertical");
529
+ transform.flipY();
530
+ },
531
+ setZoom: transform.setZoom,
532
+ zoomBy: transform.zoomBy,
533
+ center: transform.center,
534
+ reset: () => {
535
+ pushHistory("Reset");
536
+ transform.reset();
537
+ engine.fitToImage();
538
+ },
539
+ applyFilter,
540
+ setFinetune,
541
+ resetFilters,
542
+ undo: () => {
543
+ const s = history.undo();
544
+ if (s)
545
+ restoreState(s);
546
+ },
547
+ redo: () => {
548
+ const s = history.redo();
549
+ if (s)
550
+ restoreState(s);
551
+ },
552
+ export: doExport,
553
+ getResult,
554
+ download,
555
+ bindCanvas,
556
+ bindContainer,
557
+ on,
558
+ destroy: () => {
559
+ pinchCleanup?.();
560
+ if (resizeObserver && containerEl)
561
+ resizeObserver.unobserve(containerEl);
562
+ loader.destroy();
563
+ emit("destroy");
564
+ },
565
+ };
566
+ // ─── Plugin setup ─────────────────────────────────────────
567
+ if (options.plugins) {
568
+ for (const plugin of options.plugins) {
569
+ const ctx = {
570
+ editor,
571
+ registerAction: (action) => {
572
+ actions = [...actions, action];
573
+ },
574
+ registerShortcut: (shortcut) => {
575
+ // Shortcuts stored per-plugin — currently handled via handleKeyboard
576
+ },
577
+ showPanel: (name) => {
578
+ activeTab = name;
579
+ },
580
+ registerPostProcessor: (fn) => {
581
+ postProcessors.push(fn);
582
+ },
583
+ registerFilterPresets: (presets) => {
584
+ filterPresets = [...filterPresets, ...presets];
585
+ },
586
+ on,
587
+ };
588
+ plugin.setup(ctx);
589
+ }
590
+ }
591
+ $effect(() => {
592
+ if (options.src)
593
+ loadImage(options.src);
594
+ });
595
+ return editor;
596
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * svelte-chop-chop — Standalone transform composable
3
+ * Wraps TransformEngine for use in headless mode.
4
+ */
5
+ import type { Point, TransformState } from '../core/types.js';
6
+ export interface TransformOptions {
7
+ initialTransforms?: Partial<TransformState>;
8
+ minZoom?: number;
9
+ maxZoom?: number;
10
+ onChange?: (transforms: TransformState) => void;
11
+ }
12
+ export interface TransformReturn {
13
+ get transforms(): TransformState;
14
+ rotate: (degrees: number) => void;
15
+ setRotation: (degrees: number) => void;
16
+ flipX: () => void;
17
+ flipY: () => void;
18
+ setZoom: (zoom: number) => void;
19
+ zoomBy: (delta: number) => void;
20
+ setPan: (pan: Point) => void;
21
+ panBy: (delta: Point) => void;
22
+ center: () => void;
23
+ reset: () => void;
24
+ }
25
+ /**
26
+ * Create standalone transform composable.
27
+ */
28
+ export declare function createTransform(options?: TransformOptions): TransformReturn;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * svelte-chop-chop — Standalone transform composable
3
+ * Wraps TransformEngine for use in headless mode.
4
+ */
5
+ import { createTransformEngine } from '../core/transform-engine.svelte.js';
6
+ /**
7
+ * Create standalone transform composable.
8
+ */
9
+ export function createTransform(options = {}) {
10
+ const engine = createTransformEngine(options);
11
+ return {
12
+ get transforms() {
13
+ return engine.transforms;
14
+ },
15
+ rotate: engine.rotate,
16
+ setRotation: engine.setRotation,
17
+ flipX: engine.flipX,
18
+ flipY: engine.flipY,
19
+ setZoom: engine.setZoom,
20
+ zoomBy: engine.zoomBy,
21
+ setPan: engine.setPan,
22
+ panBy: engine.panBy,
23
+ center: engine.center,
24
+ reset: engine.reset,
25
+ };
26
+ }
File without changes
@@ -0,0 +1,39 @@
1
+ /**
2
+ * svelte-chop-chop — Color matrix utilities for filters
3
+ * 4x5 matrix application and preset matrices.
4
+ */
5
+ /**
6
+ * Apply a 4x5 color matrix to ImageData.
7
+ */
8
+ export declare function applyColorMatrix(imageData: ImageData, matrix: number[]): void;
9
+ /**
10
+ * Combine multiple 4x5 matrices by multiplication.
11
+ */
12
+ export declare function multiplyMatrices(a: number[], b: number[]): number[];
13
+ /** Brightness matrix (value: -100 to 100) */
14
+ export declare function brightnessMatrix(value: number): number[];
15
+ /** Contrast matrix (value: -100 to 100) */
16
+ export declare function contrastMatrix(value: number): number[];
17
+ /** Saturation matrix (value: -100 to 100) */
18
+ export declare function saturationMatrix(value: number): number[];
19
+ /**
20
+ * Exposure matrix (value: -100 to 100).
21
+ * Simulates photographic exposure via a power-of-2 gain on all channels.
22
+ */
23
+ export declare function exposureMatrix(value: number): number[];
24
+ /**
25
+ * Temperature matrix (value: -100 cool to 100 warm).
26
+ * Shifts red/blue channels in opposite directions.
27
+ */
28
+ export declare function temperatureMatrix(value: number): number[];
29
+ /**
30
+ * Clarity matrix (value: 0 to 100).
31
+ * Increases midtone contrast to add presence/structure.
32
+ */
33
+ export declare function clarityMatrix(value: number): number[];
34
+ /**
35
+ * Apply a non-linear gamma correction directly to ImageData.
36
+ * @param imageData - The ImageData to mutate in-place.
37
+ * @param gamma - Gamma value (1 = no change, <1 = lighter, >1 = darker).
38
+ */
39
+ export declare function applyGamma(imageData: ImageData, gamma: number): void;