@zumer/orbit-kit 0.1.0 → 2.0.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/src/o-radar.js CHANGED
@@ -1,145 +1,102 @@
1
+ import { OrbitKitElement, define, injectStyleOnce, universe, finiteNumber, cssValue, fixed, normalizeRange, escapeHTML as e, withShared, FORMAT_ATTRS } from './base.js';
2
+
1
3
  /**
2
- * <o-radar> — radar / scope display.
3
- *
4
- * Built the idiomatic Orbit way: the range rings ARE orbits — each `.orbit-N`
5
- * is a concentric circle, so we just style its border instead of stacking
6
- * full-circle arcs. On top of that grid: radial cross-hair spokes (`.vector`),
7
- * a rotating sweep wedge with a bright leading edge, and glowing blips placed
8
- * on the orbit matching their distance and aimed with `--o-angle-composite`.
9
- *
10
- * .bigbang > .gravity-spot
11
- * ├── .orbit-{2..outer} (styled border) — range rings
12
- * ├── .orbit-{outer} fit-range range-360 > .vector × spokes
13
- * ├── .orbit-{outer} (rotating) > <o-arc> wedge — the sweep
14
- * ├── .orbit-{level} > .satellite (angled) — one per blip
15
- * └── .orbit-0 > .satellite.at-center — hub + label
4
+ * <o-radar>: a scope with range rings, spokes, a rotating sweep and blips.
16
5
  *
17
- * <o-radar rings="4" color="#22d3ee" spokes="8"
18
- * targets='[{"angle":60,"dist":0.8},{"angle":200,"dist":0.45}]'></o-radar>
6
+ * .orbit-12 ok-radar-grid range rings (one element, any count)
7
+ * .orbit-0 > .vector × spokes rays from the center, one radius long
8
+ * .orbit-12 o-radar-sweep conic-gradient beam, rotating
9
+ * .orbit-12 > .satellite × targets blips: angle from --o-angle-composite,
10
+ * distance from --o-alignment
19
11
  *
20
- * `angle` is degrees clockwise from North (top); `dist` is 0–1 from center.
12
+ * Target updates touch only the blip layers, so the sweep never restarts.
21
13
  */
22
- import { OrbitKitElement, define, injectStyleOnce } from './base.js';
23
-
24
14
  const RADAR_CSS = `
25
- @keyframes o-radar-sweep { to { transform: rotate(360deg); } }
26
- @keyframes o-radar-ping {
27
- 0% { transform: scale(.5); opacity: 1; }
28
- 70% { opacity: .45; }
29
- 100% { transform: scale(1.7); opacity: 0; }
15
+ o-radar { --ok-font-size: 6cqmin; }
16
+ @keyframes o-radar-sweep { to { rotate: 360deg; } }
17
+ @keyframes o-radar-ping { 0% { scale: .5; opacity: 1; } 70% { opacity: .45; } 100% { scale: 1.7; opacity: 0; } }
18
+ o-radar .ok-radar-grid {
19
+ --c: var(--ok-radar-color, #22d3ee);
20
+ background: repeating-radial-gradient(closest-side, transparent 0 calc(100% / var(--ok-radar-rings) - 1px),
21
+ color-mix(in srgb, var(--c) 22%, transparent) calc(100% / var(--ok-radar-rings) - 1px) calc(100% / var(--ok-radar-rings)));
22
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 35%, transparent);
23
+ }
24
+ o-radar .ok-radar-spoke { background: color-mix(in srgb, var(--ok-radar-color, #22d3ee) 14%, transparent); height: 1px; }
25
+ o-radar .o-radar-sweep {
26
+ --c: var(--ok-radar-color, #22d3ee);
27
+ background: conic-gradient(from 0deg, transparent 0deg,
28
+ color-mix(in srgb, var(--c) 5%, transparent) calc(var(--ok-radar-beam) / 2),
29
+ color-mix(in srgb, var(--c) 30%, transparent) calc(var(--ok-radar-beam) - 3deg),
30
+ color-mix(in srgb, var(--c) 85%, transparent) var(--ok-radar-beam), transparent var(--ok-radar-beam));
31
+ animation: o-radar-sweep var(--ok-radar-speed, var(--o-radar-speed, 4s)) linear infinite;
30
32
  }
31
- o-radar .o-radar-sweep { animation: o-radar-sweep var(--o-radar-speed, 4s) linear infinite; transform-origin: center; }
32
- o-radar .o-radar-blip { position: relative; border-radius: 50%; }
33
- o-radar .o-radar-blip::after {
34
- content: ""; position: absolute; inset: -4px; border-radius: 50%;
35
- border: 1px solid currentColor; animation: o-radar-ping 1.8s ease-out infinite;
33
+ o-radar .ok-radar-target > .satellite { color: var(--ok-radar-color, #22d3ee); }
34
+ o-radar .o-radar-blip {
35
+ position: relative; border-radius: 50%; width: max(5px, 3.5cqmin); aspect-ratio: 1;
36
+ background: currentColor; box-shadow: 0 0 10px currentColor;
36
37
  }
38
+ o-radar .o-radar-blip::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 1px solid currentColor; animation: o-radar-ping 1.8s ease-out infinite; }
39
+ o-radar .ok-radar-center { display: flex; flex-direction: column; align-items: center; gap: .3em; color: var(--ok-radar-color, #22d3ee); }
40
+ o-radar .ok-radar-center::before { content: ""; width: max(4px, 2.4cqmin); aspect-ratio: 1; border-radius: 50%; background: currentColor; }
41
+ o-radar .ok-radar-label { font-weight: 800; white-space: nowrap; }
42
+ @media (prefers-reduced-motion: reduce) { o-radar .o-radar-sweep, o-radar .o-radar-blip::after { animation: none; } }
37
43
  `;
