Repository navigation
Expand file tree
/
Copy pathscript.js
More file actions
2317 lines (2072 loc) · 75.6 KB
/
Copy pathscript.js
File metadata and controls
2317 lines (2072 loc) · 75.6 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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Lookout OS — script.js
* Handcrafted Vanilla OS Engine: State, Window Manager, Dock, Control Center & App Suite.
*/
// -----------------------------------------------------------------------------
// DOM Helper Utilities
// -----------------------------------------------------------------------------
const $ = (selector, parent = document) => parent.querySelector(selector);
const $$ = (selector, parent = document) => Array.from(parent.querySelectorAll(selector));
// -----------------------------------------------------------------------------
// 1. Shared OS State & Persistence
// -----------------------------------------------------------------------------
const lookoutState = {
storageKeys: {
theme: 'lookout-theme',
night: 'lookout-night',
brightness: 'lookout-brightness',
volume: 'lookout-volume',
wallpaper: 'lookout-wallpaper',
customWallpapers: 'lookout-custom-walls'
},
theme: '#F2B65A',
night: false,
brightness: 100,
volume: 70,
wallpaper: './lookout.png',
customWallpapers: [],
isLocked: true,
load() {
try {
const storedTheme = localStorage.getItem(this.storageKeys.theme);
if (storedTheme) this.theme = storedTheme;
const storedNight = localStorage.getItem(this.storageKeys.night);
if (storedNight !== null) this.night = storedNight === 'true';
const storedBrightness = localStorage.getItem(this.storageKeys.brightness);
if (storedBrightness !== null) this.brightness = Math.max(0, Math.min(100, Number(storedBrightness)));
const storedVolume = localStorage.getItem(this.storageKeys.volume);
if (storedVolume !== null) this.volume = Math.max(0, Math.min(100, Number(storedVolume)));
const storedWall = localStorage.getItem(this.storageKeys.wallpaper);
if (storedWall) this.wallpaper = storedWall;
} catch {
// Graceful fallback when localStorage is blocked or unavailable
}
},
save() {
try {
localStorage.setItem(this.storageKeys.theme, this.theme);
localStorage.setItem(this.storageKeys.night, String(this.night));
localStorage.setItem(this.storageKeys.brightness, String(this.brightness));
localStorage.setItem(this.storageKeys.volume, String(this.volume));
localStorage.setItem(this.storageKeys.wallpaper, this.wallpaper);
} catch {
// Storage quota or permissions error
}
},
apply() {
document.documentElement.style.setProperty('--theme-color', this.theme);
// Apply Night Mode class
document.body.classList.toggle('night', this.night);
// Set background wallpaper
document.body.style.backgroundImage = `url('${this.wallpaper}')`;
// Viewport Dimming Overlay (100% -> alpha 0, 0% -> alpha 1)
const overlay = $('#brightnessOverlay');
if (overlay) {
const alpha = (1 - (this.brightness / 100)).toFixed(3);
overlay.style.backgroundColor = `rgba(0, 0, 0, ${alpha})`;
}
// Sync Control Center UI Controls
const brightSlider = $('#cc-brightness');
if (brightSlider) brightSlider.value = String(this.brightness);
const volumeSlider = $('#cc-volume');
if (volumeSlider) volumeSlider.value = String(this.volume);
const nightBtn = $('#cc-nightmode');
if (nightBtn) {
nightBtn.textContent = this.night ? 'On' : 'Off';
nightBtn.classList.toggle('on', this.night);
}
const nightInd = $('#nightIndicator');
if (nightInd) {
nightInd.style.display = this.night ? 'inline-block' : 'none';
}
// Highlight active theme swatch
$$('.cc-swatch').forEach(sw => {
const match = sw.getAttribute('data-color')?.toLowerCase() === this.theme.toLowerCase();
sw.classList.toggle('active', match);
});
}
};
// Initial state boot
lookoutState.load();
lookoutState.apply();
// -----------------------------------------------------------------------------
// 2. Window Manager System
// -----------------------------------------------------------------------------
var apps = window.apps = {};
var highestZIndex = window.biggestIndex = 100;
const openWindows = new Set();
function bringToFront(win) {
if (!win) return;
highestZIndex += 1;
win.style.zIndex = String(highestZIndex);
window.biggestIndex = highestZIndex;
}
function openWindow(win) {
if (!win) return;
win.style.display = 'flex';
bringToFront(win);
const id = win.id;
openWindows.add(id);
const dockIcon = $(`#${id}Dock`);
if (dockIcon) dockIcon.classList.add('running');
}
function closeWindow(win) {
if (!win) return;
win.style.display = 'none';
const id = win.id;
openWindows.delete(id);
const dockIcon = $(`#${id}Dock`);
if (dockIcon) dockIcon.classList.remove('running');
}
function toggleWindow(win) {
if (!win) return;
if (win.style.display === 'none' || getComputedStyle(win).display === 'none') {
openWindow(win);
} else if (Number(win.style.zIndex) < highestZIndex) {
bringToFront(win);
} else {
closeWindow(win);
}
}
function dragElement(element) {
let pointerX = 0;
let pointerY = 0;
let shiftX = 0;
let shiftY = 0;
const handle = document.getElementById(element.id + 'header') || element;
handle.addEventListener('mousedown', startDragging);
handle.addEventListener('touchstart', startDragging, { passive: false });
function startDragging(e) {
if (e.target.closest && e.target.closest('.closebutton')) return;
e.preventDefault();
bringToFront(element);
const point = e.touches ? e.touches[0] : e;
pointerX = point.clientX;
pointerY = point.clientY;
element.classList.add('dragging');
document.addEventListener('mousemove', dragMove);
document.addEventListener('mouseup', stopDragging);
document.addEventListener('touchmove', dragMove, { passive: false });
document.addEventListener('touchend', stopDragging);
}
function dragMove(e) {
e.preventDefault();
const point = e.touches ? e.touches[0] : e;
shiftX = pointerX - point.clientX;
shiftY = pointerY - point.clientY;
pointerX = point.clientX;
pointerY = point.clientY;
const nextTop = element.offsetTop - shiftY;
const nextLeft = element.offsetLeft - shiftX;
const topBarEl = document.querySelector('#top');
const minTop = topBarEl && topBarEl.offsetHeight ? topBarEl.offsetHeight : 34;
const maxTop = window.innerHeight - 60;
const maxLeft = window.innerWidth - 80;
const minLeft = 80 - element.offsetWidth;
element.style.top = Math.min(Math.max(nextTop, minTop), maxTop) + 'px';
element.style.left = Math.min(Math.max(nextLeft, minLeft), maxLeft) + 'px';
}
function stopDragging() {
element.classList.remove('dragging');
document.removeEventListener('mousemove', dragMove);
document.removeEventListener('mouseup', stopDragging);
document.removeEventListener('touchmove', dragMove);
document.removeEventListener('touchend', stopDragging);
}
}
function initializeWindow(id) {
const win = $(`#${id}`);
if (!win) return null;
apps[id] = win;
const closeBtn = $(`#${id}close`);
const icon = $(`#${id}Icon`);
const opener = $(`#${id}open`);
const dockIcon = $(`#${id}Dock`);
dragElement(win);
win.addEventListener('mousedown', () => bringToFront(win));
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
closeWindow(win);
});
}
if (icon) {
icon.addEventListener('click', (e) => {
e.stopPropagation();
icon.classList.toggle('selected');
});
icon.addEventListener('dblclick', (e) => {
e.stopPropagation();
openWindow(win);
});
}
if (opener) {
opener.addEventListener('click', (e) => {
e.stopPropagation();
openWindow(win);
});
}
if (dockIcon) {
dockIcon.addEventListener('click', (e) => {
e.stopPropagation();
toggleWindow(win);
});
}
if (win.style.display !== 'none' && getComputedStyle(win).display !== 'none') {
openWindows.add(id);
if (dockIcon) dockIcon.classList.add('running');
}
return win;
}
// -----------------------------------------------------------------------------
// 3. System Clock & Pill Dock
// -----------------------------------------------------------------------------
function updateClock() {
const now = new Date();
const timeStr = now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
const dateShort = now.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
const dateLong = now.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' });
// Update Top Bar Clock
const topClock = $('#clock');
if (topClock) {
topClock.innerHTML = `${timeStr} · ${dateShort}`;
}
// Update Dock clock
const dockClock = $('.dock-clock');
if (dockClock) {
dockClock.innerHTML = `${timeStr} · ${dateShort}`;
}
// Update Lock Screen clock
const lockClock = $('#lockClock');
if (lockClock) lockClock.textContent = timeStr;
const lockDate = $('#lockDate');
if (lockDate) {
lockDate.textContent = now.toLocaleDateString('en-GB', { weekday: 'long', month: 'long', day: 'numeric' });
}
// Update Control Center date
const ccDate = $('#cc-date');
if (ccDate) ccDate.textContent = dateLong;
}
updateClock();
setInterval(updateClock, 1000);
// Desktop icon selection & deselection
document.addEventListener('click', (e) => {
if (!e.target.closest || !e.target.closest('.appicon')) {
$$('#desktopApps .appicon').forEach(i => i.classList.remove('selected'));
}
});
// Dock Icon Click Routing
$$('.dock-app').forEach(app => {
app.addEventListener('click', (e) => {
e.stopPropagation();
const appName = app.getAttribute('data-app');
if (appName === 'lock') {
const lockScreen = $('#lockScreen');
if (lockScreen) {
lockScreen.classList.remove('unlocked');
lookoutState.isLocked = true;
}
}
});
});
// App Drawer / Launchpad
const appDrawer = $('#appDrawer');
const launcherToggle = $('#launcherToggle');
const appDrawerClose = $('#appDrawerClose');
const appDrawerBg = $('#appDrawerBg');
const registeredApps = [
{ id: 'welcome', name: 'Welcome', icon: './lookout.png' },
{ id: 'crate', name: 'Crate', icon: './crate.svg' },
{ id: 'terminal', name: 'Terminal', icon: './terminal.svg' },
{ id: 'calculator', name: 'Calculator', icon: './icons/calculator.svg' },
{ id: 'projects', name: 'Projects', icon: './icons/projects.svg' },
{ id: 'weather', name: 'Weather', icon: './icons/weather.svg' },
{ id: 'game', name: '2048', icon: './icons/game.svg' },
{ id: 'music', name: 'Music', icon: './icons/music.svg' },
{ id: 'notes', name: 'Notes', icon: './icons/notes.svg' }
];
function renderAppDrawer() {
const grid = $('#appDrawerGrid');
if (!grid) return;
grid.innerHTML = registeredApps.map(app => `
<div class="appdrawer-item" data-app="${app.id}">
<img src="${app.icon}" alt="${app.name}">
<span>${app.name}</span>
</div>
`).join('');
$$('.appdrawer-item', grid).forEach(item => {
item.addEventListener('click', () => {
const id = item.getAttribute('data-app');
const win = $(`#${id}`);
if (win) {
win.style.display = 'flex';
highestZIndex += 1;
win.style.zIndex = String(highestZIndex);
openWindows.add(id);
const dockIcon = $(`#${id}Dock`);
if (dockIcon) dockIcon.classList.add('running');
}
closeAppDrawer();
});
});
}
function openAppDrawer() {
if (appDrawer) {
renderAppDrawer();
appDrawer.style.display = 'flex';
}
}
function closeAppDrawer() {
if (appDrawer) appDrawer.style.display = 'none';
}
if (launcherToggle) launcherToggle.addEventListener('click', openAppDrawer);
if (appDrawerClose) appDrawerClose.addEventListener('click', closeAppDrawer);
if (appDrawerBg) appDrawerBg.addEventListener('click', closeAppDrawer);
// -----------------------------------------------------------------------------
// 4. Control Center
// -----------------------------------------------------------------------------
const controlToggle = $('#controlToggle');
const controlCenter = $('#controlCenter');
function toggleControlCenter(e) {
if (e) e.stopPropagation();
if (!controlCenter) return;
const isClosed = controlCenter.style.display === 'none' || !controlCenter.style.display;
controlCenter.style.display = isClosed ? 'block' : 'none';
}
if (controlToggle) {
controlToggle.addEventListener('click', toggleControlCenter);
}
// Close Control Center on outside click
document.addEventListener('click', (e) => {
if (!controlCenter || controlCenter.style.display === 'none') return;
if (!controlCenter.contains(e.target) && !controlToggle?.contains(e.target)) {
controlCenter.style.display = 'none';
}
});
// Control Center Controls: Sliders & Toggles
const brightnessSlider = $('#cc-brightness');
if (brightnessSlider) {
brightnessSlider.addEventListener('input', (e) => {
lookoutState.brightness = Number(e.target.value);
lookoutState.apply();
lookoutState.save();
});
}
const volumeSlider = $('#cc-volume');
if (volumeSlider) {
volumeSlider.addEventListener('input', (e) => {
lookoutState.volume = Number(e.target.value);
lookoutState.apply();
lookoutState.save();
});
}
const nightBtn = $('#cc-nightmode');
if (nightBtn) {
nightBtn.addEventListener('click', () => {
lookoutState.night = !lookoutState.night;
lookoutState.apply();
lookoutState.save();
});
}
$$('.cc-swatch').forEach(swatch => {
swatch.addEventListener('click', () => {
const color = swatch.getAttribute('data-color');
if (color) {
lookoutState.theme = color;
lookoutState.apply();
lookoutState.save();
}
});
});
// Wallpaper Picker
const wallpaperPresets = ['./lookout.png'];
function renderWallpapers() {
const container = $('#cc-wallpapers');
if (!container) return;
container.innerHTML = wallpaperPresets.map(url => `
<div class="cc-wall-thumb ${lookoutState.wallpaper === url ? 'active' : ''}"
style="background-image: url('${url}')" data-url="${url}"></div>
`).join('');
$$('.cc-wall-thumb', container).forEach(thumb => {
thumb.addEventListener('click', () => {
const url = thumb.getAttribute('data-url');
if (url) {
lookoutState.wallpaper = url;
lookoutState.apply();
lookoutState.save();
renderWallpapers();
}
});
});
}
renderWallpapers();
// -----------------------------------------------------------------------------
// 5. Lock Screen Gestures & Unlocking
// -----------------------------------------------------------------------------
const lockScreen = $('#lockScreen');
function unlockOS() {
if (!lockScreen) return;
lockScreen.classList.add('unlocked');
lookoutState.isLocked = false;
}
if (lockScreen) {
$('#unlockText')?.addEventListener('click', unlockOS);
$('#lockClock')?.addEventListener('click', unlockOS);
$('#lockDate')?.addEventListener('click', unlockOS);
// Swipe up gesture detection
let touchStartY = 0;
lockScreen.addEventListener('touchstart', (e) => {
touchStartY = e.touches[0].clientY;
}, { passive: true });
lockScreen.addEventListener('touchend', (e) => {
const touchEndY = e.changedTouches[0].clientY;
if (touchStartY - touchEndY > 80) {
unlockOS();
}
}, { passive: true });
// Key press fallback (Space or Enter to unlock)
window.addEventListener('keydown', (e) => {
if (lookoutState.isLocked && (e.key === ' ' || e.key === 'Enter')) {
unlockOS();
}
});
}
// -----------------------------------------------------------------------------
// 6. Core Apps: Welcome, Crate & Terminal
// -----------------------------------------------------------------------------
// Window Initializations
const welcomeWin = initializeWindow('welcome');
const crateWin = initializeWindow('crate');
const terminalWin = initializeWindow('terminal');
const calculatorWin = initializeWindow('calculator');
const projectsWin = initializeWindow('projects');
const weatherWin = initializeWindow('weather');
const gameWin = initializeWindow('game');
const musicWin = initializeWindow('music');
const notesWin = initializeWindow('notes');
// -----------------------------------------------------------------------------
// LookoutAudio: Procedural Sound Engine (Zero external dependencies)
// -----------------------------------------------------------------------------
const LookoutAudio = {
ctx: null,
enabled: true,
synthInterval: null,
analyser: null,
init() {
if (!this.ctx && (window.AudioContext || window.webkitAudioContext)) {
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioCtx();
this.analyser = this.ctx.createAnalyser();
this.analyser.fftSize = 64;
} catch {
// AudioContext restricted or unsupported
}
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume().catch(() => {});
}
},
playNeedleDrop() {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
try {
const now = this.ctx.currentTime;
// Low-frequency vinyl thud
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(85, now);
osc.frequency.exponentialRampToValueAtTime(30, now + 0.12);
gain.gain.setValueAtTime(0.18, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.15);
// Analog vinyl dust crackle
const bufferSize = Math.floor(this.ctx.sampleRate * 0.18);
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.35));
}
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = 1800;
const noiseGain = this.ctx.createGain();
noiseGain.gain.setValueAtTime(0.05, now + 0.04);
noiseGain.gain.linearRampToValueAtTime(0.001, now + 0.2);
noise.connect(filter);
filter.connect(noiseGain);
noiseGain.connect(this.ctx.destination);
noise.start(now + 0.04);
} catch {}
},
playKeyClick() {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
try {
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(650, now);
osc.frequency.exponentialRampToValueAtTime(180, now + 0.035);
gain.gain.setValueAtTime(0.06, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.035);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.035);
} catch {}
},
playTileMerge(val = 2) {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
try {
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
const scale = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25, 783.99, 880.00, 1046.50];
const log2 = Math.min(scale.length - 1, Math.max(0, Math.round(Math.log2(val || 2)) - 1));
const freq = scale[log2];
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now);
osc.frequency.exponentialRampToValueAtTime(freq * 1.5, now + 0.12);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.18);
} catch {}
},
// Generative Lo-Fi Ambient Chord Synthesizer
startLoFiSynth(onBeat) {
this.init();
if (!this.ctx) return;
this.stopLoFiSynth();
const chords = [
[261.63, 329.63, 392.00, 493.88], // Cmaj7
[220.00, 261.63, 329.63, 392.00], // Am7
[174.61, 220.00, 261.63, 329.63], // Fmaj7
[196.00, 246.94, 293.66, 349.23] // G7
];
let chordIdx = 0;
const playChord = () => {
if (!this.ctx) return;
try {
const now = this.ctx.currentTime;
const currentChord = chords[chordIdx];
chordIdx = (chordIdx + 1) % chords.length;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(700, now);
const masterGain = this.ctx.createGain();
const vol = Math.max(0.01, (lookoutState.volume / 100) * 0.14);
masterGain.gain.setValueAtTime(0.001, now);
masterGain.gain.linearRampToValueAtTime(vol, now + 0.4);
masterGain.gain.exponentialRampToValueAtTime(0.001, now + 2.7);
currentChord.forEach((freq, i) => {
const osc = this.ctx.createOscillator();
osc.type = i % 2 === 0 ? 'sine' : 'triangle';
osc.frequency.setValueAtTime(freq, now + i * 0.06);
osc.connect(filter);
osc.start(now + i * 0.06);
osc.stop(now + 2.9);
});
filter.connect(masterGain);
if (this.analyser) {
masterGain.connect(this.analyser);
}
masterGain.connect(this.ctx.destination);
if (onBeat) onBeat();
} catch {}
};
playChord();
this.synthInterval = setInterval(playChord, 2800);
},
stopLoFiSynth() {
if (this.synthInterval) {
clearInterval(this.synthInterval);
this.synthInterval = null;
}
}
};
// --- Crate App ---
var crateRecords = window.crateRecords = [
{
id: '1',
title: 'Mayonaka no Door / Stay With Me',
artist: 'Miki Matsubara',
year: 1979,
cover: 'covers/01-mayonaka-no-door.jpg',
note: 'Found this on YouTube at 2 AM while fixing a stubborn CSS flexbox bug. That opening bassline rewired my brain and carried me through the night.'
},
{
id: '2',
title: 'Billie Jean',
artist: 'Michael Jackson',
year: 1982,
cover: 'covers/02-billie-jean.jpg',
note: 'My dad had this on cassette tapes during every childhood highway road trip. Still the cleanest, most iconic snare drum punch ever recorded.'
},
{
id: '3',
title: 'Smooth Criminal',
artist: 'Michael Jackson',
year: 1987,
cover: 'covers/03-smooth-criminal.jpg',
note: 'The brass stabs when racing against a deadline are pure rocket fuel. If you need 10x coding momentum, this is the sleeve you pull out.'
},
{
id: '4',
title: "They Don't Care About Us",
artist: 'Michael Jackson',
year: 1995,
cover: 'covers/04-they-dont-care-about-us.jpg',
note: "Olodum's thunderous live Salvador percussion hits like a freight train. When energy is at zero, this track is an instant adrenaline reboot."
},
{
id: '5',
title: 'Magic in the Air',
artist: 'Magic System',
year: 2014,
cover: 'covers/05-magic-in-the-air.jpg',
note: 'Pure summer euphoria and festival horns. Literally scientifically impossible to stay stressed or annoyed while this is playing.'
},
{
id: '6',
title: 'Levitating',
artist: 'Dua Lipa',
year: 2020,
cover: 'covers/06-levitating.jpg',
note: "Modern nu-disco perfection. The bassline bounce is what inspired the snappy physics curves on Lookout's window animations."
},
{
id: '7',
title: 'Paint My Love',
artist: 'Michael Learns to Rock',
year: 1996,
cover: 'covers/07-paint-my-love.jpg',
note: 'Warm 90s acoustic nostalgia. The exact track you want playing when the terminal is quiet and you are making your final git commit of the night.'
}
];
function initCrate() {
const shelf = $('#crateShelf');
const playing = $('#cratePlaying');
if (!shelf || !playing) return;
function renderPlaying(record) {
playing.innerHTML = `
<div class="crate-spin-vinyl"></div>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 700; font-size: 13px; color: #FFF;">${record.title}</div>
<div style="font-size: 12px; color: var(--accent-sky); margin-bottom: 4px;">${record.artist} (${record.year})</div>
<div style="font-size: 11px; color: var(--text-secondary); line-height: 1.4;">${record.note}</div>
</div>
`;
}
shelf.innerHTML = crateRecords.map((rec, i) => `
<div class="crate-record ${i === 0 ? 'active' : ''}" data-index="${i}">
<img class="crate-cover" src="${rec.cover}" alt="${rec.title}">
<div class="crate-title">${rec.title}</div>
<div class="crate-artist">${rec.artist}</div>
</div>
`).join('');
$$('.crate-record', shelf).forEach(el => {
el.addEventListener('click', () => {
LookoutAudio.playNeedleDrop();
$$('.crate-record', shelf).forEach(r => r.classList.remove('active'));
el.classList.add('active');
const idx = Number(el.getAttribute('data-index'));
renderPlaying(crateRecords[idx]);
});
});
renderPlaying(crateRecords[0]);
}
initCrate();
// --- Terminal App ---
var terminalFiles = {
"about.txt":
"Syed Sufiyan Hamza (@Yourfiyan)\n" +
"Builder, designer, and tinkerer.\n\n" +
"I believe the web should feel tactile, personal, and fun again.\n" +
"Lookout OS is my attempt at building an in-browser operating system\n" +
"from scratch using optical Liquid Glass, native Web APIs, and\n" +
"zero external build tools. Just files, physics, and patience.",
"stack.txt":
"Lookout OS Architecture:\n" +
"------------------------\n" +
"• Core: HTML5, CSS3 Custom Properties, Vanilla ES6+ JavaScript\n" +
"• Physics: Custom cubic-bezier spring curves & pointer dragging\n" +
"• Audio: Procedural Web Audio API synth (zero mp3 dependencies)\n" +
"• Storage: Reactive local state synced with localStorage\n" +
"• Dependencies: 0 npm packages, 0 frameworks, 0 bundlers.\n" +
"Run `python -m http.server` and everything just boots.",
"devlog-01-system-bar.md":
"Devlog 1 — The System Bar & The Glass Refraction Wars\n" +
"====================================================\n" +
"Date: October 2024\n" +
"I wanted the top bar to feel like real frosted glass hovering over a dusk horizon.\n" +
"Turns out backdrop-filter is a beast to balance across browsers — especially when\n" +
"nested inside transformed containers with z-index stacking contexts.\n" +
"Ended up crafting an optical displacement filter and multi-layered specular\n" +
"inner rims. When that first window smoothly slid under the blurred bar at 60fps,\n" +
"it was worth every minute.",
"devlog-02-control-center.md":
"Devlog 2 — Building the Control Center\n" +
"======================================\n" +
"Date: November 2024\n" +
"Mac OS and iOS both have control centers, but how does one fit into a web OS?\n" +
"The tricky part was touch gestures: making sliders feel responsive on touchscreens\n" +
"without accidentally triggering window drags or browser pull-to-refresh.\n" +
"Also decided that appearance toggles (Night Mode & Theme Swatches) shouldn't\n" +
"just change one color — they cascade through CSS variables across all apps.",
"devlog-03-integration.md":
"Devlog 3 — Terminal + UI State Unification\n" +
"==========================================\n" +
"Date: December 2024\n" +
"The breakthrough moment: wiring the CLI Terminal directly into the exact same\n" +
"reactive state engine that the Control Center uses.\n" +
"Now if you type `theme rose` or `night on` or `brightness 70`, the UI sliders\n" +
"and backdrop filters update in real time. It makes the terminal feel like a\n" +
"genuine kernel shell, not just a static toy.",
"todo.txt":
"Lookout OS Dev Todo:\n" +
"--------------------\n" +
"[x] Fix window dragging boundaries against top bar\n" +
"[x] Add tactile Web Audio sound effects\n" +
"[x] Build dev-mode 2048 with tech stack evolution\n" +
"[x] Add desktop right-click context menu\n" +
"[ ] Fix that tiny 1px subpixel border gap on Safari\n" +
"[ ] Write more music synth chord progressions\n" +
"[ ] Stop drinking iced coffee at midnight while coding\n" +
"[x] Add secret terminal commands for friends to discover",
"manifesto.md":
"The Handcrafted Web Manifesto\n" +
"-----------------------------\n" +
"In a world where every website is 200MB of bloated node_modules,\n" +
"there is deep joy in crafting things by hand.\n" +
"Lookout OS was born at the Hack Club webOS jam and expanded into\n" +
"a digital haven. No trackers, no cookies, no analytics.\n" +
"Just an open window to the horizon.",
"secrets.txt":
"Psst... you found the secrets file!\n" +
"Try these hidden commands in this terminal:\n" +
" • `neofetch` or `fetch` — System telemetry & ASCII tower\n" +
" • `matrix` — Digital rain effect\n" +
" • `coffee` — Brew an ASCII cup of coffee\n" +
" • `cowsay <words>` — Retro ASCII cow\n" +
" • `sudo <command>` — Try it and see what happens\n" +
" • `history` — View all commands typed in this session\n" +
" • `sound` — Toggle system sound effects on or off"
};
var commandHistory = window.commandHistory = [];
var historyCursor = window.historyCursor = 0;
var terminalInput = $('#terminalInput');
var terminalOutput = $('#terminalOutput');
function terminalPrint(text, className) {
if (!terminalOutput) return null;
var line = document.createElement("p");
line.className = "terminal-line " + (className || "terminal-reply");
line.textContent = text;
terminalOutput.appendChild(line);
terminalOutput.scrollTop = terminalOutput.scrollHeight;
return line;
}
function terminalEcho(command) {
if (!terminalOutput) return null;
var line = document.createElement("p");
line.className = "terminal-line terminal-echo";
var prompt = document.createElement("span");
prompt.className = "terminal-prompt";
prompt.textContent = "guest@lookout:~$";
line.appendChild(prompt);
if (command) {
line.appendChild(document.createTextNode(" " + command));
}
terminalOutput.appendChild(line);
terminalOutput.scrollTop = terminalOutput.scrollHeight;
return line;
}
function terminalPrintBlock(text, className) {
text.split("\n").forEach(function (line) {
terminalPrint(line, className);
});
}
var terminalCommands = window.terminalCommands = {
help: {
usage: "help",
blurb: "list every command",
run: function () {
terminalPrint("available commands", "terminal-banner");
Object.keys(terminalCommands).forEach(function (name) {
var command = terminalCommands[name];
var usage = command.usage;
while (usage.length < 14) usage += " ";
terminalPrint(" " + usage + command.blurb);
});
},
},
whoami: {
usage: "whoami",
blurb: "who you're talking to",
run: function () {
terminalPrintBlock(terminalFiles["about.txt"]);
},
},
ls: {
usage: "ls",
blurb: "list files here",
run: function () {
terminalPrint(Object.keys(terminalFiles).join(" "));
},
},
cat: {
usage: "cat <file>",
blurb: "print a file",
run: function (args) {
if (!args || !args.length) {
terminalPrint("cat: needs a filename. try `ls` first.", "terminal-error");
return;
}
var name = args[0];
if (!terminalFiles[name]) {
terminalPrint("cat: " + name + ": no such file", "terminal-error");
return;
}
terminalPrintBlock(terminalFiles[name]);
},
},
apps: {
usage: "apps",
blurb: "list installed apps",
run: function () {
var allNames = Object.keys(apps);
var coreThree = ["welcome", "crate", "terminal"];
var others = allNames.filter(function (n) { return !coreThree.includes(n); });
var ordered = others.concat(coreThree.filter(function (n) { return allNames.includes(n); }));
ordered.forEach(function (name) {
var win = apps[name];
var isOpen = win && win.style.display !== "none";
terminalPrint(" " + name + (isOpen ? " [open]" : ""));
});
},
},
open: {
usage: "open <app>",
blurb: "launch an app window",
run: function (args) {
if (!args || !args.length) {
terminalPrint("open: needs an app. try `apps`.", "terminal-error");
return;
}
var name = args[0].toLowerCase();
if (!apps[name]) {
terminalPrint("open: " + args[0] + ": no such app. try `apps`.", "terminal-error");
return;
}
openWindow(apps[name]);
terminalPrint("launching " + name + "…", "terminal-banner");
},
},
date: {
usage: "date",