THEZYZSTUDIO commited on
Commit
aa6d98d
·
verified ·
1 Parent(s): 18f55d6

Delete index.html

Browse files
Files changed (1) hide show
  1. index.html +0 -1385
index.html DELETED
@@ -1,1385 +0,0 @@
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, maximum-scale=1.0, user-scalable=no">
6
- <title>THE Z CAM</title>
7
- <style>
8
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap');
9
-
10
- :root {
11
- --bg: #000000;
12
- --surface: rgba(255,255,255,0.06);
13
- --surface2: rgba(255,255,255,0.10);
14
- --border: rgba(255,255,255,0.10);
15
- --text: #FFFFFF;
16
- --text-dim: rgba(255,255,255,0.45);
17
- --accent: #00CFFF;
18
- --accent2: #BF5FFF;
19
- --accent-glow: rgba(0,207,255,0.25);
20
- --red: #FF3B3B;
21
- --red-glow: rgba(255,59,59,0.35);
22
- --yellow: #FFD60A;
23
- --green: #30D158;
24
- --radius: 18px;
25
- --radius-sm: 10px;
26
- }
27
-
28
- * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; user-select:none; }
29
-
30
- html, body {
31
- width:100%; height:100%; background:#000; overflow:hidden;
32
- font-family:'Inter', sans-serif; color:var(--text);
33
- }
34
-
35
- /* ══════════════════════════════════════
36
- SPLASH SCREEN
37
- ══════════════════════════════════════ */
38
- #splash {
39
- position:fixed; inset:0; background:#000;
40
- display:flex; flex-direction:column; align-items:center; justify-content:center;
41
- z-index:1000; transition:opacity 0.6s ease;
42
- }
43
- #splash.hide { opacity:0; pointer-events:none; }
44
-
45
- .splash-logo {
46
- display:flex; flex-direction:column; align-items:center; gap:8px;
47
- animation: splashIn 0.8s cubic-bezier(0.16,1,0.3,1) both;
48
- }
49
- .splash-icon {
50
- width:80px; height:80px; background:linear-gradient(135deg, var(--accent), var(--accent2));
51
- border-radius:22px; display:flex; align-items:center; justify-content:center;
52
- box-shadow:0 0 40px var(--accent-glow), 0 0 80px rgba(191,95,255,0.15);
53
- font-size:36px;
54
- }
55
- .splash-title {
56
- font-size:28px; font-weight:900; letter-spacing:3px;
57
- background:linear-gradient(90deg, var(--accent), var(--accent2));
58
- -webkit-background-clip:text; -webkit-text-fill-color:transparent;
59
- }
60
- .splash-sub {
61
- font-size:10px; font-weight:500; letter-spacing:4px; color:var(--text-dim);
62
- text-transform:uppercase; margin-top:-4px;
63
- }
64
- .splash-bar {
65
- width:120px; height:2px; background:var(--border);
66
- border-radius:2px; margin-top:32px; overflow:hidden;
67
- }
68
- .splash-bar-fill {
69
- height:100%; background:linear-gradient(90deg, var(--accent), var(--accent2));
70
- border-radius:2px; width:0; animation:loadBar 1.4s ease forwards 0.3s;
71
- }
72
- @keyframes splashIn {
73
- from { opacity:0; transform:scale(0.85) translateY(20px); }
74
- to { opacity:1; transform:scale(1) translateY(0); }
75
- }
76
- @keyframes loadBar {
77
- to { width:100%; }
78
- }
79
-
80
- /* ══════════════════════════════════════
81
- MAIN APP
82
- ══════════════════════════════════════ */
83
- #app {
84
- position:fixed; inset:0; display:flex; flex-direction:column;
85
- background:#000; opacity:0; transition:opacity 0.5s ease;
86
- }
87
- #app.visible { opacity:1; }
88
-
89
- /* ══════════════════════════════════════
90
- VIEWFINDER
91
- ══════════════════════════════════════ */
92
- #viewfinder {
93
- position:absolute; inset:0; overflow:hidden; background:#0a0a0a;
94
- }
95
-
96
- #video-main {
97
- position:absolute; inset:0; width:100%; height:100%;
98
- object-fit:cover; transform-origin:center center;
99
- transition:transform 0.05s ease;
100
- }
101
-
102
- /* Portrait mode pip */
103
- #video-pip {
104
- position:absolute; top:110px; right:16px;
105
- width:90px; height:130px; border-radius:14px;
106
- overflow:hidden; display:none; border:2px solid rgba(255,255,255,0.3);
107
- box-shadow:0 8px 24px rgba(0,0,0,0.5);
108
- background:#111;
109
- }
110
- #video-pip video {
111
- width:100%; height:100%; object-fit:cover;
112
- }
113
- #video-pip.show { display:block; animation:pipIn 0.3s ease; }
114
- @keyframes pipIn {
115
- from { opacity:0; transform:scale(0.8); }
116
- to { opacity:1; transform:scale(1); }
117
- }
118
-
119
- /* Grid overlay */
120
- #grid-overlay {
121
- position:absolute; inset:0; pointer-events:none; display:none;
122
- }
123
- #grid-overlay.show { display:block; }
124
- #grid-overlay svg { width:100%; height:100%; }
125
-
126
- /* Focus ring */
127
- #focus-ring {
128
- position:absolute; width:60px; height:60px;
129
- border:2px solid var(--accent); border-radius:50%;
130
- transform:translate(-50%, -50%) scale(1.5);
131
- opacity:0; pointer-events:none;
132
- box-shadow:0 0 12px var(--accent-glow);
133
- transition:all 0.3s cubic-bezier(0.16,1,0.3,1);
134
- }
135
- #focus-ring.active {
136
- opacity:1; transform:translate(-50%, -50%) scale(1);
137
- }
138
- #focus-ring.locking {
139
- opacity:0; transform:translate(-50%, -50%) scale(0.8);
140
- }
141
-
142
- /* Exposure slider */
143
- #exposure-drag {
144
- position:absolute; right:20px; top:50%; transform:translateY(-50%);
145
- height:160px; width:32px; display:none; flex-direction:column;
146
- align-items:center; gap:4px; opacity:0;
147
- transition:opacity 0.3s ease;
148
- }
149
- #exposure-drag.show { display:flex; opacity:1; }
150
- .exp-track {
151
- flex:1; width:3px; background:rgba(255,255,255,0.2);
152
- border-radius:2px; position:relative; cursor:pointer;
153
- }
154
- .exp-fill {
155
- position:absolute; bottom:0; width:100%;
156
- background:linear-gradient(to top, var(--yellow), rgba(255,214,10,0.5));
157
- border-radius:2px; height:50%; transition:height 0.1s;
158
- }
159
- .exp-thumb {
160
- position:absolute; left:50%; transform:translateX(-50%);
161
- width:14px; height:14px; border-radius:50%;
162
- background:var(--yellow); box-shadow:0 0 8px rgba(255,214,10,0.5);
163
- bottom:50%; margin-bottom:-7px;
164
- }
165
- .exp-icon { font-size:13px; }
166
-
167
- /* ══════════════════════════════════════
168
- TOP BAR
169
- ══════════════════════════════════════ */
170
- #top-bar {
171
- position:absolute; top:0; left:0; right:0;
172
- padding:16px 20px 12px;
173
- background:linear-gradient(to bottom, rgba(0,0,0,0.75) 0%, transparent 100%);
174
- display:flex; align-items:center; justify-content:space-between;
175
- z-index:10;
176
- }
177
-
178
- .top-left, .top-right {
179
- display:flex; align-items:center; gap:10px;
180
- }
181
-
182
- .brand-badge {
183
- display:flex; align-items:center; gap:6px;
184
- }
185
- .brand-dot {
186
- width:6px; height:6px; border-radius:50%;
187
- background:linear-gradient(135deg, var(--accent), var(--accent2));
188
- box-shadow:0 0 8px var(--accent-glow);
189
- animation:pulse 2s ease infinite;
190
- }
191
- @keyframes pulse {
192
- 0%,100% { box-shadow:0 0 8px var(--accent-glow); }
193
- 50% { box-shadow:0 0 16px var(--accent-glow), 0 0 24px var(--accent-glow); }
194
- }
195
- .brand-name {
196
- font-size:11px; font-weight:700; letter-spacing:2px;
197
- background:linear-gradient(90deg, var(--accent), var(--accent2));
198
- -webkit-background-clip:text; -webkit-text-fill-color:transparent;
199
- }
200
-
201
- .top-btn {
202
- width:36px; height:36px; border-radius:50%;
203
- background:rgba(0,0,0,0.45); backdrop-filter:blur(12px);
204
- border:1px solid var(--border); display:flex; align-items:center;
205
- justify-content:center; cursor:pointer; font-size:16px;
206
- transition:all 0.2s ease; position:relative;
207
- }
208
- .top-btn:active { transform:scale(0.88); }
209
- .top-btn.active { background:rgba(0,207,255,0.2); border-color:var(--accent); }
210
- .top-btn .badge {
211
- position:absolute; top:-2px; right:-2px;
212
- width:8px; height:8px; border-radius:50%;
213
- background:var(--accent); border:1.5px solid #000;
214
- display:none;
215
- }
216
- .top-btn.active .badge { display:block; }
217
-
218
- /* Flash modes */
219
- #flash-btn { font-size:18px; }
220
-
221
- /* ══════════════════════════════════════
222
- ZOOM HUD
223
- ══════════════════════════════════════ */
224
- #zoom-hud {
225
- position:absolute; top:50%; left:50%;
226
- transform:translate(-50%, -50%);
227
- pointer-events:none; z-index:5;
228
- display:flex; flex-direction:column; align-items:center; gap:6px;
229
- opacity:0; transition:opacity 0.3s ease;
230
- }
231
- #zoom-hud.show { opacity:1; }
232
- .zoom-value-big {
233
- font-size:52px; font-weight:900; letter-spacing:-2px;
234
- background:linear-gradient(135deg, #fff 60%, var(--accent));
235
- -webkit-background-clip:text; -webkit-text-fill-color:transparent;
236
- text-shadow:none; line-height:1;
237
- }
238
- .zoom-value-big span {
239
- font-size:22px; font-weight:500; letter-spacing:0;
240
- -webkit-text-fill-color:rgba(255,255,255,0.7);
241
- }
242
- .zoom-ai-badge {
243
- font-size:10px; font-weight:600; letter-spacing:2px;
244
- background:linear-gradient(90deg, var(--accent), var(--accent2));
245
- -webkit-background-clip:text; -webkit-text-fill-color:transparent;
246
- opacity:0; transition:opacity 0.3s;
247
- }
248
- .zoom-ai-badge.show { opacity:1; }
249
-
250
- /* ══════════════════════════════════════
251
- ZOOM BAR (bottom area)
252
- ══════════════════════════════════════ */
253
- #zoom-bar {
254
- position:absolute; bottom:190px; left:0; right:0;
255
- display:flex; flex-direction:column; align-items:center; gap:10px;
256
- z-index:10; pointer-events:auto;
257
- }
258
-
259
- /* Quick zoom presets */
260
- .zoom-presets {
261
- display:flex; gap:6px; align-items:center;
262
- }
263
- .zoom-preset {
264
- min-width:44px; height:32px; border-radius:16px;
265
- background:rgba(0,0,0,0.5); backdrop-filter:blur(16px);
266
- border:1px solid rgba(255,255,255,0.15);
267
- display:flex; align-items:center; justify-content:center;
268
- font-size:12px; font-weight:600; color:rgba(255,255,255,0.7);
269
- cursor:pointer; padding:0 10px;
270
- transition:all 0.2s cubic-bezier(0.16,1,0.3,1);
271
- }
272
- .zoom-preset:active { transform:scale(0.9); }
273
- .zoom-preset.active {
274
- background:rgba(0,207,255,0.2); border-color:var(--accent);
275
- color:var(--accent); box-shadow:0 0 12px rgba(0,207,255,0.2);
276
- }
277
-
278
- /* Main zoom slider */
279
- .zoom-slider-wrap {
280
- display:flex; align-items:center; gap:10px; width:90%; max-width:340px;
281
- }
282
- .zoom-icon { font-size:14px; opacity:0.5; }
283
- .zoom-track {
284
- flex:1; height:4px; background:rgba(255,255,255,0.15);
285
- border-radius:2px; position:relative; cursor:pointer;
286
- }
287
- .zoom-fill {
288
- position:absolute; left:0; top:0; height:100%;
289
- background:linear-gradient(90deg, var(--accent), var(--accent2));
290
- border-radius:2px; transition:width 0.05s;
291
- }
292
- .zoom-thumb {
293
- position:absolute; top:50%; transform:translate(-50%, -50%);
294
- width:18px; height:18px; border-radius:50%;
295
- background:#fff; box-shadow:0 2px 8px rgba(0,0,0,0.4), 0 0 0 3px rgba(0,207,255,0.3);
296
- cursor:grab; transition:left 0.05s;
297
- }
298
- .zoom-thumb:active { cursor:grabbing; transform:translate(-50%,-50%) scale(1.15); }
299
-
300
- /* ══════════════════════════════════════
301
- BOTTOM CONTROLS
302
- ══════════════════════════════════════ */
303
- #bottom-controls {
304
- position:absolute; bottom:0; left:0; right:0;
305
- padding:0 20px 40px;
306
- background:linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
307
- z-index:10;
308
- }
309
-
310
- /* Mode tabs */
311
- .mode-tabs {
312
- display:flex; align-items:center; justify-content:center;
313
- gap:0; margin-bottom:20px; overflow-x:auto;
314
- scrollbar-width:none;
315
- }
316
- .mode-tabs::-webkit-scrollbar { display:none; }
317
- .mode-tab {
318
- padding:6px 14px; font-size:11px; font-weight:600; letter-spacing:1.5px;
319
- color:var(--text-dim); cursor:pointer; white-space:nowrap;
320
- transition:all 0.2s; text-transform:uppercase; border-radius:20px;
321
- border:1px solid transparent;
322
- }
323
- .mode-tab:active { transform:scale(0.92); }
324
- .mode-tab.active {
325
- color:var(--yellow); border-color:rgba(255,214,10,0.3);
326
- background:rgba(255,214,10,0.08);
327
- }
328
-
329
- /* Main shutter row */
330
- .shutter-row {
331
- display:flex; align-items:center; justify-content:space-between;
332
- }
333
-
334
- /* Gallery thumb */
335
- .gallery-thumb {
336
- width:56px; height:56px; border-radius:12px;
337
- background:var(--surface); border:2px solid rgba(255,255,255,0.15);
338
- overflow:hidden; cursor:pointer; position:relative;
339
- transition:all 0.2s ease;
340
- }
341
- .gallery-thumb:active { transform:scale(0.88); }
342
- .gallery-thumb img {
343
- width:100%; height:100%; object-fit:cover; display:none;
344
- }
345
- .gallery-thumb .empty-icon {
346
- position:absolute; inset:0; display:flex; align-items:center;
347
- justify-content:center; font-size:22px; opacity:0.3;
348
- }
349
- .gallery-count {
350
- position:absolute; top:-4px; right:-4px;
351
- min-width:18px; height:18px; border-radius:9px;
352
- background:var(--accent); font-size:9px; font-weight:700;
353
- display:flex; align-items:center; justify-content:center;
354
- border:1.5px solid #000; padding:0 3px; display:none;
355
- }
356
-
357
- /* Shutter button */
358
- .shutter-btn {
359
- width:76px; height:76px; border-radius:50%;
360
- background:#fff; border:none; cursor:pointer; position:relative;
361
- transition:transform 0.15s ease, box-shadow 0.15s ease;
362
- box-shadow:0 0 0 4px rgba(255,255,255,0.15);
363
- }
364
- .shutter-btn::after {
365
- content:''; position:absolute; inset:4px; border-radius:50%;
366
- background:#fff; transition:all 0.15s;
367
- }
368
- .shutter-btn:active { transform:scale(0.88); }
369
- .shutter-btn.recording {
370
- background:var(--red);
371
- box-shadow:0 0 0 4px rgba(255,59,59,0.25), 0 0 24px var(--red-glow);
372
- animation:recordPulse 1.5s ease infinite;
373
- }
374
- .shutter-btn.recording::after {
375
- inset:20px; border-radius:6px; background:var(--red);
376
- }
377
- @keyframes recordPulse {
378
- 0%,100% { box-shadow:0 0 0 4px rgba(255,59,59,0.25), 0 0 24px var(--red-glow); }
379
- 50% { box-shadow:0 0 0 8px rgba(255,59,59,0.15), 0 0 40px var(--red-glow); }
380
- }
381
-
382
- /* Video shutter outer ring animation */
383
- .shutter-ring {
384
- position:absolute; inset:-5px; border-radius:50%;
385
- border:2px solid transparent; pointer-events:none;
386
- }
387
- .shutter-btn.recording .shutter-ring {
388
- border-color:var(--red); animation:rotateSlow 3s linear infinite;
389
- }
390
- @keyframes rotateSlow { to { transform:rotate(360deg); } }
391
-
392
- /* Flip camera */
393
- .flip-btn {
394
- width:56px; height:56px; border-radius:50%;
395
- background:rgba(0,0,0,0.4); backdrop-filter:blur(16px);
396
- border:1px solid var(--border);
397
- display:flex; align-items:center; justify-content:center;
398
- font-size:22px; cursor:pointer;
399
- transition:all 0.2s ease;
400
- }
401
- .flip-btn:active { transform:scale(0.85) rotate(180deg); }
402
-
403
- /* ════════════════════════════���═════════
404
- RECORD TIMER
405
- ══════════════════════════════════════ */
406
- #rec-timer {
407
- position:absolute; top:70px; left:50%; transform:translateX(-50%);
408
- display:none; align-items:center; gap:8px;
409
- background:rgba(0,0,0,0.55); backdrop-filter:blur(10px);
410
- border:1px solid rgba(255,59,59,0.3); border-radius:20px;
411
- padding:6px 14px; z-index:10;
412
- }
413
- #rec-timer.show { display:flex; }
414
- .rec-dot {
415
- width:8px; height:8px; border-radius:50%; background:var(--red);
416
- animation:blink 1s step-end infinite;
417
- }
418
- @keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0;} }
419
- .rec-time { font-size:13px; font-weight:600; letter-spacing:1px; font-variant-numeric:tabular-nums; }
420
-
421
- /* ══════════════════════════════════════
422
- RESOLUTION / SETTINGS PANEL
423
- ══════════════════════════════════════ */
424
- #settings-panel {
425
- position:absolute; top:70px; left:0; right:0;
426
- background:rgba(0,0,0,0.85); backdrop-filter:blur(24px);
427
- border-bottom:1px solid var(--border);
428
- padding:16px 20px; z-index:20;
429
- transform:translateY(-120%); transition:transform 0.4s cubic-bezier(0.16,1,0.3,1);
430
- }
431
- #settings-panel.open { transform:translateY(0); }
432
-
433
- .settings-title {
434
- font-size:11px; font-weight:700; letter-spacing:3px; color:var(--text-dim);
435
- text-transform:uppercase; margin-bottom:14px;
436
- }
437
- .settings-row {
438
- display:flex; align-items:center; justify-content:space-between;
439
- padding:10px 0; border-bottom:1px solid var(--border);
440
- }
441
- .settings-row:last-child { border-bottom:none; }
442
- .settings-label {
443
- font-size:13px; font-weight:500; display:flex; align-items:center; gap:8px;
444
- }
445
- .settings-label span { font-size:16px; }
446
- .settings-chips { display:flex; gap:6px; }
447
- .chip {
448
- padding:5px 12px; border-radius:20px; font-size:11px; font-weight:600;
449
- background:rgba(255,255,255,0.08); border:1px solid var(--border);
450
- cursor:pointer; transition:all 0.15s; color:var(--text-dim);
451
- }
452
- .chip.active {
453
- background:rgba(0,207,255,0.15); border-color:var(--accent); color:var(--accent);
454
- }
455
- .chip:active { transform:scale(0.92); }
456
-
457
- /* ══════════════════════════════════════
458
- FLASH OVERLAY
459
- ══════════════════════════════════════ */
460
- #flash-overlay {
461
- position:absolute; inset:0; background:#fff;
462
- opacity:0; pointer-events:none; z-index:100;
463
- transition:opacity 0.08s;
464
- }
465
- #flash-overlay.flash { opacity:1; }
466
-
467
- /* ══════════════════════════════════════
468
- CAPTURE FEEDBACK
469
- ══════════════════════════════════════ */
470
- #capture-feedback {
471
- position:absolute; inset:0; border:3px solid #fff;
472
- opacity:0; pointer-events:none; z-index:99;
473
- border-radius:0; transition:opacity 0.1s;
474
- }
475
- #capture-feedback.flash { opacity:0.7; }
476
-
477
- /* ══════════════════════════════════════
478
- NO CAMERA MESSAGE
479
- ══════════════════════════════════════ */
480
- #no-camera {
481
- position:absolute; inset:0; display:none;
482
- flex-direction:column; align-items:center; justify-content:center;
483
- gap:16px; z-index:50; text-align:center; padding:32px;
484
- background:rgba(0,0,0,0.92);
485
- }
486
- #no-camera.show { display:flex; }
487
- .no-cam-icon { font-size:56px; }
488
- .no-cam-title { font-size:20px; font-weight:800; letter-spacing:0.5px; }
489
- .no-cam-sub { font-size:13px; opacity:0.5; line-height:1.7; max-width:280px; }
490
- .open-tab-btn {
491
- margin-top:8px;
492
- padding:14px 32px; border-radius:30px;
493
- background:linear-gradient(135deg, var(--accent), var(--accent2));
494
- color:#fff; font-size:14px; font-weight:700; letter-spacing:1px;
495
- cursor:pointer; border:none;
496
- box-shadow:0 0 24px var(--accent-glow), 0 4px 20px rgba(0,0,0,0.4);
497
- transition:transform 0.15s, box-shadow 0.15s;
498
- text-decoration:none; display:inline-flex; align-items:center; gap:8px;
499
- }
500
- .open-tab-btn:active { transform:scale(0.94); }
501
- .iframe-note {
502
- font-size:11px; color:rgba(255,255,255,0.3); line-height:1.6; max-width:260px;
503
- border:1px solid rgba(255,255,255,0.08); border-radius:10px; padding:10px 14px;
504
- }
505
-
506
- /* ══════════════════════════════════════
507
- LEVEL INDICATOR
508
- ══════════════════════════════════════ */
509
- #level-indicator {
510
- position:absolute; bottom:190px; left:50%; transform:translateX(-50%);
511
- display:flex; align-items:center; gap:3px; opacity:0;
512
- transition:opacity 0.3s; z-index:5; pointer-events:none;
513
- }
514
- #level-indicator.show { opacity:1; }
515
- .level-line {
516
- height:2px; background:var(--yellow); border-radius:1px; transition:width 0.1s;
517
- }
518
- .level-dot {
519
- width:6px; height:6px; border-radius:50%;
520
- background:var(--yellow); box-shadow:0 0 6px rgba(255,214,10,0.6);
521
- }
522
-
523
- /* ══════════════════════════════════════
524
- PINCH HINT
525
- ══════════════════════════════════════ */
526
- #pinch-hint {
527
- position:absolute; top:40%; left:50%; transform:translate(-50%, -50%);
528
- background:rgba(0,0,0,0.6); backdrop-filter:blur(10px);
529
- border:1px solid var(--border); border-radius:12px;
530
- padding:10px 16px; font-size:12px; color:var(--text-dim);
531
- pointer-events:none; opacity:0; z-index:20;
532
- transition:opacity 0.3s;
533
- }
534
- #pinch-hint.show { opacity:1; }
535
-
536
- /* ══════════════════════════════════════
537
- RESPONSIVE
538
- ══════════════════════════════════════ */
539
- @media (max-width:380px) {
540
- .zoom-preset { min-width:36px; padding:0 8px; font-size:11px; }
541
- .shutter-btn { width:68px; height:68px; }
542
- .gallery-thumb, .flip-btn { width:48px; height:48px; }
543
- }
544
- </style>
545
- </head>
546
- <body>
547
-
548
- <!-- SPLASH -->
549
- <div id="splash">
550
- <div class="splash-logo">
551
- <div class="splash-icon">📷</div>
552
- <div class="splash-title">THE Z CAM</div>
553
- <div class="splash-sub">by THE ZYZ STUDIO</div>
554
- <div class="splash-bar"><div class="splash-bar-fill"></div></div>
555
- </div>
556
- </div>
557
-
558
- <!-- MAIN APP -->
559
- <div id="app">
560
-
561
- <!-- VIEWFINDER -->
562
- <div id="viewfinder">
563
- <video id="video-main" autoplay playsinline muted></video>
564
-
565
- <!-- PiP (dual camera feel) -->
566
- <div id="video-pip">
567
- <video id="video-secondary" autoplay playsinline muted></video>
568
- </div>
569
-
570
- <!-- Grid overlay -->
571
- <div id="grid-overlay">
572
- <svg viewBox="0 0 100 100" preserveAspectRatio="none">
573
- <line x1="33.3" y1="0" x2="33.3" y2="100" stroke="rgba(255,255,255,0.2)" stroke-width="0.3"/>
574
- <line x1="66.6" y1="0" x2="66.6" y2="100" stroke="rgba(255,255,255,0.2)" stroke-width="0.3"/>
575
- <line x1="0" y1="33.3" x2="100" y2="33.3" stroke="rgba(255,255,255,0.2)" stroke-width="0.3"/>
576
- <line x1="0" y1="66.6" x2="100" y2="66.6" stroke="rgba(255,255,255,0.2)" stroke-width="0.3"/>
577
- </svg>
578
- </div>
579
-
580
- <!-- Focus ring -->
581
- <div id="focus-ring"></div>
582
-
583
- <!-- Flash overlay -->
584
- <div id="flash-overlay"></div>
585
- <div id="capture-feedback"></div>
586
-
587
- <!-- No camera -->
588
- <div id="no-camera">
589
- <div class="no-cam-icon">📵</div>
590
- <div class="no-cam-title">Camera Access Needed</div>
591
- <div class="no-cam-sub">Please tap Allow when your browser asks for camera permission.</div>
592
- <button class="open-tab-btn" onclick="startCamera()" style="margin-top:8px;">
593
- <span>🔄</span> Try Again
594
- </button>
595
- </div>
596
-
597
- <!-- Zoom HUD (shown during active zoom) -->
598
- <div id="zoom-hud">
599
- <div class="zoom-value-big" id="zoom-display">1<span>×</span></div>
600
- <div class="zoom-ai-badge" id="zoom-ai-badge">⚡ AI ENHANCED ZOOM</div>
601
- </div>
602
-
603
- <!-- Pinch hint -->
604
- <div id="pinch-hint">👌 Pinch to zoom</div>
605
-
606
- <!-- Exposure drag -->
607
- <div id="exposure-drag">
608
- <span class="exp-icon">☀️</span>
609
- <div class="exp-track" id="exp-track">
610
- <div class="exp-fill" id="exp-fill"></div>
611
- <div class="exp-thumb" id="exp-thumb"></div>
612
- </div>
613
- <span class="exp-icon">🌑</span>
614
- </div>
615
-
616
- <!-- Level indicator -->
617
- <div id="level-indicator">
618
- <div class="level-line" style="width:30px;"></div>
619
- <div class="level-dot"></div>
620
- <div class="level-line" style="width:30px;"></div>
621
- </div>
622
-
623
- </div>
624
-
625
- <!-- TOP BAR -->
626
- <div id="top-bar">
627
- <div class="top-left">
628
- <div class="brand-badge">
629
- <div class="brand-dot"></div>
630
- <div class="brand-name">Z CAM</div>
631
- </div>
632
- </div>
633
- <div class="top-right">
634
- <div class="top-btn" id="grid-btn" title="Grid" onclick="toggleGrid()">⊞</div>
635
- <div class="top-btn" id="flash-btn" onclick="cycleFlash()">⚡</div>
636
- <div class="top-btn" id="settings-btn" onclick="toggleSettings()">⚙️</div>
637
- </div>
638
- </div>
639
-
640
- <!-- SETTINGS PANEL -->
641
- <div id="settings-panel">
642
- <div class="settings-title">⚙ Camera Settings</div>
643
-
644
- <div class="settings-row">
645
- <div class="settings-label"><span>📐</span> Resolution</div>
646
- <div class="settings-chips">
647
- <div class="chip" onclick="setRes(this,'4K')">4K</div>
648
- <div class="chip active" onclick="setRes(this,'1080p')">1080p</div>
649
- <div class="chip" onclick="setRes(this,'720p')">720p</div>
650
- </div>
651
- </div>
652
-
653
- <div class="settings-row">
654
- <div class="settings-label"><span>🌙</span> Night Mode</div>
655
- <div class="settings-chips">
656
- <div class="chip active" id="night-chip" onclick="toggleNight(this)">Auto</div>
657
- <div class="chip" id="night-on" onclick="setNightOn(this)">ON</div>
658
- <div class="chip" id="night-off" onclick="setNightOff(this)">OFF</div>
659
- </div>
660
- </div>
661
-
662
- <div class="settings-row">
663
- <div class="settings-label"><span>🎨</span> Color Profile</div>
664
- <div class="settings-chips">
665
- <div class="chip active" onclick="setColor(this,'vivid')">Vivid</div>
666
- <div class="chip" onclick="setColor(this,'natural')">Natural</div>
667
- <div class="chip" onclick="setColor(this,'flat')">Flat</div>
668
- </div>
669
- </div>
670
-
671
- <div class="settings-row">
672
- <div class="settings-label"><span>🖼️</span> Aspect Ratio</div>
673
- <div class="settings-chips">
674
- <div class="chip active" onclick="setAspect(this,'9:16')">9:16</div>
675
- <div class="chip" onclick="setAspect(this,'4:3')">4:3</div>
676
- <div class="chip" onclick="setAspect(this,'1:1')">1:1</div>
677
- </div>
678
- </div>
679
-
680
- <div class="settings-row">
681
- <div class="settings-label"><span>📱</span> Dual View</div>
682
- <div class="settings-chips">
683
- <div class="chip" id="dual-chip" onclick="toggleDual(this)">Enable</div>
684
- </div>
685
- </div>
686
- </div>
687
-
688
- <!-- REC TIMER -->
689
- <div id="rec-timer">
690
- <div class="rec-dot"></div>
691
- <div class="rec-time" id="rec-time-txt">00:00</div>
692
- </div>
693
-
694
- <!-- BOTTOM CONTROLS -->
695
- <div id="bottom-controls">
696
-
697
- <!-- Mode tabs -->
698
- <div class="mode-tabs">
699
- <div class="mode-tab" onclick="setMode(this,'SLOW-MO')">Slow-Mo</div>
700
- <div class="mode-tab" onclick="setMode(this,'VIDEO')">Video</div>
701
- <div class="mode-tab active" id="tab-PHOTO" onclick="setMode(this,'PHOTO')">Photo</div>
702
- <div class="mode-tab" onclick="setMode(this,'PORTRAIT')">Portrait</div>
703
- <div class="mode-tab" onclick="setMode(this,'NIGHT')">Night</div>
704
- </div>
705
-
706
- <!-- Zoom presets -->
707
- <div id="zoom-bar">
708
- <div class="zoom-presets">
709
- <div class="zoom-preset" onclick="setZoomPreset(this,0.6)">0.6×</div>
710
- <div class="zoom-preset active" id="preset-1x" onclick="setZoomPreset(this,1)">1×</div>
711
- <div class="zoom-preset" onclick="setZoomPreset(this,2)">2×</div>
712
- <div class="zoom-preset" onclick="setZoomPreset(this,5)">5×</div>
713
- <div class="zoom-preset" onclick="setZoomPreset(this,10)">10×</div>
714
- <div class="zoom-preset" onclick="setZoomPreset(this,30)">30×</div>
715
- <div class="zoom-preset" onclick="setZoomPreset(this,100)">100×</div>
716
- </div>
717
- <div class="zoom-slider-wrap">
718
- <span class="zoom-icon">🔭</span>
719
- <div class="zoom-track" id="zoom-track">
720
- <div class="zoom-fill" id="zoom-fill"></div>
721
- <div class="zoom-thumb" id="zoom-thumb"></div>
722
- </div>
723
- <span class="zoom-icon" style="font-size:18px;">🔭</span>
724
- </div>
725
- </div>
726
-
727
- <!-- Shutter row -->
728
- <div class="shutter-row">
729
- <div class="gallery-thumb" id="gallery-thumb" onclick="openGallery()">
730
- <div class="empty-icon">🖼️</div>
731
- <img id="last-photo" src="" alt="">
732
- <div class="gallery-count" id="gallery-count">0</div>
733
- </div>
734
-
735
- <button class="shutter-btn" id="shutter-btn" onclick="handleShutter()">
736
- <div class="shutter-ring"></div>
737
- </button>
738
-
739
- <div class="flip-btn" onclick="flipCamera()">🔄</div>
740
- </div>
741
- </div>
742
-
743
- </div>
744
-
745
- <!-- HIDDEN CANVAS FOR CAPTURE -->
746
- <canvas id="capture-canvas" style="display:none;"></canvas>
747
-
748
- <script>
749
- /* ═══════════════════════════════════════════════════════════
750
- THE Z CAM — Core Engine
751
- ═══════════════════════════════════════════════════════════ */
752
-
753
- const state = {
754
- stream: null,
755
- secondaryStream: null,
756
- facingMode: 'environment',
757
- mode: 'PHOTO',
758
- zoom: 1,
759
- maxZoom: 100,
760
- nativeMaxZoom: 1,
761
- flashMode: 'auto', // off, auto, on, torch
762
- gridOn: false,
763
- nightMode: 'auto',
764
- colorProfile: 'vivid',
765
- recording: false,
766
- recorder: null,
767
- recSeconds: 0,
768
- recInterval: null,
769
- photos: [],
770
- dualMode: false,
771
- pinching: false,
772
- pinchStartDist: 0,
773
- pinchStartZoom: 1,
774
- settingsOpen: false,
775
- cameraReady: false,
776
- hintShown: false,
777
- };
778
-
779
- /* ──────────────────────────────────────
780
- INIT
781
- ─────────────────────────────────────── */
782
- window.addEventListener('load', () => {
783
- setTimeout(() => {
784
- document.getElementById('splash').classList.add('hide');
785
- document.getElementById('app').classList.add('visible');
786
- startCamera();
787
- initZoomSlider();
788
- initPinchZoom();
789
- initFocusTap();
790
- setTimeout(() => {
791
- if (!state.hintShown) {
792
- document.getElementById('pinch-hint').classList.add('show');
793
- state.hintShown = true;
794
- setTimeout(() => document.getElementById('pinch-hint').classList.remove('show'), 2500);
795
- }
796
- }, 1800);
797
- }, 1700);
798
- });
799
-
800
- /* ──────────────────────────────────────
801
- CAMERA START
802
- ─────────────────────────────────────── */
803
- async function startCamera() {
804
- if (state.stream) {
805
- state.stream.getTracks().forEach(t => t.stop());
806
- state.stream = null;
807
- }
808
-
809
- const constraints = {
810
- video: {
811
- facingMode: state.facingMode,
812
- width: { ideal: 1920 },
813
- height: { ideal: 1080 },
814
- },
815
- audio: false
816
- };
817
-
818
- try {
819
- const s = await navigator.mediaDevices.getUserMedia(constraints);
820
- state.stream = s;
821
- const videoEl = document.getElementById('video-main');
822
- videoEl.srcObject = s;
823
-
824
- // Get native zoom capability
825
- const track = s.getVideoTracks()[0];
826
- if (track) {
827
- const caps = track.getCapabilities ? track.getCapabilities() : {};
828
- if (caps.zoom) {
829
- state.nativeMaxZoom = caps.zoom.max || 1;
830
- } else {
831
- state.nativeMaxZoom = 1;
832
- }
833
- }
834
-
835
- document.getElementById('no-camera').classList.remove('show');
836
- state.cameraReady = true;
837
- applyZoom(state.zoom);
838
-
839
- } catch(e) {
840
- document.getElementById('no-camera').classList.add('show');
841
- state.cameraReady = false;
842
- console.warn('Camera error:', e);
843
- }
844
- }
845
-
846
- /* ──────────────────────────────────────
847
- ZOOM ENGINE
848
- Native zoom via constraints OR CSS transform fallback
849
- Max visual zoom: 100× (AI-enhanced range shown)
850
- ─────────────────────────────────────── */
851
- function applyZoom(z) {
852
- z = Math.max(0.5, Math.min(z, state.maxZoom));
853
- state.zoom = z;
854
-
855
- const videoEl = document.getElementById('video-main');
856
- const track = state.stream && state.stream.getVideoTracks()[0];
857
-
858
- // Try native zoom (only works on mobile with real camera)
859
- if (track && track.getCapabilities) {
860
- const caps = track.getCapabilities();
861
- if (caps.zoom) {
862
- const nativeZ = Math.min(z, caps.zoom.max);
863
- track.applyConstraints({ advanced: [{ zoom: nativeZ }] }).catch(()=>{});
864
- }
865
- }
866
-
867
- // CSS scale for beyond-native and desktop
868
- const scale = z;
869
- videoEl.style.transform = `scale(${Math.min(scale, 12)})`;
870
-
871
- // Update display
872
- updateZoomUI(z);
873
- }
874
-
875
- function updateZoomUI(z) {
876
- // Display
877
- const disp = document.getElementById('zoom-display');
878
- if (z < 10) {
879
- disp.innerHTML = z.toFixed(1).replace('.0','') + '<span>×</span>';
880
- } else {
881
- disp.innerHTML = Math.round(z) + '<span>×</span>';
882
- }
883
-
884
- // AI badge for high zoom
885
- const badge = document.getElementById('zoom-ai-badge');
886
- if (z >= 10) {
887
- badge.classList.add('show');
888
- } else {
889
- badge.classList.remove('show');
890
- }
891
-
892
- // Slider position
893
- const pct = zoomToPct(z);
894
- document.getElementById('zoom-fill').style.width = pct + '%';
895
- document.getElementById('zoom-thumb').style.left = pct + '%';
896
-
897
- // Presets highlight
898
- document.querySelectorAll('.zoom-preset').forEach(p => {
899
- const pv = parseFloat(p.dataset.zoom);
900
- p.classList.toggle('active', Math.abs(pv - z) < 0.05 * pv);
901
- });
902
-
903
- // HUD visibility
904
- const hud = document.getElementById('zoom-hud');
905
- if (z === 1) {
906
- hud.classList.remove('show');
907
- clearTimeout(window._zoomHudTimer);
908
- } else {
909
- hud.classList.add('show');
910
- clearTimeout(window._zoomHudTimer);
911
- window._zoomHudTimer = setTimeout(() => hud.classList.remove('show'), 2000);
912
- }
913
- }
914
-
915
- // Logarithmic scale for better zoom feel
916
- function zoomToPct(z) {
917
- const logMin = Math.log(0.5);
918
- const logMax = Math.log(state.maxZoom);
919
- return ((Math.log(z) - logMin) / (logMax - logMin)) * 100;
920
- }
921
- function pctToZoom(pct) {
922
- const logMin = Math.log(0.5);
923
- const logMax = Math.log(state.maxZoom);
924
- return Math.exp(logMin + (pct / 100) * (logMax - logMin));
925
- }
926
-
927
- /* ──────────────────────────────────────
928
- ZOOM SLIDER
929
- ─────────────────────────────────────── */
930
- function initZoomSlider() {
931
- const track = document.getElementById('zoom-track');
932
- const thumb = document.getElementById('zoom-thumb');
933
- let dragging = false;
934
-
935
- function getZoomFromEvent(e) {
936
- const rect = track.getBoundingClientRect();
937
- const clientX = e.touches ? e.touches[0].clientX : e.clientX;
938
- const pct = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100));
939
- return pctToZoom(pct);
940
- }
941
-
942
- function startDrag(e) {
943
- dragging = true;
944
- e.preventDefault();
945
- applyZoom(getZoomFromEvent(e));
946
- }
947
- function onDrag(e) {
948
- if (!dragging) return;
949
- e.preventDefault();
950
- applyZoom(getZoomFromEvent(e));
951
- }
952
- function endDrag() { dragging = false; }
953
-
954
- track.addEventListener('mousedown', startDrag);
955
- track.addEventListener('touchstart', startDrag, { passive: false });
956
- document.addEventListener('mousemove', onDrag);
957
- document.addEventListener('touchmove', onDrag, { passive: false });
958
- document.addEventListener('mouseup', endDrag);
959
- document.addEventListener('touchend', endDrag);
960
-
961
- // Init fill
962
- updateZoomUI(1);
963
- }
964
-
965
- /* ──────────────────────────────────────
966
- PINCH TO ZOOM
967
- ─────────────────────────────────────── */
968
- function initPinchZoom() {
969
- const vf = document.getElementById('viewfinder');
970
-
971
- vf.addEventListener('touchstart', (e) => {
972
- if (e.touches.length === 2) {
973
- state.pinching = true;
974
- state.pinchStartDist = getPinchDist(e);
975
- state.pinchStartZoom = state.zoom;
976
- }
977
- }, { passive: true });
978
-
979
- vf.addEventListener('touchmove', (e) => {
980
- if (e.touches.length === 2 && state.pinching) {
981
- const dist = getPinchDist(e);
982
- const scale = dist / state.pinchStartDist;
983
- applyZoom(state.pinchStartZoom * scale);
984
- }
985
- }, { passive: true });
986
-
987
- vf.addEventListener('touchend', () => {
988
- state.pinching = false;
989
- });
990
- }
991
-
992
- function getPinchDist(e) {
993
- const dx = e.touches[0].clientX - e.touches[1].clientX;
994
- const dy = e.touches[0].clientY - e.touches[1].clientY;
995
- return Math.sqrt(dx*dx + dy*dy);
996
- }
997
-
998
- /* ──────────────────────────────────────
999
- ZOOM PRESETS
1000
- ─────────────────────────────────────── */
1001
- function setZoomPreset(el, z) {
1002
- applyZoom(z);
1003
- animatePreset(el);
1004
- }
1005
- function animatePreset(el) {
1006
- el.style.transform = 'scale(0.85)';
1007
- setTimeout(() => el.style.transform = '', 150);
1008
- }
1009
-
1010
- /* ──────────────────────────────────────
1011
- TAP TO FOCUS
1012
- ─────────────────────────────────────── */
1013
- function initFocusTap() {
1014
- const vf = document.getElementById('viewfinder');
1015
- vf.addEventListener('click', (e) => {
1016
- if (state.pinching) return;
1017
- if (e.target.closest('#bottom-controls, #top-bar, #settings-panel, #zoom-bar')) return;
1018
- showFocusRing(e.clientX, e.clientY);
1019
- });
1020
- }
1021
-
1022
- function showFocusRing(x, y) {
1023
- const ring = document.getElementById('focus-ring');
1024
- ring.style.left = x + 'px';
1025
- ring.style.top = y + 'px';
1026
- ring.classList.remove('active', 'locking');
1027
- requestAnimationFrame(() => {
1028
- ring.classList.add('active');
1029
- setTimeout(() => ring.classList.add('locking'), 700);
1030
- setTimeout(() => ring.classList.remove('active','locking'), 1100);
1031
- });
1032
-
1033
- // Show exposure control near tap
1034
- const expDrag = document.getElementById('exposure-drag');
1035
- expDrag.classList.add('show');
1036
- clearTimeout(window._expTimer);
1037
- window._expTimer = setTimeout(() => expDrag.classList.remove('show'), 3500);
1038
- }
1039
-
1040
- /* ──────────────────────────────────────
1041
- SHUTTER (Photo / Video)
1042
- ─────────────────────────────────────── */
1043
- function handleShutter() {
1044
- if (state.mode === 'VIDEO' || state.mode === 'SLOW-MO') {
1045
- toggleRecord();
1046
- } else {
1047
- capturePhoto();
1048
- }
1049
- }
1050
-
1051
- function capturePhoto() {
1052
- const video = document.getElementById('video-main');
1053
- const canvas = document.getElementById('capture-canvas');
1054
- const ctx = canvas.getContext('2d');
1055
-
1056
- canvas.width = video.videoWidth || 1280;
1057
- canvas.height = video.videoHeight || 720;
1058
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
1059
-
1060
- // Apply color profile filter
1061
- if (state.colorProfile === 'vivid') {
1062
- ctx.filter = 'saturate(1.3) contrast(1.05)';
1063
- ctx.drawImage(canvas, 0, 0);
1064
- } else if (state.colorProfile === 'flat') {
1065
- ctx.filter = 'saturate(0.7) contrast(0.9)';
1066
- ctx.drawImage(canvas, 0, 0);
1067
- }
1068
-
1069
- const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
1070
-
1071
- // Save to photos array
1072
- state.photos.unshift({
1073
- dataUrl,
1074
- timestamp: Date.now(),
1075
- zoom: state.zoom,
1076
- mode: state.mode,
1077
- });
1078
-
1079
- // Update gallery thumb
1080
- const img = document.getElementById('last-photo');
1081
- img.src = dataUrl;
1082
- img.style.display = 'block';
1083
- document.querySelector('.empty-icon').style.display = 'none';
1084
- const cnt = document.getElementById('gallery-count');
1085
- cnt.textContent = state.photos.length;
1086
- cnt.style.display = 'flex';
1087
-
1088
- // Flash animation
1089
- const fl = document.getElementById('flash-overlay');
1090
- const cf = document.getElementById('capture-feedback');
1091
- fl.classList.add('flash');
1092
- cf.classList.add('flash');
1093
- setTimeout(() => { fl.classList.remove('flash'); cf.classList.remove('flash'); }, 120);
1094
-
1095
- // Haptic
1096
- if (navigator.vibrate) navigator.vibrate(20);
1097
- }
1098
-
1099
- /* ──────────────────────────────────────
1100
- VIDEO RECORDING
1101
- ─────────────────────────────────────── */
1102
- function toggleRecord() {
1103
- if (!state.recording) {
1104
- startRecording();
1105
- } else {
1106
- stopRecording();
1107
- }
1108
- }
1109
-
1110
- function startRecording() {
1111
- if (!state.stream) return;
1112
- try {
1113
- const rec = new MediaRecorder(state.stream, { mimeType: 'video/webm;codecs=vp9' });
1114
- const chunks = [];
1115
- rec.ondataavailable = (e) => chunks.push(e.data);
1116
- rec.onstop = () => {
1117
- const blob = new Blob(chunks, { type: 'video/webm' });
1118
- const url = URL.createObjectURL(blob);
1119
- state.photos.unshift({ dataUrl: null, videoUrl: url, isVideo: true, timestamp: Date.now(), duration: state.recSeconds });
1120
- updateGalleryCount();
1121
- if (navigator.vibrate) navigator.vibrate([30, 20, 30]);
1122
- };
1123
- rec.start();
1124
- state.recorder = rec;
1125
- state.recording = true;
1126
- state.recSeconds = 0;
1127
- document.getElementById('shutter-btn').classList.add('recording');
1128
- document.getElementById('rec-timer').classList.add('show');
1129
- state.recInterval = setInterval(tickRec, 1000);
1130
- if (navigator.vibrate) navigator.vibrate(40);
1131
- } catch(e) {
1132
- // Fallback for browsers without webm/vp9
1133
- try {
1134
- const rec = new MediaRecorder(state.stream);
1135
- const chunks = [];
1136
- rec.ondataavailable = (e) => chunks.push(e.data);
1137
- rec.onstop = () => {
1138
- const blob = new Blob(chunks);
1139
- const url = URL.createObjectURL(blob);
1140
- state.photos.unshift({ dataUrl: null, videoUrl: url, isVideo: true, timestamp: Date.now(), duration: state.recSeconds });
1141
- updateGalleryCount();
1142
- };
1143
- rec.start();
1144
- state.recorder = rec;
1145
- state.recording = true;
1146
- state.recSeconds = 0;
1147
- document.getElementById('shutter-btn').classList.add('recording');
1148
- document.getElementById('rec-timer').classList.add('show');
1149
- state.recInterval = setInterval(tickRec, 1000);
1150
- } catch(e2) {
1151
- console.warn('Recording not supported:', e2);
1152
- }
1153
- }
1154
- }
1155
-
1156
- function stopRecording() {
1157
- if (state.recorder) {
1158
- state.recorder.stop();
1159
- state.recorder = null;
1160
- }
1161
- state.recording = false;
1162
- document.getElementById('shutter-btn').classList.remove('recording');
1163
- document.getElementById('rec-timer').classList.remove('show');
1164
- clearInterval(state.recInterval);
1165
- }
1166
-
1167
- function tickRec() {
1168
- state.recSeconds++;
1169
- const m = String(Math.floor(state.recSeconds / 60)).padStart(2, '0');
1170
- const s = String(state.recSeconds % 60).padStart(2, '0');
1171
- document.getElementById('rec-time-txt').textContent = m + ':' + s;
1172
- }
1173
-
1174
- function updateGalleryCount() {
1175
- const cnt = document.getElementById('gallery-count');
1176
- cnt.textContent = state.photos.length;
1177
- cnt.style.display = 'flex';
1178
- }
1179
-
1180
- /* ──────────────────────────────────────
1181
- CAMERA FLIP
1182
- ─────────────────────────────────────── */
1183
- function flipCamera() {
1184
- state.facingMode = state.facingMode === 'environment' ? 'user' : 'environment';
1185
- if (navigator.vibrate) navigator.vibrate(15);
1186
- startCamera();
1187
-
1188
- // Flip animation
1189
- const vid = document.getElementById('video-main');
1190
- vid.style.transition = 'transform 0.3s ease';
1191
- vid.style.transform = 'scaleX(0)';
1192
- setTimeout(() => {
1193
- vid.style.scaleX = '';
1194
- vid.style.transform = `scale(${state.zoom})`;
1195
- vid.style.transition = '';
1196
- }, 300);
1197
- }
1198
-
1199
- /* ──────────────────────────────────────
1200
- MODE SWITCH
1201
- ─────────────────────────────────────── */
1202
- function setMode(el, mode) {
1203
- document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
1204
- el.classList.add('active');
1205
- state.mode = mode;
1206
-
1207
- // Stop recording if switching away from video
1208
- if (mode !== 'VIDEO' && mode !== 'SLOW-MO' && state.recording) {
1209
- stopRecording();
1210
- }
1211
-
1212
- // Portrait mode shows pip
1213
- if (mode === 'PORTRAIT' && state.dualMode) {
1214
- document.getElementById('video-pip').classList.add('show');
1215
- } else {
1216
- document.getElementById('video-pip').classList.remove('show');
1217
- }
1218
-
1219
- // Night mode highlights
1220
- if (mode === 'NIGHT') {
1221
- setNightOn(document.getElementById('night-on'));
1222
- }
1223
-
1224
- if (navigator.vibrate) navigator.vibrate(10);
1225
- }
1226
-
1227
- /* ──────────��───────────────────────────
1228
- FLASH
1229
- ─────────────────────────────────────── */
1230
- const flashModes = ['auto', 'on', 'off', 'torch'];
1231
- const flashIcons = { auto: '⚡', on: '🔦', off: '🚫', torch: '🔆' };
1232
-
1233
- function cycleFlash() {
1234
- const idx = flashModes.indexOf(state.flashMode);
1235
- state.flashMode = flashModes[(idx + 1) % flashModes.length];
1236
- document.getElementById('flash-btn').textContent = flashIcons[state.flashMode];
1237
-
1238
- // Torch: use torch constraint
1239
- if (state.stream) {
1240
- const track = state.stream.getVideoTracks()[0];
1241
- if (track && track.applyConstraints) {
1242
- if (state.flashMode === 'torch') {
1243
- track.applyConstraints({ advanced: [{ torch: true }] }).catch(()=>{});
1244
- } else {
1245
- track.applyConstraints({ advanced: [{ torch: false }] }).catch(()=>{});
1246
- }
1247
- }
1248
- }
1249
- }
1250
-
1251
- /* ──────────────────────────────────────
1252
- GRID
1253
- ─────────────────────────────────────── */
1254
- function toggleGrid() {
1255
- state.gridOn = !state.gridOn;
1256
- document.getElementById('grid-overlay').classList.toggle('show', state.gridOn);
1257
- document.getElementById('grid-btn').classList.toggle('active', state.gridOn);
1258
- }
1259
-
1260
- /* ──────────────────────────────────────
1261
- SETTINGS PANEL
1262
- ─────────────────────────────────────── */
1263
- function toggleSettings() {
1264
- state.settingsOpen = !state.settingsOpen;
1265
- document.getElementById('settings-panel').classList.toggle('open', state.settingsOpen);
1266
- document.getElementById('settings-btn').classList.toggle('active', state.settingsOpen);
1267
- }
1268
-
1269
- function setRes(el, res) {
1270
- el.closest('.settings-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
1271
- el.classList.add('active');
1272
- }
1273
-
1274
- function setColor(el, profile) {
1275
- el.closest('.settings-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
1276
- el.classList.add('active');
1277
- state.colorProfile = profile;
1278
- }
1279
-
1280
- function setAspect(el, ratio) {
1281
- el.closest('.settings-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
1282
- el.classList.add('active');
1283
- }
1284
-
1285
- function toggleNight(el) {
1286
- el.closest('.settings-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
1287
- el.classList.add('active');
1288
- state.nightMode = 'auto';
1289
- }
1290
- function setNightOn(el) {
1291
- el.closest('.settings-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
1292
- el.classList.add('active');
1293
- state.nightMode = 'on';
1294
- }
1295
- function setNightOff(el) {
1296
- el.closest('.settings-chips').querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
1297
- el.classList.add('active');
1298
- state.nightMode = 'off';
1299
- }
1300
-
1301
- function toggleDual(el) {
1302
- state.dualMode = !state.dualMode;
1303
- el.textContent = state.dualMode ? 'Enabled ✓' : 'Enable';
1304
- el.classList.toggle('active', state.dualMode);
1305
-
1306
- if (state.dualMode) {
1307
- // Try to show pip from front camera while main is back
1308
- navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false })
1309
- .then(s => {
1310
- state.secondaryStream = s;
1311
- const pipVid = document.getElementById('video-secondary');
1312
- pipVid.srcObject = s;
1313
- document.getElementById('video-pip').classList.add('show');
1314
- }).catch(() => {
1315
- state.dualMode = false;
1316
- el.textContent = 'Not Available';
1317
- el.classList.remove('active');
1318
- });
1319
- } else {
1320
- if (state.secondaryStream) {
1321
- state.secondaryStream.getTracks().forEach(t => t.stop());
1322
- state.secondaryStream = null;
1323
- }
1324
- document.getElementById('video-pip').classList.remove('show');
1325
- }
1326
- }
1327
-
1328
- /* ──────────────────────────────────────
1329
- GALLERY PLACEHOLDER
1330
- ─────────────────────────────────────── */
1331
- function openGallery() {
1332
- if (state.photos.length === 0) return;
1333
- // Gallery will be implemented in next phase
1334
- if (navigator.vibrate) navigator.vibrate(10);
1335
- // Simple preview for now: flash the last photo
1336
- const lastPhoto = state.photos[0];
1337
- if (lastPhoto && lastPhoto.dataUrl) {
1338
- const prev = document.createElement('div');
1339
- prev.style.cssText = `
1340
- position:fixed; inset:0; background:rgba(0,0,0,0.95); z-index:200;
1341
- display:flex; align-items:center; justify-content:center; flex-direction:column; gap:16px;
1342
- `;
1343
- const img = document.createElement('img');
1344
- img.src = lastPhoto.dataUrl;
1345
- img.style.cssText = 'max-width:90%; max-height:75vh; border-radius:12px; object-fit:contain;';
1346
-
1347
- const info = document.createElement('div');
1348
- info.style.cssText = 'color:rgba(255,255,255,0.5); font-size:12px; letter-spacing:1px;';
1349
- info.textContent = `📷 ${state.photos.length} photo${state.photos.length > 1 ? 's' : ''} — Zoom ${lastPhoto.zoom.toFixed(1)}×`;
1350
-
1351
- const closeBtn = document.createElement('div');
1352
- closeBtn.style.cssText = `
1353
- padding:12px 28px; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2);
1354
- border-radius:24px; color:#fff; font-size:13px; font-weight:600; cursor:pointer; letter-spacing:1px;
1355
- `;
1356
- closeBtn.textContent = '✕ Close';
1357
- closeBtn.onclick = () => document.body.removeChild(prev);
1358
-
1359
- prev.appendChild(img);
1360
- prev.appendChild(info);
1361
- prev.appendChild(closeBtn);
1362
- prev.onclick = (e) => { if (e.target === prev) document.body.removeChild(prev); };
1363
- document.body.appendChild(prev);
1364
- }
1365
- }
1366
-
1367
- /* ──────────────────────────────────────
1368
- ZOOM PRESET DATA ATTRS
1369
- ─────────────────────────────────────── */
1370
- document.querySelectorAll('.zoom-preset').forEach(p => {
1371
- const z = parseFloat(p.textContent);
1372
- p.dataset.zoom = z;
1373
- });
1374
-
1375
- /* ──────────────────────────────────────
1376
- CLOSE SETTINGS ON VIEWFINDER TAP
1377
- ─────────────────────────────────────── */
1378
- document.getElementById('viewfinder').addEventListener('click', (e) => {
1379
- if (state.settingsOpen && !e.target.closest('#settings-panel, #settings-btn')) {
1380
- toggleSettings();
1381
- }
1382
- });
1383
- </script>
1384
- </body>
1385
- </html>