@tsdraw/core 0.8.3 → 0.8.5

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/index.d.cts CHANGED
@@ -149,8 +149,37 @@ declare class InputManager {
149
149
  getInputs(): PointerInput;
150
150
  }
151
151
 
152
+ interface Viewport {
153
+ x: number;
154
+ y: number;
155
+ zoom: number;
156
+ }
157
+ declare function createViewport(): Viewport;
158
+ declare function screenToPage(viewport: Viewport, screenX: number, screenY: number): {
159
+ x: number;
160
+ y: number;
161
+ };
162
+ declare function pageToScreen(viewport: Viewport, pageX: number, pageY: number): {
163
+ x: number;
164
+ y: number;
165
+ };
166
+ declare function setViewport(viewport: Viewport, updater: {
167
+ x?: number;
168
+ y?: number;
169
+ zoom?: number;
170
+ }): Viewport;
171
+ declare function panViewport(viewport: Viewport, dx: number, dy: number): Viewport;
172
+ interface ZoomRange {
173
+ min: number;
174
+ max: number;
175
+ }
176
+ declare const DEFAULT_ZOOM_RANGE: ZoomRange;
177
+ declare function clampZoom(zoom: number, range?: ZoomRange): number;
178
+ declare function zoomViewport(viewport: Viewport, factor: number, centerX?: number, centerY?: number, zoomRange?: ZoomRange): Viewport;
179
+
152
180
  interface IEditor {
153
181
  getZoomLevel(): number;
182
+ readonly viewport: Viewport;
154
183
  options: {
155
184
  dragDistanceSquared: number;
156
185
  };
@@ -180,15 +209,20 @@ interface IEditor {
180
209
  fill: FillStyle;
181
210
  size: SizeStyle;
182
211
  }>): void;
212
+ setViewport(partial: Partial<Viewport>): void;
183
213
  panBy(dx: number, dy: number): void;
184
214
  }
185
215
 
186
216
  interface ToolPointerDownInfo {
187
217
  point: Vec3;
218
+ screenX?: number;
219
+ screenY?: number;
188
220
  }
189
221
  interface ToolPointerMoveInfo {
190
222
  screenDeltaX?: number;
191
223
  screenDeltaY?: number;
224
+ screenX?: number;
225
+ screenY?: number;
192
226
  }