38
44
 
39
45
  export class OrbitRadar extends OrbitKitElement {
40
46
  static get observedAttributes() {
41
- return ['targets', 'rings', 'spokes', 'size', 'color', 'sweep', 'sweep-width', 'speed', 'label'];
47
+ return withShared(['targets', 'rings', 'spokes', 'size', 'color', 'accent', 'sweep', 'sweep-width', 'speed', 'label', 'min', 'max'], FORMAT_ATTRS);
42
48
  }
43
-
44
49
  readConfig() {
45
- const targets = (this._json('targets', []) || []).map((t) => ({
46
- angle: Number(t.angle) || 0,
47
- dist: Math.min(1, Math.max(0, t.dist == null ? 0.5 : Number(t.dist))),
48
- color: t.color || null
49
- }));
50
+ // `dist` is read in min..max (0 to 1 by default): min at the center, max at the edge.
51
+ const r = normalizeRange({ min: this._num('min', 0), max: this._num('max', 1) });
52
+ const share = (d) => (r.span ? (d - r.min) / r.span : 0);
50
53
  return {
51
- targets,
52
- rings: Math.max(1, Math.round(this._num('rings', 4))),
53
- spokes: Math.max(0, Math.round(this._num('spokes', 8))),
54
- size: this._str('size', '240px'),
55
- color: this._str('color', '#22d3ee'),
56
- sweep: this._str('sweep', 'true') !== 'false',
57
- sweepWidth: Math.min(300, Math.max(10, this._num('sweep-width', 110))),
58
- speed: this._num('speed', 4),
59
- label: this.hasAttribute('label') ? this.getAttribute('label') : null
54
+ min: r.min, max: r.max,
55
+ targets: this._array('targets').map((t) => {
56
+ const raw = Math.min(r.max, Math.max(r.min, finiteNumber(t.dist, r.min + r.span / 2)));
57
+ return { angle: ((finiteNumber(t.angle) % 360) + 360) % 360, raw, dist: share(raw), color: cssValue(t.color), label: t.label == null ? null : String(t.label) };
58
+ }),
59
+ rings: Math.max(1, Math.min(60, Math.round(this._num('rings', 4)))),
60
+ spokes: Math.max(0, Math.min(360, Math.round(this._num('spokes', 8)))), size: this._size('size', '240px'),
61
+ color: this._color('color', this._color('accent', null)), sweep: this._str('sweep', 'true') !== 'false',
62
+ sweepWidth: Math.min(300, Math.max(10, this._num('sweep-width', 110))), speed: Math.max(.01, this._num('speed', 4)), label: this.getAttribute('label')
60
63
  };
61
64
  }
62
-
63
- structuralSignature(cfg) {
64
- return JSON.stringify(cfg);
65
+ structuralSignature({ targets, speed, label, color, min, max, ...structure }) { return JSON.stringify(structure); }
66
+ _target(t) {
67
+ return `<div class="orbit-12 ok-radar-target"><div class="satellite" style="--o-angle-composite:${fixed(t.angle - 90)}deg;--o-alignment:calc(var(--o-radius) * ${fixed(1 - t.dist, 4)});${t.color ? `color:${e(t.color)}` : ''}"><div class="capsule o-radar-blip"></div></div></div>`;
65
68
  }
66
-
67
69
  build(cfg) {
68
- injectStyleOnce('orbit-kit-radar', RADAR_CSS);
69
-
70
- const C = cfg.color;
71
- // Two orbit steps per ring so the grid breathes; outer ring hosts sweep/spokes.
72
- const outer = cfg.rings * 2;
73
-
74
- // Range rings — native orbit borders, outermost a touch brighter.
75
- const rings = Array.from({ length: cfg.rings }, (_, i) => {
76
- const orbit = (i + 1) * 2;
77
- const alpha = 0.1 + (i / cfg.rings) * 0.22;
78
- return `<div class="orbit-${orbit}" style="border:1px solid ${this._rgba(C, alpha)};"></div>`;
79
- }).join('');
80
-
81
- // Cross-hair spokes — radial vectors, evenly distributed.
82
- const spokes = cfg.spokes
83
- ? `<div class="orbit-${outer} fit-range range-360 from-0">
84
- ${Array.from({ length: cfg.spokes }, () =>
85
- `<div class="vector" style="background:${this._rgba(C, 0.14)};height:1px;"></div>`).join('')}
86
- </div>`
87
- : '';
88
-
89
- // Sweep — a real rotating beam: a conic-gradient disc painted on the outer
90
- // orbit circle, fading from a transparent tail to a crisp bright leading
91
- // edge over `sweepWidth` degrees. Smooth, not a blocky fan of arcs.
92
- const w = cfg.sweepWidth;
93
- const beam = `conic-gradient(from 0deg,`
94
- + ` ${this._rgba(C, 0)} 0deg,`
95
- + ` ${this._rgba(C, 0.05)} ${(w * 0.5).toFixed(1)}deg,`
96
- + ` ${this._rgba(C, 0.30)} ${(w - 3).toFixed(1)}deg,`
97
- + ` ${this._rgba(C, 0.85)} ${w}deg,`
98
- + ` transparent ${w}deg, transparent 360deg)`;
99
- const sweep = cfg.sweep
100
- ? `<div class="orbit-${outer} o-radar-sweep" style="--o-radar-speed:${cfg.speed}s;background:${beam};"></div>`
101
- : '';
102
-
103
- // Blips — each on the orbit matching its distance, aimed by angle.
104
- const blips = cfg.targets.map((t) => {
105
- const color = t.color || C;
106
- const level = Math.min(outer, Math.max(1, Math.round(t.dist * outer)));
107
- return `<div class="orbit-${level}">
108
- <div class="satellite" style="--o-angle-composite:calc(${t.angle}deg - 90deg);color:${color};">
109
- <div class="capsule o-radar-blip" style="width:9px;height:9px;background:${color};box-shadow:0 0 10px ${color};"></div>
110
- </div>
111
- </div>`;
112
- }).join('');
113
-
114
- const center = `
115
- <div class="orbit-0">
116
- <div class="satellite at-center">
117
- <div class="capsule" style="display:flex;flex-direction:column;align-items:center;gap:4px;">
118
- <span style="width:6px;height:6px;border-radius:50%;background:${C};box-shadow:0 0 8px ${C};"></span>
119
- ${cfg.label !== null ? `<span style="font-weight:800;font-size:.7em;letter-spacing:.15em;color:${C};">${cfg.label}</span>` : ''}
120
- </div>
121
- </div>
122
- </div>`;
123
-
124
- this.innerHTML = `
125
- <div class="bigbang" style="width:${cfg.size};aspect-ratio:1;">
126
- <div class="gravity-spot">
127
- ${rings}
128
- ${spokes}
129
- ${sweep}
130
- ${blips}
131
- ${center}
132
- </div>
133
- </div>`;
70
+ const spokes = cfg.spokes ? `<div class="orbit-0">${'<div class="vector outer-orbit ok-radar-spoke" style="--o-size-ratio:12"></div>'.repeat(cfg.spokes)}</div>` : '';
71
+ const sweep = cfg.sweep ? '<div class="orbit-12 o-radar-sweep"></div>' : '';
72
+ this.innerHTML = universe(cfg.size, `
73
+ <div class="orbit-12 ok-radar-grid"></div>${spokes}${sweep}${cfg.targets.map((t) => this._target(t)).join('')}
74
+ <div class="orbit-0"><div class="satellite at-center"><div class="capsule ok-radar-center"><span class="ok-radar-label">${e(cfg.label ?? '')}</span></div></div></div>`,
75
+ { spot: `--ok-radar-rings:${cfg.rings};--ok-radar-beam:${cfg.sweepWidth}deg` });
76
+ this._scope = this.querySelector('.gravity-spot');
77
+ this._targets = [...this.querySelectorAll('.ok-radar-target')];
78
+ this._label = this.querySelector('.ok-radar-label');
79
+ this._sweep = this.querySelector('.o-radar-sweep');
134
80
  }
135
-
136
- /** Convert #rrggbb (or pass-through) to rgba() at the given alpha. */
137
- _rgba(color, alpha) {
138
- const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
139
- if (!m) return color;
140
- const [r, g, b] = [m[1], m[2], m[3]].map((h) => parseInt(h, 16));
141
- return `rgba(${r},${g},${b},${alpha.toFixed(3)})`;
81
+ sync(cfg) {
82
+ this._applyTheme({ '--ok-radar-color': cfg.color, '--ok-radar-speed': `${cfg.speed}s` });
83
+ // Reconcile only the target layers, keeping the animated sweep node alive.
84
+ while (this._targets.length > cfg.targets.length) this._targets.pop().remove();
85
+ while (this._targets.length < cfg.targets.length && this._scope?.insertAdjacentHTML) {
86
+ this._scope.insertAdjacentHTML('beforeend', this._target(cfg.targets[this._targets.length]));
87
+ this._targets.push(this._scope.lastElementChild);
88
+ }
89
+ this._targets.forEach((layer, i) => {
90
+ const t = cfg.targets[i];
91
+ const satellite = layer.querySelector('.satellite');
92
+ satellite.style.setProperty('--o-angle-composite', `${fixed(t.angle - 90)}deg`);
93
+ satellite.style.setProperty('--o-alignment', `calc(var(--o-radius) * ${fixed(1 - t.dist, 4)})`);
94
+ if (t.color) satellite.style.setProperty('color', t.color);
95
+ else satellite.style.removeProperty?.('color');
96
+ });
97
+ if (this._label) { this._label.textContent = cfg.label ?? ''; this._label.hidden = !cfg.label; }
98
+ this._describe([cfg.label || 'Radar', ...cfg.targets.map((t, i) => `${t.label ?? `Target ${i + 1}`}: ${t.angle} degrees, ${this._formatOptions() || this._formatter ? `distance ${this._formatText(t.raw, { min: cfg.min, max: cfg.max, kind: 'aria' })}` : `${Math.round(t.dist * 100)}% distance`}`)].join('; '));
142
99
  }
143
100
  }
144
-
101
+ injectStyleOnce('orbit-kit-radar', RADAR_CSS);
145
102
  define('o-radar', OrbitRadar);