@jjlmoya/utils-chrono 1.3.0 → 1.5.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/package.json +1 -1
- package/src/category/i18n/de.ts +11 -9
- package/src/category/i18n/en.ts +11 -9
- package/src/category/i18n/fr.ts +11 -9
- package/src/category/i18n/id.ts +11 -9
- package/src/category/i18n/it.ts +11 -9
- package/src/category/i18n/ja.ts +11 -9
- package/src/category/i18n/ko.ts +11 -9
- package/src/category/i18n/nl.ts +11 -9
- package/src/category/i18n/pl.ts +11 -9
- package/src/category/i18n/pt.ts +11 -9
- package/src/category/i18n/ru.ts +11 -9
- package/src/category/i18n/sv.ts +11 -9
- package/src/category/i18n/tr.ts +11 -9
- package/src/category/i18n/zh.ts +11 -9
- package/src/category/index.ts +8 -0
- package/src/entries.ts +13 -1
- package/src/index.ts +4 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/no_en_dash.test.ts +41 -0
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/beat-rate-converter/bibliography.ts +1 -1
- package/src/tool/beat-rate-converter/components/ConverterPanel.astro +57 -20
- package/src/tool/beat-rate-converter/i18n/en.ts +5 -5
- package/src/tool/crown-reference-guide/bibliography.ts +3 -3
- package/src/tool/crown-reference-guide/i18n/de.ts +37 -29
- package/src/tool/crown-reference-guide/i18n/en.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/es.ts +36 -28
- package/src/tool/crown-reference-guide/i18n/fr.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/id.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/it.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/ja.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/ko.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/nl.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/pl.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/pt.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/ru.ts +41 -33
- package/src/tool/crown-reference-guide/i18n/sv.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/tr.ts +38 -30
- package/src/tool/crown-reference-guide/i18n/zh.ts +37 -29
- package/src/tool/demagnetizing-timer/components/TimerPanel.astro +45 -17
- package/src/tool/demagnetizing-timer/i18n/de.ts +3 -3
- package/src/tool/demagnetizing-timer/i18n/en.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/es.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/fr.ts +1 -1
- package/src/tool/demagnetizing-timer/i18n/id.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/it.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/ja.ts +1 -1
- package/src/tool/demagnetizing-timer/i18n/ko.ts +1 -1
- package/src/tool/demagnetizing-timer/i18n/nl.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/pl.ts +3 -3
- package/src/tool/demagnetizing-timer/i18n/pt.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/ru.ts +10 -10
- package/src/tool/demagnetizing-timer/i18n/sv.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/tr.ts +2 -2
- package/src/tool/demagnetizing-timer/i18n/zh.ts +1 -1
- package/src/tool/lume-color-simulator/bibliography.astro +16 -0
- package/src/tool/lume-color-simulator/bibliography.ts +16 -0
- package/src/tool/lume-color-simulator/client.ts +186 -0
- package/src/tool/lume-color-simulator/component.astro +17 -0
- package/src/tool/lume-color-simulator/components/LumePanel.astro +98 -0
- package/src/tool/lume-color-simulator/entry.ts +57 -0
- package/src/tool/lume-color-simulator/i18n/de.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/en.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/es.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/fr.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/id.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/it.ts +175 -0
- package/src/tool/lume-color-simulator/i18n/ja.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/ko.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/nl.ts +175 -0
- package/src/tool/lume-color-simulator/i18n/pl.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/pt.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/ru.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/sv.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/tr.ts +174 -0
- package/src/tool/lume-color-simulator/i18n/zh.ts +174 -0
- package/src/tool/lume-color-simulator/index.ts +11 -0
- package/src/tool/lume-color-simulator/lume-color-simulator.css +208 -0
- package/src/tool/lume-color-simulator/seo.astro +16 -0
- package/src/tool/moon-phase-visualizer/bibliography.astro +16 -0
- package/src/tool/moon-phase-visualizer/bibliography.ts +16 -0
- package/src/tool/moon-phase-visualizer/client.ts +243 -0
- package/src/tool/moon-phase-visualizer/component.astro +17 -0
- package/src/tool/moon-phase-visualizer/components/MoonPanel.astro +63 -0
- package/src/tool/moon-phase-visualizer/entry.ts +51 -0
- package/src/tool/moon-phase-visualizer/i18n/de.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/en.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/es.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/fr.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/id.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/it.ts +176 -0
- package/src/tool/moon-phase-visualizer/i18n/ja.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/ko.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/nl.ts +176 -0
- package/src/tool/moon-phase-visualizer/i18n/pl.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/pt.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/ru.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/sv.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/tr.ts +175 -0
- package/src/tool/moon-phase-visualizer/i18n/zh.ts +175 -0
- package/src/tool/moon-phase-visualizer/index.ts +11 -0
- package/src/tool/moon-phase-visualizer/moon-phase-visualizer.css +216 -0
- package/src/tool/moon-phase-visualizer/seo.astro +16 -0
- package/src/tool/power-reserve-estimator/bibliography.ts +2 -2
- package/src/tool/power-reserve-estimator/components/EstimatorPanel.astro +146 -39
- package/src/tool/power-reserve-estimator/i18n/de.ts +2 -2
- package/src/tool/power-reserve-estimator/i18n/en.ts +3 -3
- package/src/tool/power-reserve-estimator/i18n/es.ts +2 -2
- package/src/tool/power-reserve-estimator/i18n/fr.ts +2 -2
- package/src/tool/power-reserve-estimator/i18n/id.ts +2 -2
- package/src/tool/power-reserve-estimator/i18n/it.ts +2 -2
- package/src/tool/power-reserve-estimator/i18n/nl.ts +2 -2
- package/src/tool/power-reserve-estimator/i18n/pt.ts +2 -2
- package/src/tool/strap-taper-calculator/i18n/en.ts +2 -2
- package/src/tool/strap-taper-calculator/i18n/ru.ts +4 -4
- package/src/tool/tachymeter-calculator/bibliography.astro +16 -0
- package/src/tool/tachymeter-calculator/bibliography.ts +16 -0
- package/src/tool/tachymeter-calculator/client.ts +180 -0
- package/src/tool/tachymeter-calculator/component.astro +15 -0
- package/src/tool/tachymeter-calculator/components/CalculatorPanel.astro +121 -0
- package/src/tool/tachymeter-calculator/entry.ts +43 -0
- package/src/tool/tachymeter-calculator/i18n/de.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/en.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/es.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/fr.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/id.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/it.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/ja.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/ko.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/nl.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/pl.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/pt.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/ru.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/sv.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/tr.ts +172 -0
- package/src/tool/tachymeter-calculator/i18n/zh.ts +172 -0
- package/src/tool/tachymeter-calculator/index.ts +11 -0
- package/src/tool/tachymeter-calculator/seo.astro +16 -0
- package/src/tool/tachymeter-calculator/tachymeter-calculator.css +492 -0
- package/src/tool/tachymeter-calculator/utils.ts +10 -0
- package/src/tool/watch-accuracy-tracker/i18n/pl.ts +1 -1
- package/src/tool/watch-accuracy-tracker/i18n/ru.ts +6 -6
- package/src/tool/watch-savings-planner/i18n/en.ts +5 -5
- package/src/tool/watch-size-comparator/bibliography.astro +16 -0
- package/src/tool/watch-size-comparator/bibliography.ts +16 -0
- package/src/tool/watch-size-comparator/client.ts +287 -0
- package/src/tool/watch-size-comparator/component.astro +17 -0
- package/src/tool/watch-size-comparator/components/WatchForm.astro +121 -0
- package/src/tool/watch-size-comparator/drawing/index.ts +79 -0
- package/src/tool/watch-size-comparator/drawing/measures.ts +57 -0
- package/src/tool/watch-size-comparator/drawing/utils.ts +37 -0
- package/src/tool/watch-size-comparator/drawing/watch.ts +78 -0
- package/src/tool/watch-size-comparator/entry.ts +62 -0
- package/src/tool/watch-size-comparator/i18n/de.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/en.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/es.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/fr.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/id.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/it.ts +190 -0
- package/src/tool/watch-size-comparator/i18n/ja.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/ko.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/nl.ts +190 -0
- package/src/tool/watch-size-comparator/i18n/pl.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/pt.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/ru.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/sv.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/tr.ts +189 -0
- package/src/tool/watch-size-comparator/i18n/zh.ts +189 -0
- package/src/tool/watch-size-comparator/index.ts +11 -0
- package/src/tool/watch-size-comparator/seo.astro +16 -0
- package/src/tool/watch-size-comparator/watch-size-comparator.css +373 -0
- package/src/tool/water-resistance-converter/bibliography.ts +2 -2
- package/src/tool/water-resistance-converter/i18n/de.ts +5 -5
- package/src/tool/water-resistance-converter/i18n/en.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/es.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/fr.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/id.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/it.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/ja.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/ko.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/nl.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/pl.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/pt.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/ru.ts +8 -8
- package/src/tool/water-resistance-converter/i18n/sv.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/tr.ts +6 -6
- package/src/tool/water-resistance-converter/i18n/zh.ts +3 -3
- package/src/tool/wrist-presence-calculator/i18n/de.ts +1 -1
- package/src/tool/wrist-presence-calculator/i18n/fr.ts +1 -1
- package/src/tool/wrist-presence-calculator/i18n/pl.ts +1 -1
- package/src/tool/wrist-presence-calculator/i18n/pt.ts +1 -1
- package/src/tool/wrist-presence-calculator/i18n/ru.ts +21 -21
- package/src/tool/wrist-presence-calculator/i18n/sv.ts +1 -1
- package/src/tools.ts +8 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { drawWatch, getFitColor, getFitRatio } from './drawing';
|
|
2
|
+
|
|
3
|
+
interface WatchProfile {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
diameter: number;
|
|
7
|
+
lugToLug: number;
|
|
8
|
+
thickness: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'jjlmoya_chrono_watch_sizes';
|
|
12
|
+
const UNIT_KEY = 'jjlmoya_chrono_unit';
|
|
13
|
+
const mainEl = document.querySelector('.tool-main-card') as HTMLElement;
|
|
14
|
+
const ui = mainEl ? JSON.parse(mainEl.dataset.ui || '{}') : {};
|
|
15
|
+
|
|
16
|
+
const canvas = document.getElementById('size-canvas') as HTMLCanvasElement;
|
|
17
|
+
const ctx = canvas.getContext('2d')!;
|
|
18
|
+
const watchList = document.getElementById('watch-list') as HTMLElement;
|
|
19
|
+
const fitInfo = document.getElementById('fit-info') as HTMLElement;
|
|
20
|
+
|
|
21
|
+
const inputName = document.getElementById('watch-name-input') as HTMLInputElement;
|
|
22
|
+
const inputDiameter = document.getElementById('input-diameter') as HTMLInputElement;
|
|
23
|
+
const inputL2L = document.getElementById('input-l2l') as HTMLInputElement;
|
|
24
|
+
const inputThickness = document.getElementById('input-thickness') as HTMLInputElement;
|
|
25
|
+
const inputWrist = document.getElementById('input-wrist') as HTMLInputElement;
|
|
26
|
+
const addBtn = document.getElementById('add-watch-btn') as HTMLElement;
|
|
27
|
+
const btnUnitCm = document.getElementById('btn-unit-cm') as HTMLButtonElement;
|
|
28
|
+
const btnUnitIn = document.getElementById('btn-unit-in') as HTMLButtonElement;
|
|
29
|
+
const unitDiameter = document.getElementById('unit-diameter') as HTMLElement;
|
|
30
|
+
const unitL2l = document.getElementById('unit-l2l') as HTMLElement;
|
|
31
|
+
const unitThickness = document.getElementById('unit-thickness') as HTMLElement;
|
|
32
|
+
|
|
33
|
+
let watches: WatchProfile[] = [];
|
|
34
|
+
let activeId = '';
|
|
35
|
+
let unit: 'cm' | 'in' = 'cm';
|
|
36
|
+
|
|
37
|
+
function loadUnit(): 'cm' | 'in' {
|
|
38
|
+
const stored = localStorage.getItem(UNIT_KEY);
|
|
39
|
+
return stored === 'in' ? 'in' : 'cm';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveUnit(): void {
|
|
43
|
+
localStorage.setItem(UNIT_KEY, unit);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mmToDisplay(mm: number): string {
|
|
47
|
+
return unit === 'in' ? (mm / 25.4).toFixed(2) : Math.round(mm * 10) / 10 + '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mmUnit(): string {
|
|
51
|
+
return unit === 'in' ? 'in' : 'mm';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getWristCm(): number {
|
|
55
|
+
const val = parseFloat(inputWrist.value);
|
|
56
|
+
return unit === 'in' ? val * 2.54 : val;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadWatches(): void {
|
|
60
|
+
const data = localStorage.getItem(STORAGE_KEY);
|
|
61
|
+
watches = data ? JSON.parse(data) : [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveWatches(): void {
|
|
65
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(watches));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getFitLabel(l2l: number, wristCm: number): string {
|
|
69
|
+
const ratio = getFitRatio(l2l, wristCm);
|
|
70
|
+
if (ratio < 0.5) return ui.excellentFit || 'Excellent';
|
|
71
|
+
if (ratio < 0.58) return ui.goodFit || 'Good';
|
|
72
|
+
if (ratio < 0.65) return ui.borderlineFit || 'Borderline';
|
|
73
|
+
return ui.largeFit || 'Too Large';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getFitDescription(l2l: number, wristCm: number): string {
|
|
77
|
+
const ratio = getFitRatio(l2l, wristCm);
|
|
78
|
+
if (ratio < 0.5) return ui.excellentDesc || 'Proportional-lug-to-lug stays well within your wrist.';
|
|
79
|
+
if (ratio < 0.58) return ui.goodDesc || 'Good fit-overhangs slightly but still comfortable.';
|
|
80
|
+
if (ratio < 0.65) return ui.borderlineDesc || 'Borderline-lugs approach the edge of your wrist.';
|
|
81
|
+
return ui.largeDesc || 'Too large-lugs likely overhang your wrist.';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getFitBadgeClass(l2l: number, wristCm: number): string {
|
|
85
|
+
const ratio = getFitRatio(l2l, wristCm);
|
|
86
|
+
if (ratio < 0.5) return 'excellent';
|
|
87
|
+
if (ratio < 0.58) return 'good';
|
|
88
|
+
if (ratio < 0.65) return 'borderline';
|
|
89
|
+
return 'large';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function updateInputUnits(): void {
|
|
93
|
+
const u = mmUnit();
|
|
94
|
+
unitDiameter.textContent = u;
|
|
95
|
+
unitL2l.textContent = u;
|
|
96
|
+
unitThickness.textContent = u;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function updateUnitButtons(): void {
|
|
100
|
+
btnUnitCm.classList.toggle('active', unit === 'cm');
|
|
101
|
+
btnUnitIn.classList.toggle('active', unit === 'in');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseMm(input: HTMLInputElement, d: number): number {
|
|
105
|
+
const val = parseFloat(input.value);
|
|
106
|
+
return (unit === 'in' ? val * 25.4 : val) || d;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onRemoveClick(w: WatchProfile): void {
|
|
110
|
+
watches = watches.filter((x) => x.id !== w.id);
|
|
111
|
+
saveWatches();
|
|
112
|
+
if (activeId === w.id) {
|
|
113
|
+
activeId = watches.length > 0 ? watches[watches.length - 1].id : '';
|
|
114
|
+
}
|
|
115
|
+
renderWatchList();
|
|
116
|
+
selectWatch(activeId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createRemoveButton(w: WatchProfile): HTMLButtonElement {
|
|
120
|
+
const btn = document.createElement('button');
|
|
121
|
+
btn.className = 'btn-remove';
|
|
122
|
+
btn.title = ui.remove || 'Remove';
|
|
123
|
+
btn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/></svg>';
|
|
124
|
+
btn.addEventListener('click', (e) => {
|
|
125
|
+
e.stopPropagation();
|
|
126
|
+
onRemoveClick(w);
|
|
127
|
+
});
|
|
128
|
+
return btn;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createWatchListItem(w: WatchProfile): HTMLDivElement {
|
|
132
|
+
const wristCm = getWristCm();
|
|
133
|
+
const item = document.createElement('div');
|
|
134
|
+
item.className = `watch-list-item ${w.id === activeId ? 'active' : ''}`;
|
|
135
|
+
item.dataset.id = w.id;
|
|
136
|
+
|
|
137
|
+
const dot = document.createElement('span');
|
|
138
|
+
dot.className = 'watch-dot';
|
|
139
|
+
dot.style.background = getFitColor(w.lugToLug, wristCm);
|
|
140
|
+
|
|
141
|
+
const nameSpan = document.createElement('span');
|
|
142
|
+
nameSpan.className = 'watch-name-label';
|
|
143
|
+
nameSpan.textContent = w.name;
|
|
144
|
+
|
|
145
|
+
const dimsSpan = document.createElement('span');
|
|
146
|
+
dimsSpan.className = 'watch-dims-label';
|
|
147
|
+
dimsSpan.textContent = `${mmToDisplay(w.diameter)}\u00d7${mmToDisplay(w.lugToLug)}${mmUnit()}`;
|
|
148
|
+
|
|
149
|
+
const badge = document.createElement('span');
|
|
150
|
+
const badgeClass = getFitBadgeClass(w.lugToLug, wristCm);
|
|
151
|
+
badge.className = `watch-fit-badge fit-${badgeClass}`;
|
|
152
|
+
badge.textContent = getFitLabel(w.lugToLug, wristCm);
|
|
153
|
+
|
|
154
|
+
item.append(dot, nameSpan, dimsSpan, badge, createRemoveButton(w));
|
|
155
|
+
item.addEventListener('click', () => { selectWatch(w.id); renderWatchList(); });
|
|
156
|
+
return item;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function renderWatchList(): void {
|
|
160
|
+
watchList.innerHTML = '';
|
|
161
|
+
if (watches.length === 0) {
|
|
162
|
+
watchList.innerHTML = `<div class="empty-state">${ui.addWatch || 'Add a watch'} \u2191</div>`;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
watches.forEach((w) => {
|
|
166
|
+
watchList.appendChild(createWatchListItem(w));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function updateFitInfo(w: WatchProfile, wristCm: number): void {
|
|
171
|
+
const color = getFitColor(w.lugToLug, wristCm);
|
|
172
|
+
fitInfo.innerHTML = `<strong>${getFitLabel(w.lugToLug, wristCm)}</strong> \u2014 ${getFitDescription(w.lugToLug, wristCm)}`;
|
|
173
|
+
fitInfo.style.borderLeft = `3px solid ${color}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function selectWatch(id: string): void {
|
|
177
|
+
activeId = id;
|
|
178
|
+
const w = watches.find((x) => x.id === id);
|
|
179
|
+
if (w) {
|
|
180
|
+
inputDiameter.value = mmToDisplay(w.diameter);
|
|
181
|
+
inputL2L.value = mmToDisplay(w.lugToLug);
|
|
182
|
+
inputThickness.value = mmToDisplay(w.thickness);
|
|
183
|
+
const wristCm = getWristCm();
|
|
184
|
+
drawWatch({ ctx, canvas, ui, diameter: w.diameter, l2l: w.lugToLug, thickness: w.thickness, wristCm, unit });
|
|
185
|
+
updateFitInfo(w, wristCm);
|
|
186
|
+
} else {
|
|
187
|
+
inputDiameter.value = mmToDisplay(40);
|
|
188
|
+
inputL2L.value = mmToDisplay(48);
|
|
189
|
+
inputThickness.value = mmToDisplay(12);
|
|
190
|
+
drawWatch({ ctx, canvas, ui, diameter: 40, l2l: 48, thickness: 12, wristCm: getWristCm(), unit });
|
|
191
|
+
fitInfo.innerHTML = `<strong>${ui.addWatch || 'Add a watch'}</strong> \u2014 ${ui.estimateNote || 'Enter dimensions and add a watch to see how it fits your wrist.'}`;
|
|
192
|
+
fitInfo.style.borderLeft = 'none';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function addWatch(): void {
|
|
197
|
+
const name = inputName.value.trim();
|
|
198
|
+
if (!name) {
|
|
199
|
+
inputName.focus();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const id = 'ws_' + Date.now();
|
|
203
|
+
watches.push({
|
|
204
|
+
id,
|
|
205
|
+
name,
|
|
206
|
+
diameter: parseMm(inputDiameter, 40),
|
|
207
|
+
lugToLug: parseMm(inputL2L, 48),
|
|
208
|
+
thickness: parseMm(inputThickness, 12),
|
|
209
|
+
});
|
|
210
|
+
saveWatches();
|
|
211
|
+
activeId = id;
|
|
212
|
+
inputName.value = '';
|
|
213
|
+
renderWatchList();
|
|
214
|
+
selectWatch(id);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function onInputChange(): void {
|
|
218
|
+
const wristCm = getWristCm();
|
|
219
|
+
const dia = parseMm(inputDiameter, 40), l2l = parseMm(inputL2L, 48), thick = parseMm(inputThickness, 12);
|
|
220
|
+
const w = watches.find((x) => x.id === activeId);
|
|
221
|
+
if (w) {
|
|
222
|
+
w.diameter = dia;
|
|
223
|
+
w.lugToLug = l2l;
|
|
224
|
+
w.thickness = thick;
|
|
225
|
+
saveWatches();
|
|
226
|
+
drawWatch({ ctx, canvas, ui, diameter: dia, l2l, thickness: thick, wristCm, unit });
|
|
227
|
+
updateFitInfo(w, wristCm);
|
|
228
|
+
renderWatchList();
|
|
229
|
+
} else {
|
|
230
|
+
drawWatch({ ctx, canvas, ui, diameter: dia, l2l, thickness: thick, wristCm, unit });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleUnitChange(newUnit: 'cm' | 'in'): void {
|
|
235
|
+
if (unit === newUnit) return;
|
|
236
|
+
const prevCm = getWristCm();
|
|
237
|
+
const prevDiaMm = parseMm(inputDiameter, 40);
|
|
238
|
+
const prevL2lMm = parseMm(inputL2L, 48);
|
|
239
|
+
const prevThickMm = parseMm(inputThickness, 12);
|
|
240
|
+
unit = newUnit;
|
|
241
|
+
saveUnit();
|
|
242
|
+
updateUnitButtons();
|
|
243
|
+
updateInputUnits();
|
|
244
|
+
|
|
245
|
+
inputWrist.value = unit === 'in' ? (prevCm / 2.54).toFixed(1) : prevCm.toFixed(1);
|
|
246
|
+
inputWrist.min = unit === 'in' ? '4.7' : '12';
|
|
247
|
+
inputWrist.max = unit === 'in' ? '9.8' : '25';
|
|
248
|
+
|
|
249
|
+
inputDiameter.value = unit === 'in' ? (prevDiaMm / 25.4).toFixed(2) : Math.round(prevDiaMm).toString();
|
|
250
|
+
inputL2L.value = unit === 'in' ? (prevL2lMm / 25.4).toFixed(2) : Math.round(prevL2lMm).toString();
|
|
251
|
+
inputThickness.value = unit === 'in' ? (prevThickMm / 25.4).toFixed(2) : Math.round(prevThickMm).toString();
|
|
252
|
+
|
|
253
|
+
onInputChange();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
btnUnitCm.addEventListener('click', () => handleUnitChange('cm'));
|
|
257
|
+
btnUnitIn.addEventListener('click', () => handleUnitChange('in'));
|
|
258
|
+
addBtn.addEventListener('click', addWatch);
|
|
259
|
+
inputName.addEventListener('keydown', (e) => { if (e.key === 'Enter') addWatch(); });
|
|
260
|
+
inputDiameter.addEventListener('input', onInputChange);
|
|
261
|
+
inputL2L.addEventListener('input', onInputChange);
|
|
262
|
+
inputThickness.addEventListener('input', onInputChange);
|
|
263
|
+
inputWrist.addEventListener('input', onInputChange);
|
|
264
|
+
|
|
265
|
+
unit = loadUnit();
|
|
266
|
+
updateUnitButtons();
|
|
267
|
+
updateInputUnits();
|
|
268
|
+
|
|
269
|
+
if (unit === 'in') {
|
|
270
|
+
inputWrist.value = (17 / 2.54).toFixed(1);
|
|
271
|
+
inputWrist.min = '4.7'; inputWrist.max = '9.8';
|
|
272
|
+
inputDiameter.value = (40 / 25.4).toFixed(2);
|
|
273
|
+
inputL2L.value = (48 / 25.4).toFixed(2);
|
|
274
|
+
inputThickness.value = (12 / 25.4).toFixed(2);
|
|
275
|
+
} else {
|
|
276
|
+
inputWrist.value = '17'; inputWrist.min = '12'; inputWrist.max = '25';
|
|
277
|
+
}
|
|
278
|
+
inputWrist.step = '0.5';
|
|
279
|
+
loadWatches();
|
|
280
|
+
if (watches.length > 0) {
|
|
281
|
+
activeId = watches[watches.length - 1].id;
|
|
282
|
+
renderWatchList();
|
|
283
|
+
selectWatch(activeId);
|
|
284
|
+
} else {
|
|
285
|
+
selectWatch('');
|
|
286
|
+
renderWatchList();
|
|
287
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import WatchForm from './components/WatchForm.astro';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<link href="./watch-size-comparator.css" rel="stylesheet" />
|
|
12
|
+
|
|
13
|
+
<div class="tool-main-card" data-ui={JSON.stringify(ui)}>
|
|
14
|
+
<WatchForm labels={ui} />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<script src="./client.ts"></script>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="comparator-panel">
|
|
10
|
+
<div class="panel-section">
|
|
11
|
+
<div class="section-label">{labels.yourWatches || "Your Watches"}</div>
|
|
12
|
+
<div class="watch-input-row">
|
|
13
|
+
<input
|
|
14
|
+
type="text"
|
|
15
|
+
id="watch-name-input"
|
|
16
|
+
placeholder={labels.watchNamePlaceholder || "e.g. Rolex Submariner"}
|
|
17
|
+
/>
|
|
18
|
+
<button type="button" class="btn-add" id="add-watch-btn"
|
|
19
|
+
>{labels.addWatch || "Add Watch"}</button
|
|
20
|
+
>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="dims-grid" id="dims-grid">
|
|
25
|
+
<div class="dim-field">
|
|
26
|
+
<label for="input-diameter"
|
|
27
|
+
>{labels.caseDiameter || "Case Diameter"}</label
|
|
28
|
+
>
|
|
29
|
+
<div class="input-with-unit">
|
|
30
|
+
<input
|
|
31
|
+
type="number"
|
|
32
|
+
id="input-diameter"
|
|
33
|
+
min="20"
|
|
34
|
+
max="60"
|
|
35
|
+
value="40"
|
|
36
|
+
step="0.5"
|
|
37
|
+
/>
|
|
38
|
+
<span class="dim-unit" id="unit-diameter">mm</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="dim-field">
|
|
42
|
+
<label for="input-l2l">{labels.lugToLug || "Lug-to-Lug"}</label>
|
|
43
|
+
<div class="input-with-unit">
|
|
44
|
+
<input
|
|
45
|
+
type="number"
|
|
46
|
+
id="input-l2l"
|
|
47
|
+
min="30"
|
|
48
|
+
max="80"
|
|
49
|
+
value="48"
|
|
50
|
+
step="0.5"
|
|
51
|
+
/>
|
|
52
|
+
<span class="dim-unit" id="unit-l2l">mm</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="dim-field">
|
|
56
|
+
<label for="input-thickness">{labels.thickness || "Thickness"}</label>
|
|
57
|
+
<div class="input-with-unit">
|
|
58
|
+
<input
|
|
59
|
+
type="number"
|
|
60
|
+
id="input-thickness"
|
|
61
|
+
min="5"
|
|
62
|
+
max="25"
|
|
63
|
+
value="12"
|
|
64
|
+
step="0.5"
|
|
65
|
+
/>
|
|
66
|
+
<span class="dim-unit" id="unit-thickness">mm</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="dim-field">
|
|
70
|
+
<label for="input-wrist">{labels.wristSize || "Wrist Size"}</label>
|
|
71
|
+
<div class="wrist-row">
|
|
72
|
+
<input
|
|
73
|
+
type="number"
|
|
74
|
+
id="input-wrist"
|
|
75
|
+
min="12"
|
|
76
|
+
max="25"
|
|
77
|
+
value="17"
|
|
78
|
+
step="0.5"
|
|
79
|
+
/>
|
|
80
|
+
<div class="unit-toggle">
|
|
81
|
+
<button type="button" class="unit-btn active" id="btn-unit-cm">{labels.unitCm || "CM"}</button>
|
|
82
|
+
<button type="button" class="unit-btn" id="btn-unit-in">{labels.unitInches || "IN"}</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="visualizer-wrapper">
|
|
89
|
+
<canvas id="size-canvas" width="480" height="260"></canvas>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="legend-row">
|
|
93
|
+
<span class="legend-item">
|
|
94
|
+
<span class="legend-swatch" style="background:#22c55e"></span>
|
|
95
|
+
{labels.excellentFit || "Excellent Fit"}
|
|
96
|
+
</span>
|
|
97
|
+
<span class="legend-item">
|
|
98
|
+
<span class="legend-swatch" style="background:#86efac"></span>
|
|
99
|
+
{labels.goodFit || "Good Fit"}
|
|
100
|
+
</span>
|
|
101
|
+
<span class="legend-item">
|
|
102
|
+
<span class="legend-swatch" style="background:#facc15"></span>
|
|
103
|
+
{labels.borderlineFit || "Borderline"}
|
|
104
|
+
</span>
|
|
105
|
+
<span class="legend-item">
|
|
106
|
+
<span class="legend-swatch" style="background:#ef4444"></span>
|
|
107
|
+
{labels.largeFit || "Too Large"}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="info-box" id="fit-info">
|
|
112
|
+
<strong>{labels.fitsWell || "Fits well"}</strong>-{
|
|
113
|
+
labels.excellentDesc ||
|
|
114
|
+
"Case diameter is proportional to your wrist. Lug-to-lug stays within your wrist width."
|
|
115
|
+
}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="watch-list" id="watch-list">
|
|
119
|
+
<div class="empty-state">{labels.addWatch || "Add a watch"} ↑</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { drawWatchBody } from './watch';
|
|
2
|
+
import { drawDimensionArrows, drawVertDimension } from './measures';
|
|
3
|
+
import { getFitColor, getFitRatio } from './utils';
|
|
4
|
+
|
|
5
|
+
export { getFitColor, getFitRatio };
|
|
6
|
+
|
|
7
|
+
interface LabelInfo {
|
|
8
|
+
primary: string; secondary: string; color: string; dimColor: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function u(obj: Record<string, string>, key: string, fallback: string): string {
|
|
12
|
+
return obj[key] || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function drawWarning(ctx: CanvasRenderingContext2D, cx: number, H: number, message: string): void {
|
|
16
|
+
ctx.save();
|
|
17
|
+
ctx.fillStyle = '#ef4444';
|
|
18
|
+
ctx.font = 'bold 11px sans-serif';
|
|
19
|
+
ctx.textAlign = 'center';
|
|
20
|
+
ctx.textBaseline = 'bottom';
|
|
21
|
+
ctx.fillText('\u26A0 ' + message, cx, H - 44);
|
|
22
|
+
ctx.restore();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function drawInfoLabel(ctx: CanvasRenderingContext2D, cx: number, y: number, info: LabelInfo): void {
|
|
26
|
+
ctx.save();
|
|
27
|
+
ctx.fillStyle = info.color;
|
|
28
|
+
ctx.font = 'bold 12px sans-serif';
|
|
29
|
+
ctx.textAlign = 'center';
|
|
30
|
+
ctx.textBaseline = 'top';
|
|
31
|
+
ctx.fillText(info.primary, cx, y);
|
|
32
|
+
ctx.fillStyle = info.dimColor;
|
|
33
|
+
ctx.font = '10px sans-serif';
|
|
34
|
+
ctx.fillText(info.secondary, cx, y + 16);
|
|
35
|
+
ctx.restore();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function d(mm: number, u: 'cm' | 'in'): string {
|
|
39
|
+
return u === 'in' ? (mm / 25.4).toFixed(1) : (Math.round(mm * 10) / 10).toFixed(1).replace(/\.0$/, '');
|
|
40
|
+
}
|
|
41
|
+
function du(u: 'cm' | 'in'): string {
|
|
42
|
+
return u === 'in' ? 'in' : 'mm';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function drawWatch(p: { ctx: CanvasRenderingContext2D; canvas: HTMLCanvasElement; ui: Record<string, string>; diameter: number; l2l: number; thickness: number; wristCm: number; unit: 'cm' | 'in' }): void {
|
|
46
|
+
const { canvas, ui, diameter, l2l, thickness, wristCm, unit } = p;
|
|
47
|
+
const ctx = p.ctx, W = canvas.width, H = canvas.height;
|
|
48
|
+
const tooLargeMsg = u(ui, 'watchTooLarge', 'Watch extends beyond wrist');
|
|
49
|
+
const caseLabel = u(ui, 'caseDiameter', '').trim(), l2lLabel = u(ui, 'lugToLug', 'L2L').trim();
|
|
50
|
+
const wristLabel = u(ui, 'wristSize', 'wrist').trim(), thickLabel = u(ui, 'thickness', 'thick').trim();
|
|
51
|
+
ctx.clearRect(0, 0, W, H);
|
|
52
|
+
|
|
53
|
+
const wristWidthMm = (wristCm * 10) / Math.PI;
|
|
54
|
+
const maxDim = Math.max(diameter, l2l);
|
|
55
|
+
const scale = Math.min((W - 80) / maxDim, (H - 100) / l2l);
|
|
56
|
+
|
|
57
|
+
const cx = W / 2, cy = H / 2 + 8, r = (diameter / 2) * scale;
|
|
58
|
+
const l2lPx = l2l * scale, wristPx = wristWidthMm * scale;
|
|
59
|
+
const color = getFitColor(l2l, wristCm), isTooWide = l2lPx > wristPx;
|
|
60
|
+
const dimColor = 'rgba(128,128,128,0.5)', ratio = Math.round((l2l / wristWidthMm) * 100);
|
|
61
|
+
|
|
62
|
+
const wristDisplay = unit === 'in' ? (wristCm / 2.54).toFixed(1) + 'in' : wristCm + 'cm';
|
|
63
|
+
|
|
64
|
+
drawWatchBody(ctx, { cx, cy, r, l2lPx, color });
|
|
65
|
+
|
|
66
|
+
if (isTooWide) {
|
|
67
|
+
drawWarning(ctx, cx, H, tooLargeMsg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
drawDimensionArrows(ctx, { cx, y: cy + r + 22, halfW: r, label: `${d(diameter, unit)}${du(unit)} ${caseLabel}`.trim(), color });
|
|
71
|
+
|
|
72
|
+
if (Math.abs(l2lPx - r * 2) > 2) {
|
|
73
|
+
const dimX = cx + Math.max(wristPx / 2, l2lPx / 2) + 26;
|
|
74
|
+
drawVertDimension(ctx, { x: dimX, y1: cy - l2lPx / 2, y2: cy + l2lPx / 2, label: `${d(l2l, unit)}${du(unit)} ${l2lLabel}`.trim(), color: dimColor });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
drawDimensionArrows(ctx, { cx, y: H - 14, halfW: wristPx / 2, label: `${wristDisplay} ${wristLabel}`.trim(), color: dimColor });
|
|
78
|
+
drawInfoLabel(ctx, cx, 8, { primary: `${d(diameter, unit)}\u00d7${d(l2l, unit)}\u00d7${d(thickness, unit)}${du(unit)}`, secondary: `${ratio}% ${wristLabel} width \u00b7 ${d(thickness, unit)}${du(unit)} ${thickLabel}`, color, dimColor });
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function drawDimensionArrows(ctx: CanvasRenderingContext2D, p: { cx: number; y: number; halfW: number; label: string; color: string }): void {
|
|
2
|
+
const { cx, y, halfW, label, color } = p;
|
|
3
|
+
const x1 = cx - halfW, x2 = cx + halfW;
|
|
4
|
+
ctx.save();
|
|
5
|
+
ctx.strokeStyle = color;
|
|
6
|
+
ctx.fillStyle = color;
|
|
7
|
+
ctx.lineWidth = 1;
|
|
8
|
+
ctx.font = '10px sans-serif';
|
|
9
|
+
ctx.textAlign = 'center';
|
|
10
|
+
ctx.textBaseline = 'top';
|
|
11
|
+
ctx.beginPath();
|
|
12
|
+
ctx.moveTo(x1, y);
|
|
13
|
+
ctx.lineTo(x2, y);
|
|
14
|
+
ctx.stroke();
|
|
15
|
+
const a = 4;
|
|
16
|
+
ctx.beginPath();
|
|
17
|
+
ctx.moveTo(x1, y - a);
|
|
18
|
+
ctx.lineTo(x1, y + a);
|
|
19
|
+
ctx.stroke();
|
|
20
|
+
ctx.beginPath();
|
|
21
|
+
ctx.moveTo(x2, y - a);
|
|
22
|
+
ctx.lineTo(x2, y + a);
|
|
23
|
+
ctx.stroke();
|
|
24
|
+
ctx.fillText(label, cx, y + 5);
|
|
25
|
+
ctx.restore();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function drawVertDimension(ctx: CanvasRenderingContext2D, p: { x: number; y1: number; y2: number; label: string; color: string }): void {
|
|
29
|
+
const { x, y1, y2, label, color } = p;
|
|
30
|
+
ctx.save();
|
|
31
|
+
ctx.strokeStyle = color;
|
|
32
|
+
ctx.fillStyle = color;
|
|
33
|
+
ctx.lineWidth = 1;
|
|
34
|
+
ctx.font = '10px sans-serif';
|
|
35
|
+
ctx.textAlign = 'center';
|
|
36
|
+
ctx.beginPath();
|
|
37
|
+
ctx.moveTo(x, y1);
|
|
38
|
+
ctx.lineTo(x, y2);
|
|
39
|
+
ctx.stroke();
|
|
40
|
+
const a = 4;
|
|
41
|
+
ctx.beginPath();
|
|
42
|
+
ctx.moveTo(x - a, y1);
|
|
43
|
+
ctx.lineTo(x + a, y1);
|
|
44
|
+
ctx.stroke();
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.moveTo(x - a, y2);
|
|
47
|
+
ctx.lineTo(x + a, y2);
|
|
48
|
+
ctx.stroke();
|
|
49
|
+
ctx.save();
|
|
50
|
+
ctx.translate(x - 10, (y1 + y2) / 2);
|
|
51
|
+
ctx.rotate(-Math.PI / 2);
|
|
52
|
+
ctx.textAlign = 'center';
|
|
53
|
+
ctx.textBaseline = 'bottom';
|
|
54
|
+
ctx.fillText(label, 0, 0);
|
|
55
|
+
ctx.restore();
|
|
56
|
+
ctx.restore();
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface RectConfig {
|
|
2
|
+
x: number; y: number; w: number; h: number; r: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function roundRect(ctx: CanvasRenderingContext2D, p: RectConfig): void {
|
|
6
|
+
const { x, y, w, h, r } = p;
|
|
7
|
+
ctx.beginPath();
|
|
8
|
+
ctx.moveTo(x + r, y);
|
|
9
|
+
ctx.lineTo(x + w - r, y);
|
|
10
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
11
|
+
ctx.lineTo(x + w, y + h - r);
|
|
12
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
13
|
+
ctx.lineTo(x + r, y + h);
|
|
14
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
15
|
+
ctx.lineTo(x, y + r);
|
|
16
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
17
|
+
ctx.closePath();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hexToRgba(hex: string, a: number): string {
|
|
21
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
22
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
23
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
24
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getFitRatio(l2l: number, wristCm: number): number {
|
|
28
|
+
return l2l / (wristCm * 10);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getFitColor(l2l: number, wristCm: number): string {
|
|
32
|
+
const ratio = getFitRatio(l2l, wristCm);
|
|
33
|
+
if (ratio < 0.5) return '#22c55e';
|
|
34
|
+
if (ratio < 0.58) return '#86efac';
|
|
35
|
+
if (ratio < 0.65) return '#facc15';
|
|
36
|
+
return '#ef4444';
|
|
37
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { roundRect, hexToRgba } from './utils';
|
|
2
|
+
import type { RectConfig } from './utils';
|
|
3
|
+
|
|
4
|
+
function drawLugs(ctx: CanvasRenderingContext2D, p: { cx: number; cy: number; l2lPx: number; lugW: number; lugH: number; color: string }): void {
|
|
5
|
+
const { cx, cy, l2lPx, lugW, lugH, color } = p;
|
|
6
|
+
const lx = cx - lugW / 2;
|
|
7
|
+
const topLug: RectConfig = { x: lx, y: cy - l2lPx / 2, w: lugW, h: lugH, r: 2 };
|
|
8
|
+
const botLug: RectConfig = { x: lx, y: cy + l2lPx / 2 - lugH, w: lugW, h: lugH, r: 2 };
|
|
9
|
+
|
|
10
|
+
roundRect(ctx, topLug);
|
|
11
|
+
ctx.fillStyle = hexToRgba(color, 0.2);
|
|
12
|
+
ctx.fill();
|
|
13
|
+
ctx.strokeStyle = hexToRgba(color, 0.5);
|
|
14
|
+
ctx.lineWidth = 1.2;
|
|
15
|
+
ctx.stroke();
|
|
16
|
+
|
|
17
|
+
roundRect(ctx, botLug);
|
|
18
|
+
ctx.fillStyle = hexToRgba(color, 0.2);
|
|
19
|
+
ctx.fill();
|
|
20
|
+
ctx.strokeStyle = hexToRgba(color, 0.5);
|
|
21
|
+
ctx.lineWidth = 1.2;
|
|
22
|
+
ctx.stroke();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function drawWatchCase(ctx: CanvasRenderingContext2D, p: { cx: number; cy: number; r: number; color: string }): void {
|
|
26
|
+
const { cx, cy, r, color } = p;
|
|
27
|
+
ctx.save();
|
|
28
|
+
ctx.shadowColor = 'rgba(0,0,0,0.15)';
|
|
29
|
+
ctx.shadowBlur = 10;
|
|
30
|
+
ctx.shadowOffsetY = 2;
|
|
31
|
+
ctx.beginPath();
|
|
32
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
33
|
+
ctx.fillStyle = hexToRgba(color, 0.15);
|
|
34
|
+
ctx.fill();
|
|
35
|
+
ctx.strokeStyle = color;
|
|
36
|
+
ctx.lineWidth = 2.5;
|
|
37
|
+
ctx.stroke();
|
|
38
|
+
ctx.restore();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function drawDial(ctx: CanvasRenderingContext2D, p: { cx: number; cy: number; r: number; color: string }): void {
|
|
42
|
+
const { cx, cy, r, color } = p;
|
|
43
|
+
ctx.save();
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
ctx.arc(cx, cy, r * 0.75, 0, Math.PI * 2);
|
|
46
|
+
ctx.fillStyle = hexToRgba(color, 0.08);
|
|
47
|
+
ctx.fill();
|
|
48
|
+
ctx.strokeStyle = hexToRgba(color, 0.3);
|
|
49
|
+
ctx.lineWidth = 0.5;
|
|
50
|
+
ctx.stroke();
|
|
51
|
+
ctx.restore();
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < 12; i++) {
|
|
54
|
+
const a = (i * 30 - 90) * Math.PI / 180;
|
|
55
|
+
const outer = r * 0.7, inner = r * 0.62;
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
ctx.moveTo(cx + Math.cos(a) * inner, cy + Math.sin(a) * inner);
|
|
58
|
+
ctx.lineTo(cx + Math.cos(a) * outer, cy + Math.sin(a) * outer);
|
|
59
|
+
ctx.strokeStyle = hexToRgba(color, 0.4);
|
|
60
|
+
ctx.lineWidth = i % 3 === 0 ? 1.5 : 0.8;
|
|
61
|
+
ctx.stroke();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ctx.beginPath();
|
|
65
|
+
ctx.arc(cx, cy, 2, 0, Math.PI * 2);
|
|
66
|
+
ctx.fillStyle = hexToRgba(color, 0.7);
|
|
67
|
+
ctx.fill();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function drawWatchBody(ctx: CanvasRenderingContext2D, p: { cx: number; cy: number; r: number; l2lPx: number; color: string }): void {
|
|
71
|
+
const { cx, cy, r, l2lPx, color } = p;
|
|
72
|
+
const lugH = (l2lPx - r * 2) / 2;
|
|
73
|
+
if (lugH > 0) {
|
|
74
|
+
drawLugs(ctx, { cx, cy, l2lPx, lugW: r * 0.6, lugH, color });
|
|
75
|
+
}
|
|
76
|
+
drawWatchCase(ctx, { cx, cy, r, color });
|
|
77
|
+
drawDial(ctx, { cx, cy, r, color });
|
|
78
|
+
}
|