File size: 4,392 Bytes
e92b981
2d39154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e92b981
2d39154
 
 
 
 
e92b981
2d39154
 
 
 
 
 
 
 
 
e92b981
 
 
 
2d39154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e92b981
2d39154
 
 
 
 
 
 
 
 
 
 
e92b981
2d39154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/* DSSL - utils.js - tiny shared helpers */

export function levenshtein(a, b) {
  if (a === b) return 0;
  const m = a.length, n = b.length;
  if (!m) return n;
  if (!n) return m;
  let prev = new Array(n + 1);
  let cur = new Array(n + 1);
  for (let j = 0; j <= n; j++) prev[j] = j;
  for (let i = 1; i <= m; i++) {
    cur[0] = i;
    for (let j = 1; j <= n; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
    }
    [prev, cur] = [cur, prev];
  }
  return prev[n];
}

/** best "did you mean ..." suggestion, or undefined */
export function didYouMean(word, candidates) {
  if (!word || !candidates || !candidates.length) return undefined;
  let best = undefined, bestDist = Infinity;
  const w = word.toLowerCase();
  for (const c of candidates) {
    if (!c) continue;
    const d = levenshtein(w, String(c).toLowerCase());
    if (d < bestDist) { bestDist = d; best = c; }
  }
  const tolerance = Math.max(2, Math.floor(w.length / 3));
  return bestDist <= tolerance ? String(best) : undefined;
}

/* ------------------------------------------------------------------ *
 * v0.6.0 - the deform family (bend / bend-y / bend-up / fold / stretch)
 *
 * These are engine-native properties (`@bend`, …) that compose into ONE
 * transform string, so transitions and WAAPI tween them like any other
 * transform. Edge-pinned members (fold plants the top edge, bend-up plants
 * the bottom edge) bake their pivot into the transform with the classic
 * translate · rotate · translate trick - that way they never fight a user
 * transform-origin. In keyframe mode there is no box yet, so they emit a
 * transformOrigin for WAAPI instead.
 * ------------------------------------------------------------------ */

const r2 = (v) => Math.round(v * 100) / 100;
const r4 = (v) => Math.round(v * 1e4) / 1e4;

/**
 * Compose deform parts (+ optional base transform) into a transform string.
 *   d    - { bend, bendY, stretch, stretchAxis, fold, bendUp } numbers, 0 = off
 *   base - an existing transform string to run after the deform parts, or ''
 *   box  - { h } element height for edge-pinned pivots; null in keyframe mode
 * Returns { transform, origin } - origin is only set in keyframe mode.
 */
export function composeDeform(d, base, box) {
  const parts = [];
  let origin = null;
  const has3D = d.bend || d.bendY || d.fold || d.bendUp;
  if (has3D) parts.push('perspective(900px)');
  if (d.bend) parts.push(`rotateX(${d.bend}deg)`);
  if (d.bendY) parts.push(`rotateY(${d.bendY}deg)`);
  const h = box && box.h ? box.h : 0;
  if (d.fold) {          // top edge pinned, bottom folds up toward the viewer
    if (h > 0) parts.push(`translateY(${r2(-h / 2)}px)`, `rotateX(${d.fold}deg)`, `translateY(${r2(h / 2)}px)`);
    else { parts.push(`rotateX(${d.fold}deg)`); origin = '50% 0%'; }
  }
  if (d.bendUp) {        // bottom edge pinned, the element rocks back like a drawbridge
    if (h > 0) parts.push(`translateY(${r2(h / 2)}px)`, `rotateX(${d.bendUp}deg)`, `translateY(${r2(-h / 2)}px)`);
    else { parts.push(`rotateX(${d.bendUp}deg)`); origin = '50% 100%'; }
  }
  if (d.stretch && d.stretch !== 1) {   // squash & stretch - volume stays constant
    const inv = r4(1 / d.stretch);
    parts.push(d.stretchAxis === 'vertical'
      ? `scaleY(${d.stretch}) scaleX(${inv})`
      : `scaleX(${d.stretch}) scaleY(${inv})`);
  }
  const baseT = base && base !== 'none' ? String(base).trim() : '';
  const all = baseT ? [...parts, baseT] : parts;
  return { transform: all.length ? all.join(' ') : 'none', origin };
}

/**
 * v0.6.0 - bend-around: lay n children along a circular arc.
 * Positive deg wraps the row around an invisible ball (a dome, ends dip);
 * negative deg opens it into a smile (ends rise). Returns the pivot radius
 * R (px, measured from each child's center) and one rotation angle per
 * child, or null when there is nothing to arc (fewer than 2 children, no
 * width, or a near-zero angle).
 */
export function arcLayout(n, width, deg) {
  if (!n || n < 2 || !deg || !width || width <= 0) return null;
  const half = (Math.abs(deg) / 2) * Math.PI / 180;
  const sin = Math.sin(half);
  if (sin < 1e-4) return null;
  const R = r2((width / 2) / sin);
  const angles = [];
  for (let i = 0; i < n; i++) angles.push(r2(-deg / 2 + (deg * i) / (n - 1)));
  return { R, angles };
}