-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
326 lines (282 loc) · 12 KB
/
Copy pathapp.js
File metadata and controls
326 lines (282 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/* =====================================================================
DANIEL CARMASSI — Medidor de Aliança
app.js — vanilla JS, no dependencies, no build step.
---------------------------------------------------------------------
Flow:
hero -> step1 (calibrar cartão) -> step2 (medir aliança) -> result
Calibration (px per mm) is persisted in localStorage so returning
visitors can skip straight to step 2.
===================================================================== */
(function () {
"use strict";
/* -------------------------------------------------------------------
1. Constants (ABNT / ISO reference values — do not change casually)
------------------------------------------------------------------- */
var CARD_WIDTH_MM = 85.60; // ISO/IEC 7810 ID-1 width
var CARD_HEIGHT_MM = 53.98; // ISO/IEC 7810 ID-1 height
var CARD_ASPECT = CARD_HEIGHT_MM / CARD_WIDTH_MM; // height / width
var ARO_MIN = 8;
var ARO_MAX = 34;
var STORAGE_KEY = "dc_pxPorMM";
/* -------------------------------------------------------------------
2. Math helpers (validated against the Brazilian "aro" standard)
circumference(mm) = aro + 40
diameter(mm) = circumference / PI
aro = round(PI * diameter - 40)
------------------------------------------------------------------- */
function aroToDiameterMM(aro) {
return (aro + 40) / Math.PI;
}
function diameterMMToAro(diameterMM) {
return Math.round(Math.PI * diameterMM - 40);
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
// pt-BR number formatting: comma as decimal separator, 1 decimal place.
function formatMM(value) {
return value.toLocaleString("pt-BR", { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + " mm";
}
/* -------------------------------------------------------------------
3. DOM references
------------------------------------------------------------------- */
var panels = {
hero: document.getElementById("panel-hero"),
step1: document.getElementById("panel-step1"),
step2: document.getElementById("panel-step2"),
result: document.getElementById("panel-result")
};
var heroActionsFresh = document.getElementById("hero-actions-fresh");
var heroActionsReturning = document.getElementById("hero-actions-returning");
var btnStart = document.getElementById("btn-start");
var btnContinue = document.getElementById("btn-continue");
var btnRecalibrateFromHero = document.getElementById("btn-recalibrate-from-hero");
var cardStageFrame = document.getElementById("card-stage-frame");
var cardObject = document.getElementById("card-object");
var cardSlider = document.getElementById("card-slider");
var cardDec = document.getElementById("card-dec");
var cardInc = document.getElementById("card-inc");
var cardPxReadout = document.getElementById("card-px-readout");
var btnBackToHero = document.getElementById("btn-back-to-hero");
var btnCalibrateDone = document.getElementById("btn-calibrate-done");
var ringStageFrame = document.getElementById("ring-stage-frame");
var ringObject = document.getElementById("ring-object");
var ringLiveAro = document.getElementById("ring-live-aro");
var ringSlider = document.getElementById("ring-slider");
var ringDec = document.getElementById("ring-dec");
var ringInc = document.getElementById("ring-inc");
var diameterReadout = document.getElementById("diameter-readout");
var aroReadout = document.getElementById("aro-readout");
var btnRecalibrateFromStep2 = document.getElementById("btn-recalibrate-from-step2");
var btnSeeResult = document.getElementById("btn-see-result");
var resultAro = document.getElementById("result-aro");
var resultDiameter = document.getElementById("result-diameter");
var btnWhatsapp = document.getElementById("btn-whatsapp");
var btnRemeasure = document.getElementById("btn-remeasure");
/* -------------------------------------------------------------------
4. State
------------------------------------------------------------------- */
var state = {
pxPorMM: loadPxPorMM(),
lastAro: 16,
lastDiameterMM: aroToDiameterMM(16)
};
function loadPxPorMM() {
var raw = null;
try {
raw = localStorage.getItem(STORAGE_KEY);
} catch (e) {
// localStorage may be unavailable (privacy mode, file:// restrictions, etc.)
raw = null;
}
var value = parseFloat(raw);
return isFinite(value) && value > 0 ? value : null;
}
function savePxPorMM(value) {
state.pxPorMM = value;
try {
localStorage.setItem(STORAGE_KEY, String(value));
} catch (e) {
// Fail silently — the app still works within this session.
}
}
/* -------------------------------------------------------------------
5. Panel navigation
------------------------------------------------------------------- */
function showPanel(name) {
Object.keys(panels).forEach(function (key) {
var el = panels[key];
if (!el) return;
if (key === name) {
el.hidden = false;
el.removeAttribute("aria-hidden");
el.classList.add("is-active");
} else {
el.classList.remove("is-active");
el.setAttribute("aria-hidden", "true");
}
});
// Move focus to the new panel's heading for keyboard/screen-reader users.
var heading = panels[name] && panels[name].querySelector("h1, h2");
if (heading) {
heading.setAttribute("tabindex", "-1");
heading.focus({ preventScroll: true });
}
window.scrollTo({ top: 0, behavior: "instant" in window ? "instant" : "auto" });
}
function goToHero() {
refreshHeroActions();
showPanel("hero");
}
function goToStep1() {
showPanel("step1");
// Recompute available sizing whenever the calibration stage is shown.
requestAnimationFrame(setupCardSlider);
}
function goToStep2() {
// Guard: step 2 requires a calibration. If none exists, send the
// user back to step 1 instead of showing a broken/meaningless ring.
if (!state.pxPorMM) {
goToStep1();
return;
}
showPanel("step2");
requestAnimationFrame(setupRingSlider);
}
function goToResult() {
resultAro.textContent = String(state.lastAro);
resultDiameter.textContent = formatMM(state.lastDiameterMM);
updateWhatsappLink();
showPanel("result");
}
function refreshHeroActions() {
var hasCalibration = !!state.pxPorMM;
heroActionsFresh.hidden = hasCalibration;
heroActionsReturning.hidden = !hasCalibration;
}
/* -------------------------------------------------------------------
6. Step 1 — card calibration
------------------------------------------------------------------- */
function setupCardSlider() {
var frameWidth = cardStageFrame.clientWidth - 40; // leave breathing room inside the stage
var maxPx = Math.max(180, Math.min(600, frameWidth));
var minPx = 120;
cardSlider.min = String(minPx);
cardSlider.max = String(maxPx);
var current = parseInt(cardSlider.value, 10);
if (!current || current < minPx || current > maxPx) {
// Reasonable default assuming a common ~96dpi display; the user
// will correct this manually against their real card anyway.
current = clamp(320, minPx, maxPx);
}
cardSlider.value = String(current);
renderCard(current);
}
function renderCard(widthPx) {
var heightPx = widthPx * CARD_ASPECT;
cardObject.style.width = widthPx + "px";
cardObject.style.height = heightPx + "px";
cardPxReadout.textContent = Math.round(widthPx) + " px";
}
function stepCardSlider(delta) {
var min = parseInt(cardSlider.min, 10);
var max = parseInt(cardSlider.max, 10);
var next = clamp(parseInt(cardSlider.value, 10) + delta, min, max);
cardSlider.value = String(next);
renderCard(next);
}
cardSlider.addEventListener("input", function () {
renderCard(parseInt(cardSlider.value, 10));
});
cardDec.addEventListener("click", function () { stepCardSlider(-1); });
cardInc.addEventListener("click", function () { stepCardSlider(1); });
function confirmCalibration() {
var cardWidthPx = parseInt(cardSlider.value, 10);
var pxPorMM = cardWidthPx / CARD_WIDTH_MM;
savePxPorMM(pxPorMM);
goToStep2();
}
/* -------------------------------------------------------------------
7. Step 2 — ring measurement
------------------------------------------------------------------- */
function setupRingSlider() {
if (!state.pxPorMM) return;
// The slider must always span the FULL Brazilian range (aro 8–34),
// independent of the stage box size. We reserve enough vertical room
// for the largest circle so it never gets clipped or capped early.
var minPx = aroToDiameterMM(ARO_MIN) * state.pxPorMM;
var maxPx = aroToDiameterMM(ARO_MAX) * state.pxPorMM;
// Grow the measurement stage so the biggest ring (aro 34) always fits.
ringStageFrame.style.minHeight = Math.ceil(maxPx + 56) + "px";
ringSlider.min = String(Math.round(minPx));
ringSlider.max = String(Math.round(maxPx));
ringSlider.step = "1";
var current = parseInt(ringSlider.value, 10);
var defaultPx = Math.round(aroToDiameterMM(16) * state.pxPorMM);
if (!current || current < minPx || current > maxPx) {
current = clamp(defaultPx, Math.round(minPx), Math.round(maxPx));
}
ringSlider.value = String(current);
renderRing(current);
}
function renderRing(diameterPx) {
ringObject.style.width = diameterPx + "px";
ringObject.style.height = diameterPx + "px";
var diameterMM = diameterPx / state.pxPorMM;
var aro = clamp(diameterMMToAro(diameterMM), ARO_MIN, ARO_MAX);
state.lastDiameterMM = diameterMM;
state.lastAro = aro;
diameterReadout.textContent = formatMM(diameterMM);
aroReadout.textContent = String(aro);
ringLiveAro.textContent = String(aro);
}
function stepRingSlider(delta) {
var min = parseInt(ringSlider.min, 10);
var max = parseInt(ringSlider.max, 10);
var next = clamp(parseInt(ringSlider.value, 10) + delta, min, max);
ringSlider.value = String(next);
renderRing(next);
}
ringSlider.addEventListener("input", function () {
renderRing(parseInt(ringSlider.value, 10));
});
ringDec.addEventListener("click", function () { stepRingSlider(-1); });
ringInc.addEventListener("click", function () { stepRingSlider(1); });
/* -------------------------------------------------------------------
8. WhatsApp CTA
Replace the URL below with the real WhatsApp deep link.
See README.md for details.
------------------------------------------------------------------- */
function updateWhatsappLink() {
// TODO(daniel-carmassi): set the real WhatsApp number/link here.
var WHATSAPP_URL = "#";
btnWhatsapp.setAttribute("href", WHATSAPP_URL);
}
/* -------------------------------------------------------------------
9. Wiring
------------------------------------------------------------------- */
btnStart.addEventListener("click", goToStep1);
btnContinue.addEventListener("click", goToStep2);
btnRecalibrateFromHero.addEventListener("click", goToStep1);
btnBackToHero.addEventListener("click", goToHero);
btnCalibrateDone.addEventListener("click", confirmCalibration);
btnRecalibrateFromStep2.addEventListener("click", goToStep1);
btnSeeResult.addEventListener("click", goToResult);
btnRemeasure.addEventListener("click", function () {
// Keeps the saved calibration; only resets the ring measurement.
goToStep2();
});
var resizeTimer = null;
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
if (panels.step1.classList.contains("is-active")) setupCardSlider();
if (panels.step2.classList.contains("is-active")) setupRingSlider();
}, 120);
});
/* -------------------------------------------------------------------
10. Init
------------------------------------------------------------------- */
refreshHeroActions();
showPanel("hero");
})();