@peteai/presentation-editor 0.0.7 → 0.0.8

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 (40) hide show
  1. package/dist/components/editor/active-layers.svelte +22 -7
  2. package/dist/components/editor/active-layers.svelte.d.ts +6 -1
  3. package/dist/components/editor/editor.svelte +15 -17
  4. package/dist/components/editor/editor.svelte.js +10 -8
  5. package/dist/components/editor/header.svelte +24 -20
  6. package/dist/components/editor/hotkeys.svelte +7 -0
  7. package/dist/components/editor/layers/buttons/opacity-button/opacity-button.svelte +1 -6
  8. package/dist/components/editor/layers/controls/group-resize-control/group-resize-control.svelte +11 -11
  9. package/dist/components/editor/layers/controls/rotate-control/rotate-control.svelte +11 -5
  10. package/dist/components/editor/layers/controls/rotate-control/rotate-control.svelte.d.ts +4 -1
  11. package/dist/components/editor/layers/controls/side-resize-control/side-resize-control.svelte +13 -13
  12. package/dist/components/editor/layers/index.d.ts +2 -2
  13. package/dist/components/editor/layers/index.js +2 -2
  14. package/dist/components/editor/layers/types/background/background-content-image.svelte +15 -20
  15. package/dist/components/editor/layers/types/background/background-layer.svelte +2 -2
  16. package/dist/components/editor/layers/types/image/controls/image-rotate-control/image-rotate-control.svelte +120 -0
  17. package/dist/components/editor/layers/types/image/controls/image-rotate-control/image-rotate-control.svelte.d.ts +8 -0
  18. package/dist/components/editor/layers/types/image/controls/image-rotate-control/index.d.ts +2 -0
  19. package/dist/components/editor/layers/types/image/controls/image-rotate-control/index.js +4 -0
  20. package/dist/components/editor/layers/types/image/controls/image-scale-control/image-scale-control.svelte +154 -0
  21. package/dist/components/editor/layers/types/image/controls/image-scale-control/image-scale-control.svelte.d.ts +91 -0
  22. package/dist/components/editor/layers/types/image/controls/image-scale-control/index.d.ts +2 -0
  23. package/dist/components/editor/layers/types/image/controls/image-scale-control/index.js +4 -0
  24. package/dist/components/editor/layers/types/image/image-layer-content.svelte +3 -3
  25. package/dist/components/editor/layers/types/image/image-layer-crop.svelte +182 -0
  26. package/dist/components/editor/layers/types/image/image-layer-crop.svelte.d.ts +10 -0
  27. package/dist/components/editor/layers/types/image/image-layer.svelte +16 -0
  28. package/dist/components/editor/layers/types/image/index.d.ts +2 -1
  29. package/dist/components/editor/layers/types/image/index.js +2 -1
  30. package/dist/components/editor/layers/types/text/extensions/list-item/list-item.js +0 -2
  31. package/dist/components/editor/layers/utils.d.ts +24 -9
  32. package/dist/components/editor/layers/utils.js +107 -54
  33. package/dist/components/editor/page-editor.svelte +6 -2
  34. package/dist/components/editor/sidebar/image-crop-sidebar.svelte +112 -0
  35. package/dist/components/editor/sidebar/image-crop-sidebar.svelte.d.ts +7 -0
  36. package/dist/components/editor/sidebar/position-sidebar.svelte +0 -2
  37. package/dist/components/editor/sidebar/sidebar.svelte +6 -3
  38. package/dist/components/editor/types.d.ts +2 -1
  39. package/dist/components/editor/utils.js +1 -0
  40. package/package.json +1 -1
@@ -10,12 +10,19 @@
10
10
  import { GroupResizeControl } from './layers/controls/group-resize-control/index.js';
11
11
  import { CornerScaleControl } from './layers/controls/corner-scale-control/index.js';
12
12
  import { RotateControl } from './layers/controls/rotate-control/index.js';
13
- import { TextLayerEdit } from './layers/index.js';
14
- import type { GroupLayer, Layer, TextLayer } from './types.js';
13
+ import { ImageLayerCrop, TextLayerEdit } from './layers/index.js';
14
+ import type { GroupLayer, ImageLayer, Layer, TextLayer } from './types.js';
15
+
16
+ interface Props {
17
+ viewportRef: HTMLDivElement;
18
+ wrapperRef: HTMLDivElement;
19
+ pageRef: HTMLDivElement;
20
+ }
21
+ const { viewportRef, wrapperRef, pageRef }: Props = $props();
15
22
 
