@peteai/presentation-editor 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/editor/active-layers.svelte +24 -9
- package/dist/components/editor/active-layers.svelte.d.ts +6 -1
- package/dist/components/editor/editor.svelte +15 -17
- package/dist/components/editor/editor.svelte.js +10 -8
- package/dist/components/editor/header.svelte +25 -21
- package/dist/components/editor/hotkeys.svelte +7 -0
- package/dist/components/editor/layers/active-layer-border.svelte +1 -1
- package/dist/components/editor/layers/buttons/border-button/border-button-colors.svelte +1 -1
- package/dist/components/editor/layers/buttons/opacity-button/opacity-button.svelte +1 -6
- package/dist/components/editor/layers/controls/corner-scale-control/corner-scale-control.svelte +1 -1
- package/dist/components/editor/layers/controls/group-resize-control/group-resize-control.svelte +11 -11
- package/dist/components/editor/layers/controls/rotate-control/rotate-control.svelte +12 -6
- package/dist/components/editor/layers/controls/rotate-control/rotate-control.svelte.d.ts +4 -1
- package/dist/components/editor/layers/controls/side-resize-control/side-resize-control.svelte +13 -13
- package/dist/components/editor/layers/controls/side-scale-control/side-scale-control.svelte +1 -1
- package/dist/components/editor/layers/index.d.ts +2 -2
- package/dist/components/editor/layers/index.js +2 -2
- package/dist/components/editor/layers/layer-button.svelte +1 -1
- package/dist/components/editor/layers/types/background/background-content-image.svelte +15 -20
- package/dist/components/editor/layers/types/background/background-layer-buttons.svelte +1 -1
- package/dist/components/editor/layers/types/background/background-layer.svelte +2 -2
- package/dist/components/editor/layers/types/image/controls/image-rotate-control/image-rotate-control.svelte +120 -0
- package/dist/components/editor/layers/types/image/controls/image-rotate-control/image-rotate-control.svelte.d.ts +8 -0
- package/dist/components/editor/layers/types/image/controls/image-rotate-control/index.d.ts +2 -0
- package/dist/components/editor/layers/types/image/controls/image-rotate-control/index.js +4 -0
- package/dist/components/editor/layers/types/image/controls/image-scale-control/image-scale-control.svelte +154 -0
- package/dist/components/editor/layers/types/image/controls/image-scale-control/image-scale-control.svelte.d.ts +91 -0
- package/dist/components/editor/layers/types/image/controls/image-scale-control/index.d.ts +2 -0
- package/dist/components/editor/layers/types/image/controls/image-scale-control/index.js +4 -0
- package/dist/components/editor/layers/types/image/image-layer-content.svelte +3 -3
- package/dist/components/editor/layers/types/image/image-layer-crop.svelte +182 -0
- package/dist/components/editor/layers/types/image/image-layer-crop.svelte.d.ts +10 -0
- package/dist/components/editor/layers/types/image/image-layer.svelte +16 -0
- package/dist/components/editor/layers/types/image/index.d.ts +2 -1
- package/dist/components/editor/layers/types/image/index.js +2 -1
- package/dist/components/editor/layers/types/text/extensions/list-item/list-item.js +0 -2
- package/dist/components/editor/layers/utils.d.ts +24 -9
- package/dist/components/editor/layers/utils.js +107 -54
- package/dist/components/editor/page-editor.svelte +6 -2
- package/dist/components/editor/sidebar/color-sidebar/color-sidebar-color.svelte +2 -2
- package/dist/components/editor/sidebar/color-sidebar/color-sidebar-gradient-picker.svelte +5 -5
- package/dist/components/editor/sidebar/color-sidebar/color-sidebar.svelte +4 -4
- package/dist/components/editor/sidebar/font-sidebar/font-sidebar.svelte +1 -1
- package/dist/components/editor/sidebar/image-crop-sidebar.svelte +112 -0
- package/dist/components/editor/sidebar/image-crop-sidebar.svelte.d.ts +7 -0
- package/dist/components/editor/sidebar/position-sidebar.svelte +0 -2
- package/dist/components/editor/sidebar/sidebar-uploads-tab.svelte +3 -3
- package/dist/components/editor/sidebar/sidebar.svelte +7 -4
- package/dist/components/editor/snapping-guides.svelte +3 -3
- package/dist/components/editor/types.d.ts +2 -1
- package/dist/components/editor/utils.js +1 -0
- package/dist/components/ui/color-picker/color-picker-alpha-grid.svelte +2 -1
- package/dist/components/ui/color-picker/color-picker.svelte +6 -6
- package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-content.svelte +3 -1
- package/dist/components/ui/context-menu/context-menu-group-heading.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-item.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-radio-item.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-separator.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-shortcut.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-sub-content.svelte +1 -1
- package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte +1 -1
- package/dist/components/ui/dialog/dialog-content.svelte +4 -2
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +2 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
- package/dist/components/ui/input/input.svelte +1 -1
- package/dist/components/ui/slider/slider.svelte +3 -3
- package/dist/components/ui/tabs/index.d.ts +4 -4
- package/dist/components/ui/tabs/index.js +4 -4
- package/dist/components/ui/tabs/tabs-content.svelte +4 -4
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +1 -1
- package/dist/components/ui/tabs/tabs-list.svelte +5 -9
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +1 -1
- package/dist/components/ui/tabs/tabs-trigger.svelte +4 -4
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +1 -1
- package/package.json +19 -19
|
@@ -12,13 +12,6 @@ export type Transform = {
|
|
|
12
12
|
rotate: number;
|
|
13
13
|
scale: number;
|
|
14
14
|
};
|
|
15
|
-
export declare const rotatePointOld: (point: {
|
|
16
|
-
x: number;
|
|
17
|
-
y: number;
|
|
18
|
-
}, angleRad: number) => {
|
|
19
|
-
x: number;
|
|
20
|
-
y: number;
|
|
21
|
-
};
|
|
22
15
|
export declare const calculateLayerTransform: (layer: Layer, groupScale?: number) => Transform;
|
|
23
16
|
export declare const calculateNewPosition: (origin: Origin, transform: Transform, newWidth: number, newHeight: number) => {
|
|
24
17
|
newX: number;
|
|
@@ -31,7 +24,7 @@ export declare function calculateBoundingBox(transform: Transform): {
|
|
|
31
24
|
height: number;
|
|
32
25
|
};
|
|
33
26
|
export declare function degToRad(deg: number): number;
|
|
34
|
-
export declare function rotatePoint(
|
|
27
|
+
export declare function rotatePoint(rad: number, p: Point, center?: Point): Point;
|
|
35
28
|
export declare function getRotatedCorners(rect: Transform): Point[];
|
|
36
29
|
export declare function isRotatedVertically(rotate: number): boolean;
|
|
37
30
|
interface ScaleResult {
|
|
@@ -51,9 +44,9 @@ export declare function calculateRelativeRects(bbox: Transform, absoluteRects: T
|
|
|
51
44
|
x: number;
|
|
52
45
|
y: number;
|
|
53
46
|
rotate: number;
|
|
47
|
+
scale: number;
|
|
54
48
|
width: number;
|
|
55
49
|
height: number;
|
|
56
|
-
scale: number;
|
|
57
50
|
}[];
|
|
58
51
|
export declare function calculateAbsoluteRects(bbox: Transform, rects: Transform[]): Transform[];
|
|
59
52
|
export declare const checkPolygonsIntersect: (a: {
|
|
@@ -68,6 +61,7 @@ export declare function calculateImageCover(image: {
|
|
|
68
61
|
width: number;
|
|
69
62
|
height: number;
|
|
70
63
|
rotate?: number | null;
|
|
64
|
+
imageRotate?: number | null;
|
|
71
65
|
}, bbox: {
|
|
72
66
|
width: number;
|
|
73
67
|
height: number;
|
|
@@ -81,4 +75,25 @@ export declare function calculateImageCover(image: {
|
|
|
81
75
|
export declare const defaultColor = "#000000";
|
|
82
76
|
export declare function extractBorderColors(layers: ImageLayer[]): string[] | undefined;
|
|
83
77
|
export declare function buildBackgroundCircleStyle(colors: string[]): string;
|
|
78
|
+
export declare function getImageLayerBboxRelativeToImageCenter(layer: ImageLayer): {
|
|
79
|
+
minX: number;
|
|
80
|
+
minY: number;
|
|
81
|
+
maxX: number;
|
|
82
|
+
maxY: number;
|
|
83
|
+
};
|
|
84
|
+
export declare function calculateImageLayerPropsForImageRotate(layer: ImageLayer, imageRotate: number): {
|
|
85
|
+
imageRotate: number;
|
|
86
|
+
scale?: undefined;
|
|
87
|
+
width?: undefined;
|
|
88
|
+
height?: undefined;
|
|
89
|
+
offsetX?: undefined;
|
|
90
|
+
offsetY?: undefined;
|
|
91
|
+
} | {
|
|
92
|
+
imageRotate: number;
|
|
93
|
+
scale: number;
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
offsetX: number;
|
|
97
|
+
offsetY: number;
|
|
98
|
+
};
|
|
84
99
|
export {};
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const cos = Math.cos(angleRad);
|
|
3
|
-
const sin = Math.sin(angleRad);
|
|
4
|
-
return {
|
|
5
|
-
x: point.x * cos - point.y * sin,
|
|
6
|
-
y: point.x * sin + point.y * cos,
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
const getPoints = (origin, width, height) => {
|
|
1
|
+
const getOriginRelativePoint = (origin, width, height) => {
|
|
10
2
|
const halfWidth = width / 2;
|
|
11
3
|
const halfHeight = height / 2;
|
|
12
4
|
switch (origin) {
|
|
@@ -41,36 +33,42 @@ export const calculateLayerTransform = (layer, groupScale = 1) => ({
|
|
|
41
33
|
scale: layer.scale,
|
|
42
34
|
});
|
|
43
35
|
export const calculateNewPosition = (origin, transform, newWidth, newHeight) => {
|
|
44
|
-
const { x, y, width, height, rotate, scale } = transform;
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
const { x, y, width: rawWidth, height: rawHeight, rotate, scale } = transform;
|
|
37
|
+
const width = rawWidth * scale;
|
|
38
|
+
const height = rawHeight * scale;
|
|
39
|
+
// Get relative points based on origin for old and new dimensions
|
|
40
|
+
const oldPoint = getOriginRelativePoint(origin, width, height);
|
|
41
|
+
const newPoint = getOriginRelativePoint(origin, newWidth, newHeight);
|
|
42
|
+
// Calculate position delta
|
|
43
|
+
const delta = {
|
|
44
|
+
x: newPoint.x - oldPoint.x,
|
|
45
|
+
y: newPoint.y - oldPoint.y,
|
|
46
|
+
};
|
|
47
|
+
// Apply rotation if needed
|
|
48
|
+
const rotatedDelta = rotate !== 0 ? rotatePoint(degToRad(rotate), delta) : delta;
|
|
49
|
+
// Calculate new position accounting for size difference and delta
|
|
56
50
|
return {
|
|
57
|
-
newX: x -
|
|
58
|
-
newY: y -
|
|
51
|
+
newX: x - rotatedDelta.x - (newWidth - width) / 2,
|
|
52
|
+
newY: y - rotatedDelta.y - (newHeight - height) / 2,
|
|
59
53
|
};
|
|
60
54
|
};
|
|
61
55
|
export function calculateBoundingBox(transform) {
|
|
62
|
-
const { x, y, width, height, rotate, scale } = transform;
|
|
56
|
+
const { x, y, width: rawWidth, height: rawHeight, rotate, scale } = transform;
|
|
57
|
+
const width = rawWidth * scale;
|
|
58
|
+
const height = rawHeight * scale;
|
|
63
59
|
if (rotate === 0) {
|
|
64
|
-
return { x, y, width
|
|
60
|
+
return { x, y, width, height };
|
|
65
61
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
62
|
+
const rad = degToRad(rotate);
|
|
63
|
+
const cos = Math.cos(rad);
|
|
64
|
+
const sin = Math.sin(rad);
|
|
65
|
+
const hw = width / 2;
|
|
66
|
+
const hh = height / 2;
|
|
67
|
+
const maxX = Math.abs(hw * cos) + Math.abs(hh * sin);
|
|
68
|
+
const maxY = Math.abs(hw * sin) + Math.abs(hh * cos);
|
|
71
69
|
return {
|
|
72
|
-
x: x +
|
|
73
|
-
y: y +
|
|
70
|
+
x: x + width / 2 - maxX,
|
|
71
|
+
y: y + height / 2 - maxY,
|
|
74
72
|
width: maxX * 2,
|
|
75
73
|
height: maxY * 2,
|
|
76
74
|
};
|
|
@@ -78,20 +76,20 @@ export function calculateBoundingBox(transform) {
|
|
|
78
76
|
export function degToRad(deg) {
|
|
79
77
|
return (deg * Math.PI) / 180;
|
|
80
78
|
}
|
|
81
|
-
export function rotatePoint(p,
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
79
|
+
export function rotatePoint(rad, p, center = { x: 0, y: 0 }) {
|
|
80
|
+
const cos = Math.cos(rad);
|
|
81
|
+
const sin = Math.sin(rad);
|
|
82
|
+
const dx = p.x - center.x;
|
|
83
|
+
const dy = p.y - center.y;
|
|
86
84
|
return {
|
|
87
|
-
x:
|
|
88
|
-
y:
|
|
85
|
+
x: center.x + dx * cos - dy * sin,
|
|
86
|
+
y: center.y + dx * sin + dy * cos,
|
|
89
87
|
};
|
|
90
88
|
}
|
|
91
89
|
export function getRotatedCorners(rect) {
|
|
90
|
+
const rad = degToRad(rect.rotate);
|
|
92
91
|
const cx = rect.x + (rect.width * rect.scale) / 2;
|
|
93
92
|
const cy = rect.y + (rect.height * rect.scale) / 2;
|
|
94
|
-
const angle = degToRad(rect.rotate);
|
|
95
93
|
const hw = (rect.width * rect.scale) / 2;
|
|
96
94
|
const hh = (rect.height * rect.scale) / 2;
|
|
97
95
|
const corners = [
|
|
@@ -100,7 +98,7 @@ export function getRotatedCorners(rect) {
|
|
|
100
98
|
{ x: cx + hw, y: cy + hh },
|
|
101
99
|
{ x: cx - hw, y: cy + hh },
|
|
102
100
|
];
|
|
103
|
-
return corners.map((p) => rotatePoint(p, { x: cx, y: cy }
|
|
101
|
+
return corners.map((p) => rotatePoint(rad, p, { x: cx, y: cy }));
|
|
104
102
|
}
|
|
105
103
|
export function isRotatedVertically(rotate) {
|
|
106
104
|
const rotationNormalized = ((rotate % 360) + 360) % 360;
|
|
@@ -166,9 +164,9 @@ export function calculateGroupRotatedBoundingBox(transforms, rotate = 0) {
|
|
|
166
164
|
if (!rotate)
|
|
167
165
|
return calculateGroupBoundingBox(transforms);
|
|
168
166
|
const allCorners = transforms.flatMap((t) => getRotatedCorners(t));
|
|
169
|
-
const
|
|
167
|
+
const rad = degToRad(rotate);
|
|
170
168
|
// Rotate all points to align with desired angle
|
|
171
|
-
const rotatedPoints = allCorners.map((p) => rotatePoint(
|
|
169
|
+
const rotatedPoints = allCorners.map((p) => rotatePoint(-rad, p));
|
|
172
170
|
// Compute AABB of rotated points
|
|
173
171
|
const xs = rotatedPoints.map((p) => p.x);
|
|
174
172
|
const ys = rotatedPoints.map((p) => p.y);
|
|
@@ -183,7 +181,7 @@ export function calculateGroupRotatedBoundingBox(transforms, rotate = 0) {
|
|
|
183
181
|
y: (minY + maxY) / 2,
|
|
184
182
|
};
|
|
185
183
|
// Rotate center back to original space
|
|
186
|
-
const center = rotatePoint(
|
|
184
|
+
const center = rotatePoint(rad, centerRotated);
|
|
187
185
|
return {
|
|
188
186
|
x: center.x - width / 2,
|
|
189
187
|
y: center.y - height / 2,
|
|
@@ -194,7 +192,7 @@ export function calculateGroupRotatedBoundingBox(transforms, rotate = 0) {
|
|
|
194
192
|
};
|
|
195
193
|
}
|
|
196
194
|
export function calculateRelativeRects(bbox, absoluteRects) {
|
|
197
|
-
const
|
|
195
|
+
const rad = degToRad(bbox.rotate); // inverse to align bbox with axes
|
|
198
196
|
const bboxCenter = {
|
|
199
197
|
x: bbox.x + (bbox.width * bbox.scale) / 2,
|
|
200
198
|
y: bbox.y + (bbox.height * bbox.scale) / 2,
|
|
@@ -206,17 +204,18 @@ export function calculateRelativeRects(bbox, absoluteRects) {
|
|
|
206
204
|
y: rect.y + (rect.height * rect.scale) / 2,
|
|
207
205
|
};
|
|
208
206
|
// Rotate center ralatively to the bbox center
|
|
209
|
-
const relativeCenter = rotatePoint(center, bboxCenter
|
|
207
|
+
const relativeCenter = rotatePoint(-rad, center, bboxCenter);
|
|
210
208
|
return {
|
|
211
209
|
...rect,
|
|
212
|
-
x: (relativeCenter.x - bbox.x) / bbox.scale - (rect.width * rect.scale) / 2,
|
|
213
|
-
y: (relativeCenter.y - bbox.y) / bbox.scale - (rect.height * rect.scale) / 2,
|
|
210
|
+
x: ((relativeCenter.x - bbox.x) / bbox.scale - (rect.width * rect.scale) / 2) / bbox.scale,
|
|
211
|
+
y: ((relativeCenter.y - bbox.y) / bbox.scale - (rect.height * rect.scale) / 2) / bbox.scale,
|
|
214
212
|
rotate: rect.rotate - bbox.rotate,
|
|
213
|
+
scale: rect.scale / bbox.scale,
|
|
215
214
|
};
|
|
216
215
|
});
|
|
217
216
|
}
|
|
218
217
|
export function calculateAbsoluteRects(bbox, rects) {
|
|
219
|
-
const
|
|
218
|
+
const rad = degToRad(bbox.rotate);
|
|
220
219
|
const bboxCenter = {
|
|
221
220
|
x: bbox.x + (bbox.width * bbox.scale) / 2,
|
|
222
221
|
y: bbox.y + (bbox.height * bbox.scale) / 2,
|
|
@@ -226,7 +225,7 @@ export function calculateAbsoluteRects(bbox, rects) {
|
|
|
226
225
|
x: bbox.x + (rect.x + (rect.width * rect.scale) / 2) * bbox.scale,
|
|
227
226
|
y: bbox.y + (rect.y + (rect.height * rect.scale) / 2) * bbox.scale,
|
|
228
227
|
};
|
|
229
|
-
const center = rotatePoint(relativeCenter, bboxCenter
|
|
228
|
+
const center = rotatePoint(rad, relativeCenter, bboxCenter);
|
|
230
229
|
return {
|
|
231
230
|
...rect,
|
|
232
231
|
x: center.x - (rect.width * rect.scale * bbox.scale) / 2,
|
|
@@ -293,12 +292,14 @@ const minScaleToContain = (rotatedRect, rect) => {
|
|
|
293
292
|
};
|
|
294
293
|
export function calculateImageCover(image, bbox) {
|
|
295
294
|
const scale = minScaleToContain(image, bbox);
|
|
295
|
+
const width = bbox.width / scale;
|
|
296
|
+
const height = bbox.height / scale;
|
|
296
297
|
return {
|
|
297
|
-
width
|
|
298
|
-
height
|
|
298
|
+
width,
|
|
299
|
+
height,
|
|
299
300
|
scale,
|
|
300
|
-
offsetX: (
|
|
301
|
-
offsetY: (
|
|
301
|
+
offsetX: (width - image.width) / 2,
|
|
302
|
+
offsetY: (height - image.height) / 2,
|
|
302
303
|
};
|
|
303
304
|
}
|
|
304
305
|
export const defaultColor = '#000000';
|
|
@@ -322,3 +323,55 @@ export function buildBackgroundCircleStyle(colors) {
|
|
|
322
323
|
.join(', ');
|
|
323
324
|
return `background-image: conic-gradient(${gradientColors})`;
|
|
324
325
|
}
|
|
326
|
+
export function getImageLayerBboxRelativeToImageCenter(layer) {
|
|
327
|
+
const { width, height, image, offsetX, offsetY, imageRotate } = layer;
|
|
328
|
+
// current image center in layer coords (rotation doesn't change it)
|
|
329
|
+
const imageCenter = { x: image.width / 2 + offsetX, y: image.height / 2 + offsetY };
|
|
330
|
+
const imageRad = degToRad(imageRotate);
|
|
331
|
+
// For each corner, compute v = corner - C, rotate by -theta, accumulate max abs X/Y
|
|
332
|
+
const corners = [
|
|
333
|
+
{ x: 0, y: 0 },
|
|
334
|
+
{ x: width, y: 0 },
|
|
335
|
+
{ x: width, y: height },
|
|
336
|
+
{ x: 0, y: height },
|
|
337
|
+
].map((p) => {
|
|
338
|
+
const v = { x: p.x - imageCenter.x, y: p.y - imageCenter.y }; // layer->center
|
|
339
|
+
return rotatePoint(-imageRad, v); // bring into image unrotated axes
|
|
340
|
+
});
|
|
341
|
+
// Compute AABB of rotated points
|
|
342
|
+
const xs = corners.map((p) => p.x);
|
|
343
|
+
const ys = corners.map((p) => p.y);
|
|
344
|
+
const minX = Math.min(...xs);
|
|
345
|
+
const maxX = Math.max(...xs);
|
|
346
|
+
const minY = Math.min(...ys);
|
|
347
|
+
const maxY = Math.max(...ys);
|
|
348
|
+
return { minX, minY, maxX, maxY };
|
|
349
|
+
}
|
|
350
|
+
export function calculateImageLayerPropsForImageRotate(layer, imageRotate) {
|
|
351
|
+
const { width, height, scale, image, offsetX, offsetY } = layer;
|
|
352
|
+
// image half-width and half-height
|
|
353
|
+
const ihw = image.width / 2;
|
|
354
|
+
const ihh = image.height / 2;
|
|
355
|
+
// current image center in layer coords (rotation doesn't change it)
|
|
356
|
+
const imageCenter = { x: ihw + offsetX, y: ihh + offsetY };
|
|
357
|
+
const bbox = getImageLayerBboxRelativeToImageCenter({ ...layer, imageRotate });
|
|
358
|
+
const maxAbsX = Math.max(Math.abs(bbox.minX), Math.abs(bbox.maxX));
|
|
359
|
+
const maxAbsY = Math.max(Math.abs(bbox.minY), Math.abs(bbox.maxY));
|
|
360
|
+
// Required scale so half-extents of (Iw*s, Ih*s) cover maxAbsX/Y:
|
|
361
|
+
// Iw*s/2 >= maxAbsX => s >= 2*maxAbsX / Iw
|
|
362
|
+
// Ih*s/2 >= maxAbsY => s >= 2*maxAbsY / Ih
|
|
363
|
+
const sReqX = (2 * maxAbsX) / image.width;
|
|
364
|
+
const sReqY = (2 * maxAbsY) / image.height;
|
|
365
|
+
const minScaleFactor = Math.max(sReqX, sReqY);
|
|
366
|
+
if (minScaleFactor < 1) {
|
|
367
|
+
return { imageRotate };
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
imageRotate,
|
|
371
|
+
scale: scale * minScaleFactor,
|
|
372
|
+
width: width / minScaleFactor,
|
|
373
|
+
height: height / minScaleFactor,
|
|
374
|
+
offsetX: imageCenter.x / minScaleFactor - ihw,
|
|
375
|
+
offsetY: imageCenter.y / minScaleFactor - ihh,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
const editor = getEditorContext();
|
|
27
27
|
|
|
28
|
+
let pageRef: HTMLDivElement | null = $state(null);
|
|
29
|
+
|
|
28
30
|
let activeLayerGuides: ActiveLayerGuide[] = $state([]);
|
|
29
31
|
|
|
30
32
|
let hoveredLayerId: string | null = $state(null);
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
<div class="flex min-h-full flex-1">
|
|
39
41
|
<div class="box-border flex w-full shrink-0 items-center justify-center p-0">
|
|
40
42
|
<div bind:this={wrapperRef} class="translate-x-0 translate-y-0 p-4 transition-transform">
|
|
41
|
-
<div class="relative flex flex-col items-center"
|
|
43
|
+
<div bind:this={pageRef} class="relative flex flex-col items-center">
|
|
42
44
|
<div
|
|
43
45
|
class="m-0 overflow-hidden shadow-xl"
|
|
44
46
|
style:width="{scaledWidth}px"
|
|
@@ -89,7 +91,9 @@
|
|
|
89
91
|
{/if}
|
|
90
92
|
{/if}
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
{#if viewportRef && pageRef}
|
|
95
|
+
<ActiveLayers {viewportRef} {wrapperRef} {pageRef} />
|
|
96
|
+
{/if}
|
|
93
97
|
|
|
94
98
|
<SnappingGuides zoom={editor.zoom} guides={activeLayerGuides} />
|
|
95
99
|
</div>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
)}
|
|
31
31
|
{...restProps}
|
|
32
32
|
>
|
|
33
|
-
<div class="wrapper
|
|
33
|
+
<div class="wrapper relative h-full w-full after:absolute after:inset-0 after:shadow-inner-1">
|
|
34
34
|
<ColorPickerAlphaGrid size={12}>
|
|
35
35
|
<ColorIndicator {color} />
|
|
36
36
|
</ColorPickerAlphaGrid>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<div
|
|
41
41
|
class="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
|
|
42
42
|
>
|
|
43
|
-
<div class="bg-background text-foreground
|
|
43
|
+
<div class="rounded-full bg-background p-1 text-foreground">
|
|
44
44
|
<SettingsIcon class="h-4 w-4" />
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
<Popover.Trigger
|
|
67
67
|
class={cn(
|
|
68
68
|
buttonVariants({ variant: 'ghost', size: 'icon' }),
|
|
69
|
-
'
|
|
69
|
+
'relative h-full w-full shrink-0 overflow-hidden rounded-full after:absolute after:inset-0 after:rounded-full after:transition-shadow hover:after:shadow-hover data-[state=open]:after:shadow-active',
|
|
70
70
|
)}
|
|
71
71
|
>
|
|
72
72
|
<div
|
|
73
|
-
class="wrapper
|
|
73
|
+
class="wrapper relative h-full w-full after:absolute after:inset-0 after:rounded-full after:shadow-inner-1"
|
|
74
74
|
>
|
|
75
75
|
<ColorPickerAlphaGrid size={12}>
|
|
76
76
|
<ColorIndicator {color} />
|
|
@@ -105,11 +105,11 @@
|
|
|
105
105
|
<Button
|
|
106
106
|
variant="ghost"
|
|
107
107
|
size="icon"
|
|
108
|
-
class="
|
|
108
|
+
class="relative shrink-0 p-0 after:absolute after:inset-0 after:rounded-full after:transition-shadow hover:after:shadow-hover"
|
|
109
109
|
onclick={addColor}
|
|
110
110
|
>
|
|
111
111
|
<div
|
|
112
|
-
class="
|
|
112
|
+
class="relative h-full w-full after:absolute after:inset-0 after:rounded-full after:shadow-inner-1"
|
|
113
113
|
>
|
|
114
114
|
<div
|
|
115
115
|
class="h-full w-full rounded-full"
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
></div>
|
|
118
118
|
<span class="absolute inset-0 flex h-full w-full items-center justify-center">
|
|
119
119
|
<span
|
|
120
|
-
class="
|
|
120
|
+
class="flex h-6 w-6 items-center justify-center rounded-full bg-background text-foreground"
|
|
121
121
|
>
|
|
122
122
|
<PlusIcon />
|
|
123
123
|
</span>
|
|
@@ -325,7 +325,7 @@
|
|
|
325
325
|
)}
|
|
326
326
|
>
|
|
327
327
|
<div
|
|
328
|
-
class="
|
|
328
|
+
class="relative h-full w-full after:absolute after:inset-0 after:rounded-full after:shadow-inner-1"
|
|
329
329
|
>
|
|
330
330
|
<div
|
|
331
331
|
class="h-full w-full rounded-full"
|
|
@@ -333,7 +333,7 @@
|
|
|
333
333
|
></div>
|
|
334
334
|
<span class="absolute inset-0 flex h-full w-full items-center justify-center">
|
|
335
335
|
<span
|
|
336
|
-
class="
|
|
336
|
+
class="flex h-6 w-6 items-center justify-center rounded-full bg-background text-foreground"
|
|
337
337
|
>
|
|
338
338
|
<PlusIcon />
|
|
339
339
|
</span>
|
|
@@ -381,7 +381,7 @@
|
|
|
381
381
|
<!-- <PaletteIcon class="h-5 w-5" /> -->
|
|
382
382
|
<h5 class="text-left text-sm font-bold">Default colors</h5>
|
|
383
383
|
</div>
|
|
384
|
-
<div class="text-muted-foreground
|
|
384
|
+
<div class="text-xs text-muted-foreground">Solid colors</div>
|
|
385
385
|
<div class="grid grid-cols-6 items-stretch gap-1">
|
|
386
386
|
{#each Object.keys(defaultSolidColors) as color}
|
|
387
387
|
<div class="relative h-0 w-full" style:padding-top="100%">
|
|
@@ -393,7 +393,7 @@
|
|
|
393
393
|
</div>
|
|
394
394
|
|
|
395
395
|
{#if gradientsAllowed}
|
|
396
|
-
<div class="text-muted-foreground
|
|
396
|
+
<div class="text-xs text-muted-foreground">Gradients</div>
|
|
397
397
|
<div class="grid grid-cols-6 items-stretch gap-1">
|
|
398
398
|
{#each defaultGradients as color}
|
|
399
399
|
<div class="relative h-0 w-full" style:padding-top="100%">
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import XIcon from '@lucide/svelte/icons/x';
|
|
3
|
+
import { Button } from '../../ui/button/index.js';
|
|
4
|
+
import { Slider } from '../../ui/slider/index.js';
|
|
5
|
+
import { Input } from '../../ui/input/index.js';
|
|
6
|
+
import { getEditorContext } from '../editor.svelte.js';
|
|
7
|
+
import {
|
|
8
|
+
calculateImageLayerPropsForImageRotate,
|
|
9
|
+
calculateRelativeRects,
|
|
10
|
+
} from '../layers/utils.js';
|
|
11
|
+
import type { ImageLayer } from '../types.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
layer: ImageLayer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { layer }: Props = $props();
|
|
18
|
+
|
|
19
|
+
const editor = $derived(getEditorContext());
|
|
20
|
+
|
|
21
|
+
let value = $derived(Math.round(layer.imageRotate * 10) / 10);
|
|
22
|
+
let originalLayer = $derived(
|
|
23
|
+
editor.selectedLayersNotLocked.find((l) => l.id === layer.id),
|
|
24
|
+
) as ImageLayer;
|
|
25
|
+
|
|
26
|
+
const min = -180;
|
|
27
|
+
const max = 180;
|
|
28
|
+
const step = 0.1;
|
|
29
|
+
|
|
30
|
+
let layerCache: ImageLayer | null = $state.snapshot(null);
|
|
31
|
+
|
|
32
|
+
const setImageRotate = (value: string | number) => {
|
|
33
|
+
value = Number(value);
|
|
34
|
+
if (!layerCache) {
|
|
35
|
+
layerCache = $state.snapshot(layer);
|
|
36
|
+
}
|
|
37
|
+
const props = calculateImageLayerPropsForImageRotate(layerCache, value);
|
|
38
|
+
Object.assign(layer, props);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const applyChanges = () => {
|
|
42
|
+
if (!originalLayer) return;
|
|
43
|
+
const relativeLayer = editor.activeGroupAndChild
|
|
44
|
+
? (calculateRelativeRects(editor.activeGroupAndChild.groupLayer, [layer])[0] as ImageLayer)
|
|
45
|
+
: layer;
|
|
46
|
+
// Define keys to check for changes in image layer properties
|
|
47
|
+
const imageLayerKeys: (keyof ImageLayer)[] = [
|
|
48
|
+
'imageRotate', // Rotation angle of the image
|
|
49
|
+
'scale', // Scale/zoom level
|
|
50
|
+
'width', // Width dimension
|
|
51
|
+
'height', // Height dimension
|
|
52
|
+
'offsetX', // X offset position
|
|
53
|
+
'offsetY', // Y offset position
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Filter keys that have different values between original and relative layers
|
|
57
|
+
const changedKeys = imageLayerKeys.filter((key) => originalLayer[key] !== relativeLayer[key]);
|
|
58
|
+
if (changedKeys.length > 0) {
|
|
59
|
+
editor.historyPush({
|
|
60
|
+
type: 'layerUpdate',
|
|
61
|
+
pageId: editor.activePage.id,
|
|
62
|
+
layer: { id: layer.id, type: layer.type },
|
|
63
|
+
undo: Object.fromEntries(changedKeys.map((key) => [key, originalLayer[key]])),
|
|
64
|
+
redo: Object.fromEntries(changedKeys.map((key) => [key, relativeLayer[key]])),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
editor.imageCropLayer = null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
$effect(() => {
|
|
71
|
+
if (!originalLayer) {
|
|
72
|
+
editor.imageCropLayer = null;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<div class="flex h-full flex-col gap-2">
|
|
78
|
+
<div class="flex items-center justify-between">
|
|
79
|
+
<div class="text-sm font-bold">Crop</div>
|
|
80
|
+
<Button variant="ghost" size="icon" onclick={() => (editor.imageCropLayer = null)}>
|
|
81
|
+
<XIcon />
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="flex-1 resize-none overflow-y-auto">
|
|
85
|
+
<div class="flex items-center gap-2">
|
|
86
|
+
<div class="grow">
|
|
87
|
+
<Slider
|
|
88
|
+
type="single"
|
|
89
|
+
{value}
|
|
90
|
+
{min}
|
|
91
|
+
{max}
|
|
92
|
+
{step}
|
|
93
|
+
onValueChange={(value) => setImageRotate(value)}
|
|
94
|
+
onValueCommit={() => (layerCache = null)}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
<Input
|
|
98
|
+
class="w-12 bg-transparent p-0 text-center font-semibold leading-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
99
|
+
inputmode="decimal"
|
|
100
|
+
placeholder="--"
|
|
101
|
+
{value}
|
|
102
|
+
onchange={(e) => setImageRotate(e.currentTarget.value)}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex justify-end gap-2 p-2">
|
|
106
|
+
<Button class="flex-1" variant="outline" onclick={() => (editor.imageCropLayer = null)}>
|
|
107
|
+
Cancel
|
|
108
|
+
</Button>
|
|
109
|
+
<Button class="flex-1" onclick={applyChanges}>Done</Button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
<div class="relative flex h-full shrink-0 flex-col gap-2">
|
|
48
48
|
{#if editor.fileDragged}
|
|
49
49
|
<div
|
|
50
|
-
class="
|
|
50
|
+
class="absolute inset-0 z-10 flex h-full w-full flex-col items-center justify-center bg-background"
|
|
51
51
|
>
|
|
52
52
|
<div class="grid gap-6">
|
|
53
53
|
<div class="grid justify-center">
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<ImageIcon class="h-4 w-4" />
|
|
61
61
|
<p class="text-sm font-semibold">Images</p>
|
|
62
62
|
</div>
|
|
63
|
-
<p class="text-muted-foreground
|
|
63
|
+
<p class="text-xs text-muted-foreground">GIF, JPG, PNG</p>
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
class="col-span-3"
|
|
92
92
|
/>
|
|
93
93
|
{#if error}
|
|
94
|
-
<div class="
|
|
94
|
+
<div class="col-span-3 col-start-2 text-sm font-medium text-destructive">
|
|
95
95
|
Invalid
|
|
96
96
|
</div>
|
|
97
97
|
{/if}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import PositionSidebar from './position-sidebar.svelte';
|
|
11
11
|
import { ColorSidebar } from './color-sidebar/index.js';
|
|
12
12
|
import { FontSidebar } from './font-sidebar/index.js';
|
|
13
|
+
import ImageCropSidebar from './image-crop-sidebar.svelte';
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
editor: Editor;
|
|
@@ -62,13 +63,15 @@
|
|
|
62
63
|
{/each}
|
|
63
64
|
</div>
|
|
64
65
|
|
|
65
|
-
{#if editor.activeSidebarPopup || editor.activeSidebarTab}
|
|
66
|
-
<div class="
|
|
67
|
-
{#if editor.
|
|
66
|
+
{#if editor.activeSidebarPopup || editor.activeSidebarTab || editor.imageCropLayer}
|
|
67
|
+
<div class="h-full w-80 shrink-0 overflow-y-auto border-r border-gray-200 bg-background p-2">
|
|
68
|
+
{#if editor.imageCropLayer}
|
|
69
|
+
<ImageCropSidebar layer={editor.imageCropLayer} />
|
|
70
|
+
{:else if editor.activeSidebarPopup === 'position'}
|
|
68
71
|
<PositionSidebar />
|
|
69
72
|
{:else if editor.activeSidebarPopup === 'font'}
|
|
70
73
|
<FontSidebar />
|
|
71
|
-
{:else if editor.activeSidebarPopup}
|
|
74
|
+
{:else if editor.activeSidebarPopup?.indexOf('Color')}
|
|
72
75
|
<ColorSidebar />
|
|
73
76
|
{:else if editor.activeSidebarTab === 'text'}
|
|
74
77
|
<SidebarTextTab />
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
<div class="absolute h-full w-full">
|
|
14
14
|
{#each guides as guide (guide.id)}
|
|
15
15
|
{#if guide.type === 'box'}
|
|
16
|
-
<div class="border-primary
|
|
16
|
+
<div class="absolute border border-primary" style:inset="{guide.inset * zoom}px"></div>
|
|
17
17
|
{:else if guide.type === 'vertical'}
|
|
18
18
|
<div
|
|
19
19
|
class={cn(
|
|
20
|
-
'
|
|
20
|
+
'absolute h-full border-l border-primary',
|
|
21
21
|
guide.style === 'dashed' && 'border-dashed',
|
|
22
22
|
guide.style === 'dotted' && 'border-dotted',
|
|
23
23
|
)}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
{:else if guide.type === 'horizontal'}
|
|
29
29
|
<div
|
|
30
30
|
class={cn(
|
|
31
|
-
'
|
|
31
|
+
'absolute w-full border-t border-primary',
|
|
32
32
|
guide.style === 'dashed' && 'border-dashed',
|
|
33
33
|
guide.style === 'dotted' && 'border-dotted',
|
|
34
34
|
)}
|