squarified 0.6.2 → 1.1.0

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/plugin.js CHANGED
@@ -1,51 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var domEvent = require('./dom-event-ClwTQnot.js');
3
+ var domEvent = require('./dom-event-BkRCiIWB.js');
4
4
 
5
- // Currently, etoile is an internal module, so we won't need too much easing functions.
6
- // And the animation logic is implemented by user code.
7
- const easing = {
8
- linear: (k)=>k,
9
- quadraticIn: (k)=>k * k,
10
- quadraticOut: (k)=>k * (2 - k),
11
- quadraticInOut: (k)=>{
12
- if ((k *= 2) < 1) {
13
- return 0.5 * k * k;
14
- }
15
- return -0.5 * (--k * (k - 2) - 1);
16
- },
17
- cubicIn: (k)=>k * k * k,
18
- cubicOut: (k)=>{
19
- if ((k *= 2) < 1) {
20
- return 0.5 * k * k * k;
21
- }
22
- return 0.5 * ((k -= 2) * k * k + 2);
23
- },
24
- cubicInOut: (k)=>{
25
- if ((k *= 2) < 1) {
26
- return 0.5 * k * k * k;
27
- }
28
- return 0.5 * ((k -= 2) * k * k + 2);
29
- }
30
- };
31
-
32
- class Highlight extends domEvent.Schedule {
33
- reset() {
34
- this.destory();
35
- this.update();
36
- }
37
- get canvas() {
38
- return this.render.canvas;
39
- }
40
- setZIndexForHighlight(zIndex = '-1') {
41
- this.canvas.style.zIndex = zIndex;
42
- }
43
- init() {
44
- this.setZIndexForHighlight();
45
- this.canvas.style.position = 'absolute';
46
- this.canvas.style.pointerEvents = 'none';
47
- }
48
- }
49
5
  const ANIMATION_DURATION = 300;
50
6
  const HIGH_LIGHT_OPACITY = 0.3;