16
23
  const editor = getEditorContext();
17
24
 
18
- let textLayerEditing: { layer: TextLayer; groupLayer?: GroupLayer } | undefined = $derived.by(
25
+ let textLayerEditData: { layer: TextLayer; groupLayer?: GroupLayer } | undefined = $derived.by(
19
26
  () => {
20
27
  if (editor.activeAction?.type === 'edit') {
21
28
  if (editor.activeSelection.type === 'root-layers') {
@@ -37,6 +44,12 @@
37
44
  },
38
45
  );
39
46
 
47
+ let imageLayerCropData: { layer: ImageLayer } | undefined = $derived.by(() => {
48
+ if (editor.imageCropLayer) {
49
+ return { layer: editor.imageCropLayer };
50
+ }
51
+ });
52
+
40
53
  const group = $derived.by(() => {
41
54
  if (editor.activeSelection.type === 'background') {
42
55
  return {
@@ -119,11 +132,13 @@
119
132
  };
120
133
  </script>
121
134
 
122
- {#if textLayerEditing}
123
- <TextLayerEdit {...textLayerEditing} />
135
+ {#if textLayerEditData}
136
+ <TextLayerEdit {...textLayerEditData} />
124
137
  {/if}
125
138
 
126
- {#if group}
139
+ {#if imageLayerCropData}
140
+ <ImageLayerCrop {...imageLayerCropData} {viewportRef} {wrapperRef} {pageRef} />
141
+ {:else if group}
127
142
  <div
128
143
  class="pointer-events-none absolute left-0 top-0 h-full w-full"
129
144
  style:width={`${group.bbox.width * group.bbox.scale * editor.zoom + group.padding * 2}px`}
@@ -179,7 +194,7 @@
179
194
  <CornerScaleControl origin="bottom-left" />
180
195
  <CornerScaleControl origin="top-right" />
181
196
  <CornerScaleControl origin="top-left" />
182
- <RotateControl />
197
+ <RotateControl {pageRef} />
183
198
  </div>
184
199
  </div>
185
200
  {/if}
@@ -1,3 +1,8 @@
1
- declare const ActiveLayers: import("svelte").Component<Record<string, never>, {}, "">;
1
+ interface Props {
2
+ viewportRef: HTMLDivElement;
3
+ wrapperRef: HTMLDivElement;
4
+ pageRef: HTMLDivElement;
5
+ }
6
+ declare const ActiveLayers: import("svelte").Component<Props, {}, "">;
2
7
  type ActiveLayers = ReturnType<typeof ActiveLayers>;
3
8
  export default ActiveLayers;
@@ -118,24 +118,8 @@
118
118
  }
119
119
  };
120
120
 
121
- const onResize = () => {
122
- tick().then(() => {
123
- if (editor.zoomSnapping === 'fit') {
124
- fitToScreen();
125
- } else if (editor.zoomSnapping === 'fill') {
126
- fillScreen();
127
- }
128
- });
129
- };
130
-
131
- $effect(() => {
132
- editor.activeSidebarPopup;
133
- editor.activeSidebarTab;
134
- onResize();
135
- });
136
-
137
121
  const onDragOver = (e: DragEvent) => {
138
- // console.log('dragover windown', e);
122
+ // console.log('dragover editor', e);
139
123
  e.preventDefault();
140
124
  if (e.dataTransfer?.items.length) {
141
125
  editor.fileDragged = true;
@@ -201,10 +185,24 @@
201
185
  }
202
186
  };
203
187
 
188
+ const onResize = () => {
189
+ if (editor.zoomSnapping === 'fit') {
190
+ tick().then(fitToScreen);
191
+ } else if (editor.zoomSnapping === 'fill') {
192
+ tick().then(fillScreen);
193
+ }
194
+ };
195
+
204
196
  onMount(() => {
205
197
  tick().then(() => {
206
198
  fitToScreen();
207
199
  });
200
+
201
+ const observer = new ResizeObserver(onResize);
202
+
203
+ if (viewportRef) observer.observe(viewportRef);
204
+
205
+ return () => observer.disconnect();
208
206
  });
209
207
 
210
208
  onDestroy(() => {
@@ -42,6 +42,7 @@ export class Editor {
42
42
  onUpdate;
43
43
  activeSidebarTab = $state(null);
44
44
  activeSidebarPopup = $state(null);
45
+ imageCropLayer = $state(null);
45
46
  toggleActiveSidebarTab(tab) {
46
47
  this.activeSidebarTab = !this.activeSidebarPopup && this.activeSidebarTab === tab ? null : tab;
47
48
  if (this.activeSidebarTab) {
@@ -360,6 +361,7 @@ export class Editor {
360
361
  rotate: 0,
361
362
  opacity: 1,
362
363
  scale: 1,
364
+ imageRotate: 0,
363
365
  offsetX: 0,
364
366
  offsetY: 0,
365
367
  cornerRadius: 0,
@@ -458,7 +460,7 @@ export class Editor {
458
460
  tryToDuplicate();
459
461
  }
460
462
  getAbsoluteGroupLayers(groupLayer, children) {
461
- const theta = degToRad(groupLayer.rotate);
463
+ const rad = degToRad(groupLayer.rotate);
462
464
  const groupCenter = {
463
465
  x: groupLayer.x + (groupLayer.width * groupLayer.scale) / 2,
464
466
  y: groupLayer.y + (groupLayer.height * groupLayer.scale) / 2,
@@ -468,7 +470,7 @@ export class Editor {
468
470
  x: groupLayer.x + (l.x + (l.width * l.scale) / 2) * groupLayer.scale,
469
471
  y: groupLayer.y + (l.y + (l.height * l.scale) / 2) * groupLayer.scale,
470
472
  };
471
- const center = rotatePoint(relativeCenter, groupCenter, theta);
473
+ const center = rotatePoint(rad, relativeCenter, groupCenter);
472
474
  return {
473
475
  ...l,
474
476
  x: center.x - (l.width * l.scale * groupLayer.scale) / 2,
@@ -773,14 +775,14 @@ export class Editor {
773
775
  return false;
774
776
  if (layer.type !== 'image')
775
777
  return false;
776
- const { width, height, rotate, image, opacity, flipX, flipY, offsetX, offsetY } = layer;
777
- const cover = calculateImageCover({ width, height, rotate }, { width: this.width, height: this.height });
778
+ const { width, height, rotate, image, opacity, flipX, flipY, imageRotate, offsetX, offsetY } = $state.snapshot(layer);
779
+ const cover = calculateImageCover({ width, height, rotate, imageRotate }, { width: this.width, height: this.height });
778
780
  const backgroundImage = {
779
- image: { ...image },
781
+ image,
780
782
  scale: cover.scale,
781
- offsetX: (offsetX || 0) + cover.offsetX,
782
- offsetY: (offsetY || 0) + cover.offsetY,
783
- rotate,
783
+ offsetX: offsetX + cover.offsetX,
784
+ offsetY: offsetY + cover.offsetY,
785
+ imageRotate: rotate + imageRotate,
784
786
  opacity,
785
787
  flipX,
786
788
  flipY,
@@ -12,6 +12,8 @@
12
12
  }
13
13
 
14
14
  const { editor }: Props = $props();
15
+
16
+ let disabled = $derived(!!editor.imageCropLayer);
15
17
  </script>
16
18
 
17
19
  <div class="bg-background h-12 w-full border-b border-gray-200 px-2">
@@ -20,7 +22,7 @@
20
22
  <Button
21
23
  variant="ghost"
22
24
  size="icon-xs"
23
- disabled={editor.historyIndex === -1}
25
+ disabled={editor.historyIndex === -1 || disabled}
24
26
  onclick={() => editor.historyUndo()}
25
27
  >
26
28
  <UndoIcon class="h-6 w-6" />
@@ -28,31 +30,33 @@
28
30
  <Button
29
31
  variant="ghost"
30
32
  size="icon-xs"
31
- disabled={editor.historyIndex === editor.historyActions.length - 1}
33
+ disabled={editor.historyIndex === editor.historyActions.length - 1 || disabled}
32
34
  onclick={() => editor.historyRedo()}
33
35
  >
34
36
  <RedoIcon class="h-6 w-6" />
35
37
  </Button>
36
- <div class="grid grid-flow-col items-center gap-2 overflow-x-auto">
37
- {#if !editor.activePage.locked}
38
- {#if editor.activeSelection.type === 'background'}
39
- {#if !editor.activePage.backgroundLocked}
40
- <BackgroundLayerButtons />
38
+ {#if !disabled}
39
+ <div class="grid grid-flow-col items-center gap-2 overflow-x-auto">
40
+ {#if !editor.activePage.locked}
41
+ {#if editor.activeSelection.type === 'background'}
42
+ {#if !editor.activePage.backgroundLocked}
43
+ <BackgroundLayerButtons />
44
+ {/if}
45
+ {:else if editor.activeLayers.length}
46
+ <ActiveLayersButtons />
41
47
  {/if}
42
- {:else if editor.activeLayers.length}
43
- <ActiveLayersButtons />
44
48
  {/if}
45
- {/if}
46
- </div>
47
- <Separator orientation="vertical" />
48
- <Button
49
- variant="ghost"
50
- size="xs"
51
- active={editor.activeSidebarPopup === 'position'}
52
- onclick={() => editor.toggleActiveSidebarPopup('position')}
53
- >
54
- Position
55
- </Button>
49
+ </div>
50
+ <Separator orientation="vertical" />
51
+ <Button
52
+ variant="ghost"
53
+ size="xs"
54
+ active={editor.activeSidebarPopup === 'position'}
55
+ onclick={() => editor.toggleActiveSidebarPopup('position')}
56
+ >
57
+ Position
58
+ </Button>
59
+ {/if}
56
60
  </div>
57
61
  </div>
58
62
  </div>
@@ -12,6 +12,13 @@
12
12
  )
13
13
  return;
14
14
 
15
+ if (editor.imageCropLayer) {
16
+ if (e.code === 'Escape') {
17
+ editor.imageCropLayer = null;
18
+ }
19
+ return;
20
+ }
21
+
15
22
  if (e.code === 'Escape') {
16
23
  if (editor.activeAction) {
17
24
  editor.activeAction = null;
@@ -86,11 +86,6 @@
86
86
  initial = null;
87
87
  }
88
88
  };
89
-
90
- const onChange = (e: Event) => {
91
- const target = e.target as HTMLInputElement;
92
- setTransparency(target.value);
93
- };
94
89
  </script>
95
90
 
96
91
  <Popover.Root>
@@ -121,7 +116,7 @@
121
116
  inputmode="decimal"
122
117
  placeholder="--"
123
118
  value={trasparency}
124
- onchange={onChange}
119
+ onchange={(e) => setTransparency(e.currentTarget.value)}
125
120
  />
126
121
  </div>
127
122
  </div>
@@ -236,13 +236,13 @@
236
236
  };
237
237
  const groupNewCenter = { x: groupNewX + groupNewWidth / 2, y: groupNewY + groupNewHeight / 2 };
238
238
  // calculate rotated points relative to new group center
239
- const groupCenterRotated = rotatePoint(groupCenter, groupNewCenter, -groupAngle);
239
+ const groupCenterRotated = rotatePoint(-groupAngle, groupCenter, groupNewCenter);
240
240
 
241
241
  const buildLayerDiffs = async ({ layer, state }: LayerStateWithoutGroup) => {
242
242
  // do calculation with absolute values, but update state with relative values
243
243
  const { width, height, x, y, rotate, scale } = layer;
244
244
  const center = { x: x + (width * scale) / 2, y: y + (height * scale) / 2 };
245
- const centerRotated = rotatePoint(center, groupNewCenter, -groupAngle);
245
+ const centerRotated = rotatePoint(-groupAngle, center, groupNewCenter);
246
246
  const newCenterRotated = {
247
247
  x: groupNewCenter.x + (centerRotated.x - groupCenterRotated.x) * widthScale,
248
248
  y: groupNewCenter.y + (centerRotated.y - groupCenterRotated.y) * heightScale,
@@ -300,7 +300,7 @@
300
300
  : Math.min(Math.max(newCenterRotated.y, minY), maxY);
301
301
  }
302
302
 
303
- let newCenter = rotatePoint(newCenterRotated, groupNewCenter, degToRad(bbox.rotate));
303
+ let newCenter = rotatePoint(degToRad(bbox.rotate), newCenterRotated, groupNewCenter);
304
304
 
305
305
  if (layer.type === 'image') {
306
306
  const { offsetX, offsetY, image } = layer;
@@ -309,24 +309,24 @@
309
309
  let newOffsetY = offsetY;
310
310
 
311
311
  if (newSize.scaled === 'width') {
312
- const maxOffsetDiff = Math.min(offsetX, image.width - offsetX - width);
312
+ const maxOffsetDiff = Math.min(offsetX, image.width + offsetX - width);
313
313
  const maxWidth = (width + Math.max(maxOffsetDiff, 0) * 2) * scale;
314
314
  if (newWidth > maxWidth) {
315
315
  newScale *= newWidth / maxWidth;
316
- newOffsetX = offsetX - maxOffsetDiff;
317
- newOffsetY += ((layer.image.height / 2 - offsetY) * (newScale - scale)) / newScale;
316
+ newOffsetX = offsetX + maxOffsetDiff;
317
+ newOffsetY -= ((layer.image.height / 2 + offsetY) * (newScale - scale)) / newScale;
318
318
  } else {
319
- newOffsetX += (width - newWidth / newScale) / 2;
319
+ newOffsetX += (newWidth / newScale - width) / 2;
320
320
  }
321
321
  } else {
322
- const maxOffsetDiff = Math.min(offsetY, image.height - offsetY - height);
322
+ const maxOffsetDiff = Math.min(offsetY, image.height + offsetY - height);
323
323
  const maxHeight = (height + Math.max(maxOffsetDiff, 0) * 2) * scale;
324
324
  if (newHeight > maxHeight) {
325
325
  newScale *= newHeight / maxHeight;
326
- newOffsetY = offsetY - maxOffsetDiff;
327
- newOffsetX += ((layer.image.width / 2 - offsetX) * (newScale - scale)) / newScale;
326
+ newOffsetY = offsetY + maxOffsetDiff;
327
+ newOffsetX -= ((layer.image.width / 2 + offsetX) * (newScale - scale)) / newScale;
328
328
  } else {
329
- newOffsetY += (height - newHeight / newScale) / 2;
329
+ newOffsetY += (newHeight / newScale - height) / 2;
330
330
  }
331
331
  }
332
332
 
@@ -4,6 +4,12 @@
4
4
  import { calculateGroupRotatedBoundingBox, type Transform } from '../../utils.js';
5
5
  import type { Layer } from '../../../types.js';
6
6
 
7
+ interface Props {
8
+ pageRef: HTMLDivElement;
9
+ }
10
+
11
+ const { pageRef }: Props = $props();
12
+
7
13
  const type = 'rotate';
8
14
 
9
15
  const editor = getEditorContext();
@@ -88,16 +94,16 @@
88
94
  const onMouseMove = (e: MouseEvent) => {
89
95
  if (!initial || editor.activeAction?.id !== actionId || !element) return;
90
96
 
91
- const pageClientRect = element.closest('[data-page-container]')?.getBoundingClientRect();
92
- if (!pageClientRect) return;
97
+ const pageRect = pageRef.getBoundingClientRect();
98
+ if (!pageRect) return;
93
99
 
94
- const { x: pageX, y: pageY } = pageClientRect;
100
+ const { x: pageX, y: pageY } = pageRect;
95
101
 
96
102
  const { width, height, x, y, rotate, scale } = initial.bbox;
97
103
 
98
104
  // Calculate the center of the layer
99
- const clientX = pageX + x * editor.zoom + (width * scale * editor.zoom) / 2;
100
- const clientY = pageY + y * editor.zoom + (height * scale * editor.zoom) / 2;
105
+ const clientX = pageX + (x + (width * scale) / 2) * editor.zoom;
106
+ const clientY = pageY + (y + (height * scale) / 2) * editor.zoom;
101
107
 
102
108
  // Calculate the current angle based on the current mouse position relative to the center of the layer
103
109
  const currentAngle = Math.atan2(
@@ -1,3 +1,6 @@
1
- declare const RotateControl: import("svelte").Component<Record<string, never>, {}, "">;
1
+ interface Props {
2
+ pageRef: HTMLDivElement;
3
+ }
4
+ declare const RotateControl: import("svelte").Component<Props, {}, "">;
2
5
  type RotateControl = ReturnType<typeof RotateControl>;
3
6
  export default RotateControl;
@@ -146,13 +146,13 @@
146
146
  const snapping = 10;
147
147
  if ((origin === 'left' && !flipX) || (origin === 'right' && flipX)) {
148
148
  newWidth = Math.max(newWidth + adjustedXDiff, 10);
149
- const maxWidth = layer.image.width - offsetX;
149
+ const maxWidth = layer.image.width + offsetX;
150
150
  const maxWidthDiff = maxWidth * scale - newWidth;
151
151
  if (Math.abs(maxWidthDiff) < snapping) {
152
152
  newWidth = maxWidth * scale;
153
153
  } else if (maxWidthDiff < 0) {
154
154
  newScale = newWidth / maxWidth;
155
- newOffsetY += ((layer.image.height / 2 - offsetY) * (newScale - scale)) / newScale;
155
+ newOffsetY -= ((layer.image.height / 2 + offsetY) * (newScale - scale)) / newScale;
156
156
  }
157
157
  } else if ((origin === 'top' && !flipY) || (origin === 'bottom' && flipY)) {
158
158
  newHeight = Math.max(newHeight + adjustedYDiff, 10);
@@ -162,31 +162,31 @@
162
162
  newHeight = maxHeight * scale;
163
163
  } else if (maxHeightDiff < 0) {
164
164
  newScale = newHeight / maxHeight;
165
- newOffsetX += ((layer.image.width / 2 - offsetX) * (newScale - scale)) / newScale;
165
+ newOffsetX -= ((layer.image.width / 2 + offsetX) * (newScale - scale)) / newScale;
166
166
  }
167
167
  } else if ((origin === 'right' && !flipX) || (origin === 'left' && flipX)) {
168
168
  const adjustedDiff = Math.min(adjustedXDiff, newWidth - 10);
169
169
  newWidth -= adjustedDiff;
170
- newOffsetX += adjustedDiff / scale;
170
+ newOffsetX -= adjustedDiff / scale;
171
171
  if (Math.abs(newOffsetX * scale) < snapping) {
172
172
  newOffsetX = 0;
173
- newWidth = (width + offsetX) * scale;
174
- } else if (newOffsetX < 0) {
173
+ newWidth = (width - offsetX) * scale;
174
+ } else if (newOffsetX > 0) {
175
175
  newOffsetX = 0;
176
- newScale = newWidth / (width + offsetX);
177
- newOffsetY += ((layer.image.height / 2 - offsetY) * (newScale - scale)) / newScale;
176
+ newScale = newWidth / (width - offsetX);
177
+ newOffsetY -= ((layer.image.height / 2 + offsetY) * (newScale - scale)) / newScale;
178
178
  }
179
179
  } else if ((origin === 'bottom' && !flipY) || (origin === 'top' && flipY)) {
180
180
  const adjustedDiff = Math.min(adjustedYDiff, newHeight - 10);
181
181
  newHeight -= adjustedDiff;
182
- newOffsetY += adjustedDiff / scale;
182
+ newOffsetY -= adjustedDiff / scale;
183
183
  if (Math.abs(newOffsetY * scale) < snapping) {
184
184
  newOffsetY = 0;
185
- newHeight = (height + offsetY) * scale;
186
- } else if (newOffsetY < 0) {
185
+ newHeight = (height - offsetY) * scale;
186
+ } else if (newOffsetY > 0) {
187
187
  newOffsetY = 0;
188
- newScale = newHeight / (height + offsetY);
189
- newOffsetX += ((layer.image.width / 2 - offsetX) * (newScale - scale)) / newScale;
188
+ newScale = newHeight / (height - offsetY);
189
+ newOffsetX -= ((layer.image.width / 2 + offsetX) * (newScale - scale)) / newScale;
190
190
  }
191
191
  }
192
192
 
@@ -5,6 +5,6 @@ import ActiveBackgroundBorder from './active-background-border.svelte';
5
5
  import ActiveLayerBorder from './active-layer-border.svelte';
6
6
  import { BackgroundLayer, BackgroundLayerContent, BackgroundLayerButtons } from './types/background/index.js';
7
7
  import { TextLayer, TextLayerContent, HtmlContent, TextLayerEdit } from './types/text/index.js';
8
- import { ImageLayer, ImageLayerContent } from './types/image/index.js';
8
+ import { ImageLayer, ImageLayerContent, ImageLayerCrop } from './types/image/index.js';
9
9
  import { GroupLayer, GroupLayerContent } from './types/group/index.js';
10
- export { LayerButton, LayerThumbWrapper, LayerWrapper, BackgroundLayer, BackgroundLayerContent, BackgroundLayerButtons, TextLayer, TextLayerContent, HtmlContent, TextLayerEdit, ImageLayer, ImageLayerContent, GroupLayer, GroupLayerContent, ActiveBackgroundBorder, ActiveLayerBorder, };
10
+ export { LayerButton, LayerThumbWrapper, LayerWrapper, BackgroundLayer, BackgroundLayerContent, BackgroundLayerButtons, TextLayer, TextLayerContent, HtmlContent, TextLayerEdit, ImageLayer, ImageLayerContent, ImageLayerCrop, GroupLayer, GroupLayerContent, ActiveBackgroundBorder, ActiveLayerBorder, };
@@ -5,7 +5,7 @@ import ActiveBackgroundBorder from './active-background-border.svelte';
5
5
  import ActiveLayerBorder from './active-layer-border.svelte';
6
6
  import { BackgroundLayer, BackgroundLayerContent, BackgroundLayerButtons, } from './types/background/index.js';
7
7
  import { TextLayer, TextLayerContent, HtmlContent, TextLayerEdit } from './types/text/index.js';
8
- import { ImageLayer, ImageLayerContent } from './types/image/index.js';
8
+ import { ImageLayer, ImageLayerContent, ImageLayerCrop } from './types/image/index.js';
9
9
  import { GroupLayer, GroupLayerContent } from './types/group/index.js';
10
10
  export {
11
11
  // layer buttons
@@ -17,7 +17,7 @@ BackgroundLayer, BackgroundLayerContent, BackgroundLayerButtons,
17
17
  // text layer
18
18
  TextLayer, TextLayerContent, HtmlContent, TextLayerEdit,
19
19
  //image layer
20
- ImageLayer, ImageLayerContent,
20
+ ImageLayer, ImageLayerContent, ImageLayerCrop,
21
21
  // group layer
22
22
  GroupLayer, GroupLayerContent,
23
23
  // layers state
@@ -17,25 +17,20 @@
17
17
  );
18
18
  </script>
19
19
 
20
- <div
21
- class="h-full w-full"
22
- style:transform={backgroundImage.rotate ? `rotate(${backgroundImage.rotate}deg)` : null}
23
- style:opacity={backgroundImage.opacity}
24
- >
25
- <div class="h-full w-full transition-transform" style:transform={flipTransform}>
26
- <div
27
- class="relative"
28
- style:width="{backgroundImage.image.width * scale}px"
29
- style:height="{backgroundImage.image.height * scale}px"
30
- style:transform={`translate(${-backgroundImage.offsetX * scale}px, ${-backgroundImage.offsetY * scale}px)`}
31
- >
32
- <img
33
- src={backgroundImage.image.src}
34
- alt=""
35
- crossorigin="anonymous"
36
- class="pointer-events-none absolute h-full w-full object-fill"
37
- draggable={false}
38
- />
39
- </div>
20
+ <div class="h-full w-full transition-transform" style:transform={flipTransform}>
21
+ <div
22
+ class="relative"
23
+ style:width="{backgroundImage.image.width * scale}px"
24
+ style:height="{backgroundImage.image.height * scale}px"
25
+ style:transform={`translate(${backgroundImage.offsetX * scale}px, ${backgroundImage.offsetY * scale}px) rotate(${backgroundImage.imageRotate}deg)`}
26
+ style:opacity={backgroundImage.opacity}
27
+ >
28
+ <img
29
+ src={backgroundImage.image.src}
30
+ alt=""
31
+ crossorigin="anonymous"
32
+ class="pointer-events-none absolute h-full w-full object-fill"
33
+ draggable={false}
34
+ />
40
35
  </div>
41
36
  </div>
@@ -44,8 +44,8 @@
44
44
  hidden: false,
45
45
  customScale: scale,
46
46
  sticky: false,
47
- x: rect.left + editor.dragged.offsetX - offsetX * scale,
48
- y: rect.top + editor.dragged.offsetY - offsetY * scale,
47
+ x: rect.left + editor.dragged.offsetX + offsetX * scale,
48
+ y: rect.top + editor.dragged.offsetY + offsetY * scale,
49
49
  };
50
50
  } else {
51
51
  editor.dragged = { ...editor.dragged, inside: true };
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import RotateIcon from '@lucide/svelte/icons/refresh-cw';
3
+ import { getEditorContext } from '../../../../../editor.svelte.js';
4
+ import {
5
+ calculateImageLayerPropsForImageRotate,
6
+ degToRad,
7
+ rotatePoint,
8
+ } from '../../../../utils.js';
9
+ import type { ImageLayer } from '../../../../../types.js';
10
+
11
+ interface Props {
12
+ pageRef: HTMLDivElement;
13
+ layer: ImageLayer;
14
+ }
15
+
16
+ const { pageRef, layer }: Props = $props();
17
+
18
+ const type = 'rotate';
19
+
20
+ const editor = getEditorContext();
21
+
22
+ let initial: {
23
+ clientX: number;
24
+ clientY: number;
25
+ layer: ImageLayer;
26
+ } | null = null;
27
+
28
+ /**
29
+ * Normalize the rotation angle to be between -180 and 180 degrees.
30
+ *
31
+ * @param rotate - The rotation angle in degrees.
32
+ * @returns The normalized rotation angle.
33
+ */
34
+ const normalizeRotation = (rotate: number) => {
35
+ // Adjust the rotation to be within the range of 0 to 360 degrees
36
+ const adjustedRotate = (rotate + 360) % 360;
37
+ // If the adjusted rotation is greater than or equal to 180, subtract 360 to bring it within the range of -180 to 180
38
+ return adjustedRotate >= 180 ? adjustedRotate - 360 : adjustedRotate;
39
+ };
40
+
41
+ const onMouseDown = (e: MouseEvent) => {
42
+ if (e.button !== 0) return;
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+ console.log(type, 'mousedown', e);
46
+
47
+ initial = {
48
+ clientX: e.clientX,
49
+ clientY: e.clientY,
50
+ layer: $state.snapshot(layer),
51
+ };
52
+ addEventListener('mousemove', onMouseMove);
53
+ addEventListener('mouseup', onMouseUp);
54
+ };
55
+
56
+ const onMouseUp = (e: MouseEvent) => {
57
+ console.log(type, 'mouseup', e);
58
+ removeEventListener('mousemove', onMouseMove);
59
+ removeEventListener('mouseup', onMouseUp);
60
+ initial = null;
61
+ };
62
+
63
+ const onMouseMove = (e: MouseEvent) => {
64
+ if (!initial) return;
65
+
66
+ const pageRect = pageRef.getBoundingClientRect();
67
+ if (!pageRect) return;
68
+
69
+ const { x: pageX, y: pageY } = pageRect;
70
+
71
+ const { x, y, width, height, scale, rotate, image, offsetX, offsetY } = initial.layer;
72
+
73
+ const relativeImageCenter = rotatePoint(degToRad(rotate), {
74
+ x: image.width / 2 + offsetX - width / 2,
75
+ y: image.height / 2 + offsetY - height / 2,
76
+ });
77
+
78
+ // Calculate the center of the layer image
79
+ const clientX = pageX + (x + (width / 2 + relativeImageCenter.x) * scale) * editor.zoom;
80
+ const clientY = pageY + (y + (height / 2 + relativeImageCenter.y) * scale) * editor.zoom;
81
+
82
+ // Calculate the current angle based on the current mouse position relative to the center of the layer
83
+ const currentAngle = Math.atan2(
84
+ (e.clientY - clientY) / editor.zoom,
85
+ (e.clientX - clientX) / editor.zoom,
86
+ );
87
+
88
+ // Convert the current angle from radians to degrees and adjust to make 0 degrees the initial position
89
+ let currentRotate = (currentAngle * 180) / Math.PI - 90 - rotate;
90
+
91
+ // Add snapping
92
+ const snapThreshold = 1;
93
+ const remainder = Math.abs(currentRotate) % 45;
94
+ if (remainder < snapThreshold || remainder > 45 - snapThreshold) {
95
+ currentRotate = Math.round(currentRotate / 45) * 45;
96
+ }
97
+
98
+ currentRotate = normalizeRotation(currentRotate);
99
+
100
+ const props = calculateImageLayerPropsForImageRotate(initial.layer, currentRotate);
101
+ Object.assign(layer, props);
102
+ };
103
+ </script>
104
+
105
+ <!-- <div class="absolute top-full flex p-6"> -->
106
+ <!-- <div class="group pointer-events-none relative m-1 flex h-8 w-8 items-center justify-center"> -->
107
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
108
+ <div
109
+ class="group pointer-events-none absolute -bottom-9 left-1/2 -m-2 h-6 w-6 cursor-grab rounded-full bg-white"
110
+ onmousedown={onMouseDown}
111
+ >
112
+ <div
113
+ class="group-hover:bg-primary group-active:bg-primary group-hover:text-primary-foreground group-active:text-primary-foreground flex h-6 w-6 items-center justify-center rounded-full bg-white transition-colors after:pointer-events-auto after:absolute after:inset-0 after:rounded-full"
114
+ style:box-shadow="0 0 4px 1px rgba(57,76,96,.15), 0 0 0 1px rgba(43,59,74,.3)"
115
+ >
116
+ <RotateIcon class="h-4 w-4" />
117
+ </div>
118
+ </div>
119
+ <!-- </div> -->
120
+ <!-- </div> -->
@@ -0,0 +1,8 @@
1
+ import type { ImageLayer } from '../../../../../types.js';
2
+ interface Props {
3
+ pageRef: HTMLDivElement;
4
+ layer: ImageLayer;
5
+ }
6
+ declare const ImageRotateControl: import("svelte").Component<Props, {}, "">;
7
+ type ImageRotateControl = ReturnType<typeof ImageRotateControl>;
8
+ export default ImageRotateControl;