@griddle/svelte 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -38,6 +38,31 @@ shown above). `svelte` (^4 or ^5) is expected to already be in your app.
38
38
  </GriddleGrid>
39
39
  ```
40
40
 
41
+ ## Animation configuration
42
+
43
+ Tile repositioning and lift animations share `config.animation` with the other
44
+ Griddle adapters. Repositioning defaults to a smooth 320 ms ease-out and rapid
45
+ repacks continue from each tile's current visual position.
46
+
47
+ ```js
48
+ const api = createGriddle({
49
+ config: {
50
+ cols: 12, rows: 12, unitWidth: 75, unitHeight: 75,
51
+ animation: {
52
+ repositionDurationMs: 320,
53
+ repositionEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
54
+ liftDurationMs: 160,
55
+ liftEasing: 'cubic-bezier(0.22, 1, 0.36, 1)',
56
+ respectReducedMotion: true,
57
+ },
58
+ },
59
+ tiles,
60
+ });
61
+ ```
62
+
63
+ Use `animation.enabled: false` to disable all adapter animations. A duration of
64
+ `0` disables only that transition.
65
+
41
66
  ## Loop mode (infinite gallery)
42
67
 
43
68
  Enable `loop` in the config and the content repeats endlessly with drag-to-pan
@@ -1,6 +1,7 @@
1
1
  <script>import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
2
2
  import { visibleRange, visibleTiles, gridContentSize, DragController, computeTileLayout, resolveStickyStacking, isOutOfFlow, pixelsToPin, } from '@griddle/core';
3
3
  import LoopGrid from './LoopGrid.svelte';
4
+ import { animateReposition, liftTransition } from './animation.js';
4
5
  export let api;
5
6
  export let height = '100%';
6
7
  export let showGrid = true;
@@ -263,12 +264,7 @@ async function runFlip(_v) {
263
264
  const dx = p.x - x;
264
265
  const dy = p.y - y;
265
266
  if (dx !== 0 || dy !== 0) {
266
- node.style.transition = 'none';
267
- node.style.transform = `translate(${dx}px, ${dy}px)`;
268
- requestAnimationFrame(() => {
269
- node.style.transition = 'transform 220ms cubic-bezier(.2,.7,.2,1)';
270
- node.style.transform = 'translate(0,0)';
271
- });
267
+ animateReposition(node, dx, dy, cfg.animation);
272
268
  }
273
269
  }
274
270
  prevRects.set(t.id, { x, y });
@@ -408,6 +404,7 @@ $: indicatorRect = (() => {
408
404
  style:width={layout.width + 'px'}
409
405
  style:height={layout.height + 'px'}
410
406
  style:transform={layout.transform ?? ''}
407
+ style:transition={isDragging(tile.id) ? liftTransition(cfg.animation) : ''}
411
408
  style:z-index={layout.zIndex}
412
409
  >
413
410
  <slot name="tile" {tile} />
@@ -447,7 +444,7 @@ $: indicatorRect = (() => {
447
444
  box-sizing: border-box;
448
445
  cursor: grab;
449
446
  user-select: none;
450
- will-change: transform;
447
+ will-change: transform, translate;
451
448
  z-index: 1;
452
449
  }
453
450
  .griddle-tile.griddle-resizing {
@@ -463,7 +460,6 @@ $: indicatorRect = (() => {
463
460
  is set inline (cursor delta), so the scale is folded into a CSS filter
464
461
  to avoid clobbering it. */
465
462
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.18));
466
- transition: filter 120ms ease-out, opacity 120ms ease-out;
467
463
  }
468
464
  .griddle-drop-indicator {
469
465
  position: absolute;
@@ -4,6 +4,7 @@
4
4
  // NO native scrolling (wheel deltas feed the camera directly).
5
5
  import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
6
6
  import { DragController, PanController, loopInstances, loopPeriod, resolveLoop, } from '@griddle/core';
7
+ import { animateReposition } from './animation.js';
7
8
  export let api;
8
9
  export let height = '100%';
9
10
  export let showGrid = true;
@@ -329,12 +330,7 @@ async function runFlip(_v) {
329
330
  if ((dx !== 0 || dy !== 0) &&
330
331
  Math.abs(dx) < period.width / 2 &&
331
332
  Math.abs(dy) < period.height / 2) {
332
- node.style.transition = 'none';
333
- node.style.transform = `translate(${dx}px, ${dy}px)`;
334
- requestAnimationFrame(() => {
335
- node.style.transition = 'transform 220ms cubic-bezier(.2,.7,.2,1)';
336
- node.style.transform = 'translate(0,0)';
337
- });
333
+ animateReposition(node, dx, dy, cfg.animation);
338
334
  }
339
335
  }
340
336
  }
@@ -472,6 +468,7 @@ function instanceLayout(inst, r) {
472
468
  position: absolute;
473
469
  box-sizing: border-box;
474
470
  user-select: none;
471
+ will-change: translate;
475
472
  z-index: 1;
476
473
  border-radius: var(--griddle-tile-radius, 4px);
477
474
  }
@@ -0,0 +1,4 @@
1
+ import { type GridAnimationConfig } from '@griddle/core';
2
+ /** Continue rapid repacks from the tile's current visual position. */
3
+ export declare function animateReposition(node: HTMLElement, deltaX: number, deltaY: number, config?: GridAnimationConfig): void;
4
+ export declare function liftTransition(config?: GridAnimationConfig): string;
@@ -0,0 +1,49 @@
1
+ import { resolveAnimationConfig, } from '@griddle/core';
2
+ const pendingFrames = new WeakMap();
3
+ function reducedMotionRequested(config) {
4
+ const animation = resolveAnimationConfig(config);
5
+ return animation.respectReducedMotion &&
6
+ typeof window !== 'undefined' &&
7
+ typeof window.matchMedia === 'function' &&
8
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
9
+ }
10
+ function currentTranslation(node) {
11
+ if (typeof window === 'undefined')
12
+ return { x: 0, y: 0 };
13
+ const translate = window.getComputedStyle(node).translate;
14
+ if (!translate || translate === 'none')
15
+ return { x: 0, y: 0 };
16
+ const [x = '0', y = '0'] = translate.split(/\s+/);
17
+ return { x: Number.parseFloat(x) || 0, y: Number.parseFloat(y) || 0 };
18
+ }
19
+ /** Continue rapid repacks from the tile's current visual position. */
20
+ export function animateReposition(node, deltaX, deltaY, config) {
21
+ const pending = pendingFrames.get(node);
22
+ if (pending !== undefined)
23
+ cancelAnimationFrame(pending);
24
+ const animation = resolveAnimationConfig(config);
25
+ if (!animation.enabled || animation.repositionDurationMs === 0 || reducedMotionRequested(config)) {
26
+ node.style.transition = 'none';
27
+ node.style.translate = '0px 0px';
28
+ pendingFrames.delete(node);
29
+ return;
30
+ }
31
+ const current = currentTranslation(node);
32
+ node.style.transition = 'none';
33
+ node.style.translate = `${deltaX + current.x}px ${deltaY + current.y}px`;
34
+ void node.offsetWidth;
35
+ const frame = requestAnimationFrame(() => {
36
+ pendingFrames.delete(node);
37
+ node.style.transition =
38
+ `translate ${animation.repositionDurationMs}ms ${animation.repositionEasing}`;
39
+ node.style.translate = '0px 0px';
40
+ });
41
+ pendingFrames.set(node, frame);
42
+ }
43
+ export function liftTransition(config) {
44
+ const animation = resolveAnimationConfig(config);
45
+ const duration = !animation.enabled || reducedMotionRequested(config)
46
+ ? 0
47
+ : animation.liftDurationMs;
48
+ return `filter ${duration}ms ${animation.liftEasing}, opacity ${duration}ms ${animation.liftEasing}`;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@griddle/svelte",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Svelte bindings for Griddle — a headless, zero-dependency grid/canvas engine.",
5
5
  "keywords": [
6
6
  "grid",
@@ -49,14 +49,15 @@
49
49
  },
50
50
  "scripts": {
51
51
  "build": "svelte-package -i src -o dist",
52
- "prepublishOnly": "npm run build"
52
+ "test": "node --test test/animation.test.mjs",
53
+ "prepublishOnly": "npm run build && npm test"
53
54
  },
54
55
  "peerDependencies": {
55
- "@griddle/core": "^0.1.0",
56
+ "@griddle/core": "^0.1.3",
56
57
  "svelte": "^4.0.0 || ^5.0.0"
57
58
  },
58
59
  "devDependencies": {
59
- "@griddle/core": "^0.1.0",
60
+ "@griddle/core": "^0.1.3",
60
61
  "@sveltejs/package": "^2.5.8",
61
62
  "svelte": "^4.2.0",
62
63
  "svelte-check": "^4.7.2",