react-svg-canvas 0.1.1 → 0.1.2
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/lib/geometry/resize.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export declare function getAnchorForHandle(handle: ResizeHandle): Point;
|
|
|
17
17
|
* The anchor is rotated around the pivot point.
|
|
18
18
|
*/
|
|
19
19
|
export declare function getRotatedAnchorPosition(bounds: Bounds, anchor: Point, pivot: Point, rotationMatrix: RotationMatrix): Point;
|
|
20
|
+
/**
|
|
21
|
+
* Determine which dimension should drive the resize for aspect ratio constraint.
|
|
22
|
+
* For edge handles, the edge determines the driver.
|
|
23
|
+
* For corner handles, use proportional change to determine which dimension drives.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getDriverDimension(handle: ResizeHandle, localDx: number, localDy: number, originalWidth: number, originalHeight: number): 'width' | 'height';
|
|
20
26
|
/**
|
|
21
27
|
* Calculate new size based on handle and mouse delta in object-local space.
|
|
22
28
|
*/
|
|
@@ -67,6 +73,7 @@ export declare function initResizeState(startPoint: Point, handle: ResizeHandle,
|
|
|
67
73
|
* @param currentPoint - Current mouse/pointer position in canvas coords
|
|
68
74
|
* @param minWidth - Minimum allowed width (default 10)
|
|
69
75
|
* @param minHeight - Minimum allowed height (default 10)
|
|
76
|
+
* @param aspectRatio - Optional aspect ratio constraint (width/height). When provided, resize maintains this ratio.
|
|
70
77
|
* @returns New bounds with position adjusted to keep anchor fixed
|
|
71
78
|
*/
|
|
72
|
-
export declare function calculateResizeBounds(state: ResizeState, currentPoint: Point, minWidth?: number, minHeight?: number): Bounds;
|
|
79
|
+
export declare function calculateResizeBounds(state: ResizeState, currentPoint: Point, minWidth?: number, minHeight?: number, aspectRatio?: number): Bounds;
|
package/lib/geometry/resize.js
CHANGED
|
@@ -37,6 +37,22 @@ export function getRotatedAnchorPosition(bounds, anchor, pivot, rotationMatrix)
|
|
|
37
37
|
y: pivotAbsY + (anchorLocalX - pivotAbsX) * rotationMatrix.sin + (anchorLocalY - pivotAbsY) * rotationMatrix.cos
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Determine which dimension should drive the resize for aspect ratio constraint.
|
|
42
|
+
* For edge handles, the edge determines the driver.
|
|
43
|
+
* For corner handles, use proportional change to determine which dimension drives.
|
|
44
|
+
*/
|
|
45
|
+
export function getDriverDimension(handle, localDx, localDy, originalWidth, originalHeight) {
|
|
46
|
+
// Edge handles: the edge determines the driver
|
|
47
|
+
if (handle === 'e' || handle === 'w')
|
|
48
|
+
return 'width';
|
|
49
|
+
if (handle === 'n' || handle === 's')
|
|
50
|
+
return 'height';
|
|
51
|
+
// Corner handles: use proportional change to determine driver
|
|
52
|
+
const propX = Math.abs(localDx) / originalWidth;
|
|
53
|
+
const propY = Math.abs(localDy) / originalHeight;
|
|
54
|
+
return propX >= propY ? 'width' : 'height';
|
|
55
|
+
}
|
|
40
56
|
/**
|
|
41
57
|
* Calculate new size based on handle and mouse delta in object-local space.
|
|
42
58
|
*/
|
|
@@ -133,19 +149,49 @@ export function initResizeState(startPoint, handle, bounds, pivot, rotation) {
|
|
|
133
149
|
* @param currentPoint - Current mouse/pointer position in canvas coords
|
|
134
150
|
* @param minWidth - Minimum allowed width (default 10)
|
|
135
151
|
* @param minHeight - Minimum allowed height (default 10)
|
|
152
|
+
* @param aspectRatio - Optional aspect ratio constraint (width/height). When provided, resize maintains this ratio.
|
|
136
153
|
* @returns New bounds with position adjusted to keep anchor fixed
|
|
137
154
|
*/
|
|
138
|
-
export function calculateResizeBounds(state, currentPoint, minWidth = 10, minHeight = 10) {
|
|
155
|
+
export function calculateResizeBounds(state, currentPoint, minWidth = 10, minHeight = 10, aspectRatio) {
|
|
139
156
|
// Calculate screen delta
|
|
140
157
|
const screenDx = currentPoint.x - state.startX;
|
|
141
158
|
const screenDy = currentPoint.y - state.startY;
|
|
142
159
|
// Un-rotate to get local (object space) delta
|
|
143
160
|
const [localDx, localDy] = unrotateDeltaWithMatrix(screenDx, screenDy, state.rotationMatrix);
|
|
144
|
-
// Calculate new dimensions
|
|
161
|
+
// Calculate new dimensions (unconstrained first)
|
|
145
162
|
let { width, height } = calculateResizedDimensions(state.handle, state.originalBounds.width, state.originalBounds.height, localDx, localDy);
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
// Apply aspect ratio constraint if provided
|
|
164
|
+
if (aspectRatio !== undefined) {
|
|
165
|
+
const driver = getDriverDimension(state.handle, localDx, localDy, state.originalBounds.width, state.originalBounds.height);
|
|
166
|
+
if (driver === 'width') {
|
|
167
|
+
// Width drives, calculate height from aspect ratio
|
|
168
|
+
height = width / aspectRatio;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Height drives, calculate width from aspect ratio
|
|
172
|
+
width = height * aspectRatio;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Enforce minimum size (maintaining aspect ratio if constrained)
|
|
176
|
+
if (aspectRatio !== undefined) {
|
|
177
|
+
// Calculate effective minimum considering aspect ratio
|
|
178
|
+
const minByWidth = minWidth;
|
|
179
|
+
const minByHeight = minHeight * aspectRatio;
|
|
180
|
+
if (width < minByWidth || height < minHeight) {
|
|
181
|
+
if (minByWidth >= minByHeight) {
|
|
182
|
+
width = minByWidth;
|
|
183
|
+
height = width / aspectRatio;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
height = minHeight;
|
|
187
|
+
width = height * aspectRatio;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
width = Math.max(width, minWidth);
|
|
193
|
+
height = Math.max(height, minHeight);
|
|
194
|
+
}
|
|
149
195
|
// Calculate position to keep anchor fixed
|
|
150
196
|
const position = calculateResizedPosition(width, height, state.anchor, state.pivot, state.anchorScreenPos, state.rotationMatrix);
|
|
151
197
|
return {
|
|
@@ -75,6 +75,17 @@ export interface UseResizableOptions {
|
|
|
75
75
|
* Returns { x: pivotX, y: pivotY } in normalized coordinates (0-1).
|
|
76
76
|
*/
|
|
77
77
|
getPivot?: () => Point;
|
|
78
|
+
/**
|
|
79
|
+
* Aspect ratio constraint (width/height).
|
|
80
|
+
* When provided, resize maintains this ratio.
|
|
81
|
+
* For images, typically originalWidth / originalHeight.
|
|
82
|
+
*/
|
|
83
|
+
aspectRatio?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Optional getter for fresh aspect ratio at resize start.
|
|
86
|
+
* Use this when working with CRDT/external state to avoid stale closures.
|
|
87
|
+
*/
|
|
88
|
+
getAspectRatio?: () => number | undefined;
|
|
78
89
|
}
|
|
79
90
|
export interface UseResizableReturn {
|
|
80
91
|
/** Whether currently resizing */
|
|
@@ -38,7 +38,8 @@ export function useResizable(options) {
|
|
|
38
38
|
resizeState: null,
|
|
39
39
|
element: null,
|
|
40
40
|
pointerId: -1,
|
|
41
|
-
currentBounds: { x: 0, y: 0, width: 0, height: 0 }
|
|
41
|
+
currentBounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
42
|
+
aspectRatio: undefined
|
|
42
43
|
});
|
|
43
44
|
const optionsRef = React.useRef(options);
|
|
44
45
|
optionsRef.current = options;
|
|
@@ -56,11 +57,14 @@ export function useResizable(options) {
|
|
|
56
57
|
const { bounds, rotation = 0, pivotX = 0.5, pivotY = 0.5 } = optionsRef.current;
|
|
57
58
|
// Initialize rotation-aware resize state
|
|
58
59
|
const resizeState = initResizeState({ x: coords.x, y: coords.y }, handle, bounds, { x: pivotX, y: pivotY }, rotation);
|
|
60
|
+
// Capture aspect ratio at resize start (from getter if provided, else from prop)
|
|
61
|
+
const aspectRatio = optionsRef.current.getAspectRatio?.() ?? optionsRef.current.aspectRatio;
|
|
59
62
|
stateRef.current = {
|
|
60
63
|
resizeState,
|
|
61
64
|
element,
|
|
62
65
|
pointerId: e.pointerId,
|
|
63
|
-
currentBounds: { ...bounds }
|
|
66
|
+
currentBounds: { ...bounds },
|
|
67
|
+
aspectRatio
|
|
64
68
|
};
|
|
65
69
|
setIsResizing(true);
|
|
66
70
|
setActiveHandle(handle);
|
|
@@ -69,7 +73,7 @@ export function useResizable(options) {
|
|
|
69
73
|
bounds: { ...bounds }
|
|
70
74
|
});
|
|
71
75
|
const handlePointerMove = (moveEvent) => {
|
|
72
|
-
const { resizeState, element, pointerId } = stateRef.current;
|
|
76
|
+
const { resizeState, element, pointerId, aspectRatio } = stateRef.current;
|
|
73
77
|
if (!resizeState || !element || moveEvent.pointerId !== pointerId)
|
|
74
78
|
return;
|
|
75
79
|
const opts = optionsRef.current;
|
|
@@ -77,8 +81,8 @@ export function useResizable(options) {
|
|
|
77
81
|
// Calculate delta for snapping
|
|
78
82
|
const deltaX = moveCoords.x - resizeState.startX;
|
|
79
83
|
const deltaY = moveCoords.y - resizeState.startY;
|
|
80
|
-
// Calculate new bounds (rotation-aware)
|
|
81
|
-
let newBounds = calculateResizeBounds(resizeState, { x: moveCoords.x, y: moveCoords.y }, opts.minWidth ?? 10, opts.minHeight ?? 10);
|
|
84
|
+
// Calculate new bounds (rotation-aware, with aspect ratio constraint if provided)
|
|
85
|
+
let newBounds = calculateResizeBounds(resizeState, { x: moveCoords.x, y: moveCoords.y }, opts.minWidth ?? 10, opts.minHeight ?? 10, aspectRatio);
|
|
82
86
|
// Apply snapping if available
|
|
83
87
|
if (opts.snapResize && opts.objectId) {
|
|
84
88
|
const snapResult = opts.snapResize({
|
|
@@ -124,7 +128,8 @@ export function useResizable(options) {
|
|
|
124
128
|
resizeState: null,
|
|
125
129
|
element: null,
|
|
126
130
|
pointerId: -1,
|
|
127
|
-
currentBounds: { x: 0, y: 0, width: 0, height: 0 }
|
|
131
|
+
currentBounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
132
|
+
aspectRatio: undefined
|
|
128
133
|
};
|
|
129
134
|
setIsResizing(false);
|
|
130
135
|
setActiveHandle(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-svg-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "React library for building interactive SVG canvas applications with pan, zoom, selection, drag-and-drop, resize, and Figma-style snapping",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|