@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/README.md +108 -316
- package/package.json +20 -8
- package/src/base.js +564 -41
- package/src/color.js +139 -0
- package/src/o-clock.js +106 -129
- package/src/o-color-wheel.js +322 -0
- package/src/o-compass.js +45 -49
- package/src/o-donut.js +67 -79
- package/src/o-gauge.js +51 -117
- package/src/o-instrument.js +116 -105
- package/src/o-knob.js +137 -129
- package/src/o-menu.js +271 -183
- package/src/o-meter.js +35 -89
- package/src/o-radar-chart.js +391 -0
- package/src/o-radar.js +80 -123
- package/src/o-range.js +509 -0
- package/src/o-rings.js +66 -77
- package/src/o-spinner.js +214 -0
- package/src/o-status.js +209 -0
- package/src/o-sunburst.js +569 -0
- package/src/o-time-picker.js +447 -0
- package/src/o-timer.js +240 -0
- package/src/o-wheel.js +436 -0
- package/src/orbit-kit.js +23 -5
package/src/color.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color helpers for the kit: parsing, HSV/HSL/RGB conversions, contrast and
|
|
3
|
+
* plain-English names. Pure functions, safe in Node; named CSS colors are
|
|
4
|
+
* resolved through a canvas when there is a document.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
8
|
+
const round = (n, digits = 0) => Number(Number(n).toFixed(digits));
|
|
9
|
+
const hex2 = (n) => Math.round(clamp(n, 0, 255)).toString(16).padStart(2, '0');
|
|
10
|
+
|
|
11
|
+
/** { r, g, b } (0-255) → "#rrggbb". */
|
|
12
|
+
export const rgbToHex = ({ r, g, b }) => `#${hex2(r)}${hex2(g)}${hex2(b)}`;
|
|
13
|
+
|
|
14
|
+
/** h 0-360, s and v 0-1 → { r, g, b } 0-255. */
|
|
15
|
+
export function hsvToRgb({ h, s, v }) {
|
|
16
|
+
const f = (n) => {
|
|
17
|
+
const k = (n + h / 60) % 6;
|
|
18
|
+
return (v - v * s * Math.max(0, Math.min(k, 4 - k, 1))) * 255;
|
|
19
|
+
};
|
|
20
|
+
return { r: f(5), g: f(3), b: f(1) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** { r, g, b } 0-255 → { h, s, v }, h 0-360 (0 for grays), s and v 0-1. */
|
|
24
|
+
export function rgbToHsv({ r, g, b }) {
|
|
25
|
+
r /= 255; g /= 255; b /= 255;
|
|
26
|
+
const max = Math.max(r, g, b), d = max - Math.min(r, g, b);
|
|
27
|
+
let h = 0;
|
|
28
|
+
if (d) h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
29
|
+
return { h: (h * 60 + 360) % 360, s: max ? d / max : 0, v: max };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** { h, s, v } → { h, s, l }, all but h 0-1. */
|
|
33
|
+
export function hsvToHsl({ h, s, v }) {
|
|
34
|
+
const l = v * (1 - s / 2);
|
|
35
|
+
return { h, s: l === 0 || l === 1 ? 0 : (v - l) / Math.min(l, 1 - l), l };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** { h, s, l } → { h, s, v }. */
|
|
39
|
+
export function hslToHsv({ h, s, l }) {
|
|
40
|
+
const v = l + s * Math.min(l, 1 - l);
|
|
41
|
+
return { h, s: v ? 2 * (1 - l / v) : 0, v };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let probe;
|
|
45
|
+
/** A named or any other CSS color, through the browser; null when there is none. */
|
|
46
|
+
function browserColor(text) {
|
|
47
|
+
if (typeof document === 'undefined' || !document.createElement) return null;
|
|
48
|
+
try {
|
|
49
|
+
probe ??= document.createElement('canvas').getContext('2d');
|
|
50
|
+
if (!probe) return null;
|
|
51
|
+
probe.fillStyle = '#010203';
|
|
52
|
+
probe.fillStyle = text;
|
|
53
|
+
const out = probe.fillStyle;
|
|
54
|
+
if (out === '#010203' && !/^#?010203$/i.test(text.trim())) return null;
|
|
55
|
+
return out.startsWith('#') ? out : null;
|
|
56
|
+
} catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const num = (part, scale) => {
|
|
60
|
+
const text = String(part).trim();
|
|
61
|
+
if (text.endsWith('%')) return Number.parseFloat(text) / 100 * scale;
|
|
62
|
+
return Number.parseFloat(text);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A CSS color → { r, g, b } (0-255), or null. Reads #rgb, #rrggbb (alpha is
|
|
67
|
+
* ignored), rgb()/rgba(), hsl()/hsla() and, in a browser, any named color.
|
|
68
|
+
*/
|
|
69
|
+
export function parseColor(value) {
|
|
70
|
+
const text = String(value ?? '').trim().toLowerCase();
|
|
71
|
+
if (!text) return null;
|
|
72
|
+
let m = text.match(/^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/);
|
|
73
|
+
if (m) {
|
|
74
|
+
let hex = m[1];
|
|
75
|
+
if (hex.length <= 4) hex = hex.slice(0, 3).split('').map((c) => c + c).join('');
|
|
76
|
+
return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16) };
|
|
77
|
+
}
|
|
78
|
+
m = text.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
79
|
+
if (m) {
|
|
80
|
+
const parts = m[2].split(/[\s,/]+/).filter(Boolean);
|
|
81
|
+
if (parts.length < 3) return null;
|
|
82
|
+
if (m[1].startsWith('rgb')) {
|
|
83
|
+
const [r, g, b] = parts.slice(0, 3).map((p) => num(p, 255));
|
|
84
|
+
return [r, g, b].every(Number.isFinite) ? { r: clamp(r, 0, 255), g: clamp(g, 0, 255), b: clamp(b, 0, 255) } : null;
|
|
85
|
+
}
|
|
86
|
+
const h = Number.parseFloat(parts[0]), s = num(parts[1], 1), l = num(parts[2], 1);
|
|
87
|
+
const S = parts[1].endsWith('%') ? s : s / 100, L = parts[2].endsWith('%') ? l : l / 100;
|
|
88
|
+
if (![h, S, L].every(Number.isFinite)) return null;
|
|
89
|
+
return hsvToRgb(hslToHsv({ h: ((h % 360) + 360) % 360, s: clamp(S, 0, 1), l: clamp(L, 0, 1) }));
|
|
90
|
+
}
|
|
91
|
+
const resolved = browserColor(text);
|
|
92
|
+
return resolved ? parseColor(resolved) : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** WCAG relative luminance, 0 (black) to 1 (white). */
|
|
96
|
+
export function luminance({ r, g, b }) {
|
|
97
|
+
const lin = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
|
|
98
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Dark or light text, whichever reads better on `color`. */
|
|
102
|
+
export function contrastText(color, dark = '#0c0f13', light = '#ffffff') {
|
|
103
|
+
const rgb = typeof color === 'string' ? parseColor(color) : color;
|
|
104
|
+
if (!rgb) return light;
|
|
105
|
+
const L = luminance(rgb);
|
|
106
|
+
// Contrast against near-black (#0c0f13, L≈.005) versus white.
|
|
107
|
+
return (L + 0.05) / 0.055 >= 1.05 / (L + 0.05) ? dark : light;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const HUES = [
|
|
111
|
+
[15, 'red'], [40, 'orange'], [64, 'yellow'], [80, 'lime'], [150, 'green'], [178, 'teal'],
|
|
112
|
+
[200, 'cyan'], [242, 'blue'], [270, 'indigo'], [292, 'purple'], [325, 'magenta'], [345, 'pink'], [360, 'red']
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** A short plain-English name: "dark blue", "light gray", "orange", "brown". */
|
|
116
|
+
export function colorName(color) {
|
|
117
|
+
const rgb = typeof color === 'string' ? parseColor(color) : color;
|
|
118
|
+
if (!rgb) return '';
|
|
119
|
+
const { h, s, l } = hsvToHsl(rgbToHsv(rgb));
|
|
120
|
+
if (l < 0.07) return 'black';
|
|
121
|
+
if (l > 0.96) return 'white';
|
|
122
|
+
if (s < 0.1 || (s < 0.2 && (l < 0.2 || l > 0.85))) return l < 0.3 ? 'dark gray' : l > 0.72 ? 'light gray' : 'gray';
|
|
123
|
+
let hue = HUES.find(([end]) => h < end)?.[1] || 'red';
|
|
124
|
+
if ((hue === 'orange' || hue === 'red' || hue === 'yellow') && l < 0.4 && h >= 8 && h < 50) hue = 'brown';
|
|
125
|
+
else if (hue === 'red' && l > 0.7) return 'pink';
|
|
126
|
+
if (hue === 'brown') return l < 0.18 ? 'dark brown' : 'brown';
|
|
127
|
+
const tone = l < 0.36 ? 'dark ' : l > 0.78 ? 'light ' : s < 0.35 ? 'muted ' : (s > 0.85 && l > 0.4 && l < 0.6 ? 'vivid ' : '');
|
|
128
|
+
return tone + hue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** "#1e90ff", "rgb(30, 144, 255)" or "hsl(210, 100%, 56%)". */
|
|
132
|
+
export function formatColor(rgb, format = 'hex') {
|
|
133
|
+
if (format === 'rgb') return `rgb(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)})`;
|
|
134
|
+
if (format === 'hsl') {
|
|
135
|
+
const { h, s, l } = hsvToHsl(rgbToHsv(rgb));
|
|
136
|
+
return `hsl(${round(h) % 360}, ${round(s * 100)}%, ${round(l * 100)}%)`;
|
|
137
|
+
}
|
|
138
|
+
return rgbToHex(rgb);
|
|
139
|
+
}
|
package/src/o-clock.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <o-clock
|
|
2
|
+
* <o-clock>: an analog clock or watch face.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* center orbit, pointed by `--o-angle-composite`; ticks are vectors
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Every part is a plain Orbit element and the rotation math is Orbit's job.
|
|
5
|
+
* The dial is the universe (orbit-12 is its edge). Hands are `.vector`s on the
|
|
6
|
+
* center orbit, pointed by `--o-angle-composite`; ticks are vectors spread
|
|
7
|
+
* around orbit-11; numerals are satellites on orbit-9; sub-dials are
|
|
8
|
+
* satellites that hold a universe of their own, with their own hands.
|
|
9
9
|
*
|
|
10
10
|
* Variants:
|
|
11
11
|
* - case: round | square | rounded | rectangle (the body/bezel)
|
|
@@ -17,53 +17,57 @@
|
|
|
17
17
|
* - presets: classic | roman | minimal | chrono | smartwatch
|
|
18
18
|
*
|
|
19
19
|
* <o-clock live seconds></o-clock>
|
|
20
|
-
* <o-clock preset="chrono" live brand="
|
|
20
|
+
* <o-clock preset="chrono" live brand="Orbit"></o-clock>
|
|
21
21
|
* <o-clock case="rectangle" dial="oval" live smooth></o-clock> <!-- tonneau -->
|
|
22
22
|
*
|
|
23
23
|
* Hands/readouts are patched in place every frame/second; the structure is
|
|
24
24
|
* rebuilt only when the face itself changes.
|
|
25
25
|
*/
|
|
26
|
-
import { OrbitKitElement, define, injectStyleOnce } from './base.js';
|
|
26
|
+
import { OrbitKitElement, define, injectStyleOnce, universe, hub, escapeHTML as e } from './base.js';
|
|
27
27
|
|
|
28
28
|
const CLOCK_CSS = `
|
|
29
|
+
o-clock { --ok-font-size: 7.5cqmin; }
|
|
29
30
|
o-clock .ok-clock-case {
|
|
30
31
|
box-sizing: border-box; display: flex; align-items: center; justify-content: center;
|
|
31
32
|
}
|
|
33
|
+
o-clock .ok-clock-case > .bigbang { width: 100%; }
|
|
32
34
|
o-clock .ok-clock-case--round { border-radius: 50%; }
|
|
33
|
-
o-clock .ok-clock-case--square { border-radius: 9%; }
|
|
34
|
-
o-clock .ok-clock-case--rounded { border-radius: 26%; }
|
|
35
|
-
o-clock .ok-clock-case--rectangle { border-radius: 24%; }
|
|
35
|
+
o-clock .ok-clock-case--square { border-radius: 9%; padding: 8%; }
|
|
36
|
+
o-clock .ok-clock-case--rounded { border-radius: 26%; padding: 8%; }
|
|
37
|
+
o-clock .ok-clock-case--rectangle { border-radius: 24%; padding: 8%; }
|
|
38
|
+
o-clock .ok-clock-case--rectangle > .bigbang { height: auto; align-self: stretch; aspect-ratio: auto; }
|
|
36
39
|
o-clock .ok-clock-case--square,
|
|
37
40
|
o-clock .ok-clock-case--rounded,
|
|
38
41
|
o-clock .ok-clock-case--rectangle {
|
|
39
|
-
background: var(--ok-clock-case-bg, linear-gradient(160deg, #232a34, #11151b));
|
|
42
|
+
background: var(--ok-clock-case-bg, var(--ok-case, linear-gradient(160deg, #232a34, #11151b)));
|
|
40
43
|
box-shadow: 0 10px 30px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.06);
|
|
41
44
|
}
|
|
42
|
-
o-clock .ok-clock-face { color: var(--ok-clock-numeral, #e6edf3); }
|
|
45
|
+
o-clock .ok-clock-face { color: var(--ok-clock-numeral, var(--ok-text, #e6edf3)); }
|
|
43
46
|
o-clock .ok-clock-dial {
|
|
44
|
-
background: var(--ok-clock-face, radial-gradient(circle at 50% 35%, #1b212b, #0c0f14));
|
|
45
|
-
box-shadow: inset 0 0 0 4px var(--ok-clock-bezel, #2a3340), inset 0 4px 18px rgba(0,0,0,.6);
|
|
47
|
+
background: var(--ok-clock-face, var(--ok-face, radial-gradient(circle at 50% 35%, #1b212b, #0c0f14)));
|
|
48
|
+
box-shadow: inset 0 0 0 4px var(--ok-clock-bezel, var(--ok-bezel, #2a3340)), inset 0 4px 18px rgba(0,0,0,.6);
|
|
46
49
|
}
|
|
47
|
-
o-clock .ok-clock-tick { background: var(--ok-clock-tick, rgba(230,237,243,.35)); height:
|
|
48
|
-
o-clock .ok-clock-tick-h { background: var(--ok-clock-tick-h, #e6edf3); height:
|
|
50
|
+
o-clock .ok-clock-tick { background: var(--ok-clock-tick, var(--ok-tick, rgba(230,237,243,.35))); height: max(1px, .6cqmin); border-radius: 2px; }
|
|
51
|
+
o-clock .ok-clock-tick-h { background: var(--ok-clock-tick-h, var(--ok-hand, #e6edf3)); height: max(2px, 1cqmin); border-radius: 2px; }
|
|
49
52
|
o-clock .ok-clock-num { font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
50
|
-
o-clock .ok-clock-brand { font-size: .
|
|
51
|
-
o-clock .ok-clock-digital { font-size: .7em; font-weight: 700;
|
|
52
|
-
o-clock .ok-clock-date { font-size: .62em; font-weight: 800; font-variant-numeric: tabular-nums; padding: .12em .4em; border-radius: 4px; background:
|
|
53
|
-
o-clock .ok-clock-hour { background: var(--ok-clock-hand, #e6edf3); height:
|
|
54
|
-
o-clock .ok-clock-min { background: var(--ok-clock-hand, #e6edf3); height:
|
|
55
|
-
o-clock .ok-clock-sec { background: var(--ok-clock-accent, #ef4565); height:
|
|
56
|
-
o-clock .ok-clock-cap { background: var(--ok-clock-accent, #ef4565); box-shadow: 0 0 0
|
|
57
|
-
o-clock .ok-clock-ringtime { font-weight: 800; font-variant-numeric: tabular-nums;
|
|
53
|
+
o-clock .ok-clock-brand { font-size: .6em; font-weight: 800; white-space: nowrap; color: var(--ok-clock-accent, var(--ok-accent, #ef4565)); }
|
|
54
|
+
o-clock .ok-clock-digital { font-size: .7em; font-weight: 700; font-variant-numeric: tabular-nums; opacity: .85; }
|
|
55
|
+
o-clock .ok-clock-date { font-size: .62em; font-weight: 800; font-variant-numeric: tabular-nums; padding: .12em .4em; border-radius: 4px; background: color-mix(in srgb, currentColor 12%, transparent); }
|
|
56
|
+
o-clock .ok-clock-hour { background: var(--ok-clock-hand, var(--ok-hand, #e6edf3)); height: max(3px, 2.4cqmin); border-radius: 9px; }
|
|
57
|
+
o-clock .ok-clock-min { background: var(--ok-clock-hand, var(--ok-hand, #e6edf3)); height: max(2px, 1.7cqmin); border-radius: 9px; }
|
|
58
|
+
o-clock .ok-clock-sec { background: var(--ok-clock-accent, var(--ok-accent, #ef4565)); height: max(1px, .7cqmin); border-radius: 2px; }
|
|
59
|
+
o-clock .ok-clock-cap { background: var(--ok-clock-accent, var(--ok-accent, #ef4565)); box-shadow: 0 0 0 max(2px, .8cqmin) var(--ok-clock-face-cap, var(--ok-surface, #0c0f14)); }
|
|
60
|
+
o-clock .ok-clock-ringtime { font-weight: 800; font-variant-numeric: tabular-nums; color: var(--ok-clock-numeral, var(--ok-text, #e6edf3)); text-align: center; --ok-hub-size: 1.1em; }
|
|
58
61
|
/* sub-dials */
|
|
59
62
|
o-clock .ok-subdial-ring { border: 1px solid var(--ok-clock-tick, rgba(230,237,243,.3)); box-shadow: inset 0 0 8px rgba(0,0,0,.4); }
|
|
60
63
|
o-clock .ok-sub-tick { background: var(--ok-clock-tick, rgba(230,237,243,.3)); height: 1px; }
|
|
61
|
-
o-clock .ok-subdial-hand { background: var(--ok-clock-accent, #ef4565); height:
|
|
62
|
-
o-clock .ok-subdial-cap { background: var(--ok-clock-tick-h, #e6edf3); }
|
|
63
|
-
o-clock .ok-subdial-label{ font-size: .4em; opacity: .
|
|
64
|
+
o-clock .ok-subdial-hand { background: var(--ok-clock-accent, var(--ok-accent, #ef4565)); height: max(1px, .6cqmin); border-radius: 2px; }
|
|
65
|
+
o-clock .ok-subdial-cap { background: var(--ok-clock-tick-h, var(--ok-hand, #e6edf3)); }
|
|
66
|
+
o-clock .ok-subdial-label{ font-size: .4em; opacity: .6; }
|
|
64
67
|
o-clock.ok-glow .ok-clock-hour,
|
|
65
|
-
o-clock.ok-glow .ok-clock-min { filter: drop-shadow(0 0 4px var(--ok-clock-hand, #e6edf3)); }
|
|
66
|
-
o-clock.ok-glow .ok-clock-sec { filter: drop-shadow(0 0 5px var(--ok-clock-accent, #ef4565)); }
|
|
68
|
+
o-clock.ok-glow .ok-clock-min { filter: drop-shadow(0 0 var(--ok-clock-glow, 4px) var(--ok-clock-hand, var(--ok-hand, #e6edf3))); }
|
|
69
|
+
o-clock.ok-glow .ok-clock-sec { filter: drop-shadow(0 0 var(--ok-clock-glow, 5px) var(--ok-clock-accent, var(--ok-accent, #ef4565))); }
|
|
70
|
+
o-clock .ok-ring-track, o-clock .ok-ring-value { --o-stroke: var(--o-fill); }
|
|
67
71
|
`;
|
|
68
72
|
|
|
69
73
|
const ROMAN = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
|
|
@@ -74,7 +78,7 @@ const PRESETS = {
|
|
|
74
78
|
classic: { case: 'round', numerals: 'arabic' },
|
|
75
79
|
roman: { case: 'round', numerals: 'roman' },
|
|
76
80
|
minimal: { case: 'round', numerals: 'none' },
|
|
77
|
-
chrono: { case: 'round', numerals: 'arabic', subdials: true, brand: '
|
|
81
|
+
chrono: { case: 'round', numerals: 'arabic', subdials: true, brand: 'Orbit' },
|
|
78
82
|
smartwatch: { case: 'rounded', numerals: 'arabic', seconds: true, smooth: true,
|
|
79
83
|
digital: true, date: true, accent: '#3da9fc' }
|
|
80
84
|
};
|
|
@@ -84,7 +88,8 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
84
88
|
static get observedAttributes() {
|
|
85
89
|
return ['time', 'live', 'smooth', 'seconds', 'numerals', 'size', 'preset',
|
|
86
90
|
'case', 'dial', 'digital', 'date', 'brand', 'subdials',
|
|
87
|
-
'face', 'bezel', 'case-bg', 'hand-color', 'accent', 'tick-color', 'glow'
|
|
91
|
+
'face', 'bezel', 'case-bg', 'hand-color', 'accent', 'tick-color', 'glow',
|
|
92
|
+
'hour-color', 'minute-color', 'second-color'];
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
/** Resolve a string attribute against the active preset, then a hard default. */
|
|
@@ -107,7 +112,7 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
107
112
|
const smooth = this._flag('smooth');
|
|
108
113
|
return {
|
|
109
114
|
time: this._str('time', null),
|
|
110
|
-
live: this.
|
|
115
|
+
live: this._flag('live'),
|
|
111
116
|
smooth,
|
|
112
117
|
seconds: this._flag('seconds') || smooth,
|
|
113
118
|
numerals: this._opt('numerals', 'arabic'),
|
|
@@ -118,76 +123,58 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
118
123
|
date: this._flag('date'),
|
|
119
124
|
subdials: this._flag('subdials'),
|
|
120
125
|
brand: this._opt('brand', null),
|
|
121
|
-
size: this.
|
|
122
|
-
glow: this.
|
|
123
|
-
theme: ['face', 'bezel', 'case-bg', 'hand-color', 'accent', 'tick-color']
|
|
126
|
+
size: this._size('size', '300px'),
|
|
127
|
+
glow: this.getAttribute('glow'),
|
|
128
|
+
theme: ['face', 'bezel', 'case-bg', 'hand-color', 'accent', 'tick-color', 'hour-color', 'minute-color', 'second-color']
|
|
124
129
|
.map((a) => this.getAttribute(a)).join('|')
|
|
125
130
|
};
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
structuralSignature(cfg) {
|
|
129
134
|
// Hands + digital/date/sub-dial readouts are patched live; only the face rebuilds.
|
|
130
|
-
const { time, ...rest } = cfg;
|
|
135
|
+
const { time, live, smooth, ...rest } = cfg;
|
|
131
136
|
return JSON.stringify(rest);
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
build(cfg) {
|
|
135
140
|
injectStyleOnce('orbit-kit-clock', CLOCK_CSS);
|
|
136
|
-
this.classList.toggle('ok-glow', cfg.glow);
|
|
137
|
-
this._ringTime = null; // reset face mode each build
|
|
141
|
+
this.classList.toggle('ok-glow', cfg.glow != null);
|
|
142
|
+
this._ringTime = this._ringH = this._ringM = this._ringS = null; // reset face mode each build
|
|
138
143
|
|
|
139
144
|
// Convenience attributes mirror the CSS custom properties.
|
|
140
145
|
const map = {
|
|
141
146
|
face: '--ok-clock-face', bezel: '--ok-clock-bezel', 'case-bg': '--ok-clock-case-bg',
|
|
142
147
|
'hand-color': '--ok-clock-hand', accent: '--ok-clock-accent', 'tick-color': '--ok-clock-tick'
|
|
143
148
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (this.getAttribute('accent') == null) {
|
|
149
|
-
const p = PRESETS[this.getAttribute('preset')];
|
|
150
|
-
if (p && p.accent) this.style.setProperty('--ok-clock-accent', p.accent);
|
|
151
|
-
}
|
|
149
|
+
const overrides = Object.fromEntries(Object.entries(map).map(([attr, prop]) => [prop, this.getAttribute(attr)]));
|
|
150
|
+
overrides['--ok-clock-accent'] = this.getAttribute('accent') ?? PRESETS[this.getAttribute('preset')]?.accent ?? null;
|
|
151
|
+
overrides['--ok-clock-glow'] = cfg.glow == null ? null : `${Math.max(0, this._num('glow', 5))}px`;
|
|
152
|
+
this._applyTheme(overrides);
|
|
152
153
|
|
|
153
154
|
if (cfg.ringsFace) { this._buildRingsFace(cfg); return; }
|
|
154
155
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const force = (dialPx * 12 / 11).toFixed(1);
|
|
160
|
-
const frame = cfg.caseShape === 'round' ? 0 : Math.round(dialPx * 0.08);
|
|
161
|
-
const caseW = dialPx + frame * 2;
|
|
162
|
-
const caseH = caseW + (cfg.caseShape === 'rectangle' ? Math.round(dialPx * 0.26) : 0);
|
|
163
|
-
const baseFont = (dialPx * 0.075).toFixed(1);
|
|
164
|
-
|
|
165
|
-
// Oval dial: stretch the orbits to match the CASE orientation so a tall
|
|
166
|
-
// (rectangle) case gives a tall dial (true tonneau), not a sideways oval.
|
|
167
|
-
let ellipse = '';
|
|
168
|
-
if (cfg.oval) {
|
|
169
|
-
if (caseH > caseW) ellipse = `--o-ellipse-x:1;--o-ellipse-y:${(caseW / caseH).toFixed(3)};`;
|
|
170
|
-
else if (caseW > caseH) ellipse = `--o-ellipse-x:${(caseH / caseW).toFixed(3)};--o-ellipse-y:1;`;
|
|
171
|
-
else ellipse = '--o-ellipse-x:1;--o-ellipse-y:.82;'; // square box → gentle vertical oval
|
|
172
|
-
}
|
|
156
|
+
// The case is a box around the dial; the dial is the universe. A
|
|
157
|
+
// rectangle case gives a tall universe whose shorter side is its width.
|
|
158
|
+
const caseH = cfg.caseShape === 'rectangle' ? 126 : 100;
|
|
159
|
+
const ellipse = cfg.oval ? `--o-ellipse-x:1;--o-ellipse-y:${cfg.caseShape === 'rectangle' ? (100 / caseH).toFixed(3) : '.82'};` : '';
|
|
173
160
|
|
|
174
161
|
// --- ticks: 60 minute, 12 hour (overdraw every 5th) ---
|
|
175
162
|
const minuteTicks = Array.from({ length: 60 }, () =>
|
|
176
|
-
'<div class="vector inner-orbit
|
|
163
|
+
'<div class="vector inner-orbit ok-clock-tick"></div>').join('');
|
|
177
164
|
const hourTicks = Array.from({ length: 12 }, () =>
|
|
178
|
-
'<div class="vector inner-orbit
|
|
165
|
+
'<div class="vector inner-orbit ok-clock-tick-h"></div>').join('');
|
|
179
166
|
|
|
180
167
|
// --- numerals ---
|
|
181
168
|
const labels = cfg.numerals === 'roman' ? ROMAN
|
|
182
169
|
: cfg.numerals === 'none' ? null : ARABIC;
|
|
183
170
|
const numerals = labels
|
|
184
|
-
? `<div class="orbit
|
|
171
|
+
? `<div class="orbit-${cfg.numerals === 'roman' ? 8 : 9}">${labels.map((n) =>
|
|
185
172
|
`<div class="satellite"><div class="capsule ok-clock-num">${n}</div></div>`).join('')}</div>`
|
|
186
173
|
: '';
|
|
187
174
|
|
|
188
175
|
// --- complications (suppressed when sub-dials own the lower face) ---
|
|
189
176
|
const brand = cfg.brand
|
|
190
|
-
? `<div class="orbit-4"><div class="satellite angle-0"><div class="capsule ok-clock-brand">${cfg.brand}</div></div></div>`
|
|
177
|
+
? `<div class="orbit-4"><div class="satellite angle-0"><div class="capsule ok-clock-brand">${e(cfg.brand)}</div></div></div>`
|
|
191
178
|
: '';
|
|
192
179
|
const digital = (cfg.digital && !cfg.subdials)
|
|
193
180
|
? `<div class="orbit-4"><div class="satellite angle-180"><div class="capsule ok-clock-digital"></div></div></div>`
|
|
@@ -197,32 +184,31 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
197
184
|
: '';
|
|
198
185
|
|
|
199
186
|
// --- sub-dials: chronograph tri-compax at 3/6/9 ---
|
|
200
|
-
const subForce = (dialPx * 0.96).toFixed(1);
|
|
201
187
|
const subdials = cfg.subdials
|
|
202
|
-
? this._subdial('sec', 'angle-270',
|
|
203
|
-
+ this._subdial('h24', 'angle-90',
|
|
204
|
-
+ this._subdial('date', 'angle-180',
|
|
188
|
+
? this._subdial('sec', 'angle-270', 'S') // 9 o'clock: small seconds
|
|
189
|
+
+ this._subdial('h24', 'angle-90', '24') // 3 o'clock: 24-hour
|
|
190
|
+
+ this._subdial('date', 'angle-180', '31') // 6 o'clock: date
|
|
205
191
|
: '';
|
|
206
192
|
|
|
207
|
-
// --- hands ---
|
|
193
|
+
// --- hands (length in lanes; outer-orbit starts them at the center) ---
|
|
208
194
|
const secondHand = cfg.seconds
|
|
209
|
-
? '<div class="vector outer-orbit
|
|
195
|
+
? '<div class="vector outer-orbit ok-clock-hand ok-clock-sec" style="--o-size-ratio:10"></div>'
|
|
210
196
|
: '';
|
|
211
197
|
|
|
212
198
|
this.innerHTML = `
|
|
213
|
-
<div class="ok-clock-case ok-clock-case--${cfg.caseShape}" style="width:${
|
|
214
|
-
<div class="bigbang"
|
|
215
|
-
<div class="gravity-spot ok-clock-face" style="
|
|
216
|
-
<div class="orbit-
|
|
217
|
-
<div class="orbit-
|
|
218
|
-
<div class="orbit-
|
|
199
|
+
<div class="ok-clock-case ok-clock-case--${cfg.caseShape}${cfg.caseShape === 'round' ? '' : ' ok-surface'}" style="width:${e(cfg.size)};aspect-ratio:100 / ${caseH}">
|
|
200
|
+
<div class="bigbang">
|
|
201
|
+
<div class="gravity-spot ok-clock-face"${ellipse ? ` style="${ellipse}"` : ''}>
|
|
202
|
+
<div class="orbit-12 ok-clock-dial ok-surface"></div>
|
|
203
|
+
<div class="orbit-11" style="--o-size-ratio:.45">${minuteTicks}</div>
|
|
204
|
+
<div class="orbit-11" style="--o-size-ratio:.9">${hourTicks}</div>
|
|
219
205
|
${numerals}
|
|
220
206
|
${brand}${digital}${date}${subdials}
|
|
221
207
|
<div class="orbit-0">
|
|
222
|
-
<div class="vector outer-orbit
|
|
223
|
-
<div class="vector outer-orbit
|
|
208
|
+
<div class="vector outer-orbit ok-clock-hand ok-clock-hour" style="--o-size-ratio:5.4"></div>
|
|
209
|
+
<div class="vector outer-orbit ok-clock-hand ok-clock-min" style="--o-size-ratio:8.2"></div>
|
|
224
210
|
${secondHand}
|
|
225
|
-
<div class="satellite at-center
|
|
211
|
+
<div class="satellite at-center ok-clock-cap" style="--o-size-ratio:.9"></div>
|
|
226
212
|
</div>
|
|
227
213
|
</div>
|
|
228
214
|
</div>
|
|
@@ -239,22 +225,22 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
239
225
|
}
|
|
240
226
|
|
|
241
227
|
/**
|
|
242
|
-
* One chronograph sub-dial: a
|
|
243
|
-
*
|
|
228
|
+
* One chronograph sub-dial: a satellite that is a universe of its own, with
|
|
229
|
+
* a tick ring, a labeled counter and a single live hand.
|
|
244
230
|
*/
|
|
245
|
-
_subdial(kind, angleClass,
|
|
231
|
+
_subdial(kind, angleClass, label) {
|
|
246
232
|
const ticks = Array.from({ length: 12 }, () =>
|
|
247
|
-
'<div class="vector inner-orbit
|
|
233
|
+
'<div class="vector inner-orbit ok-sub-tick"></div>').join('');
|
|
248
234
|
return `
|
|
249
|
-
<div class="orbit-5">
|
|
235
|
+
<div class="orbit-5" style="--o-size-ratio:5.8">
|
|
250
236
|
<div class="satellite ${angleClass}">
|
|
251
|
-
<div class="gravity-spot"
|
|
252
|
-
<div class="orbit-
|
|
253
|
-
<div class="orbit-
|
|
254
|
-
<div class="orbit-
|
|
237
|
+
<div class="gravity-spot">
|
|
238
|
+
<div class="orbit-12 ok-subdial-ring"></div>
|
|
239
|
+
<div class="orbit-12" style="--o-size-ratio:1.6">${ticks}</div>
|
|
240
|
+
<div class="orbit-5"><div class="satellite angle-180"><div class="capsule ok-subdial-label">${label}</div></div></div>
|
|
255
241
|
<div class="orbit-0">
|
|
256
|
-
<div class="vector outer-orbit
|
|
257
|
-
<div class="satellite at-center
|
|
242
|
+
<div class="vector outer-orbit ok-subdial-hand ok-sub-${kind}" style="--o-size-ratio:8.5"></div>
|
|
243
|
+
<div class="satellite at-center ok-subdial-cap" style="--o-size-ratio:1.4"></div>
|
|
258
244
|
</div>
|
|
259
245
|
</div>
|
|
260
246
|
</div>
|
|
@@ -268,35 +254,22 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
268
254
|
this._subSec = this._sub24 = this._subDate = this._digital = this._dateEl = null;
|
|
269
255
|
|
|
270
256
|
const defs = [
|
|
271
|
-
{ key: 'H', color: this.
|
|
272
|
-
{ key: 'M', color: this.
|
|
257
|
+
{ key: 'H', color: this._color('hour-color', '#ff453a') },
|
|
258
|
+
{ key: 'M', color: this._color('minute-color', '#32d74b') }
|
|
273
259
|
];
|
|
274
|
-
if (cfg.seconds) defs.push({ key: 'S', color: this.
|
|
275
|
-
|
|
276
|
-
const outer = 1 + defs.length;
|
|
277
|
-
const sizeNum = parseFloat(cfg.size) || 300;
|
|
278
|
-
const force = (sizeNum * 12 / (outer + 1)).toFixed(1);
|
|
279
|
-
const sw = Math.max(6, Math.round(sizeNum * 0.05));
|
|
260
|
+
if (cfg.seconds) defs.push({ key: 'S', color: this._color('second-color', this._color('accent', '#0a84ff')) });
|
|
280
261
|
|
|
262
|
+
const thickness = 2.6;
|
|
281
263
|
const rings = defs.map((r, i) => {
|
|
282
|
-
const
|
|
283
|
-
const track = `color-mix(in srgb, ${r.color}
|
|
284
|
-
const glow = cfg.glow ? `filter:drop-shadow(0 0
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
+ `<div class="orbit-${orbit}"><o-progress class="ok-ring-${r.key}" value="0" max="100" shape="rounded" style="${fill}"></o-progress></div>`;
|
|
264
|
+
const ring = `class="orbit-${12 - i * 3}" style="--o-size-ratio:${thickness};--o-gap:0"`;
|
|
265
|
+
const track = `color-mix(in srgb, ${r.color} 18%, transparent)`;
|
|
266
|
+
const glow = cfg.glow != null ? `filter:drop-shadow(0 0 ${Math.max(0, this._num('glow', 5))}px ${r.color});` : '';
|
|
267
|
+
// Solid track arc behind the progress value.
|
|
268
|
+
return `<div ${ring}><o-arc class="inner-orbit ok-ring-track" value="100" shape="rounded" style="--o-fill:${track}"></o-arc></div>`
|
|
269
|
+
+ `<div ${ring}><o-progress class="inner-orbit ok-ring-value ok-ring-${r.key}" value="0" max="100" shape="rounded" style="${e(`--o-fill:${r.color};${glow}`)}"></o-progress></div>`;
|
|
289
270
|
}).join('');
|
|
290
|
-
|
|
291
|
-
this.innerHTML = `
|
|
292
|
-
<div class="bigbang" style="width:${cfg.size};aspect-ratio:1;">
|
|
293
|
-
<div class="gravity-spot" style="--o-force:${force}px;">
|
|
294
|
-
${rings}
|
|
295
|
-
<div class="orbit-0"><div class="satellite at-center">
|
|
296
|
-
<div class="capsule ok-clock-ringtime" style="font-size:${(sizeNum * 0.13).toFixed(1)}px;"></div>
|
|
297
|
-
</div></div>
|
|
298
|
-
</div>
|
|
299
|
-
</div>`;
|
|
271
|
+
const hole = 12 - (defs.length - 1) * 3 - thickness;
|
|
272
|
+
this.innerHTML = universe(cfg.size, `${rings}${hub(2 * hole - .5, '', 'ok-hub ok-clock-ringtime')}`);
|
|
300
273
|
|
|
301
274
|
this._ringH = this.querySelector('.ok-ring-H');
|
|
302
275
|
this._ringM = this.querySelector('.ok-ring-M');
|
|
@@ -317,15 +290,17 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
317
290
|
/** Point all hands and update digital/date readouts for a given Date. */
|
|
318
291
|
_paint(date, smooth) {
|
|
319
292
|
const h = date.getHours(), m = date.getMinutes();
|
|
293
|
+
this._describe(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`);
|
|
294
|
+
const text = (el, value) => { if (el && el.textContent !== value) el.textContent = value; };
|
|
320
295
|
const s = smooth ? date.getSeconds() + date.getMilliseconds() / 1000 : date.getSeconds();
|
|
321
296
|
|
|
322
297
|
// Rings face (iOS-style): fill concentric progress rings by time.
|
|
323
298
|
if (this._ringTime) {
|
|
324
|
-
const setV = (el, frac) => el && el.
|
|
299
|
+
const setV = (el, frac) => { const v = (frac * 100).toFixed(2); if (el && el.getAttribute('value') !== v) el.setAttribute('value', v); };
|
|
325
300
|
setV(this._ringH, ((h % 12) + m / 60) / 12);
|
|
326
301
|
setV(this._ringM, (m + s / 60) / 60);
|
|
327
302
|
setV(this._ringS, (s % 60) / 60);
|
|
328
|
-
this._ringTime
|
|
303
|
+
text(this._ringTime, `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
|
|
329
304
|
return;
|
|
330
305
|
}
|
|
331
306
|
|
|
@@ -340,18 +315,20 @@ export class OrbitClock extends OrbitKitElement {
|
|
|
340
315
|
set(this._subDate, ((date.getDate() - 1) / 31) * 360 - 90);
|
|
341
316
|
// Digital / date readouts.
|
|
342
317
|
if (this._digital) {
|
|
343
|
-
this._digital
|
|
318
|
+
text(this._digital, `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`);
|
|
344
319
|
}
|
|
345
|
-
|
|
320
|
+
text(this._dateEl, String(date.getDate()));
|
|
346
321
|
}
|
|
347
322
|
|
|
348
323
|
/** Resolve the configured time: explicit `time="HH:MM[:SS]"` or now. */
|
|
349
324
|
_resolveDate(cfg) {
|
|
350
|
-
if (cfg.time) {
|
|
351
|
-
const [hh, mm, ss] = cfg.time.split(':').map(
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
325
|
+
if (!cfg.live && /^\d{1,2}:\d{2}(?::\d{2})?$/.test(cfg.time || '')) {
|
|
326
|
+
const [hh, mm, ss = 0] = cfg.time.split(':').map(Number);
|
|
327
|
+
if (hh < 24 && mm < 60 && ss < 60) {
|
|
328
|
+
const d = new Date();
|
|
329
|
+
d.setHours(hh, mm, ss, 0);
|
|
330
|
+
return d;
|
|
331
|
+
}
|
|
355
332
|
}
|
|
356
333
|
return new Date();
|
|
357
334
|
}
|