51
7
  const fill = {
@@ -58,43 +14,80 @@ const fill = {
58
14
  };
59
15
  const presetHighlightPlugin = domEvent.definePlugin({
60
16
  name: 'treemap:preset-highlight',
61
- onLoad () {
62
- const meta = this.getPluginMetadata('treemap:preset-highlight');
63
- if (!meta) {
17
+ onDOMEventTriggered (name, _, graphic, { stateManager: state, matrix, component }) {
18
+ // Any interaction that isn't a pure hover must reset the overlay so we never
19
+ // show a stale highlight after zoom / drag / pan transitions.
20
+ if (name !== 'mousemove') {
21
+ const meta = this.getPluginMetadata('treemap:preset-highlight');
22
+ if (meta && meta.lastDirtyRect) {
23
+ component.clearOverlay();
24
+ meta.overlayGraphic = null;
25
+ meta.lastDirtyRect = null;
26
+ }
64
27
  return;
65
28
  }
66
- if (!meta.highlight) {
67
- meta.highlight = new Highlight(this.instance.to);
68
- }
69
- },
70
- onDOMEventTriggered (name, _, module, { stateManager: state, matrix }) {
71
29
  if (name === 'mousemove') {
72
30
  if (state.canTransition('MOVE')) {
73
31
  const meta = this.getPluginMetadata('treemap:preset-highlight');
74
- if (!module) {
75
- meta.highlight?.reset();
76
- meta.highlight?.update();
77
- meta.highlight?.setZIndexForHighlight();
32
+ if (!meta) {
78
33
  return;
79
34
  }
35
+ const oldDirtyRect = meta.lastDirtyRect;
36
+ if (!graphic) {
37
+ if (oldDirtyRect) {
38
+ component.clearOverlay();
39
+ component.updateDirty([
40
+ oldDirtyRect
41
+ ]);
42
+ meta.overlayGraphic = null;
43
+ meta.lastDirtyRect = null;
44
+ }
45
+ return;
46
+ }
47
+ const module = graphic.__widget__;
80
48
  const [x, y, w, h] = module.layout;
81
- const effectiveRadius = Math.min(module.config.rectRadius, w / 4, h / 4);
82
- domEvent.smoothFrame((_, cleanup)=>{
83
- cleanup();
84
- meta.highlight?.reset();
85
- const mask = domEvent.createRoundBlock(x, y, w, h, {
86
- fill,
87
- opacity: HIGH_LIGHT_OPACITY,
88
- radius: effectiveRadius,
89
- padding: 0
90
- });
91
- meta.highlight?.add(mask);
92
- meta.highlight?.setZIndexForHighlight('1');
93
- domEvent.stackMatrixTransform(mask, matrix.e, matrix.f, 1);
94
- meta.highlight?.update();
95
- }, {
96
- duration: ANIMATION_DURATION
49
+ const rect = graphic.elements[0];
50
+ if (!rect || !domEvent.asserts.isRoundRect(rect)) {
51
+ return;
52
+ }
53
+ const effectiveRadius = rect.style.radius;
54
+ // Layout coordinates are already in visual (zoomed) space; matrix.e/f
55
+ // is the pan translation that gets added to every element position.
56
+ const visualX = x + matrix.e;
57
+ const visualY = y + matrix.f;
58
+ // Expand dirty rect by 1 CSS px on each side to cover anti-aliased edges.
59
+ const pad = 1;
60
+ const newDirtyRect = {
61
+ x: visualX - pad,
62
+ y: visualY - pad,
63
+ width: w + pad * 2,
64
+ height: h + pad * 2
65
+ };
66
+ const mask = domEvent.createRoundBlock(visualX, visualY, w, h, {
67
+ fill,
68
+ opacity: HIGH_LIGHT_OPACITY,
69
+ radius: effectiveRadius,
70
+ padding: 0
97
71
  });
72
+ component.clearOverlay();
73
+ component.addOverlay(mask);
74
+ meta.overlayGraphic = mask;
75
+ meta.lastDirtyRect = newDirtyRect;
76
+ const dirtyRects = oldDirtyRect ? [
77
+ newDirtyRect,
78
+ oldDirtyRect
79
+ ] : [
80
+ newDirtyRect
81
+ ];
82
+ component.updateDirty(dirtyRects);
83
+ } else {
84
+ // State changed away from hoverable (e.g. dragging / zooming) — clear overlay.
85
+ const meta = this.getPluginMetadata('treemap:preset-highlight');
86
+ if (meta && meta.lastDirtyRect) {
87
+ component.clearOverlay();
88
+ meta.overlayGraphic = null;
89
+ meta.lastDirtyRect = null;
90
+ }
98
91
  }
99
92
  }
100
93
  },
@@ -103,21 +96,21 @@ const presetHighlightPlugin = domEvent.definePlugin({
103
96
  if (!meta) {
104
97
  return;
105
98
  }
106
- meta.highlight?.render.initOptions({
107
- ...this.instance.render.options
108
- });
109
- meta.highlight?.reset();
110
- meta.highlight?.init();
99
+ this.instance.clearOverlay();
100
+ meta.overlayGraphic = null;
101
+ meta.lastDirtyRect = null;
111
102
  },
112
103
  onDispose () {
113
104
  const meta = this.getPluginMetadata('treemap:preset-highlight');
114
- if (meta && meta.highlight) {
115
- meta.highlight.destory();
116
- meta.highlight = null;
105
+ if (meta) {
106
+ this.instance.clearOverlay();
107
+ meta.overlayGraphic = null;
108
+ meta.lastDirtyRect = null;
117
109
  }
118
110
  },
119
111
  meta: {
120
- highlight: null
112
+ overlayGraphic: null,
113
+ lastDirtyRect: null
121
114
  }
122
115
  });
123
116
 
@@ -143,7 +136,6 @@ const presetDragElementPlugin = domEvent.definePlugin({
143
136
  }
144
137
  state.transition('DRAGGING');
145
138
  if (state.isInState('DRAGGING')) {
146
- const highlight = getHighlightInstance.call(this);
147
139
  domEvent.smoothFrame((_, cleanup)=>{
148
140
  cleanup();
149
141
  const { offsetX, offsetY } = event.native;
@@ -151,18 +143,30 @@ const presetDragElementPlugin = domEvent.definePlugin({
151
143
  const drawY = offsetY - meta.dragOptions.y;
152
144
  const lastX = meta.dragOptions.x;
153
145
  const lastY = meta.dragOptions.y;
154
- if (highlight?.highlight) {
155
- highlight.highlight.reset();
156
- highlight.highlight.setZIndexForHighlight();
157
- }
146
+ component.clearOverlay();
158
147
  matrix.translation(drawX, drawY);
159
148
  meta.dragOptions.x = offsetX;
160
149
  meta.dragOptions.y = offsetY;
161
150
  meta.dragOptions.lastX = lastX;
162
151
  meta.dragOptions.lastY = lastY;
152
+ const cloned = component.elements.map((el)=>domEvent.isBox(el) ? el.clone() : el);
163
153
  component.cleanup();
164
- component.draw(false, false);
165
- domEvent.stackMatrixTransformWithGraphAndLayer(component.elements, matrix.e, matrix.f, 1);
154
+ component.add(...cloned);
155
+ domEvent.traverse(component.elements, (graph)=>{
156
+ if (domEvent.isText(graph)) {
157
+ const { textX, textY } = graph.__widget__;
158
+ graph.x = textX;
159
+ graph.y = textY;
160
+ }
161
+ if (domEvent.isRoundRect(graph)) {
162
+ const { x, y, w, h } = graph.__widget__;
163
+ graph.x = x;
164
+ graph.y = y;
165
+ graph.width = w;
166
+ graph.height = h;
167
+ }
168
+ domEvent.stackMatrixTransform(graph, matrix.e, matrix.f, 1);
169
+ });
166
170
  component.update();
167
171
  return true;
168
172
  }, {
@@ -186,11 +190,7 @@ const presetDragElementPlugin = domEvent.definePlugin({
186
190
  }
187
191
  }
188
192
  if (state.isInState('DRAGGING') && state.canTransition('IDLE')) {
189
- const highlight = getHighlightInstance.call(this);
190
- if (highlight && highlight.highlight) {
191
- highlight.highlight.reset();
192
- highlight.highlight.setZIndexForHighlight();
193
- }
193
+ component.clearOverlay();
194
194
  const meta = getDragOptions.call(this);
195
195
  if (meta && meta.dragOptions) {
196
196
  meta.dragOptions.x = 0;
@@ -233,9 +233,6 @@ const presetDragElementPlugin = domEvent.definePlugin({
233
233
  state.reset();
234
234
  }
235
235
  });
236
- function getHighlightInstance() {
237
- return this.getPluginMetadata('treemap:preset-highlight');
238
- }
239
236
  function getDragOptions() {
240
237
  const meta = this.getPluginMetadata('treemap:preset-drag-element');
241
238
  return meta;
@@ -259,9 +256,9 @@ function presetMenuPlugin(options) {
259
256
  return;
260
257
  }
261
258
  if (options?.onClick) {
262
- options.onClick(action, domEvent$1.findRelativeNode({
263
- native: e,
264
- kind: undefined
259
+ options.onClick(action, domEvent$1.findRelativeGraphicNode({
260
+ kind: 'click',
261
+ native: e
265
262
  }));
266
263
  }
267
264
  }
@@ -363,6 +360,15 @@ function adjustColorToComfortableForHumanEye(hue, saturation, lightness) {
363
360
  };
364
361
  }
365
362
 
363
+ // refer https://developer.mozilla.org/en-US/docs/Web/API/Element/mousewheel_event
364
+ // we shouldn't use wheelDelta property anymore.
365
+ function getScaleOptions() {
366
+ const meta = this.getPluginMetadata('treemap:preset-scale');
367
+ if (!meta) {
368
+ throw new Error('treemap:preset-scale metadata missing; ensure presetScalePlugin is registered');
369
+ }
370
+ return meta;
371
+ }
366
372
  function presetScalePlugin(options) {
367
373
  return domEvent.definePlugin({
368
374
  name: 'treemap:preset-scale',
@@ -374,9 +380,19 @@ function presetScalePlugin(options) {
374
380
  meta: {
375
381
  scaleOptions: {
376
382
  scale: 1,
377
- minScale: options?.min || 0.1,
378
- maxScale: options?.max || Infinity,
379
- scaleFactor: 0.05
383
+ virtualScale: 1,
384
+ minScale: options?.min ?? 0.1,
385
+ maxScale: options?.max ?? Infinity,
386
+ scaleFactor: 0.05,
387
+ springStiffness: options?.springStiffness ?? 300,
388
+ springDamping: options?.springDamping ?? 35,
389
+ overshootResistance: options?.overshootResistance ?? 0.35,
390
+ overshootLimitFactor: options?.overshootLimitFactor ?? 0.05,
391
+ lastAnchorX: undefined,
392
+ lastAnchorY: undefined,
393
+ springRafId: null,
394
+ animationsEnabled: options?.animationsEnabled ?? true,
395
+ wheelDebounce: options?.wheelDebounce ?? 200
380
396
  },
381
397
  gestureState: {
382
398
  isTrackingGesture: false,
@@ -386,23 +402,19 @@ function presetScalePlugin(options) {
386
402
  totalDeltaX: 0,
387
403
  consecutivePinchEvents: 0,
388
404
  gestureType: 'unknown',
389
- lockGestureType: false
405
+ lockGestureType: false,
406
+ wheelEndTimeoutId: null
390
407
  }
391
408
  },
392
409
  onResize ({ matrix, stateManager: state }) {
393
410
  const meta = getScaleOptions.call(this);
394
- if (meta) {
395
- meta.scaleOptions.scale = 1;
396
- }
411
+ meta.scaleOptions.scale = 1;
412
+ meta.scaleOptions.virtualScale = 1;
397
413
  matrix.create(domEvent.DEFAULT_MATRIX_LOC);
398
414
  state.reset();
399
415
  }
400
416
  });
401
417
  }
402
- function getScaleOptions() {
403
- const meta = this.getPluginMetadata('treemap:preset-scale');
404
- return meta;
405
- }
406
418
  function determineGestureType(event, gestureState) {
407
419
  const now = Date.now();
408
420
  const timeDiff = now - gestureState.lastEventTime;
@@ -429,7 +441,7 @@ function determineGestureType(event, gestureState) {
429
441
  return 'zoom';
430
442
  }
431
443
  // windows/macos mouse wheel
432
- // Usually the dettaY is large and deltaX maybe 0 or small number.
444
+ // Usually the deltaY is large and deltaX maybe 0 or small number.
433
445
  const isMouseWheel = Math.abs(event.deltaX) >= 100 && Math.abs(event.deltaX) <= 10 || Math.abs(event.deltaY) > 50 && Math.abs(event.deltaX) < Math.abs(event.deltaY) * 0.1;
434
446
  if (isMouseWheel) {
435
447
  gestureState.gestureType = 'zoom';
@@ -460,9 +472,6 @@ function determineGestureType(event, gestureState) {
460
472
  function onWheel(pluginContext, event, domEvent) {
461
473
  event.native.preventDefault();
462
474
  const meta = getScaleOptions.call(pluginContext);
463
- if (!meta) {
464
- return;
465
- }
466
475
  const gestureType = determineGestureType(event.native, meta.gestureState);
467
476
  if (gestureType === 'zoom') {
468
477
  handleZoom(pluginContext, event, domEvent);
@@ -471,12 +480,8 @@ function onWheel(pluginContext, event, domEvent) {
471
480
  }
472
481
  }
473
482
  function updateViewport(pluginContext, { stateManager: state, component, matrix }, useAnimation = false) {
474
- const highlight = getHighlightInstance.apply(pluginContext);
475
483
  const doUpdate = ()=>{
476
- if (highlight && highlight.highlight) {
477
- highlight.highlight.reset();
478
- highlight.highlight.setZIndexForHighlight();
479
- }
484
+ component.clearOverlay();
480
485
  component.cleanup();
481
486
  const { width, height } = component.render.options;
482
487
  component.layoutNodes = component.calculateLayoutNodes(component.data, {
@@ -504,43 +509,197 @@ function updateViewport(pluginContext, { stateManager: state, component, matrix
504
509
  doUpdate();
505
510
  }
506
511
  }
507
- function handleZoom(pluginContext, event, domEvent) {
508
- const { stateManager: state, matrix, component } = domEvent;
512
+ function cancelSpringAnimationIfAny(meta) {
513
+ const rafId = meta.scaleOptions.springRafId;
514
+ if (typeof rafId === 'number' && rafId !== null) {
515
+ cancelAnimationFrame(rafId);
516
+ meta.scaleOptions.springRafId = null;
517
+ }
518
+ }
519
+ function springAnimateToScale(pluginContext, domEvent, targetScale, anchorX, anchorY) {
509
520
  const meta = getScaleOptions.call(pluginContext);
510
- if (!meta) {
521
+ const { matrix, component } = domEvent;
522
+ // if animations disabled, snap immediately and return
523
+ if (!meta.scaleOptions.animationsEnabled) {
524
+ const oldMatrix = {
525
+ e: matrix.e,
526
+ f: matrix.f
527
+ };
528
+ const finalScaleDiff = targetScale / meta.scaleOptions.scale;
529
+ if (isFinite(finalScaleDiff) && finalScaleDiff > 0 && Math.abs(finalScaleDiff - 1) > 1e-12) {
530
+ matrix.scale(finalScaleDiff, finalScaleDiff);
531
+ matrix.e = anchorX - (anchorX - matrix.e) * finalScaleDiff;
532
+ matrix.f = anchorY - (anchorY - matrix.f) * finalScaleDiff;
533
+ }
534
+ meta.scaleOptions.scale = targetScale;
535
+ meta.scaleOptions.virtualScale = targetScale;
536
+ try {
537
+ component.handleTransformCacheInvalidation(oldMatrix, {
538
+ e: matrix.e,
539
+ f: matrix.f
540
+ });
541
+ } catch {}
542
+ updateViewport(pluginContext, domEvent, false);
511
543
  return;
512
544
  }
513
- const { scale, minScale, maxScale, scaleFactor } = meta.scaleOptions;
545
+ cancelSpringAnimationIfAny(meta);
546
+ const stiffness = meta.scaleOptions.springStiffness ?? 300;
547
+ const damping = meta.scaleOptions.springDamping ?? 35;
548
+ let position = meta.scaleOptions.scale;
549
+ let velocity = 0;
550
+ let lastTime = performance.now();
551
+ const thresholdPos = Math.max(1e-4, Math.abs(targetScale) * 1e-3);
552
+ const thresholdVel = 1e-3;
553
+ const oldMatrix = {
554
+ e: matrix.e,
555
+ f: matrix.f
556
+ };
557
+ function step(now) {
558
+ const dt = Math.min((now - lastTime) / 1000, 0.033);
559
+ lastTime = now;
560
+ const force = stiffness * (targetScale - position);
561
+ const accel = force - damping * velocity;
562
+ velocity += accel * dt;
563
+ const prev = position;
564
+ position += velocity * dt;
565
+ const scaleDiff = position / prev;
566
+ if (isFinite(scaleDiff) && scaleDiff > 0) {
567
+ matrix.scale(scaleDiff, scaleDiff);
568
+ matrix.e = anchorX - (anchorX - matrix.e) * scaleDiff;
569
+ matrix.f = anchorY - (anchorY - matrix.f) * scaleDiff;
570
+ meta.scaleOptions.scale = position;
571
+ meta.scaleOptions.virtualScale = position;
572
+ updateViewport(pluginContext, domEvent, false);
573
+ }
574
+ const isSettled = Math.abs(targetScale - position) <= thresholdPos && Math.abs(velocity) <= thresholdVel;
575
+ if (isSettled) {
576
+ const finalScaleDiff = targetScale / meta.scaleOptions.scale;
577
+ if (isFinite(finalScaleDiff) && finalScaleDiff > 0 && Math.abs(finalScaleDiff - 1) > 1e-12) {
578
+ matrix.scale(finalScaleDiff, finalScaleDiff);
579
+ matrix.e = anchorX - (anchorX - matrix.e) * finalScaleDiff;
580
+ matrix.f = anchorY - (anchorY - matrix.f) * finalScaleDiff;
581
+ }
582
+ meta.scaleOptions.scale = targetScale;
583
+ meta.scaleOptions.virtualScale = targetScale;
584
+ try {
585
+ component.handleTransformCacheInvalidation(oldMatrix, {
586
+ e: matrix.e,
587
+ f: matrix.f
588
+ });
589
+ } catch {}
590
+ updateViewport(pluginContext, domEvent, false);
591
+ meta.scaleOptions.springRafId = null;
592
+ return;
593
+ }
594
+ meta.scaleOptions.springRafId = requestAnimationFrame(step);
595
+ }
596
+ meta.scaleOptions.springRafId = requestAnimationFrame(step);
597
+ }
598
+ function handleWheelEnd(pluginContext, domEvent) {
599
+ const meta = getScaleOptions.call(pluginContext);
600
+ const { scale, minScale, maxScale } = meta.scaleOptions;
601
+ const eps = 1e-6;
602
+ if (scale + eps < minScale) {
603
+ const target = minScale;
604
+ const anchorX = meta.scaleOptions.lastAnchorX ?? domEvent.component.render.options.width / 2;
605
+ const anchorY = meta.scaleOptions.lastAnchorY ?? domEvent.component.render.options.height / 2;
606
+ springAnimateToScale(pluginContext, domEvent, target, anchorX, anchorY);
607
+ } else if (scale - eps > maxScale) {
608
+ const target = maxScale;
609
+ const anchorX = meta.scaleOptions.lastAnchorX ?? domEvent.component.render.options.width / 2;
610
+ const anchorY = meta.scaleOptions.lastAnchorY ?? domEvent.component.render.options.height / 2;
611
+ springAnimateToScale(pluginContext, domEvent, target, anchorX, anchorY);
612
+ } else {
613
+ // inside bounds: sync virtualScale to visible scale to avoid sudden jumps later
614
+ meta.scaleOptions.virtualScale = meta.scaleOptions.scale;
615
+ if (Math.abs(scale - minScale) < 1e-8) {
616
+ meta.scaleOptions.scale = minScale;
617
+ }
618
+ if (Math.abs(scale - maxScale) < 1e-8) {
619
+ meta.scaleOptions.scale = maxScale;
620
+ }
621
+ }
622
+ }
623
+ function handleZoom(pluginContext, event, domEvent) {
624
+ const { stateManager: state, matrix, component } = domEvent;
625
+ const meta = getScaleOptions.call(pluginContext);
626
+ // read currentVisible and currentVirtual separately to avoid destructuring-default warnings
627
+ const currentVisible = meta.scaleOptions.scale;
628
+ const prevVirtualRaw = meta.scaleOptions.virtualScale ?? currentVisible;
629
+ const minScale = meta.scaleOptions.minScale;
630
+ const maxScale = meta.scaleOptions.maxScale;
631
+ const scaleFactor = meta.scaleOptions.scaleFactor;
632
+ const overshootResistance = meta.scaleOptions.overshootResistance ?? 0.35;
633
+ const overshootLimitFactor = meta.scaleOptions.overshootLimitFactor ?? 0.05;
634
+ cancelSpringAnimationIfAny(meta);
514
635
  const oldMatrix = {
515
636
  e: matrix.e,
516
637
  f: matrix.f
517
638
  };
518
- const dynamicScaleFactor = Math.max(scaleFactor, scale * 0.1);
639
+ const dynamicScaleFactor = Math.max(scaleFactor, currentVisible * 0.1);
519
640
  const delta = event.native.deltaY < 0 ? dynamicScaleFactor : -dynamicScaleFactor;
520
- const newScale = Math.max(minScale, Math.min(maxScale, scale + delta));
521
- if (newScale === scale) {
641
+ let newVirtual = prevVirtualRaw + delta;
642
+ let newVisible;
643
+ if (newVirtual >= minScale && newVirtual <= maxScale) {
644
+ newVisible = newVirtual;
645
+ } else if (newVirtual < minScale) {
646
+ newVisible = minScale + (newVirtual - minScale) * overshootResistance;
647
+ const lowerBound = Math.max(0, minScale * overshootLimitFactor);
648
+ if (newVisible < lowerBound) {
649
+ newVisible = lowerBound;
650
+ // sync virtual so further moves are consistent with clamped visible value
651
+ newVirtual = minScale + (newVisible - minScale) / Math.max(1e-6, overshootResistance);
652
+ }
653
+ } else {
654
+ newVisible = maxScale + (newVirtual - maxScale) * overshootResistance;
655
+ const upperBound = maxScale * (1 + Math.max(overshootLimitFactor, 0.05));
656
+ if (newVisible > upperBound) {
657
+ newVisible = upperBound;
658
+ newVirtual = maxScale + (newVisible - maxScale) / Math.max(1e-6, overshootResistance);
659
+ }
660
+ }
661
+ const prevVisible = currentVisible;
662
+ if (newVisible === prevVisible) {
663
+ meta.scaleOptions.virtualScale = newVirtual;
522
664
  return;
523
665
  }
524
666
  state.transition('SCALING');
525
667
  const mouseX = event.native.offsetX;
526
668
  const mouseY = event.native.offsetY;
527
- const scaleDiff = newScale / scale;
528
- meta.scaleOptions.scale = newScale;
669
+ // remember anchor for later spring animation
670
+ meta.scaleOptions.lastAnchorX = mouseX;
671
+ meta.scaleOptions.lastAnchorY = mouseY;
672
+ const scaleDiff = newVisible / prevVisible;
673
+ meta.scaleOptions.virtualScale = newVirtual;
674
+ meta.scaleOptions.scale = newVisible;
529
675
  matrix.scale(scaleDiff, scaleDiff);
530
676
  matrix.e = mouseX - (mouseX - matrix.e) * scaleDiff;
531
677
  matrix.f = mouseY - (mouseY - matrix.f) * scaleDiff;
532
- const newMatrix = {
533
- e: matrix.e,
534
- f: matrix.f
535
- };
536
- component.handleTransformCacheInvalidation(oldMatrix, newMatrix);
678
+ try {
679
+ component.handleTransformCacheInvalidation(oldMatrix, {
680
+ e: matrix.e,
681
+ f: matrix.f
682
+ });
683
+ } catch {}
537
684
  updateViewport(pluginContext, domEvent, false);
685
+ const g = meta.gestureState;
686
+ if (g.wheelEndTimeoutId) {
687
+ clearTimeout(g.wheelEndTimeoutId);
688
+ g.wheelEndTimeoutId = null;
689
+ }
690
+ const debounceMs = meta.scaleOptions.wheelDebounce ?? 200;
691
+ g.wheelEndTimeoutId = window.setTimeout(()=>{
692
+ g.wheelEndTimeoutId = null;
693
+ handleWheelEnd(pluginContext, domEvent);
694
+ }, debounceMs);
538
695
  }
539
696
  function handlePan(pluginContext, event, domEvent) {
540
697
  const { stateManager: state, matrix } = domEvent;
541
698
  const panSpeed = 0.8;
542
699
  const deltaX = event.native.deltaX * panSpeed;
543
700
  const deltaY = event.native.deltaY * panSpeed;
701
+ const meta = getScaleOptions.call(pluginContext);
702
+ cancelSpringAnimationIfAny(meta);
544
703
  state.transition('PANNING');
545
704
  matrix.e -= deltaX;
546
705
  matrix.f -= deltaY;
@@ -597,7 +756,6 @@ const presetZoomablePlugin = domEvent.definePlugin({
597
756
  if (scaleMeta) {
598
757
  scaleMeta.scaleOptions.scale = targetScale;
599
758
  }
600
- const highlight = getHighlightInstance.call(this);
601
759
  const dragMeta = getDragOptions.call(this);
602
760
  if (dragMeta) {
603
761
  Object.assign(dragMeta.dragOptions, {
@@ -619,16 +777,13 @@ const presetZoomablePlugin = domEvent.definePlugin({
619
777
  };
620
778
  component.handleTransformCacheInvalidation(oldMatrix, finalMatrix);
621
779
  domEvent.smoothFrame((progress)=>{
622
- const easedProgress = easing.cubicInOut(progress);
780
+ const easedProgress = domEvent.easing.cubicInOut(progress);
623
781
  matrix.create(domEvent.DEFAULT_MATRIX_LOC);
624
782
  matrix.e = startMatrix.e + (targetE - startMatrix.e) * easedProgress;
625
783
  matrix.f = startMatrix.f + (targetF - startMatrix.f) * easedProgress;
626
784
  matrix.a = startMatrix.a + (targetScale - startMatrix.a) * easedProgress;
627
785
  matrix.d = startMatrix.d + (targetScale - startMatrix.d) * easedProgress;
628
- if (highlight?.highlight) {
629
- highlight.highlight.reset();
630
- highlight.highlight.setZIndexForHighlight();
631
- }
786
+ component.clearOverlay();
632
787
  component.cleanup();
633
788
  component.layoutNodes = component.calculateLayoutNodes(component.data, {
634
789
  w: width * matrix.a,