@ai-markdown/core 3.4.0 → 3.4.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.
@@ -413,7 +413,11 @@ interface ImageGeometry {
413
413
  }
414
414
  export declare const initialImageTransform: () => ImageTransform;
415
415
  export declare function imageTransformStyle(transform: ImageTransform): string;
416
- export declare function zoomImage(transform: ImageTransform, ratio: number, geometry: ImageGeometry, centerX?: number, centerY?: number, touch?: boolean): ImageTransform;
416
+ /** Scales by `ratio` about a viewport point, keeping the image point under it in place, within `limits`. */
417
+ export declare function zoomImage(transform: ImageTransform, ratio: number, geometry: ImageGeometry, centerX?: number, centerY?: number, touch?: boolean, limits?: {
418
+ min: number;
419
+ max: number;
420
+ }): ImageTransform;
417
421
  /**
418
422
  * The zoom factor for one wheel event. A full notch (100 pixels) is one 1.5x
419
423
  * step, and a smaller delta a proportional part of it.
@@ -421,13 +425,14 @@ export declare function zoomImage(transform: ImageTransform, ratio: number, geom
421
425
  * pages, not pixels: three lines or one page count as a notch.
422
426
  * - `pinch` is a trackpad pinch, which arrives as a wheel event with the
423
427
  * control key set and small deltas. It scales continuously with the delta.
428
+ * - `step` is what a full notch adds to the scale factor: 0.5 makes it 1.5x.
424
429
  */
425
- export declare function imageWheelRatio(delta: number, mode?: number, pinch?: boolean): number;
430
+ export declare function imageWheelRatio(delta: number, mode?: number, pinch?: boolean, step?: number): number;
426
431
  /** Snap a moved image back inside the viewport, taking quarter turns into account. */
427
432
  export declare function reboundImage(transform: ImageTransform, geometry: ImageGeometry, bounds: {
428
433
  left: number;
429
434
  top: number;
430
- }): ImageTransform;
435
+ }, minScale?: number): ImageTransform;
431
436
  /** Upstream pinch center weights each finger by how far it moved. */
