File size: 5,302 Bytes
0c4637f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/* =========================================================================
 * DSSL: effects.js (0.10.0)
 *
 *   glass: 18                          frosted glass: blur what is behind it
 *   glass: 24 saturate 1.8 tint white/12
 *   tilt: 10                           follows the pointer in 3D
 *   tilt: 12 lift 1.04 glare           also scales up, and exposes --dssl-gx/--dssl-gy
 *
 * glass is plain CSS (backdrop-filter plus a translucent background), so it
 * survives `dssl build`. tilt needs the pointer, so it stays in the engine.
 * ========================================================================= */

import { splitWords } from './texture.js';

const isNum = (w) => w !== undefined && w !== '' && !Number.isNaN(Number(w));

export const GLASS_WORDS = {
  blur: 'a number: how many px to blur what is behind the element (default 16)',
  saturate: 'saturate <n>: colour boost for what shows through, 1 is unchanged (default 1.5)',
  bright: 'bright <n>: brightness of what shows through, 1 is unchanged',
  tint: 'tint <color> | tint none: the milky layer on top (default: a faint wash of the text colour)',
};

export const TILT_WORDS = {
  max: 'a number: the largest tilt in degrees (default 8)',
  lift: 'lift <n>: scale while the pointer is over it, e.g. 1.04',
  perspective: 'perspective <px>: how strong the 3D depth looks (default 900)',
  speed: 'speed <0.05 to 1>: how quickly it follows (default 0.18)',
  invert: 'invert: tilt away from the pointer instead of toward it',
  glare: 'glare: also keep --dssl-gx and --dssl-gy (0% to 100%) pointing at the pointer, for your own highlight',
};

/** `glass:` value -> declarations, or null after reporting an error */
export function glassDecls(value, hooks) {
  const w = splitWords(String(value || '').trim());
  if (w[0] === 'none') return [{ prop: 'backdrop-filter', value: 'none' }];
  let blur = 16, sat = 1.5, bright = null, tint;
  const bad = (m, h) => { hooks.error(m, h); return null; };
  for (let i = 0; i < w.length; i++) {
    const x = w[i];
    if (isNum(x)) { blur = Number(x); continue; }
    if (x === 'saturate' || x === 'bright') {
      if (!isNum(w[i + 1])) return bad(`${x} needs a number`, GLASS_WORDS[x]);
      if (x === 'saturate') sat = Number(w[i + 1]); else bright = Number(w[i + 1]);
      i++; continue;
    }
    if (x === 'tint') {
      const c = w[i + 1];
      if (!c) return bad('tint needs a color or "none"', GLASS_WORDS.tint);
      if (c === 'none') tint = null;
      else { const css = hooks.color(c); if (css === null) return null; tint = css; }
      i++; continue;
    }
    // a bare colour word is a tint, anything else is a typo
    if (hooks.looksLikeColor && hooks.looksLikeColor(x)) {
      const css = hooks.color(x); if (css === null) return null; tint = css; continue;
    }
    return bad(`unknown glass setting "${x}"`, hooks.suggest(x, ['saturate', 'bright', 'tint', 'none']) || 'settings: a blur number, saturate, bright, tint');
  }
  if (blur < 0) return bad('glass blur cannot be negative', GLASS_WORDS.blur);
  const fx = [`blur(${blur}px)`, `saturate(${sat})`];
  if (bright !== null) fx.push(`brightness(${bright})`);
  const out = [{ prop: 'backdrop-filter', value: fx.join(' ') }];
  if (tint === undefined) tint = 'color-mix(in srgb, currentColor 8%, transparent)';
  if (tint !== null) out.push({ prop: 'background-color', value: tint });
  return out;
}

/** `tilt:` value -> a spec for the engine, `null` to clear it, or undefined after an error */
export function parseTilt(value, hooks) {
  const w = splitWords(String(value || '').trim());
  if (!w.length) { hooks.error('tilt needs a number of degrees', 'e.g. tilt: 10'); return undefined; }
  if (w[0] === 'none') return null;
  const spec = { max: 8, lift: 1, perspective: 900, speed: 0.18, invert: false, glare: false };
  const bad = (m, h) => { hooks.error(m, h); return undefined; };
  const needNum = (name, i) => (isNum(w[i + 1]) ? Number(w[i + 1]) : null);
  for (let i = 0; i < w.length; i++) {
    const x = w[i];
    if (isNum(x)) { spec.max = Number(x); continue; }
    if (x === 'invert') { spec.invert = true; continue; }
    if (x === 'glare') { spec.glare = true; continue; }
    if (x === 'lift' || x === 'perspective' || x === 'speed') {
      const n = needNum(x, i);
      if (n === null) return bad(`${x} needs a number`, TILT_WORDS[x]);
      spec[x] = n; i++; continue;
    }
    return bad(`unknown tilt setting "${x}"`, hooks.suggest(x, ['lift', 'perspective', 'speed', 'invert', 'glare', 'none']) || 'settings: a degrees number, lift, perspective, speed, invert, glare');
  }
  if (spec.max < 0 || spec.max > 45) return bad('tilt degrees should be between 0 and 45', TILT_WORDS.max);
  if (spec.lift <= 0) return bad('lift must be above 0', TILT_WORDS.lift);
  if (spec.perspective < 100) return bad('perspective must be at least 100px', TILT_WORDS.perspective);
  if (spec.speed < 0.02 || spec.speed > 1) return bad('speed must be between 0.02 and 1', TILT_WORDS.speed);
  return spec;
}

/** the transform pieces tilt adds after whatever the author wrote; they read variables the engine updates */
export function tiltTransform(spec) {
  return `perspective(${spec.perspective}px) rotateX(var(--dssl-tx, 0deg)) rotateY(var(--dssl-ty, 0deg)) scale(var(--dssl-ts, 1))`;
}