sfilata commited on
Commit
6e7e270
·
verified ·
1 Parent(s): 0942cbf

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +855 -19
index.html CHANGED
@@ -1,19 +1,855 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Breathe - 4-7-8 Breath Guide</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <style>
12
+ :root {
13
+ --bg: #0a0f1a;
14
+ --bg-secondary: #111827;
15
+ --fg: #e8f0f2;
16
+ --muted: #6b7a8c;
17
+ --accent: #4fd1c5;
18
+ --accent-glow: rgba(79, 209, 197, 0.3);
19
+ --inhale: #60a5fa;
20
+ --hold: #a78bfa;
21
+ --exhale: #4fd1c5;
22
+ }
23
+
24
+ * {
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ body {
29
+ font-family: 'DM Sans', sans-serif;
30
+ background: var(--bg);
31
+ color: var(--fg);
32
+ min-height: 100vh;
33
+ overflow-x: hidden;
34
+ }
35
+
36
+ .font-display {
37
+ font-family: 'Cormorant Garamond', serif;
38
+ }
39
+
40
+ /* Background atmosphere */
41
+ .bg-atmosphere {
42
+ position: fixed;
43
+ inset: 0;
44
+ z-index: 0;
45
+ background:
46
+ radial-gradient(ellipse 80% 50% at 50% -20%, rgba(79, 209, 197, 0.08) 0%, transparent 50%),
47
+ radial-gradient(ellipse 60% 40% at 20% 100%, rgba(96, 165, 250, 0.06) 0%, transparent 40%),
48
+ radial-gradient(ellipse 50% 30% at 80% 80%, rgba(167, 139, 250, 0.05) 0%, transparent 40%),
49
+ var(--bg);
50
+ }
51
+
52
+ /* Floating particles */
53
+ .particle {
54
+ position: absolute;
55
+ width: 2px;
56
+ height: 2px;
57
+ background: var(--accent);
58
+ border-radius: 50%;
59
+ opacity: 0;
60
+ animation: float 8s infinite ease-in-out;
61
+ }
62
+
63
+ @keyframes float {
64
+ 0%, 100% {
65
+ opacity: 0;
66
+ transform: translateY(100vh) scale(0);
67
+ }
68
+ 10% {
69
+ opacity: 0.6;
70
+ }
71
+ 90% {
72
+ opacity: 0.6;
73
+ }
74
+ 100% {
75
+ opacity: 0;
76
+ transform: translateY(-20vh) scale(1);
77
+ }
78
+ }
79
+
80
+ /* Main breathing circle */
81
+ .breath-circle {
82
+ position: relative;
83
+ width: clamp(200px, 50vw, 320px);
84
+ height: clamp(200px, 50vw, 320px);
85
+ border-radius: 50%;
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ transition: transform 0.1s ease-out;
90
+ }
91
+
92
+ .breath-circle::before {
93
+ content: '';
94
+ position: absolute;
95
+ inset: -4px;
96
+ border-radius: 50%;
97
+ background: conic-gradient(from 0deg, var(--inhale), var(--hold), var(--exhale), var(--inhale));
98
+ opacity: 0.6;
99
+ animation: rotate 20s linear infinite;
100
+ filter: blur(8px);
101
+ }
102
+
103
+ .breath-circle::after {
104
+ content: '';
105
+ position: absolute;
106
+ inset: 0;
107
+ border-radius: 50%;
108
+ background: var(--bg-secondary);
109
+ border: 1px solid rgba(255, 255, 255, 0.05);
110
+ }
111
+
112
+ @keyframes rotate {
113
+ to { transform: rotate(360deg); }
114
+ }
115
+
116
+ .breath-inner {
117
+ position: relative;
118
+ z-index: 1;
119
+ width: 90%;
120
+ height: 90%;
121
+ border-radius: 50%;
122
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.05), transparent 50%),
123
+ linear-gradient(135deg, rgba(79, 209, 197, 0.1), rgba(167, 139, 250, 0.05));
124
+ display: flex;
125
+ flex-direction: column;
126
+ align-items: center;
127
+ justify-content: center;
128
+ backdrop-filter: blur(10px);
129
+ transition: all 0.3s ease;
130
+ }
131
+
132
+ /* Phase indicator rings */
133
+ .phase-ring {
134
+ position: absolute;
135
+ border-radius: 50%;
136
+ border: 2px solid transparent;
137
+ opacity: 0;
138
+ transition: all 0.5s ease;
139
+ }
140
+
141
+ .phase-ring.inhale {
142
+ inset: -20px;
143
+ border-color: var(--inhale);
144
+ box-shadow: 0 0 30px var(--inhale);
145
+ }
146
+
147
+ .phase-ring.hold {
148
+ inset: -30px;
149
+ border-color: var(--hold);
150
+ box-shadow: 0 0 30px var(--hold);
151
+ }
152
+
153
+ .phase-ring.exhale {
154
+ inset: -40px;
155
+ border-color: var(--exhale);
156
+ box-shadow: 0 0 30px var(--exhale);
157
+ }
158
+
159
+ .phase-ring.active {
160
+ opacity: 1;
161
+ animation: pulse-ring 2s ease-in-out infinite;
162
+ }
163
+
164
+ @keyframes pulse-ring {
165
+ 0%, 100% { transform: scale(1); opacity: 0.8; }
166
+ 50% { transform: scale(1.05); opacity: 1; }
167
+ }
168
+
169
+ /* Timer display */
170
+ .timer-display {
171
+ font-size: clamp(3rem, 10vw, 5rem);
172
+ font-weight: 300;
173
+ letter-spacing: -0.02em;
174
+ line-height: 1;
175
+ background: linear-gradient(135deg, var(--fg), var(--muted));
176
+ -webkit-background-clip: text;
177
+ -webkit-text-fill-color: transparent;
178
+ background-clip: text;
179
+ }
180
+
181
+ /* Progress arc */
182
+ .progress-arc {
183
+ position: absolute;
184
+ inset: -4px;
185
+ border-radius: 50%;
186
+ }
187
+
188
+ .progress-arc svg {
189
+ width: 100%;
190
+ height: 100%;
191
+ transform: rotate(-90deg);
192
+ }
193
+
194
+ .progress-arc circle {
195
+ fill: none;
196
+ stroke-width: 3;
197
+ stroke-linecap: round;
198
+ }
199
+
200
+ .progress-arc .track {
201
+ stroke: rgba(255, 255, 255, 0.05);
202
+ }
203
+
204
+ .progress-arc .progress {
205
+ stroke: var(--accent);
206
+ stroke-dasharray: 1000;
207
+ stroke-dashoffset: 1000;
208
+ transition: stroke-dashoffset 0.1s linear;
209
+ filter: drop-shadow(0 0 6px var(--accent));
210
+ }
211
+
212
+ /* Control button */
213
+ .control-btn {
214
+ position: relative;
215
+ padding: 1rem 3rem;
216
+ font-size: 1rem;
217
+ font-weight: 500;
218
+ letter-spacing: 0.1em;
219
+ text-transform: uppercase;
220
+ background: linear-gradient(135deg, rgba(79, 209, 197, 0.15), rgba(167, 139, 250, 0.1));
221
+ border: 1px solid rgba(79, 209, 197, 0.3);
222
+ border-radius: 100px;
223
+ color: var(--accent);
224
+ cursor: pointer;
225
+ overflow: hidden;
226
+ transition: all 0.3s ease;
227
+ }
228
+
229
+ .control-btn::before {
230
+ content: '';
231
+ position: absolute;
232
+ inset: 0;
233
+ background: linear-gradient(135deg, var(--accent), var(--hold));
234
+ opacity: 0;
235
+ transition: opacity 0.3s ease;
236
+ }
237
+
238
+ .control-btn:hover {
239
+ border-color: var(--accent);
240
+ box-shadow: 0 0 30px var(--accent-glow);
241
+ transform: translateY(-2px);
242
+ }
243
+
244
+ .control-btn:hover::before {
245
+ opacity: 0.1;
246
+ }
247
+
248
+ .control-btn:active {
249
+ transform: translateY(0);
250
+ }
251
+
252
+ .control-btn span {
253
+ position: relative;
254
+ z-index: 1;
255
+ }
256
+
257
+ .control-btn.active {
258
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.05));
259
+ border-color: rgba(239, 68, 68, 0.3);
260
+ color: #ef4444;
261
+ }
262
+
263
+ .control-btn.active:hover {
264
+ border-color: #ef4444;
265
+ box-shadow: 0 0 30px rgba(239, 68, 68, 0.3);
266
+ }
267
+
268
+ /* Phase labels */
269
+ .phase-label {
270
+ font-size: 0.75rem;
271
+ letter-spacing: 0.2em;
272
+ text-transform: uppercase;
273
+ opacity: 0.6;
274
+ transition: all 0.3s ease;
275
+ }
276
+
277
+ .phase-label.inhale { color: var(--inhale); }
278
+ .phase-label.hold { color: var(--hold); }
279
+ .phase-label.exhale { color: var(--exhale); }
280
+
281
+ /* Stats cards */
282
+ .stat-card {
283
+ background: linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
284
+ border: 1px solid rgba(255, 255, 255, 0.05);
285
+ border-radius: 16px;
286
+ padding: 1.5rem;
287
+ backdrop-filter: blur(10px);
288
+ transition: all 0.3s ease;
289
+ }
290
+
291
+ .stat-card:hover {
292
+ border-color: rgba(79, 209, 197, 0.2);
293
+ transform: translateY(-2px);
294
+ }
295
+
296
+ /* Sound toggle */
297
+ .sound-toggle {
298
+ width: 48px;
299
+ height: 48px;
300
+ border-radius: 50%;
301
+ background: rgba(255, 255, 255, 0.05);
302
+ border: 1px solid rgba(255, 255, 255, 0.1);
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: center;
306
+ cursor: pointer;
307
+ transition: all 0.3s ease;
308
+ }
309
+
310
+ .sound-toggle:hover {
311
+ background: rgba(79, 209, 197, 0.1);
312
+ border-color: var(--accent);
313
+ }
314
+
315
+ .sound-toggle.muted {
316
+ opacity: 0.5;
317
+ }
318
+
319
+ /* Instructions panel */
320
+ .instructions {
321
+ background: linear-gradient(135deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
322
+ border: 1px solid rgba(255, 255, 255, 0.05);
323
+ border-radius: 20px;
324
+ padding: 2rem;
325
+ backdrop-filter: blur(10px);
326
+ }
327
+
328
+ .instruction-step {
329
+ display: flex;
330
+ align-items: flex-start;
331
+ gap: 1rem;
332
+ padding: 1rem 0;
333
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
334
+ }
335
+
336
+ .instruction-step:last-child {
337
+ border-bottom: none;
338
+ }
339
+
340
+ .step-number {
341
+ width: 32px;
342
+ height: 32px;
343
+ border-radius: 50%;
344
+ display: flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ font-weight: 600;
348
+ font-size: 0.875rem;
349
+ flex-shrink: 0;
350
+ }
351
+
352
+ .step-number.inhale { background: rgba(96, 165, 250, 0.2); color: var(--inhale); }
353
+ .step-number.hold { background: rgba(167, 139, 250, 0.2); color: var(--hold); }
354
+ .step-number.exhale { background: rgba(79, 209, 197, 0.2); color: var(--exhale); }
355
+
356
+ /* Entrance animations */
357
+ .fade-in {
358
+ opacity: 0;
359
+ transform: translateY(20px);
360
+ animation: fadeIn 0.8s ease forwards;
361
+ }
362
+
363
+ @keyframes fadeIn {
364
+ to {
365
+ opacity: 1;
366
+ transform: translateY(0);
367
+ }
368
+ }
369
+
370
+ .delay-1 { animation-delay: 0.1s; }
371
+ .delay-2 { animation-delay: 0.2s; }
372
+ .delay-3 { animation-delay: 0.3s; }
373
+ .delay-4 { animation-delay: 0.4s; }
374
+ .delay-5 { animation-delay: 0.5s; }
375
+
376
+ /* Reduced motion */
377
+ @media (prefers-reduced-motion: reduce) {
378
+ *, *::before, *::after {
379
+ animation-duration: 0.01ms !important;
380
+ animation-iteration-count: 1 !important;
381
+ transition-duration: 0.01ms !important;
382
+ }
383
+ }
384
+
385
+ /* Focus states */
386
+ button:focus-visible, .sound-toggle:focus-visible {
387
+ outline: 2px solid var(--accent);
388
+ outline-offset: 2px;
389
+ }
390
+ </style>
391
+ </head>
392
+ <body class="min-h-screen">
393
+ <!-- Background atmosphere -->
394
+ <div class="bg-atmosphere" aria-hidden="true">
395
+ <div id="particles"></div>
396
+ </div>
397
+
398
+ <!-- Main content -->
399
+ <main class="relative z-10 min-h-screen flex flex-col">
400
+ <!-- Header -->
401
+ <header class="p-6 flex justify-between items-center fade-in">
402
+ <div>
403
+ <h1 class="font-display text-2xl font-light tracking-wide">Breathe</h1>
404
+ <p class="text-xs text-[var(--muted)] tracking-widest uppercase mt-1">4-7-8 Method</p>
405
+ </div>
406
+ <button
407
+ class="sound-toggle"
408
+ id="soundToggle"
409
+ aria-label="Toggle sound"
410
+ title="Toggle audio guide"
411
+ >
412
+ <svg id="soundOnIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
413
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
414
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
415
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
416
+ </svg>
417
+ <svg id="soundOffIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
418
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
419
+ <line x1="23" y1="9" x2="17" y2="15"></line>
420
+ <line x1="17" y1="9" x2="23" y2="15"></line>
421
+ </svg>
422
+ </button>
423
+ </header>
424
+
425
+ <!-- Main breathing section -->
426
+ <section class="flex-1 flex flex-col items-center justify-center px-6 py-8">
427
+ <!-- Phase indicator -->
428
+ <div class="mb-6 fade-in delay-1">
429
+ <span id="phaseLabel" class="phase-label text-center block">Ready to begin</span>
430
+ </div>
431
+
432
+ <!-- Breathing circle -->
433
+ <div class="breath-circle fade-in delay-2" id="breathCircle">
434
+ <!-- Phase rings -->
435
+ <div class="phase-ring inhale" id="inhaleRing"></div>
436
+ <div class="phase-ring hold" id="holdRing"></div>
437
+ <div class="phase-ring exhale" id="exhaleRing"></div>
438
+
439
+ <!-- Progress arc -->
440
+ <div class="progress-arc">
441
+ <svg viewBox="0 0 100 100">
442
+ <circle class="track" cx="50" cy="50" r="48"></circle>
443
+ <circle class="progress" id="progressCircle" cx="50" cy="50" r="48"></circle>
444
+ </svg>
445
+ </div>
446
+
447
+ <!-- Inner content -->
448
+ <div class="breath-inner" id="breathInner">
449
+ <span id="timerDisplay" class="timer-display">4</span>
450
+ <span id="phaseText" class="text-sm text-[var(--muted)] mt-2 tracking-wider uppercase">Inhale</span>
451
+ </div>
452
+ </div>
453
+
454
+ <!-- Control button -->
455
+ <div class="mt-10 fade-in delay-3">
456
+ <button class="control-btn" id="controlBtn" aria-label="Start breathing exercise">
457
+ <span id="btnText">Begin Session</span>
458
+ </button>
459
+ </div>
460
+
461
+ <!-- Session info -->
462
+ <div class="mt-8 flex gap-8 text-center fade-in delay-4">
463
+ <div>
464
+ <span class="block text-2xl font-light" id="cycleCount">0</span>
465
+ <span class="text-xs text-[var(--muted)] uppercase tracking-wider">Cycles</span>
466
+ </div>
467
+ <div class="w-px bg-white/10"></div>
468
+ <div>
469
+ <span class="block text-2xl font-light" id="totalTime">0:00</span>
470
+ <span class="text-xs text-[var(--muted)] uppercase tracking-wider">Duration</span>
471
+ </div>
472
+ </div>
473
+ </section>
474
+
475
+ <!-- Instructions section -->
476
+ <section class="px-6 pb-8 max-w-2xl mx-auto w-full fade-in delay-5">
477
+ <div class="instructions">
478
+ <h2 class="font-display text-xl font-light mb-4 text-center">The 4-7-8 Technique</h2>
479
+ <div class="instruction-step">
480
+ <div class="step-number inhale">1</div>
481
+ <div>
482
+ <h3 class="font-medium text-[var(--inhale)]">Inhale for 4 seconds</h3>
483
+ <p class="text-sm text-[var(--muted)] mt-1">Breathe in quietly through your nose, filling your lungs completely.</p>
484
+ </div>
485
+ </div>
486
+ <div class="instruction-step">
487
+ <div class="step-number hold">2</div>
488
+ <div>
489
+ <h3 class="font-medium text-[var(--hold)]">Hold for 7 seconds</h3>
490
+ <p class="text-sm text-[var(--muted)] mt-1">Retain your breath, keeping your body relaxed and still.</p>
491
+ </div>
492
+ </div>
493
+ <div class="instruction-step">
494
+ <div class="step-number exhale">3</div>
495
+ <div>
496
+ <h3 class="font-medium text-[var(--exhale)]">Exhale for 8 seconds</h3>
497
+ <p class="text-sm text-[var(--muted)] mt-1">Release slowly through your mouth, making a gentle whoosh sound.</p>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ </section>
502
+
503
+ <!-- Footer -->
504
+ <footer class="p-6 text-center">
505
+ <p class="text-xs text-[var(--muted)]">
506
+ Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener noreferrer" class="text-[var(--accent)] hover:underline">anycoder</a>
507
+ </p>
508
+ </footer>
509
+ </main>
510
+
511
+ <script>
512
+ // Audio Context for sound generation
513
+ let audioCtx = null;
514
+ let soundEnabled = true;
515
+
516
+ // State
517
+ let isRunning = false;
518
+ let currentPhase = 'idle'; // idle, inhale, hold, exhale
519
+ let currentTime = 0;
520
+ let cycleCount = 0;
521
+ let totalSeconds = 0;
522
+ let animationFrame = null;
523
+ let lastTimestamp = 0;
524
+
525
+ // Phase durations in seconds
526
+ const phases = {
527
+ inhale: { duration: 4, next: 'hold' },
528
+ hold: { duration: 7, next: 'exhale' },
529
+ exhale: { duration: 8, next: 'inhale' }
530
+ };
531
+
532
+ // DOM Elements
533
+ const breathCircle = document.getElementById('breathCircle');
534
+ const breathInner = document.getElementById('breathInner');
535
+ const timerDisplay = document.getElementById('timerDisplay');
536
+ const phaseText = document.getElementById('phaseText');
537
+ const phaseLabel = document.getElementById('phaseLabel');
538
+ const controlBtn = document.getElementById('controlBtn');
539
+ const btnText = document.getElementById('btnText');
540
+ const cycleCountEl = document.getElementById('cycleCount');
541
+ const totalTimeEl = document.getElementById('totalTime');
542
+ const progressCircle = document.getElementById('progressCircle');
543
+ const soundToggle = document.getElementById('soundToggle');
544
+ const soundOnIcon = document.getElementById('soundOnIcon');
545
+ const soundOffIcon = document.getElementById('soundOffIcon');
546
+ const inhaleRing = document.getElementById('inhaleRing');
547
+ const holdRing = document.getElementById('holdRing');
548
+ const exhaleRing = document.getElementById('exhaleRing');
549
+
550
+ // Progress circle circumference
551
+ const circumference = 2 * Math.PI * 48;
552
+ progressCircle.style.strokeDasharray = circumference;
553
+ progressCircle.style.strokeDashoffset = circumference;
554
+
555
+ // Initialize audio context
556
+ function initAudio() {
557
+ if (!audioCtx) {
558
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
559
+ }
560
+ if (audioCtx.state === 'suspended') {
561
+ audioCtx.resume();
562
+ }
563
+ }
564
+
565
+ // Generate soothing tones
566
+ function playTone(frequency, duration, type = 'sine', fadeIn = 0.1, fadeOut = 0.3) {
567
+ if (!soundEnabled || !audioCtx) return;
568
+
569
+ const oscillator = audioCtx.createOscillator();
570
+ const gainNode = audioCtx.createGain();
571
+ const filter = audioCtx.createBiquadFilter();
572
+
573
+ oscillator.type = type;
574
+ oscillator.frequency.setValueAtTime(frequency, audioCtx.currentTime);
575
+
576
+ filter.type = 'lowpass';
577
+ filter.frequency.setValueAtTime(2000, audioCtx.currentTime);
578
+
579
+ gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
580
+ gainNode.gain.linearRampToValueAtTime(0.15, audioCtx.currentTime + fadeIn);
581
+ gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration - fadeOut);
582
+
583
+ oscillator.connect(filter);
584
+ filter.connect(gainNode);
585
+ gainNode.connect(audioCtx.destination);
586
+
587
+ oscillator.start(audioCtx.currentTime);
588
+ oscillator.stop(audioCtx.currentTime + duration);
589
+ }
590
+
591
+ // Play phase-specific sounds
592
+ function playPhaseSound(phase) {
593
+ if (!soundEnabled) return;
594
+
595
+ initAudio();
596
+
597
+ switch (phase) {
598
+ case 'inhale':
599
+ // Rising tone
600
+ playTone(220, 0.5, 'sine', 0.05, 0.2);
601
+ setTimeout(() => playTone(330, 0.5, 'sine', 0.05, 0.2), 100);
602
+ break;
603
+ case 'hold':
604
+ // Sustained gentle tone
605
+ playTone(440, 0.8, 'sine', 0.1, 0.3);
606
+ break;
607
+ case 'exhale':
608
+ // Falling tone
609
+ playTone(330, 0.6, 'sine', 0.05, 0.3);
610
+ setTimeout(() => playTone(220, 0.8, 'sine', 0.1, 0.4), 150);
611
+ break;
612
+ }
613
+ }
614
+
615
+ // Play tick sound for countdown
616
+ function playTick() {
617
+ if (!soundEnabled || !audioCtx) return;
618
+
619
+ const oscillator = audioCtx.createOscillator();
620
+ const gainNode = audioCtx.createGain();
621
+
622
+ oscillator.type = 'sine';
623
+ oscillator.frequency.setValueAtTime(800, audioCtx.currentTime);
624
+
625
+ gainNode.gain.setValueAtTime(0.03, audioCtx.currentTime);
626
+ gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.05);
627
+
628
+ oscillator.connect(gainNode);
629
+ gainNode.connect(audioCtx.destination);
630
+
631
+ oscillator.start(audioCtx.currentTime);
632
+ oscillator.stop(audioCtx.currentTime + 0.05);
633
+ }
634
+
635
+ // Update UI based on phase
636
+ function updateUI(phase, time, progress) {
637
+ // Update timer
638
+ timerDisplay.textContent = Math.ceil(time);
639
+
640
+ // Update phase text
641
+ const phaseNames = {
642
+ inhale: 'Inhale',
643
+ hold: 'Hold',
644
+ exhale: 'Exhale',
645
+ idle: 'Ready'
646
+ };
647
+ phaseText.textContent = phaseNames[phase];
648
+
649
+ // Update phase label
650
+ const labelNames = {
651
+ inhale: 'Breathe in slowly',
652
+ hold: 'Hold your breath',
653
+ exhale: 'Release gently',
654
+ idle: 'Ready to begin'
655
+ };
656
+ phaseLabel.textContent = labelNames[phase];
657
+ phaseLabel.className = `phase-label ${phase}`;
658
+
659
+ // Update progress circle color
660
+ const phaseColors = {
661
+ inhale: '#60a5fa',
662
+ hold: '#a78bfa',
663
+ exhale: '#4fd1c5',
664
+ idle: '#4fd1c5'
665
+ };
666
+ progressCircle.style.stroke = phaseColors[phase];
667
+
668
+ // Update progress arc
669
+ const offset = circumference * (1 - progress);
670
+ progressCircle.style.strokeDashoffset = offset;
671
+
672
+ // Update phase rings
673
+ inhaleRing.classList.toggle('active', phase === 'inhale');
674
+ holdRing.classList.toggle('active', phase === 'hold');
675
+ exhaleRing.classList.toggle('active', phase === 'exhale');
676
+
677
+ // Animate breathing circle
678
+ const scales = {
679
+ inhale: 1 + (progress * 0.15),
680
+ hold: 1.15,
681
+ exhale: 1.15 - (progress * 0.15),
682
+ idle: 1
683
+ };
684
+ breathCircle.style.transform = `scale(${scales[phase]})`;
685
+ }
686
+
687
+ // Format time as M:SS
688
+ function formatTime(seconds) {
689
+ const mins = Math.floor(seconds / 60);
690
+ const secs = Math.floor(seconds % 60);
691
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
692
+ }
693
+
694
+ // Main animation loop
695
+ function tick(timestamp) {
696
+ if (!isRunning) return;
697
+
698
+ if (!lastTimestamp) lastTimestamp = timestamp;
699
+ const delta = (timestamp - lastTimestamp) / 1000;
700
+ lastTimestamp = timestamp;
701
+
702
+ // Update total time
703
+ totalSeconds += delta;
704
+ totalTimeEl.textContent = formatTime(totalSeconds);
705
+
706
+ // Update current time
707
+ currentTime -= delta;
708
+
709
+ if (currentTime <= 0) {
710
+ // Transition to next phase
711
+ const currentPhaseData = phases[currentPhase];
712
+
713
+ if (currentPhaseData) {
714
+ // Play transition sound
715
+ playPhaseSound(currentPhaseData.next);
716
+
717
+ // Check if completing a full cycle
718
+ if (currentPhase === 'exhale') {
719
+ cycleCount++;
720
+ cycleCountEl.textContent = cycleCount;
721
+ }
722
+
723
+ currentPhase = currentPhaseData.next;
724
+ currentTime = phases[currentPhase].duration;
725
+ }
726
+ }
727
+
728
+ // Calculate progress
729
+ const phaseDuration = phases[currentPhase]?.duration || 1;
730
+ const progress = 1 - (currentTime / phaseDuration);
731
+
732
+ // Update UI
733
+ updateUI(currentPhase, currentTime, progress);
734
+
735
+ // Play tick on integer seconds
736
+ if (Math.abs(currentTime - Math.ceil(currentTime)) < 0.05 && currentTime > 0.5) {
737
+ playTick();
738
+ }
739
+
740
+ animationFrame = requestAnimationFrame(tick);
741
+ }
742
+
743
+ // Start session
744
+ function startSession() {
745
+ initAudio();
746
+ isRunning = true;
747
+ currentPhase = 'inhale';
748
+ currentTime = phases.inhale.duration;
749
+ lastTimestamp = 0;
750
+
751
+ controlBtn.classList.add('active');
752
+ btnText.textContent = 'Pause Session';
753
+
754
+ playPhaseSound('inhale');
755
+ updateUI(currentPhase, currentTime, 0);
756
+
757
+ animationFrame = requestAnimationFrame(tick);
758
+ }
759
+
760
+ // Pause session
761
+ function pauseSession() {
762
+ isRunning = false;
763
+ if (animationFrame) {
764
+ cancelAnimationFrame(animationFrame);
765
+ }
766
+
767
+ controlBtn.classList.remove('active');
768
+ btnText.textContent = 'Resume Session';
769
+ }
770
+
771
+ // Reset session
772
+ function resetSession() {
773
+ isRunning = false;
774
+ currentPhase = 'idle';
775
+ currentTime = 0;
776
+ cycleCount = 0;
777
+ totalSeconds = 0;
778
+ lastTimestamp = 0;
779
+
780
+ if (animationFrame) {
781
+ cancelAnimationFrame(animationFrame);
782
+ }
783
+
784
+ controlBtn.classList.remove('active');
785
+ btnText.textContent = 'Begin Session';
786
+ cycleCountEl.textContent = '0';
787
+ totalTimeEl.textContent = '0:00';
788
+
789
+ updateUI('idle', 4, 0);
790
+ }
791
+
792
+ // Toggle sound
793
+ function toggleSound() {
794
+ soundEnabled = !soundEnabled;
795
+ soundToggle.classList.toggle('muted', !soundEnabled);
796
+ soundOnIcon.style.display = soundEnabled ? 'block' : 'none';
797
+ soundOffIcon.style.display = soundEnabled ? 'none' : 'block';
798
+
799
+ if (soundEnabled) {
800
+ initAudio();
801
+ playTone(440, 0.3, 'sine', 0.05, 0.1);
802
+ }
803
+ }
804
+
805
+ // Event listeners
806
+ controlBtn.addEventListener('click', () => {
807
+ if (!isRunning && currentPhase === 'idle') {
808
+ startSession();
809
+ } else if (!isRunning) {
810
+ startSession();
811
+ } else {
812
+ pauseSession();
813
+ }
814
+ });
815
+
816
+ soundToggle.addEventListener('click', toggleSound);
817
+
818
+ // Double-click to reset
819
+ controlBtn.addEventListener('dblclick', resetSession);
820
+
821
+ // Create floating particles
822
+ function createParticles() {
823
+ const container = document.getElementById('particles');
824
+ const particleCount = 20;
825
+
826
+ for (let i = 0; i < particleCount; i++) {
827
+ const particle = document.createElement('div');
828
+ particle.className = 'particle';
829
+ particle.style.left = `${Math.random() * 100}%`;
830
+ particle.style.animationDelay = `${Math.random() * 8}s`;
831
+ particle.style.animationDuration = `${6 + Math.random() * 4}s`;
832
+ container.appendChild(particle);
833
+ }
834
+ }
835
+
836
+ // Initialize
837
+ createParticles();
838
+ updateUI('idle', 4, 0);
839
+
840
+ // Keyboard support
841
+ document.addEventListener('keydown', (e) => {
842
+ if (e.code === 'Space') {
843
+ e.preventDefault();
844
+ controlBtn.click();
845
+ }
846
+ if (e.code === 'KeyM') {
847
+ toggleSound();
848
+ }
849
+ if (e.code === 'Escape') {
850
+ resetSession();
851
+ }
852
+ });
853
+ </script>
854
+ </body>
855
+ </html>