432
437
  export declare function imagePinchCenter(oldA: {
433
438
  x: number;
@@ -464,6 +469,14 @@ interface ImagePreviewState {
464
469
  status: ImageLoadStatus;
465
470
  /** A drag or a pinch is in progress: the image follows the pointer without a transition. */
466
471
  moving: boolean;
472
+ /** The image can be dragged and pinched (the `movable` option). */
473
+ movable: boolean;
474
+ /** Viewport point the preview grows out of and shrinks back into: the anchor of the image shown when it opened,
475
+ * then of the image shown when it closes. Undefined means the centre of the viewport. */
476
+ origin: {
477
+ x: number;
478
+ y: number;
479
+ } | undefined;
467
480
  canPrevious: boolean;
468
481
  canNext: boolean;
469
482
  canZoomIn: boolean;
@@ -475,12 +488,46 @@ interface ImagePreviewHost {
475
488
  geometry(): ImageGeometry | undefined;
476
489
  /** Runs the callback before the next paint and returns a cancel function. `requestAnimationFrame` in a browser. */
477
490
  frame(callback: () => void): () => void;
491
+ /** Where the image registered under `id` sits on the page, as a viewport point: the centre of its thumbnail.
492
+ * Undefined when it has none or it is out of view. Asked when the preview opens and again when it closes. */
493
+ anchor?(id: object): {
494
+ x: number;
495
+ y: number;
496
+ } | undefined;
478
497
  }
479
498
  interface ImagePreviewOptions {
480
499
  /** How long the leaving style stays up, in milliseconds. Zero closes at once. A function is read at each close,
481
500
  * so a host can answer zero while the user prefers reduced motion. Default 300. */
482
501
  closeDuration?: number | (() => number);
502
+ /** What one zoom step adds to the scale factor: the toolbar, a double click and a full wheel notch multiply or
503
+ * divide by `1 + scaleStep`. Default 0.5. */
504
+ scaleStep?: number;
505
+ /** Smallest scale the toolbar and the wheel reach, and the one a pinch returns to. Default 1. */
506
+ minScale?: number;
507
+ /** Largest scale. Default 50. */
508
+ maxScale?: number;
509
+ /** Whether the image can be dragged and pinched. Default true. */
510
+ movable?: boolean;
511
+ /** Whether the wheel and a trackpad pinch zoom. Default true. */
512
+ wheel?: boolean;
513
+ /** The preview started to open (true) or to close (false). */
514
+ onOpenChange?(open: boolean, info: {
515
+ current: number;
516
+ item: ImageGalleryItem | undefined;
517
+ }): void;
518
+ /** An opening or a closing ran to its end. One that was interrupted, by reopening while it closed or by
519
+ * `dispose`, reports nothing. */
520
+ afterOpenChange?(open: boolean): void;
521
+ /** The user moved to another image of the gallery. */
522
+ onChange?(current: number, previous: number): void;
523
+ /** The transform changed. A drag or a wheel burst reports once per frame, with the last action. */
524
+ onTransform?(info: {
525
+ transform: ImageTransform;
526
+ action: ImagePreviewAction;
527
+ }): void;
483
528
  }
529
+ /** What changed the transform, as reported to `onTransform`. */
530
+ type ImagePreviewAction = 'zoomIn' | 'zoomOut' | 'rotateLeft' | 'rotateRight' | 'flipX' | 'flipY' | 'wheel' | 'doubleClick' | 'move' | 'pinch' | 'rebound' | 'prev' | 'next';
484
531
  interface ImagePointer {
485
532
  /** Pointer id: one drag, or one finger of a pinch. */
486
533
  id: number;
@@ -507,8 +554,10 @@ interface ImagePointer {
507
554
  * else is published at once. Each update is applied to the current state, so
508
555
  * several in one frame add up.
509
556
  * - Switching images resets the transform. Zoom is limited to 1x to 50x in
510
- * 1.5x steps, about the pointer for the wheel and a double click, about the
511
- * viewport centre for the toolbar.
557
+ * 1.5x steps unless the options say otherwise, about the pointer for the
558
+ * wheel and a double click, about the viewport centre for the toolbar.
559
+ * - The `on…` options report what the user did; they are called after the
560
+ * state that reflects it has been published.
512
561
  */
513
562
  export declare function createImagePreviewController(host: ImagePreviewHost, publish: (state: ImagePreviewState) => void, options?: ImagePreviewOptions): {
514
563
  state: () => ImagePreviewState;
@@ -536,7 +585,7 @@ export declare function createImagePreviewController(host: ImagePreviewHost, pub
536
585
  mode?: number;
537
586
  pinch?: boolean;
538
587
  }): boolean;
539
- /** Returns to the natural size, or zooms one step about the point when already there. */
588
+ /** Returns to the starting size, or zooms one step about the point when already there. */
540
589
  doubleClick(x: number, y: number): void;
541
590
  /** True when the key was used: prevent the default action. Left and right move through a gallery. */
542
591
  key(key: string): boolean;
@@ -565,4 +614,4 @@ export interface CodeBlockOptions {
565
614
  /** Text of the code block, as overrides of the English defaults. A function is called on every render. */
566
615
  labels?: LabelsOption<CodeBlockLabels>;
567
616
  }
568
- export type { CodeBlockInput, CodeBlockLabels, CodeBlockSnapshot, CodeFrame, CodeSource, DiagramEngine, DiagramError, DiagramLabels, DiagramRequest, DiagramState, DiagramView, ImageActionIconName, ImageGallery, ImageGalleryItem, ImageGeometry, ImageIconName, ImageLabels, ImageLoadStatus, ImagePointer, ImagePreviewHost, ImagePreviewOptions, ImagePreviewPhase, ImagePreviewState, ImageTransform, LabelsOption, MermaidInitTarget, TableExportReason, TableLabels, TableProjection };
617
+ export type { CodeBlockInput, CodeBlockLabels, CodeBlockSnapshot, CodeFrame, CodeSource, DiagramEngine, DiagramError, DiagramLabels, DiagramRequest, DiagramState, DiagramView, ImageActionIconName, ImageGallery, ImageGalleryItem, ImageGeometry, ImageIconName, ImageLabels, ImageLoadStatus, ImagePointer, ImagePreviewAction, ImagePreviewHost, ImagePreviewOptions, ImagePreviewPhase, ImagePreviewState, ImageTransform, LabelsOption, MermaidInitTarget, TableExportReason, TableLabels, TableProjection };
@@ -413,7 +413,11 @@ interface ImageGeometry {
413
413
  }
414
414
  export declare const initialImageTransform: () => ImageTransform;
415
415
  export declare function imageTransformStyle(transform: ImageTransform): string;
416
- export declare function zoomImage(transform: ImageTransform, ratio: number, geometry: ImageGeometry, centerX?: number, centerY?: number, touch?: boolean): ImageTransform;
416
+ /** Scales by `ratio` about a viewport point, keeping the image point under it in place, within `limits`. */
417
+ export declare function zoomImage(transform: ImageTransform, ratio: number, geometry: ImageGeometry, centerX?: number, centerY?: number, touch?: boolean, limits?: {
418
+ min: number;
419
+ max: number;
420
+ }): ImageTransform;
417
421
  /**
418
422
  * The zoom factor for one wheel event. A full notch (100 pixels) is one 1.5x
419
423
  * step, and a smaller delta a proportional part of it.
@@ -421,13 +425,14 @@ export declare function zoomImage(transform: ImageTransform, ratio: number, geom
421
425
  * pages, not pixels: three lines or one page count as a notch.
422
426
  * - `pinch` is a trackpad pinch, which arrives as a wheel event with the
423
427
  * control key set and small deltas. It scales continuously with the delta.
428
+ * - `step` is what a full notch adds to the scale factor: 0.5 makes it 1.5x.
424
429
  */
425
- export declare function imageWheelRatio(delta: number, mode?: number, pinch?: boolean): number;
430
+ export declare function imageWheelRatio(delta: number, mode?: number, pinch?: boolean, step?: number): number;
426
431
  /** Snap a moved image back inside the viewport, taking quarter turns into account. */
427
432
  export declare function reboundImage(transform: ImageTransform, geometry: ImageGeometry, bounds: {
428
433
  left: number;
429
434
  top: number;
430
- }): ImageTransform;
435
+ }, minScale?: number): ImageTransform;
431
436
  /** Upstream pinch center weights each finger by how far it moved. */
432
437
  export declare function imagePinchCenter(oldA: {
433
438
  x: number;
@@ -464,6 +469,14 @@ interface ImagePreviewState {
464
469
  status: ImageLoadStatus;
465
470
  /** A drag or a pinch is in progress: the image follows the pointer without a transition. */
466
471
  moving: boolean;
472
+ /** The image can be dragged and pinched (the `movable` option). */
473
+ movable: boolean;
474
+ /** Viewport point the preview grows out of and shrinks back into: the anchor of the image shown when it opened,
475
+ * then of the image shown when it closes. Undefined means the centre of the viewport. */
476
+ origin: {
477
+ x: number;
478
+ y: number;
479
+ } | undefined;
467
480
  canPrevious: boolean;
468
481
  canNext: boolean;
469
482
  canZoomIn: boolean;
@@ -475,12 +488,46 @@ interface ImagePreviewHost {
475
488
  geometry(): ImageGeometry | undefined;
476
489
  /** Runs the callback before the next paint and returns a cancel function. `requestAnimationFrame` in a browser. */
477
490
  frame(callback: () => void): () => void;
491
+ /** Where the image registered under `id` sits on the page, as a viewport point: the centre of its thumbnail.
492
+ * Undefined when it has none or it is out of view. Asked when the preview opens and again when it closes. */
493
+ anchor?(id: object): {
494
+ x: number;
495
+ y: number;
496
+ } | undefined;
478
497
  }
479
498
  interface ImagePreviewOptions {
480
499
  /** How long the leaving style stays up, in milliseconds. Zero closes at once. A function is read at each close,
481
500
  * so a host can answer zero while the user prefers reduced motion. Default 300. */
482
501
  closeDuration?: number | (() => number);
502
+ /** What one zoom step adds to the scale factor: the toolbar, a double click and a full wheel notch multiply or
503
+ * divide by `1 + scaleStep`. Default 0.5. */
504
+ scaleStep?: number;
505
+ /** Smallest scale the toolbar and the wheel reach, and the one a pinch returns to. Default 1. */
506
+ minScale?: number;
507
+ /** Largest scale. Default 50. */
508
+ maxScale?: number;
509
+ /** Whether the image can be dragged and pinched. Default true. */
510
+ movable?: boolean;
511
+ /** Whether the wheel and a trackpad pinch zoom. Default true. */
512
+ wheel?: boolean;
513
+ /** The preview started to open (true) or to close (false). */
514
+ onOpenChange?(open: boolean, info: {
515
+ current: number;
516
+ item: ImageGalleryItem | undefined;
517
+ }): void;
518
+ /** An opening or a closing ran to its end. One that was interrupted, by reopening while it closed or by
519
+ * `dispose`, reports nothing. */
520
+ afterOpenChange?(open: boolean): void;
521
+ /** The user moved to another image of the gallery. */
522
+ onChange?(current: number, previous: number): void;
523
+ /** The transform changed. A drag or a wheel burst reports once per frame, with the last action. */
524
+ onTransform?(info: {
525
+ transform: ImageTransform;
526
+ action: ImagePreviewAction;
527
+ }): void;
483
528
  }
529
+ /** What changed the transform, as reported to `onTransform`. */
530
+ type ImagePreviewAction = 'zoomIn' | 'zoomOut' | 'rotateLeft' | 'rotateRight' | 'flipX' | 'flipY' | 'wheel' | 'doubleClick' | 'move' | 'pinch' | 'rebound' | 'prev' | 'next';
484
531
  interface ImagePointer {
485
532
  /** Pointer id: one drag, or one finger of a pinch. */
486
533
  id: number;
@@ -507,8 +554,10 @@ interface ImagePointer {
507
554
  * else is published at once. Each update is applied to the current state, so
508
555
  * several in one frame add up.
509
556
  * - Switching images resets the transform. Zoom is limited to 1x to 50x in
510
- * 1.5x steps, about the pointer for the wheel and a double click, about the
511
- * viewport centre for the toolbar.
557
+ * 1.5x steps unless the options say otherwise, about the pointer for the
558
+ * wheel and a double click, about the viewport centre for the toolbar.
559
+ * - The `on…` options report what the user did; they are called after the
560
+ * state that reflects it has been published.
512
561
  */
513
562
  export declare function createImagePreviewController(host: ImagePreviewHost, publish: (state: ImagePreviewState) => void, options?: ImagePreviewOptions): {
514
563
  state: () => ImagePreviewState;
@@ -536,7 +585,7 @@ export declare function createImagePreviewController(host: ImagePreviewHost, pub
536
585
  mode?: number;
537
586
  pinch?: boolean;
538
587
  }): boolean;
539
- /** Returns to the natural size, or zooms one step about the point when already there. */
588
+ /** Returns to the starting size, or zooms one step about the point when already there. */
540
589
  doubleClick(x: number, y: number): void;
541
590
  /** True when the key was used: prevent the default action. Left and right move through a gallery. */
542
591
  key(key: string): boolean;
@@ -565,4 +614,4 @@ export interface CodeBlockOptions {
565
614
  /** Text of the code block, as overrides of the English defaults. A function is called on every render. */
566
615
  labels?: LabelsOption<CodeBlockLabels>;
567
616
  }
568
- export type { CodeBlockInput, CodeBlockLabels, CodeBlockSnapshot, CodeFrame, CodeSource, DiagramEngine, DiagramError, DiagramLabels, DiagramRequest, DiagramState, DiagramView, ImageActionIconName, ImageGallery, ImageGalleryItem, ImageGeometry, ImageIconName, ImageLabels, ImageLoadStatus, ImagePointer, ImagePreviewHost, ImagePreviewOptions, ImagePreviewPhase, ImagePreviewState, ImageTransform, LabelsOption, MermaidInitTarget, TableExportReason, TableLabels, TableProjection };
617
+ export type { CodeBlockInput, CodeBlockLabels, CodeBlockSnapshot, CodeFrame, CodeSource, DiagramEngine, DiagramError, DiagramLabels, DiagramRequest, DiagramState, DiagramView, ImageActionIconName, ImageGallery, ImageGalleryItem, ImageGeometry, ImageIconName, ImageLabels, ImageLoadStatus, ImagePointer, ImagePreviewAction, ImagePreviewHost, ImagePreviewOptions, ImagePreviewPhase, ImagePreviewState, ImageTransform, LabelsOption, MermaidInitTarget, TableExportReason, TableLabels, TableProjection };
@@ -694,9 +694,14 @@ function imageTransformStyle(transform) {
694
694
  const { x, y, scale, rotate, flipX, flipY } = transform;
695
695
  return `translate3d(${x}px, ${y}px, 0) scale3d(${flipX ? "-" : ""}${scale}, ${flipY ? "-" : ""}${scale}, 1) rotate(${rotate}deg)`;
696
696
  }
697
- function zoomImage(transform, ratio, geometry, centerX = geometry.viewportWidth / 2, centerY = geometry.viewportHeight / 2, touch = false) {
697
+ const DEFAULT_SCALE_LIMITS = {
698
+ min: 1,
699
+ max: 50
700
+ };
701
+ function zoomImage(transform, ratio, geometry, centerX = geometry.viewportWidth / 2, centerY = geometry.viewportHeight / 2, touch = false, limits = DEFAULT_SCALE_LIMITS) {
698
702
  if (!Number.isFinite(ratio) || ratio <= 0) return transform;
699
- const scale = Math.min(50, touch ? Math.max(.01, transform.scale * ratio) : Math.max(1, transform.scale * ratio));
703
+ const floor = touch ? .01 : limits.min;
704
+ const scale = Math.min(limits.max, Math.max(floor, transform.scale * ratio));
700
705
  const difference = scale / transform.scale - 1;
701
706
  let x = transform.x - difference * (centerX - transform.x - geometry.left - geometry.width / 2);
702
707
  let y = transform.y - difference * (centerY - transform.y - geometry.top - geometry.height / 2);
@@ -708,19 +713,19 @@ function zoomImage(transform, ratio, geometry, centerX = geometry.viewportWidth
708
713
  scale
709
714
  };
710
715
  }
711
- function imageWheelRatio(delta, mode = 0, pinch = false) {
716
+ function imageWheelRatio(delta, mode = 0, pinch = false, step = .5) {
712
717
  if (!Number.isFinite(delta) || delta === 0) return 1;
713
- if (pinch) return Math.min(1.5, Math.max(1 / 1.5, Math.exp(-delta / 100)));
718
+ if (pinch) return Math.min(1 + step, Math.max(1 / (1 + step), Math.exp(-delta / 100)));
714
719
  const pixels = mode === 1 ? delta * 100 / 3 : mode === 2 ? delta * 100 : delta;
715
- const ratio = 1 + Math.min(Math.abs(pixels / 100), 1) * .5;
720
+ const ratio = 1 + Math.min(Math.abs(pixels / 100), 1) * step;
716
721
  return pixels > 0 ? 1 / ratio : ratio;
717
722
  }
718
- function reboundImage(transform, geometry, bounds) {
719
- if (transform.scale < 1) return {
723
+ function reboundImage(transform, geometry, bounds, minScale = 1) {
724
+ if (transform.scale < minScale) return {
720
725
  ...transform,
721
726
  x: 0,
722
727
  y: 0,
723
- scale: 1
728
+ scale: minScale
724
729
  };
725
730
  const rotated = transform.rotate % 180 !== 0;
726
731
  const width = (rotated ? geometry.height : geometry.width) * transform.scale;
@@ -752,9 +757,8 @@ function imagePinchCenter(oldA, oldB, a, b) {
752
757
  y: oldA.y + ratio * (oldB.y - oldA.y)
753
758
  };
754
759
  }
755
- const ZOOM_STEP = 1.5;
756
- const MAX_SCALE = 50;
757
760
  const CLOSE_MS = 300;
761
+ const positive = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
758
762
  const closedState = Object.freeze({
759
763
  phase: "closed",
760
764
  items: Object.freeze([]),
@@ -763,19 +767,34 @@ const closedState = Object.freeze({
763
767
  transform: initialImageTransform(),
764
768
  status: "loading",
765
769
  moving: false,
770
+ movable: true,
771
+ origin: void 0,
766
772
  canPrevious: false,
767
773
  canNext: false,
768
774
  canZoomIn: false,
769
775
  canZoomOut: false
770
776
  });
771
777
  function createImagePreviewController(host, publish, options = {}) {
778
+ const limits = {
779
+ min: positive(options.minScale, 1),
780
+ max: 0
781
+ };
782
+ limits.max = Math.max(limits.min, positive(options.maxScale, 50));
783
+ const baseScale = Math.min(limits.max, Math.max(limits.min, 1));
784
+ const scaleStep = positive(options.scaleStep, .5);
785
+ const movable = options.movable ?? true;
786
+ const wheelZoom = options.wheel ?? true;
772
787
  let phase = "closed";
773
788
  let items = [];
774
789
  let selected;
775
790
  let current = -1;
776
- let transform = initialImageTransform();
791
+ let transform = {
792
+ ...initialImageTransform(),
793
+ scale: baseScale
794
+ };
777
795
  let status = "loading";
778
796
  let moving = false;
797
+ let origin;
779
798
  let unsubscribe;
780
799
  let cancelPublish;
781
800
  let cancelOpening;
@@ -794,23 +813,40 @@ function createImagePreviewController(host, publish, options = {}) {
794
813
  transform,
795
814
  status,
796
815
  moving,
816
+ movable,
817
+ origin,
797
818
  canPrevious: current > 0,
798
819
  canNext: current < items.length - 1,
799
- canZoomIn: ready && transform.scale < MAX_SCALE,
800
- canZoomOut: ready && transform.scale > 1
820
+ canZoomIn: ready && transform.scale < limits.max,
821
+ canZoomOut: ready && transform.scale > limits.min
801
822
  };
802
823
  };
824
+ let reported;
825
+ const flush = () => {
826
+ publish(snapshot());
827
+ const action = reported;
828
+ reported = void 0;
829
+ if (action) options.onTransform?.({
830
+ transform,
831
+ action
832
+ });
833
+ };
803
834
  const emit = () => {
804
835
  cancelPublish?.();
805
836
  cancelPublish = void 0;
806
- publish(snapshot());
837
+ flush();
807
838
  };
808
839
  const emitNextFrame = () => {
809
840
  cancelPublish ??= host.frame(() => {
810
841
  cancelPublish = void 0;
811
- publish(snapshot());
842
+ flush();
812
843
  });
813
844
  };
845
+ const apply = (next, action) => {
846
+ if (next.x === transform.x && next.y === transform.y && next.scale === transform.scale && next.rotate === transform.rotate && next.flipX === transform.flipX && next.flipY === transform.flipY) return;
847
+ transform = next;
848
+ reported = action;
849
+ };
814
850
  const endGesture = () => {
815
851
  pointers.clear();
816
852
  grip = void 0;
@@ -818,22 +854,26 @@ function createImagePreviewController(host, publish, options = {}) {
818
854
  moving = false;
819
855
  };
820
856
  const startImage = () => {
821
- transform = initialImageTransform();
857
+ transform = {
858
+ ...initialImageTransform(),
859
+ scale: baseScale
860
+ };
861
+ reported = void 0;
822
862
  status = "loading";
823
863
  endGesture();
824
864
  };
825
- const zoom = (ratio, x, y, touch = false) => {
865
+ const zoom = (action, ratio, x, y, touch = false) => {
826
866
  const box = host.geometry();
827
- if (box && status === "ready") transform = zoomImage(transform, ratio, box, x, y, touch);
867
+ if (box && status === "ready") apply(zoomImage(transform, ratio, box, x, y, touch, limits), action);
828
868
  };
829
869
  const rebound = () => {
830
- if (transform.scale < 1) {
831
- transform = {
870
+ if (transform.scale < limits.min) {
871
+ apply({
832
872
  ...transform,
833
873
  x: 0,
834
874
  y: 0,
835
- scale: 1
836
- };
875
+ scale: limits.min
876
+ }, "rebound");
837
877
  return;
838
878
  }
839
879
  const box = host.geometry();
@@ -841,18 +881,28 @@ function createImagePreviewController(host, publish, options = {}) {
841
881
  const turned = transform.rotate % 180 !== 0;
842
882
  const width = (turned ? box.height : box.width) * transform.scale;
843
883
  const height = (turned ? box.width : box.height) * transform.scale;
844
- transform = reboundImage(transform, box, {
884
+ apply(reboundImage(transform, box, {
845
885
  left: box.left + box.width / 2 + transform.x - width / 2,
846
886
  top: box.top + box.height / 2 + transform.y - height / 2
847
- });
887
+ }, limits.min), "rebound");
888
+ };
889
+ const anchorOf = (id) => {
890
+ const point = host.anchor?.(id);
891
+ return point && Number.isFinite(point.x) && Number.isFinite(point.y) ? {
892
+ x: point.x,
893
+ y: point.y
894
+ } : void 0;
848
895
  };
849
896
  const finishClosing = () => {
850
897
  closeTimer = void 0;
851
898
  phase = "closed";
852
899
  items = [];
853
900
  selected = void 0;
901
+ origin = void 0;
854
902
  current = -1;
903
+ reported = void 0;
855
904
  emit();
905
+ options.afterOpenChange?.(false);
856
906
  };
857
907
  const close = () => {
858
908
  if (phase === "closed" || phase === "closing") return;
@@ -861,20 +911,36 @@ function createImagePreviewController(host, publish, options = {}) {
861
911
  unsubscribe?.();
862
912
  unsubscribe = void 0;
863
913
  endGesture();
914
+ reported = void 0;
915
+ origin = selected ? anchorOf(selected) : void 0;
864
916
  phase = "closing";
917
+ const info = {
918
+ current,
919
+ item: items[current]
920
+ };
865
921
  const duration = typeof options.closeDuration === "function" ? options.closeDuration() : options.closeDuration ?? CLOSE_MS;
866
922
  if (duration > 0) {
867
923
  emit();
868
924
  closeTimer = setTimeout(finishClosing, duration);
869
- } else finishClosing();
925
+ options.onOpenChange?.(false, info);
926
+ } else try {
927
+ options.onOpenChange?.(false, info);
928
+ } finally {
929
+ if (phase === "closing") finishClosing();
930
+ }
870
931
  };
871
932
  const go = (delta) => {
872
933
  const target = current + delta;
873
934
  if (phase === "closed" || phase === "closing" || target < 0 || target >= items.length || target === current) return;
935
+ const previous = current;
936
+ const { x, y, scale, rotate, flipX, flipY } = transform;
937
+ const transformed = x !== 0 || y !== 0 || scale !== baseScale || rotate !== 0 || flipX || flipY;
874
938
  current = target;
875
939
  selected = items[target].id;
876
940
  startImage();
941
+ if (transformed) reported = delta < 0 ? "prev" : "next";
877
942
  emit();
943
+ options.onChange?.(current, previous);
878
944
  };
879
945
  const follow = (gallery) => {
880
946
  const next = gallery.items();
@@ -898,6 +964,7 @@ function createImagePreviewController(host, publish, options = {}) {
898
964
  items = list;
899
965
  current = index;
900
966
  selected = id;
967
+ origin = anchorOf(id);
901
968
  startImage();
902
969
  unsubscribe = gallery.subscribe(() => follow(gallery));
903
970
  if (phase === "open") {
@@ -905,15 +972,26 @@ function createImagePreviewController(host, publish, options = {}) {
905
972
  return true;
906
973
  }
907
974
  cancelOpening = void 0;
975
+ const starting = phase !== "opening";
908
976
  phase = "opening";
909
977
  emit();
910
978
  cancelOpening = host.frame(() => {
979
+ const measured = selected ? anchorOf(selected) : void 0;
980
+ if (measured?.x !== origin?.x || measured?.y !== origin?.y) {
981
+ origin = measured;
982
+ emit();
983
+ }
911
984
  cancelOpening = host.frame(() => {
912
985
  cancelOpening = void 0;
913
986
  phase = "open";
914
987
  emit();
988
+ options.afterOpenChange?.(true);
915
989
  });
916
990
  });
991
+ if (starting) options.onOpenChange?.(true, {
992
+ current,
993
+ item: items[current]
994
+ });
917
995
  return true;
918
996
  },
919
997
  close,
@@ -924,24 +1002,24 @@ function createImagePreviewController(host, publish, options = {}) {
924
1002
  if (name === "prev") return go(-1);
925
1003
  if (name === "next") return go(1);
926
1004
  if (status !== "ready") return;
927
- if (name === "zoomIn") zoom(ZOOM_STEP);
928
- else if (name === "zoomOut") zoom(1 / ZOOM_STEP);
929
- else if (name === "rotateLeft") transform = {
1005
+ if (name === "zoomIn") zoom(name, 1 + scaleStep);
1006
+ else if (name === "zoomOut") zoom(name, 1 / (1 + scaleStep));
1007
+ else if (name === "rotateLeft") apply({
930
1008
  ...transform,
931
1009
  rotate: transform.rotate - 90
932
- };
933
- else if (name === "rotateRight") transform = {
1010
+ }, name);
1011
+ else if (name === "rotateRight") apply({
934
1012
  ...transform,
935
1013
  rotate: transform.rotate + 90
936
- };
937
- else if (name === "flipX") transform = {
1014
+ }, name);
1015
+ else if (name === "flipX") apply({
938
1016
  ...transform,
939
1017
  flipX: !transform.flipX
940
- };
941
- else if (name === "flipY") transform = {
1018
+ }, name);
1019
+ else if (name === "flipY") apply({
942
1020
  ...transform,
943
1021
  flipY: !transform.flipY
944
- };
1022
+ }, name);
945
1023
  emit();
946
1024
  },
947
1025
  loaded() {
@@ -956,7 +1034,7 @@ function createImagePreviewController(host, publish, options = {}) {
956
1034
  emit();
957
1035
  },
958
1036
  pointerDown(pointer) {
959
- if (phase !== "open" && phase !== "opening" || status !== "ready") return false;
1037
+ if (!movable || phase !== "open" && phase !== "opening" || status !== "ready") return false;
960
1038
  if (pointer.button !== void 0 && pointer.button !== 0) return false;
961
1039
  if (pointers.size >= 2 && !pointers.has(pointer.id)) return false;
962
1040
  pointers.set(pointer.id, {
@@ -983,12 +1061,12 @@ function createImagePreviewController(host, publish, options = {}) {
983
1061
  const [a, b] = [...pointers.values()];
984
1062
  const span = Math.hypot(before[0].x - before[1].x, before[0].y - before[1].y);
985
1063
  const center = imagePinchCenter(before[0], before[1], a, b);
986
- if (span > 0) zoom(Math.hypot(a.x - b.x, a.y - b.y) / span, center.x, center.y, true);
987
- } else if (grip) transform = {
1064
+ if (span > 0) zoom("pinch", Math.hypot(a.x - b.x, a.y - b.y) / span, center.x, center.y, true);
1065
+ } else if (grip) apply({
988
1066
  ...transform,
989
1067
  x: pointer.x - grip.x,
990
1068
  y: pointer.y - grip.y
991
- };
1069
+ }, "move");
992
1070
  displaced = true;
993
1071
  emitNextFrame();
994
1072
  },
@@ -1008,20 +1086,20 @@ function createImagePreviewController(host, publish, options = {}) {
1008
1086
  emit();
1009
1087
  },
1010
1088
  wheel(input) {
1011
- if (phase !== "open" || status !== "ready" || !input.deltaY) return false;
1012
- zoom(imageWheelRatio(input.deltaY, input.mode, input.pinch), input.x, input.y);
1089
+ if (!wheelZoom || phase !== "open" || status !== "ready" || !input.deltaY) return false;
1090
+ zoom("wheel", imageWheelRatio(input.deltaY, input.mode, input.pinch, scaleStep), input.x, input.y);
1013
1091
  emitNextFrame();
1014
1092
  return true;
1015
1093
  },
1016
1094
  doubleClick(x, y) {
1017
1095
  if (phase !== "open" || status !== "ready") return;
1018
- if (transform.scale !== 1) transform = {
1096
+ if (transform.scale !== baseScale) apply({
1019
1097
  ...transform,
1020
1098
  x: 0,
1021
1099
  y: 0,
1022
- scale: 1
1023
- };
1024
- else zoom(ZOOM_STEP, x, y);
1100
+ scale: baseScale
1101
+ }, "doubleClick");
1102
+ else zoom("doubleClick", 1 + scaleStep, x, y);
1025
1103
  emit();
1026
1104
  },
1027
1105
  key(key) {
@@ -1041,7 +1119,9 @@ function createImagePreviewController(host, publish, options = {}) {
1041
1119
  phase = "closed";
1042
1120
  items = [];
1043
1121
  selected = void 0;
1122
+ origin = void 0;
1044
1123
  current = -1;
1124
+ reported = void 0;
1045
1125
  }
1046
1126
  };
1047
1127
  }