diff --git a/src/frontend/src/app/application/services/retro-audio.service.ts b/src/frontend/src/app/application/services/retro-audio.service.ts index d9428fd..805a618 100644 --- a/src/frontend/src/app/application/services/retro-audio.service.ts +++ b/src/frontend/src/app/application/services/retro-audio.service.ts @@ -101,4 +101,36 @@ export class RetroAudioService { osc.stop(startTime + 0.06); }); } + + playStartup(): void { + const ctx = this.getContext(); + if (!ctx) return; + // Acorde de inicio ascendente emblemático (Eb, Bb, Eb, G, Ab, Bb) + const startupChords = [ + { f: 155.56, t: 0.0, d: 1.2, v: 0.04 }, // Eb3 + { f: 233.08, t: 0.15, d: 1.2, v: 0.04 }, // Bb3 + { f: 311.13, t: 0.35, d: 1.4, v: 0.05 }, // Eb4 + { f: 392.00, t: 0.55, d: 1.4, v: 0.05 }, // G4 + { f: 466.16, t: 0.75, d: 1.8, v: 0.06 }, // Bb4 + { f: 622.25, t: 0.95, d: 2.2, v: 0.05 } // Eb5 + ]; + + startupChords.forEach(note => { + const startTime = ctx.currentTime + note.t; + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(note.f, startTime); + + gain.gain.setValueAtTime(0.001, startTime); + gain.gain.linearRampToValueAtTime(note.v, startTime + 0.08); + gain.gain.exponentialRampToValueAtTime(0.0001, startTime + note.d); + + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(startTime); + osc.stop(startTime + note.d); + }); + } } diff --git a/src/frontend/src/app/application/stores/retro-os.store.ts b/src/frontend/src/app/application/stores/retro-os.store.ts index cb2f95d..0b5a613 100644 --- a/src/frontend/src/app/application/stores/retro-os.store.ts +++ b/src/frontend/src/app/application/stores/retro-os.store.ts @@ -1,5 +1,5 @@ import { Injectable, signal, computed, inject, effect } from '@angular/core'; -import { WindowId, RetroWindow, DesktopIcon, ZenOsTheme, ZenOsConfig, WindowPosition, WindowSize } from '../../domain/models/retro-os.model'; +import { WindowId, RetroWindow, DesktopIcon, ZenOsTheme, ZenOsConfig, WindowPosition, WindowSize, OsPowerState } from '../../domain/models/retro-os.model'; import { RetroAudioService } from '../services/retro-audio.service'; import { I18nService } from '../services/i18n.service'; @@ -56,6 +56,7 @@ export class RetroOsStore { private readonly audio = inject(RetroAudioService); private readonly i18n = inject(I18nService); + private readonly _powerState = signal('off'); private readonly _windows = signal>(INITIAL_WINDOWS); private readonly _activeWindowId = signal(null); private readonly _topZIndex = signal(20); @@ -64,6 +65,8 @@ export class RetroOsStore { private readonly _scanlines = signal(this.getSavedBoolean('zenos_scanlines', true)); private readonly _soundEnabled = signal(this.getSavedBoolean('zenos_sound', true)); + readonly powerState = this._powerState.asReadonly(); + readonly desktopIcons = computed(() => { const t = this.i18n.t(); return [ @@ -219,6 +222,29 @@ export class RetroOsStore { }); } + startBootSequence(): void { + if (this._powerState() !== 'off') return; + this._powerState.set('booting'); + + // Transición directa de barra de carga XP a escritorio + setTimeout(() => { + if (this._powerState() === 'booting') { + this.audio.playStartup(); + this._powerState.set('desktop'); + } + }, 2400); + } + + forceDesktop(): void { + this._powerState.set('desktop'); + } + + resetToOff(): void { + this._powerState.set('off'); + this._windows.set(INITIAL_WINDOWS); + this._activeWindowId.set(null); + } + private applyThemeToDocument(theme: ZenOsTheme): void { if (typeof document !== 'undefined' && document.documentElement) { document.documentElement.setAttribute('data-theme', theme); diff --git a/src/frontend/src/app/domain/models/retro-os.model.ts b/src/frontend/src/app/domain/models/retro-os.model.ts index bd95154..b4211d2 100644 --- a/src/frontend/src/app/domain/models/retro-os.model.ts +++ b/src/frontend/src/app/domain/models/retro-os.model.ts @@ -1,5 +1,7 @@ export type WindowId = 'games' | 'cv' | 'settings' | 'scores'; +export type OsPowerState = 'off' | 'booting' | 'desktop'; + export type ZenOsTheme = 'zen-dark' | 'retro-amber' | 'matrix-green'; export interface WindowPosition { diff --git a/src/frontend/src/app/presentation/features/landing/landing.component.css b/src/frontend/src/app/presentation/features/landing/landing.component.css index 3f84af0..52ab0ac 100644 --- a/src/frontend/src/app/presentation/features/landing/landing.component.css +++ b/src/frontend/src/app/presentation/features/landing/landing.component.css @@ -106,9 +106,10 @@ .interactive-stage { width: 100%; - padding: var(--space-12) clamp(1rem, 3vw, 2.5rem) var(--space-16) clamp(1rem, 3vw, 2.5rem); + padding: var(--space-16) clamp(1rem, 3vw, 2.5rem) var(--space-16) clamp(1rem, 3vw, 2.5rem); background-color: #121517; background: linear-gradient(180deg, #000000 0%, #121517 var(--space-12)); + scroll-margin-top: 40px; } .stage-grid { diff --git a/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.css b/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.css new file mode 100644 index 0000000..d993fd0 --- /dev/null +++ b/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.css @@ -0,0 +1,179 @@ +:host { + display: block; + position: absolute; + inset: 0; + z-index: 50; + pointer-events: auto; +} + +.power-overlay { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + overflow: hidden; + user-select: none; +} + +/* ============================================================================== + 1. PANTALLA APAGADA (CRT STANDBY) + ============================================================================== */ +.crt-off-screen { + position: absolute; + inset: 0; + background: #000000; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.standby-indicator { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: rgba(18, 21, 27, 0.7); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.standby-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #e69d45; + box-shadow: 0 0 8px #e69d45; + animation: crt-standby-blink 1.8s infinite ease-in-out; +} + +.standby-text { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.1em; + color: #a0aab8; +} + +@keyframes crt-standby-blink { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +/* ============================================================================== + 2. PANTALLA DE INICIO WINDOWS XP (BOOT SCREEN) + ============================================================================== */ +.xp-boot-screen { + position: absolute; + inset: 0; + background: #000000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; +} + +.xp-boot-logo { + margin-bottom: 28px; +} + +.logo-row { + display: flex; + align-items: center; + gap: 12px; +} + +.boot-flag-svg { + width: 44px; + height: 44px; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8)); +} + +.boot-text-brand { + display: flex; + flex-direction: column; + line-height: 1; +} + +.brand-ms { + font-family: var(--font-sans); + font-size: 0.6875rem; + color: #ffffff; + letter-spacing: 0.05em; + font-weight: 500; + margin-bottom: 2px; +} + +.brand-xp { + display: flex; + align-items: baseline; + gap: 4px; +} + +.brand-windows { + font-family: var(--font-sans); + font-size: 1.45rem; + font-weight: 800; + color: #ffffff; + letter-spacing: -0.02em; +} + +.brand-xp-badge { + font-family: var(--font-sans); + font-size: 0.875rem; + font-weight: 900; + color: #EB3C00; + letter-spacing: 0.02em; +} + +/* Barra de Carga de XP */ +.xp-loader-container { + width: 140px; + height: 14px; + background: #000000; + border: 1px solid #5a5a5a; + border-radius: 3px; + padding: 2px; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.9); + overflow: hidden; + position: relative; +} + +.xp-loader-track { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +.xp-loader-slider { + display: flex; + gap: 2px; + position: absolute; + top: 0; + left: -40px; + height: 100%; + animation: xp-progress-slide 1.4s linear infinite; +} + +.xp-loader-slider .block { + width: 9px; + height: 100%; + background: linear-gradient(180deg, #3870e8 0%, #204db5 50%, #153282 100%); + border-radius: 1px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4); +} + +@keyframes xp-progress-slide { + 0% { left: -36px; } + 100% { left: 140px; } +} + +.boot-footer-note { + position: absolute; + bottom: 12px; + font-family: var(--font-sans); + font-size: 0.5625rem; + color: #666666; +} diff --git a/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.html b/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.html new file mode 100644 index 0000000..27a3606 --- /dev/null +++ b/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.html @@ -0,0 +1,52 @@ +
+ + + @if (powerState() === 'off') { +
+
+ + STANDBY // CLICK TO POWER ON +
+
+ } + + + @if (powerState() === 'booting') { +
+ + + + +
+
+
+ + + +
+
+
+ + +
+ } +
diff --git a/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.ts b/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.ts new file mode 100644 index 0000000..cd0b3bb --- /dev/null +++ b/src/frontend/src/app/presentation/features/retro-os/components/retro-power-screen/retro-power-screen.component.ts @@ -0,0 +1,29 @@ +import { + Component, + ChangeDetectionStrategy, + input, + output, + inject +} from '@angular/core'; +import { OsPowerState } from '../../../../../domain/models/retro-os.model'; +import { I18nService } from '../../../../../application/services/i18n.service'; + +@Component({ + selector: 'app-retro-power-screen', + standalone: true, + templateUrl: './retro-power-screen.component.html', + styleUrl: './retro-power-screen.component.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RetroPowerScreenComponent { + readonly powerState = input.required(); + readonly powerOn = output(); + + protected readonly i18n = inject(I18nService); + + onScreenClick(): void { + if (this.powerState() === 'off') { + this.powerOn.emit(); + } + } +} diff --git a/src/frontend/src/app/presentation/features/retro-os/components/retro-start-menu/retro-start-menu.component.html b/src/frontend/src/app/presentation/features/retro-os/components/retro-start-menu/retro-start-menu.component.html index 57cff6b..5030aa9 100644 --- a/src/frontend/src/app/presentation/features/retro-os/components/retro-start-menu/retro-start-menu.component.html +++ b/src/frontend/src/app/presentation/features/retro-os/components/retro-start-menu/retro-start-menu.component.html @@ -90,15 +90,11 @@ - +
-
diff --git a/src/frontend/src/app/presentation/features/retro-os/retro-os.component.html b/src/frontend/src/app/presentation/features/retro-os/retro-os.component.html index b155f20..3ca4158 100644 --- a/src/frontend/src/app/presentation/features/retro-os/retro-os.component.html +++ b/src/frontend/src/app/presentation/features/retro-os/retro-os.component.html @@ -4,6 +4,11 @@
+ + @if (store.powerState() !== 'desktop') { + + } +
(false); protected readonly currentTime = signal('12:00'); private clockInterval: ReturnType | null = null; + private observer: IntersectionObserver | null = null; + private hasBootedOnce = false; + private readonly timeFormatter = new Intl.DateTimeFormat('es-EC', { hour: '2-digit', minute: '2-digit', @@ -52,6 +60,27 @@ export class RetroOsComponent implements OnInit, OnDestroy { this.clockInterval = setInterval(() => this.updateClock(), 1000); } + ngAfterViewInit(): void { + if (typeof window !== 'undefined' && 'IntersectionObserver' in window) { + this.observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + // Al entrar visible en pantalla tras bajar del Hero 3D + if (entry.isIntersecting && entry.intersectionRatio >= 0.25) { + if (this.store.powerState() === 'off' && !this.hasBootedOnce) { + this.hasBootedOnce = true; + this.store.startBootSequence(); + // Desconectar observer: una vez encendido se queda siempre cargado + this.observer?.disconnect(); + } + } + }, + { threshold: [0.25] } + ); + this.observer.observe(this.elRef.nativeElement); + } + } + private updateClock(): void { this.currentTime.set(this.timeFormatter.format(new Date())); } @@ -75,5 +104,8 @@ export class RetroOsComponent implements OnInit, OnDestroy { if (this.clockInterval !== null) { clearInterval(this.clockInterval); } + if (this.observer) { + this.observer.disconnect(); + } } }