atagen

malice

public
Star0
Watch0
81d6bf15e59bfcfc57f792500008a796939cdbd5
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/motion.tsTypescript86 lines2.9 KB
1
import { cubicOut } from 'svelte/easing';
2
import { fade, slide, type TransitionConfig } from 'svelte/transition';
3
4
const entering = new Set<string>();
5
const leaving = new Set<string>();
6
let busyUntil = 0;
7
8
export function motionDuration(ms: number): number {
9
  return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : ms;
10
}
11
12
export function motionActive(): boolean {
13
  return performance.now() < busyUntil;
14
}
15
16
function noteBusy(ms: number): void {
17
  busyUntil = Math.max(busyUntil, performance.now() + ms + 60);
18
}
19
20
export function setRowMotion(enter: Iterable<string>, leave: Iterable<string>): void {
21
  entering.clear();
22
  leaving.clear();
23
  for (const key of enter) entering.add(key);
24
  for (const key of leave) leaving.add(key);
25
}
26
27
function rowKey(node: Element): string {
28
  const { item, row } = (node as HTMLElement).dataset;
29
  return item ?? row ?? '';
30
}
31
32
/** Sub-pixel sizes from many rows easing out together made the pinned bottom flip between roundings. */
33
function wholePixels(css: string): string {
34
  return css.replace(/(-?\d*\.?\d+)px/g, (_, n: string) => `${Math.round(Number(n))}px`);
35
}
36
37
function reveal(node: Element, ms: number, axis: 'x' | 'y' = 'y'): TransitionConfig {
38
  const duration = motionDuration(ms);
39
  if (duration === 0) return { duration: 0 };
40
  noteBusy(duration);
41
  const base = slide(node, { duration, easing: cubicOut, axis });
42
  return { duration, easing: cubicOut, css: (t, u) => `${wholePixels(base.css?.(t, u) ?? '')};opacity: ${t};` };
43
}
44
45
export function rowIn(node: Element): TransitionConfig {
46
  return entering.has(rowKey(node)) ? reveal(node, 200) : { duration: 0 };
47
}
48
49
export function rowOut(node: Element): TransitionConfig {
50
  return leaving.has(rowKey(node)) ? reveal(node, 160) : { duration: 0 };
51
}
52
53
export function expand(node: Element, params: { axis?: 'x' | 'y' } = {}): TransitionConfig {
54
  return reveal(node, 170, params.axis);
55
}
56
57
export function dissolve(node: Element): TransitionConfig {
58
  const duration = motionDuration(150);
59
  if (duration > 0) noteBusy(duration);
60
  return fade(node, { duration });
61
}
62
63
const settling = new WeakSet<HTMLElement>();
64
65
export function settleHeight(node: HTMLElement, from: number): void {
66
  const duration = motionDuration(180);
67
  const to = node.offsetHeight;
68
  if (duration === 0 || Math.abs(to - from) < 2 || settling.has(node) || node.getAnimations({ subtree: true }).length > 0) return;
69
  noteBusy(duration);
70
  settling.add(node);
71
  const start = performance.now();
72
  node.style.overflow = 'hidden';
73
  const step = (now: number) => {
74
    const t = Math.min(1, (now - start) / duration);
75
    if (t < 1) {
76
      node.style.height = `${Math.round(from + (to - from) * cubicOut(t))}px`;
77
      requestAnimationFrame(step);
78
    } else {
79
      node.style.height = '';
80
      node.style.overflow = '';
81
      settling.delete(node);
82
    }
83
  };
84
  node.style.height = `${Math.round(from)}px`;
85
  requestAnimationFrame(step);
86
}