193
227
  interface ToolKeyInfo {
194
228
  key: string;
@@ -217,27 +251,29 @@ declare abstract class StateNode {
217
251
  onInterrupt(): void;
218
252
  }
219
253
 
220
- interface Viewport {
221
- x: number;
222
- y: number;
223
- zoom: number;
224
- }
225
- declare function createViewport(): Viewport;
226
- declare function screenToPage(viewport: Viewport, screenX: number, screenY: number): {
254
+ interface CameraPanSession {
255
+ initialViewportX: number;
256
+ initialViewportY: number;
257
+ originScreenX: number;
258
+ originScreenY: number;
259
+ velocityX: number;
260
+ velocityY: number;
261
+ previousScreenX: number;
262
+ previousScreenY: number;
263
+ lastMoveTime: number;
264
+ }
265
+ declare function beginCameraPan(viewport: Viewport, screenX: number, screenY: number): CameraPanSession;
266
+ declare function moveCameraPan(session: CameraPanSession, currentScreenX: number, currentScreenY: number): {
227
267
  x: number;
228
268
  y: number;
229
269
  };
230
- declare function pageToScreen(viewport: Viewport, pageX: number, pageY: number): {
231
- x: number;
232
- y: number;
233
- };
234
- declare function setViewport(viewport: Viewport, updater: {
235
- x?: number;
236
- y?: number;
237
- zoom?: number;
238
- }): Viewport;
239
- declare function panViewport(viewport: Viewport, dx: number, dy: number): Viewport;
240
- declare function zoomViewport(viewport: Viewport, factor: number, centerX?: number, centerY?: number): Viewport;
270
+ interface CameraSlideAnimation {
271
+ stop: () => void;
272
+ }
273
+ interface CameraSlideOptions {
274
+ friction?: number;
275
+ }
276
+ declare function startCameraSlide(session: CameraPanSession, applyPan: (dx: number, dy: number) => void, onFrame: () => void, slideOptions?: CameraSlideOptions): CameraSlideAnimation | null;
241
277
 
242
278
  type TsdrawRenderTheme = 'light' | 'dark';
243
279
  declare function resolveThemeColor(colorStyle: string, theme: TsdrawRenderTheme): string;
@@ -286,6 +322,7 @@ interface EditorOptions {
286
322
  dragDistanceSquared?: number;
287
323
  toolDefinitions?: ToolDefinition[];
288
324
  initialToolId?: ToolId;
325
+ zoomRange?: ZoomRange;
289
326
  }
290
327
  type EditorListener = () => void;
291
328
  declare class Editor {
@@ -296,6 +333,7 @@ declare class Editor {
296
333
  viewport: Viewport;
297
334
  readonly options: {
298
335
  dragDistanceSquared: number;
336
+ zoomRange?: ZoomRange;
299
337
  };
300
338
  private drawStyle;
301
339
  private readonly toolStateContext;
@@ -608,8 +646,12 @@ declare class HandIdleState extends StateNode {
608
646
 
609
647
  declare class HandDraggingState extends StateNode {
610
648
  static id: string;
649
+ private panSession;
650
+ onEnter(info?: ToolStateTransitionInfo): void;
611
651
  onPointerMove(info?: ToolPointerMoveInfo): void;
652
+ getPanSession(): CameraPanSession | null;
612
653
  onPointerUp(): void;
654
+ onExit(): void;
613
655
  onCancel(): void;
614
656
  onInterrupt(): void;
615
657
  }
@@ -625,4 +667,4 @@ declare function decodePathToPoints(segments: {
625
667
  y: number;
626
668
  }[];
627
669
 
628
- export { type Bounds, CanvasRenderer, CircleDrawingState, CircleIdleState, type ColorStyle, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DocumentStoreSnapshot, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, type FillStyle, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, SquareDrawingState, SquareIdleState, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawDocumentSnapshot, type TsdrawEditorSnapshot, type TsdrawHistorySnapshot, type TsdrawPageRecord, type TsdrawPersistedRecord, type TsdrawRenderTheme, type TsdrawSessionStateSnapshot, type TsdrawShapeRecord, type Vec3, type Viewport, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
670
+ export { type Bounds, type CameraPanSession, type CameraSlideAnimation, type CameraSlideOptions, CanvasRenderer, CircleDrawingState, CircleIdleState, type ColorStyle, DEFAULT_COLORS, DEFAULT_ZOOM_RANGE, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DocumentStoreSnapshot, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, type FillStyle, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, SquareDrawingState, SquareIdleState, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawDocumentSnapshot, type TsdrawEditorSnapshot, type TsdrawHistorySnapshot, type TsdrawPageRecord, type TsdrawPersistedRecord, type TsdrawRenderTheme, type TsdrawSessionStateSnapshot, type TsdrawShapeRecord, type Vec3, type Viewport, type ZoomRange, applyMove, applyResize, applyRotation, beginCameraPan, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, clampZoom, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, moveCameraPan, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, startCameraSlide, zoomViewport };
package/dist/index.d.ts CHANGED
@@ -149,8 +149,37 @@ declare class InputManager {
149
149
  getInputs(): PointerInput;
150
150
  }
151
151
 
152
+ interface Viewport {
153
+ x: number;
154
+ y: number;
155
+ zoom: number;
156
+ }
157
+ declare function createViewport(): Viewport;
158
+ declare function screenToPage(viewport: Viewport, screenX: number, screenY: number): {
159
+ x: number;
160
+ y: number;
161
+ };
162
+ declare function pageToScreen(viewport: Viewport, pageX: number, pageY: number): {
163
+ x: number;
164
+ y: number;
165
+ };
166
+ declare function setViewport(viewport: Viewport, updater: {
167
+ x?: number;
168
+ y?: number;
169
+ zoom?: number;
170
+ }): Viewport;
171
+ declare function panViewport(viewport: Viewport, dx: number, dy: number): Viewport;
172
+ interface ZoomRange {
173
+ min: number;
174
+ max: number;
175
+ }
176
+ declare const DEFAULT_ZOOM_RANGE: ZoomRange;
177
+ declare function clampZoom(zoom: number, range?: ZoomRange): number;
178
+ declare function zoomViewport(viewport: Viewport, factor: number, centerX?: number, centerY?: number, zoomRange?: ZoomRange): Viewport;
179
+
152
180
  interface IEditor {
153
181
  getZoomLevel(): number;
182
+ readonly viewport: Viewport;
154
183
  options: {
155
184
  dragDistanceSquared: number;
156
185
  };
@@ -180,15 +209,20 @@ interface IEditor {
180
209
  fill: FillStyle;
181
210
  size: SizeStyle;
182
211
  }>): void;
212
+ setViewport(partial: Partial<Viewport>): void;
183
213
  panBy(dx: number, dy: number): void;
184
214
  }
185
215
 
186
216
  interface ToolPointerDownInfo {
187
217
  point: Vec3;
218
+ screenX?: number;
219
+ screenY?: number;
188
220
  }
189
221
  interface ToolPointerMoveInfo {
190
222
  screenDeltaX?: number;
191
223
  screenDeltaY?: number;
224
+ screenX?: number;
225
+ screenY?: number;
192
226
  }
193
227
  interface ToolKeyInfo {
194
228
  key: string;
@@ -217,27 +251,29 @@ declare abstract class StateNode {
217
251
  onInterrupt(): void;
218
252
  }
219
253
 
220
- interface Viewport {
221
- x: number;
222
- y: number;
223
- zoom: number;
224
- }
225
- declare function createViewport(): Viewport;
226
- declare function screenToPage(viewport: Viewport, screenX: number, screenY: number): {
254
+ interface CameraPanSession {
255
+ initialViewportX: number;
256
+ initialViewportY: number;
257
+ originScreenX: number;
258
+ originScreenY: number;
259
+ velocityX: number;
260
+ velocityY: number;
261
+ previousScreenX: number;
262
+ previousScreenY: number;
263
+ lastMoveTime: number;
264
+ }
265
+ declare function beginCameraPan(viewport: Viewport, screenX: number, screenY: number): CameraPanSession;
266
+ declare function moveCameraPan(session: CameraPanSession, currentScreenX: number, currentScreenY: number): {
227
267
  x: number;
228
268
  y: number;
229
269
  };
230
- declare function pageToScreen(viewport: Viewport, pageX: number, pageY: number): {
231
- x: number;
232
- y: number;
233
- };
234
- declare function setViewport(viewport: Viewport, updater: {
235
- x?: number;
236
- y?: number;
237
- zoom?: number;
238
- }): Viewport;
239
- declare function panViewport(viewport: Viewport, dx: number, dy: number): Viewport;
240
- declare function zoomViewport(viewport: Viewport, factor: number, centerX?: number, centerY?: number): Viewport;
270
+ interface CameraSlideAnimation {
271
+ stop: () => void;
272
+ }
273
+ interface CameraSlideOptions {
274
+ friction?: number;
275
+ }
276
+ declare function startCameraSlide(session: CameraPanSession, applyPan: (dx: number, dy: number) => void, onFrame: () => void, slideOptions?: CameraSlideOptions): CameraSlideAnimation | null;
241
277
 
242
278
  type TsdrawRenderTheme = 'light' | 'dark';
243
279
  declare function resolveThemeColor(colorStyle: string, theme: TsdrawRenderTheme): string;
@@ -286,6 +322,7 @@ interface EditorOptions {
286
322
  dragDistanceSquared?: number;
287
323
  toolDefinitions?: ToolDefinition[];
288
324
  initialToolId?: ToolId;
325
+ zoomRange?: ZoomRange;
289
326
  }
290
327
  type EditorListener = () => void;
291
328
  declare class Editor {
@@ -296,6 +333,7 @@ declare class Editor {
296
333
  viewport: Viewport;
297
334
  readonly options: {
298
335
  dragDistanceSquared: number;
336
+ zoomRange?: ZoomRange;
299
337
  };
300
338
  private drawStyle;
301
339
  private readonly toolStateContext;
@@ -608,8 +646,12 @@ declare class HandIdleState extends StateNode {
608
646
 
609
647
  declare class HandDraggingState extends StateNode {
610
648
  static id: string;
649
+ private panSession;
650
+ onEnter(info?: ToolStateTransitionInfo): void;
611
651
  onPointerMove(info?: ToolPointerMoveInfo): void;
652
+ getPanSession(): CameraPanSession | null;
612
653
  onPointerUp(): void;
654
+ onExit(): void;
613
655
  onCancel(): void;
614
656
  onInterrupt(): void;
615
657
  }
@@ -625,4 +667,4 @@ declare function decodePathToPoints(segments: {
625
667
  y: number;
626
668
  }[];
627
669
 
628
- export { type Bounds, CanvasRenderer, CircleDrawingState, CircleIdleState, type ColorStyle, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DocumentStoreSnapshot, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, type FillStyle, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, SquareDrawingState, SquareIdleState, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawDocumentSnapshot, type TsdrawEditorSnapshot, type TsdrawHistorySnapshot, type TsdrawPageRecord, type TsdrawPersistedRecord, type TsdrawRenderTheme, type TsdrawSessionStateSnapshot, type TsdrawShapeRecord, type Vec3, type Viewport, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
670
+ export { type Bounds, type CameraPanSession, type CameraSlideAnimation, type CameraSlideOptions, CanvasRenderer, CircleDrawingState, CircleIdleState, type ColorStyle, DEFAULT_COLORS, DEFAULT_ZOOM_RANGE, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DocumentStoreSnapshot, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, type FillStyle, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, SquareDrawingState, SquareIdleState, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawDocumentSnapshot, type TsdrawEditorSnapshot, type TsdrawHistorySnapshot, type TsdrawPageRecord, type TsdrawPersistedRecord, type TsdrawRenderTheme, type TsdrawSessionStateSnapshot, type TsdrawShapeRecord, type Vec3, type Viewport, type ZoomRange, applyMove, applyResize, applyRotation, beginCameraPan, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, clampZoom, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, moveCameraPan, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, startCameraSlide, zoomViewport };
package/dist/index.js CHANGED
@@ -247,8 +247,13 @@ function setViewport(viewport, updater) {
247
247
  function panViewport(viewport, dx, dy) {
248
248
  return { ...viewport, x: viewport.x + dx, y: viewport.y + dy };
249
249
  }
250
- function zoomViewport(viewport, factor, centerX, centerY) {
251
- const zoom = Math.max(0.1, Math.min(4, viewport.zoom * factor));
250
+ var DEFAULT_ZOOM_RANGE = { min: 0.1, max: 4 };
251
+ function clampZoom(zoom, range) {
252
+ const { min, max } = range ?? DEFAULT_ZOOM_RANGE;
253
+ return Math.max(min, Math.min(max, zoom));
254
+ }
255
+ function zoomViewport(viewport, factor, centerX, centerY, zoomRange) {
256
+ const zoom = clampZoom(viewport.zoom * factor, zoomRange);
252
257
  if (centerX == null || centerY == null) {
253
258
  return { ...viewport, zoom };
254
259
  }
@@ -258,6 +263,90 @@ function zoomViewport(viewport, factor, centerX, centerY) {
258
263
  return { x, y, zoom };
259
264
  }
260
265
 
266
+ // src/canvas/cameraPan.ts
267
+ var VELOCITY_LERP_FACTOR = 0.5;
268
+ var VELOCITY_ZERO_THRESHOLD = 0.01;
269
+ function beginCameraPan(viewport, screenX, screenY) {
270
+ return {
271
+ initialViewportX: viewport.x,
272
+ initialViewportY: viewport.y,
273
+ originScreenX: screenX,
274
+ originScreenY: screenY,
275
+ velocityX: 0,
276
+ velocityY: 0,
277
+ previousScreenX: screenX,
278
+ previousScreenY: screenY,
279
+ lastMoveTime: performance.now()
280
+ };
281
+ }
282
+ function moveCameraPan(session, currentScreenX, currentScreenY) {
283
+ const now = performance.now();
284
+ const elapsed = now - session.lastMoveTime;
285
+ if (elapsed > 0) {
286
+ const moveDx = currentScreenX - session.previousScreenX;
287
+ const moveDy = currentScreenY - session.previousScreenY;
288
+ const moveLen = Math.hypot(moveDx, moveDy);
289
+ if (moveLen > 0) {
290
+ const dirX = moveDx / moveLen;
291
+ const dirY = moveDy / moveLen;
292
+ const speed = moveLen / elapsed;
293
+ session.velocityX += (dirX * speed - session.velocityX) * VELOCITY_LERP_FACTOR;
294
+ session.velocityY += (dirY * speed - session.velocityY) * VELOCITY_LERP_FACTOR;
295
+ }
296
+ if (Math.abs(session.velocityX) < VELOCITY_ZERO_THRESHOLD) session.velocityX = 0;
297
+ if (Math.abs(session.velocityY) < VELOCITY_ZERO_THRESHOLD) session.velocityY = 0;
298
+ }
299
+ session.previousScreenX = currentScreenX;
300
+ session.previousScreenY = currentScreenY;
301
+ session.lastMoveTime = now;
302
+ return {
303
+ x: session.initialViewportX + (currentScreenX - session.originScreenX),
304
+ y: session.initialViewportY + (currentScreenY - session.originScreenY)
305
+ };
306
+ }
307
+ var SLIDE_FRICTION = 0.92;
308
+ var SLIDE_MIN_SPEED = 0.01;
309
+ var SLIDE_MAX_SPEED = 2;
310
+ var SLIDE_MIN_VELOCITY_TO_START = 0.1;
311
+ function startCameraSlide(session, applyPan, onFrame, slideOptions) {
312
+ const friction = slideOptions?.friction ?? SLIDE_FRICTION;
313
+ const timeSinceLastMove = performance.now() - session.lastMoveTime;
314
+ const FRAME_DURATION = 16;
315
+ const decayFactor = Math.pow(1 - VELOCITY_LERP_FACTOR, timeSinceLastMove / FRAME_DURATION);
316
+ const effectiveVx = session.velocityX * decayFactor;
317
+ const effectiveVy = session.velocityY * decayFactor;
318
+ const speed = Math.hypot(effectiveVx, effectiveVy);
319
+ const clampedSpeed = Math.min(speed, SLIDE_MAX_SPEED);
320
+ if (clampedSpeed < SLIDE_MIN_VELOCITY_TO_START) return null;
321
+ const dirX = effectiveVx / speed;
322
+ const dirY = effectiveVy / speed;
323
+ let currentSpeed = clampedSpeed;
324
+ let lastTime = performance.now();
325
+ let rafId = 0;
326
+ const tick = () => {
327
+ const now = performance.now();
328
+ const elapsed = now - lastTime;
329
+ lastTime = now;
330
+ applyPan(dirX * currentSpeed * elapsed, dirY * currentSpeed * elapsed);
331
+ onFrame();
332
+ currentSpeed *= friction;
333
+ if (currentSpeed < SLIDE_MIN_SPEED) {
334
+ rafId = 0;
335
+ return;
336
+ }
337
+ rafId = requestAnimationFrame(tick);
338
+ };
339
+ rafId = requestAnimationFrame(tick);
340
+ return {
341
+ stop() {
342
+ if (rafId !== 0) {
343
+ cancelAnimationFrame(rafId);
344
+ rafId = 0;
345
+ }
346
+ }
347
+ };
348
+ }
349
+
261
350
  // src/utils/colors.ts
262
351
  var DARK_COLORS = {
263
352
  black: "#f0f0f0",
@@ -1490,16 +1579,29 @@ var HandIdleState = class extends StateNode {
1490
1579
  // src/tools/hand/states/HandDraggingState.ts
1491
1580
  var HandDraggingState = class extends StateNode {
1492
1581
  static id = "hand_dragging";
1582
+ panSession = null;
1583
+ onEnter(info) {
1584
+ const downInfo = info;
1585
+ const screenX = downInfo?.screenX ?? 0;
1586
+ const screenY = downInfo?.screenY ?? 0;
1587
+ this.panSession = beginCameraPan(this.editor.viewport, screenX, screenY);
1588
+ }
1493
1589
  onPointerMove(info) {
1494
- const move = info ?? {};
1495
- const dx = move.screenDeltaX ?? 0;
1496
- const dy = move.screenDeltaY ?? 0;
1497
- if (dx === 0 && dy === 0) return;
1498
- this.editor.panBy(dx, dy);
1590
+ if (!this.panSession) return;
1591
+ const screenX = info?.screenX ?? 0;
1592
+ const screenY = info?.screenY ?? 0;
1593
+ const target = moveCameraPan(this.panSession, screenX, screenY);
1594
+ this.editor.setViewport({ x: target.x, y: target.y });
1595
+ }
1596
+ getPanSession() {
1597
+ return this.panSession;
1499
1598
  }
1500
1599
  onPointerUp() {
1501
1600
  this.ctx.transition("hand_idle");
1502
1601
  }
1602
+ onExit() {
1603
+ this.panSession = null;
1604
+ }
1503
1605
  onCancel() {
1504
1606
  this.ctx.transition("hand_idle");
1505
1607
  }
@@ -1613,7 +1715,7 @@ var Editor = class {
1613
1715
  historyBatchChanged = false;
1614
1716
  // Creates a new editor instance with the given options (with defaults if not provided)
1615
1717
  constructor(opts = {}) {
1616
- this.options = { dragDistanceSquared: opts.dragDistanceSquared ?? DRAG_DISTANCE_SQUARED };
1718
+ this.options = { dragDistanceSquared: opts.dragDistanceSquared ?? DRAG_DISTANCE_SQUARED, zoomRange: opts.zoomRange };
1617
1719
  this.lastDocumentSnapshot = this.getDocumentSnapshot();
1618
1720
  this.store.listen(() => {
1619
1721
  this.captureDocumentHistory();
@@ -1727,7 +1829,7 @@ var Editor = class {
1727
1829
  this.viewport = {
1728
1830
  x: partial.x ?? this.viewport.x,
1729
1831
  y: partial.y ?? this.viewport.y,
1730
- zoom: Math.max(0.1, Math.min(4, rawZoom))
1832
+ zoom: clampZoom(rawZoom, this.options.zoomRange)
1731
1833
  };
1732
1834
  this.emitChange();
1733
1835
  }
@@ -1738,7 +1840,7 @@ var Editor = class {
1738
1840
  });
1739
1841
  }
1740
1842
  zoomAt(factor, screenX, screenY) {
1741
- this.viewport = zoomViewport(this.viewport, factor, screenX, screenY);
1843
+ this.viewport = zoomViewport(this.viewport, factor, screenX, screenY, this.options.zoomRange);
1742
1844
  this.emitChange();
1743
1845
  }
1744
1846
  deleteShapes(ids) {
@@ -2158,6 +2260,6 @@ function applyResize(editor, handle, startBounds, startShapes, pointer, lockAspe
2158
2260
  }
2159
2261
  }
2160
2262
 
2161
- export { CanvasRenderer, CircleDrawingState, CircleIdleState, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, DocumentStore, ERASER_MARGIN, Editor, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, InputManager, MAX_POINTS_PER_SHAPE, PenDrawingState, PenIdleState, STROKE_WIDTHS, SelectIdleState, SquareDrawingState, SquareIdleState, StateNode, ToolManager, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds2 as getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
2263
+ export { CanvasRenderer, CircleDrawingState, CircleIdleState, DEFAULT_COLORS, DEFAULT_ZOOM_RANGE, DRAG_DISTANCE_SQUARED, DocumentStore, ERASER_MARGIN, Editor, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, InputManager, MAX_POINTS_PER_SHAPE, PenDrawingState, PenIdleState, STROKE_WIDTHS, SelectIdleState, SquareDrawingState, SquareIdleState, StateNode, ToolManager, applyMove, applyResize, applyRotation, beginCameraPan, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, clampZoom, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds2 as getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, moveCameraPan, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, startCameraSlide, zoomViewport };
2162
2264
  //# sourceMappingURL=index.js.map
2163
2265
  //# sourceMappingURL=index.js.map