Starter kit: Gadget Radio cyberpunk visualizer

- Updated index.html, main.ts, audio types, visualizer, tsconfig
- Added public/favicon.svg, src/vite-env.d.ts
- Added presidential-projects.md reference doc
This commit is contained in:
Rob Colbert 2026-05-15 18:48:34 -04:00
parent 769221948d
commit ed951f0bd5
8 changed files with 549 additions and 254 deletions

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gadget Radio - Cyberpunk Mainframe HUD</title> <title>Gadget Radio - Cyberpunk Mainframe HUD</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</head> </head>
<body class="bg-black overflow-hidden font-mono"> <body class="bg-black overflow-hidden font-mono">
<!-- Three.js Canvas Container --> <!-- Three.js Canvas Container -->

264
presidential-projects.md Normal file
View File

@ -0,0 +1,264 @@
# Presidential Construction Projects: A Tale of Two Libraries (and a Ballroom)
## Deep Dive: Obama Library vs. Trump Ballroom Construction & Maintenance Costs
*Research conducted May 10, 2026*
---
## Executive Summary
When it comes to presidential legacy projects, the numbers tell a fascinating story. We conducted a deep-dive analysis comparing the **Obama Presidential Center** in Chicago with President Trump's **White House Ballroom** project. The findings reveal stark differences in funding models, long-term taxpayer obligations, and regulatory compliance.
| Metric | Obama Presidential Center | Trump White House Ballroom |
|--------|--------------------------|---------------------------|
| **Initial Estimate** | $300 million (2017) | $200 million (July 2025) |
| **Current Estimated Cost** | $615-850 million | $400+ million |
| **Cost Increase** | ~103-183% | ~100% |
| **Size** | 19.3 acres (campus) | 90,000 sq ft (building) |
| **Funding Source** | Private donations (Obama Foundation) | Private donations (Trump + donors) |
| **Annual Maintenance** | ~$1M endowment (City requirement) | Unknown (likely taxpayer-funded) |
| **Federal Taxpayer Cost** | Minimal (virtual library model) | Significant (security, operations, staffing) |
| **Status** | Opening June 2026 | Under construction (completion: Summer 2028) |
---
## 1. Construction Costs: A Moving Target
### Obama Presidential Center (Chicago, IL)
The Obama Presidential Center's cost trajectory shows a familiar pattern in major construction projects: significant cost overruns from initial estimates.
| Date | Estimated Cost | Source |
|------|---------------|--------|
| 2017 | ~$300 million | Initial estimate |
| 2020-2021 | ~$500 million | City Journal |
| 2021-2022 | ~$700 million | Chicago Sun-Times |
| 2024 | $615 million | Chicago Tribune |
| 2025 | $615-815 million | ENR Construction |
| Final (Wikipedia) | $850 million (est.) | Wikipedia |
**What's Included:**
- Museum tower (180,000 sq ft)
- Library building
- Community facilities
- Athletic facilities
- Landscaping (19.3 acres in Jackson Park)
- Chicago Public Library branch
- Parking garage
### Trump White House Ballroom (Washington, D.C.)
The Trump Ballroom project has seen its estimates double in less than a year.
| Date | Estimated Cost | Source |
|------|---------------|--------|
| July 2025 | $200 million | White House official estimate |
| October 2025 | $300 million | Trump statement |
| December 2025 | $400 million | Trump statement |
| May 2026 | $400+ million | TIME Magazine |
**What's Included:**
- 90,000 sq ft ballroom structure
- Demolition of historic East Wing (55,000 sq ft)
- Underground construction
- Security infrastructure
- Relocation of East Wing offices
---
## 2. Funding Sources: Private Money, Public Questions
### Obama Presidential Center
**Private Funding (100% of construction):**
- Raised by Obama Foundation
- Major donors: Anonymous mega-donors, corporate contributions
- **No federal taxpayer dollars for construction**
- City of Chicago required only **$1 million endowment** (vs. typical 60% of construction cost = ~$470M)
**Notable Cost Savings:**
The Obama Foundation's decision to create a "virtual library" rather than a traditional NARA-run facility eliminated the need for:
- A federal archival building
- NARA security infrastructure
- Long-term federal maintenance obligations
As the Chicago Sun-Times reported: *"Not including an official NARA-run presidential library in the Obama Presidential Center saves the Obama Presidential Foundation tens of millions of dollars."*
### Trump White House Ballroom
**Private Funding (claimed for construction only):**
- Donald Trump (personal funds)
- Corporate donors: Amazon, Apple, Caterpillar, Comcast, Google, Lockheed Martin, Meta, Microsoft, T-Mobile, Union Pacific Railroad
- Wealthy individuals (some anonymous)
**The Catch: Inevitable Taxpayer Costs**
While construction is privately funded, experts agree taxpayers will bear significant ongoing costs:
| Cost Category | Estimated Amount | Notes |
|--------------|-----------------|-------|
| **Security Infrastructure** | $1 billion (proposed) | GOP bill (May 2026) |
| **Annual Security** | Unknown (ongoing) | Secret Service, surveillance, perimeter |
| **Annual Maintenance** | Part of $18.6M EOP budget | Executive Residence maintenance |
| **Staff Salaries** | Federally appropriated | First Lady's office, Social Secretary |
| **Utilities** | Federally funded | Electricity, heating, cooling |
| **Communications** | Federally maintained | Secure systems, IT infrastructure |
**Expert Warning:**
> "What's been obvious from the beginning is the whole idea that this is a gift to the American people is really a misdirection. The cost is going to overrun significantly... There are almost certainly going to be approaches to Congress for funding after this is done."
>
> — **Edward Lengel**, former Chief Historian, White House Historical Association
> "Once the space is erected and fully operational, its upkeep and staffing would become a part of the executive branch's annual congressional appropriations. That means, one way or another, taxpayers' money will eventually end up flowing into Trump's ballroom."
>
> — **Rollcall**, October 2025
---
## 3. Annual Maintenance & Operating Costs: The Hidden Burden
### Obama Presidential Center
| Cost Category | Amount | Notes |
|--------------|--------|-------|
| **Endowment Requirement** | $1 million | City of Chicago requirement (vs. $470M typical for NARA libraries) |
| **Annual Operating Costs** | Privately funded | Obama Foundation responsible |
| **Federal Taxpayer Cost** | Minimal | Virtual library stored at existing NARA facilities |
| **NARA System-Wide** | $100+ million/year | All 16 presidential libraries combined |
**Key Innovation:** The Obama Presidential Library is the first fully "virtual" presidential library. NARA stores records at existing facilities (Hoffman Estates, IL and College Park, MD), eliminating the need for a dedicated federal building and its associated maintenance costs.
### Trump White House Ballroom
Unlike the Obama Center, the Trump Ballroom will be a federal facility on federal land, meaning:
- All operations become part of the Executive Office of the President budget
- Security costs are borne by taxpayers (Secret Service, surveillance, etc.)
- Maintenance falls under the White House Repair and Restoration budget
- Staff salaries are congressionally appropriated
**The Bottom Line:** According to Rollcall, *"Whether he uses the $230 million he's suing the Justice Department for, or he dips into first lady or social secretary appropriations, that's all taxpayer money."*
---
## 4. Legal & Regulatory Context
### Obama Presidential Center
- ✅ Complied with all federal, state, and local regulations
- ✅ Underwent extensive federal review (completed December 2020)
- ✅ City of Chicago approval process followed
- ✅ NARA agreement for virtual library model
### Trump White House Ballroom
- ⚠️ **Lawsuit filed** by National Trust for Historic Preservation
- ⚠️ **Allegations**: Broke ground before plans submitted to National Capital Planning Commission; no Congressional approval for federal park land construction
- ⚠️ **Anti-Deficiency Act concerns**: Private funding for government purpose may violate federal law
- ⚠️ **Commission of Fine Arts**: All 6 members fired by Trump (October 2025); no quorum to review plans
- ⚠️ **Court ruling**: Judge allowed construction to proceed (December 2025), but hearing for preliminary injunction scheduled for January 2026
**Legal Expert Opinion:**
> "Private money to build a White House ballroom flagrantly violates the Anti-Deficiency Act, which safeguards the congressional power of the purse by prohibiting the president from using non-appropriated funds donated by private parties for a government purpose indistinguishable from plutocracy."
>
> — **Bruce Fein**, Constitutional and Administrative Law Attorney
---
## 5. Timeline Comparison
### Obama Presidential Center
| Milestone | Date |
|-----------|------|
| Site selection | 2015 |
| Initial estimate | 2017 ($300M) |
| Federal review completed | December 2020 |
| Groundbreaking | August 2021 |
| Tower topped out | Mid-2024 |
| **Expected opening** | **June 18-19, 2026** |
### Trump White House Ballroom
| Milestone | Date |
|-----------|------|
| Initial announcement | July 2025 |
| Initial estimate | $200 million |
| East Wing demolition | October 2025 |
| Construction start | November 2025 |
| Above-ground work | April 2026 (earliest) |
| **Expected completion** | **Summer 2028** |
---
## 6. Key Differences at a Glance
| Aspect | Obama Center | Trump Ballroom |
|--------|-------------|----------------|
| **Purpose** | Museum, education, community center | Event space, state dinners |
| **Location** | Chicago, IL (private land, city-owned) | Washington, D.C. (federal land) |
| **Federal oversight** | Full compliance | Lawsuits, regulatory bypass alleged |
| **Long-term taxpayer cost** | Minimal (virtual library) | Significant (security, operations) |
| **Endowment** | $1M (Chicago requirement) | None required (but taxpayer-funded operations) |
| **Transparency** | Public financials, donor disclosures | Limited donor disclosure, anonymous gifts |
| **Historic preservation** | Preserved Jackson Park (with modifications) | Demolished historic East Wing |
---
## 7. The Bottom Line
### Obama Presidential Center
- **Construction cost**: $615-850 million (private)
- **Annual taxpayer cost**: Minimal (virtual library model)
- **Endowment**: $1 million (vs. $470M typical requirement)
- **Key takeaway**: Innovative model reduces long-term taxpayer burden
### Trump White House Ballroom
- **Construction cost**: $400+ million (private, but legal questions remain)
- **Annual taxpayer cost**: Significant (security, operations, staffing)
- **Endowment**: None (taxpayer-funded operations inevitable)
- **Key takeaway**: Initial private funding masks inevitable long-term taxpayer costs
---
## 8. Expert Analysis
**On Obama's Virtual Library Model:**
> "Not including an official NARA-run presidential library in the Obama Presidential Center saves the Obama Presidential Foundation tens of millions of dollars. NARA presidential library buildings are privately funded, and the agency mandates an endowment equal to 60% of construction costs of the library."
>
> — **Chicago Sun-Times**
**On Trump's Ballroom:**
> "Whether he uses the $230 million he's suing the Justice Department for, or he dips into first lady or social secretary appropriations, that's all taxpayer money. So that's the people's money, however it comes to him."
>
> — **Longtime White House observer** (Rollcall)
> "The only thing that prevented this in the past was previous presidents followed norms and traditions. But Trump figured out that they weren't legally required to do so."
>
> — **Edward Lengel**, former Chief Historian, White House Historical Association
---
## Sources
1. Chicago Tribune, "What is the cost of Obama's presidential center?" (September 27, 2025)
2. ENR Construction, "Behind Schedule, Obama Presidential Center Construction Budget Balloons to $615M" (September 30, 2025)
3. Chicago Sun-Times, "New details on little-known Obama Presidential Library" (August 25, 2023)
4. National Archives, "Updated Information About Obama Presidential Library"
5. TIME Magazine, "Trump's White House Ballroom Keeps Growing in Size and Cost" (May 6, 2026)
6. Rollcall, "Taxpayers will pay plenty over the years for Trump's massive ballroom" (October 27, 2025)
7. NBC News, "Judge lets Trump's White House ballroom construction move forward" (December 16, 2025)
8. USAFacts, "Ask an Analyst: Putting White House ballroom construction in context" (February 12, 2026)
9. NPR, "Republicans want $1 billion for Trump's ballroom security" (May 6, 2026)
10. Congressional Research Service, "Presidential Libraries and Museums" (October 10, 2024)
---
*This report was compiled using publicly available information from government documents, news sources, and expert analysis. All cost figures are estimates and subject to change as projects progress.*

6
public/favicon.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#000"/>
<polygon points="16,4 28,28 4,28" fill="none" stroke="#00ffff" stroke-width="2"/>
<polygon points="16,8 25,26 7,26" fill="#00ffff" opacity="0.3"/>
<circle cx="16" cy="18" r="3" fill="#00ffff"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@ -6,7 +6,6 @@ import './styles.css'
class GadgetRadioApp { class GadgetRadioApp {
private player: RadioPlayer | null = null private player: RadioPlayer | null = null
private animationFrameId: number | null = null private animationFrameId: number | null = null
private glitchTimeoutId: number | null = null
// DOM element references - all declared in index.html // DOM element references - all declared in index.html
private readonly elements = { private readonly elements = {
@ -128,18 +127,6 @@ class GadgetRadioApp {
isLooping: boolean isLooping: boolean
isGlitching?: boolean isGlitching?: boolean
}): void { }): void {
if (state.isGlitching) {
// Clear any pending glitch timeout to avoid conflicts
if (this.glitchTimeoutId !== null) {
clearTimeout(this.glitchTimeoutId)
}
document.body.classList.add('glitch-active')
this.glitchTimeoutId = window.setTimeout(() => {
document.body.classList.remove('glitch-active')
this.glitchTimeoutId = null
}, 450)
}
this.updateStatus(state.isPlaying ? 'STREAMING' : 'PAUSED') this.updateStatus(state.isPlaying ? 'STREAMING' : 'PAUSED')
} }
@ -168,9 +155,6 @@ class GadgetRadioApp {
if (this.animationFrameId !== null) { if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId) cancelAnimationFrame(this.animationFrameId)
} }
if (this.glitchTimeoutId !== null) {
clearTimeout(this.glitchTimeoutId)
}
this.player?.dispose() this.player?.dispose()
} }
} }

View File

@ -1,40 +1,40 @@
export interface AudioData { export interface AudioData {
bass: number bass: number;
mids: number mids: number;
treble: number treble: number;
volume: number volume: number;
frequencyData?: Uint8Array frequencyData?: Uint8Array;
} }
export interface VisualizerState { export interface VisualizerState {
isPlaying: boolean isPlaying: boolean;
isLooping: boolean isLooping: boolean;
duration: number duration: number;
currentTime: number currentTime: number;
} }
export interface GlitchState { export interface GlitchState {
isGlitching: boolean isGlitching: boolean;
} }
export type RadioState = VisualizerState & Partial<GlitchState> export type RadioState = VisualizerState & Partial<GlitchState>;
export interface AppConfig { export interface AppConfig {
fftSize: number fftSize: number;
smoothingTimeConstant: number smoothingTimeConstant: number;
minDecibels: number minDecibels: number;
maxDecibels: number maxDecibels: number;
bassRange: [number, number] bassRange: [number, number];
midsRange: [number, number] midsRange: [number, number];
trebleRange: [number, number] trebleRange: [number, number];
} }
export const DEFAULT_CONFIG: AppConfig = { export const DEFAULT_CONFIG: AppConfig = {
fftSize: 2048, fftSize: 2048,
smoothingTimeConstant: 0.8, smoothingTimeConstant: 0.5,
minDecibels: -90, minDecibels: -90,
maxDecibels: -10, maxDecibels: -10,
bassRange: [20, 150], bassRange: [20, 150],
midsRange: [150, 2000], midsRange: [150, 2000],
trebleRange: [2000, 8000], trebleRange: [2000, 8000],
} };

View File

@ -1,81 +1,81 @@
import * as THREE from 'three' import * as THREE from "three";
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js' import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
export interface VisualizerConfig { export interface VisualizerConfig {
container: HTMLElement container: HTMLElement;
onGlitch?: () => void onGlitch?: () => void;
} }
interface FrequencyBand { interface FrequencyBand {
mesh: THREE.Mesh | THREE.Points mesh: THREE.Mesh | THREE.Points;
getScale: () => number getScale: () => number;
getPosition: () => number getPosition: () => number;
setScale: (s: number) => void setScale: (s: number) => void;
} }
export class AudioVisualizer { export class AudioVisualizer {
private scene!: THREE.Scene private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera private camera!: THREE.PerspectiveCamera;
private renderer!: THREE.WebGLRenderer private renderer!: THREE.WebGLRenderer;
private composer!: EffectComposer private composer!: EffectComposer;
private controls!: OrbitControls private controls!: OrbitControls;
private analyser: AnalyserNode | null = null private analyser: AnalyserNode | null = null;
private frequencyData: Uint8Array | null = null private frequencyData: Uint8Array | null = null;
private animationId: number | null = null private animationId: number | null = null;
private config: VisualizerConfig private config: VisualizerConfig;
private isGlitching = false private isGlitching = false;
private clock = new THREE.Clock() private clock = new THREE.Clock();
// Objects // Objects
private icosahedron!: THREE.Mesh private icosahedron!: THREE.Mesh;
private torus!: THREE.Mesh private torus!: THREE.Mesh;
private cubes: THREE.Mesh[] = [] private cubes: THREE.Mesh[] = [];
private particles!: THREE.Points private particles!: THREE.Points;
private ring!: THREE.Mesh private ring!: THREE.Mesh;
private gridHelper!: THREE.GridHelper private gridHelper!: THREE.GridHelper;
private ringGroup!: THREE.Group private ringGroup!: THREE.Group;
// Frequency bands for visual elements // Frequency bands for visual elements
private bassBand!: FrequencyBand private bassBand!: FrequencyBand;
private midBand!: FrequencyBand private midBand!: FrequencyBand;
private trebleBand!: FrequencyBand private trebleBand!: FrequencyBand;
// Audio frequency values (smoothed) // Audio frequency values (smoothed)
private bassValue = 0 private bassValue = 0;
private midsValue = 0 private midsValue = 0;
private trebleValue = 0 private trebleValue = 0;
constructor(config: VisualizerConfig) { constructor(config: VisualizerConfig) {
this.config = config this.config = config;
this.init() this.init();
} }
private init(): void { private init(): void {
this.setupRenderer() this.setupRenderer();
this.setupScene() this.setupScene();
this.setupCamera() this.setupCamera();
this.setupControls() this.setupControls();
this.setupPostProcessing() this.setupPostProcessing();
this.setupGeometry() this.setupGeometry();
this.setupEventListeners() this.setupEventListeners();
this.animate() this.animate();
} }
private setupRenderer(): void { private setupRenderer(): void {
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }) this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.toneMapping = THREE.ReinhardToneMapping this.renderer.toneMapping = THREE.ReinhardToneMapping;
this.renderer.toneMappingExposure = 1.5 this.renderer.toneMappingExposure = 1.5;
this.config.container.appendChild(this.renderer.domElement) this.config.container.appendChild(this.renderer.domElement);
} }
private setupScene(): void { private setupScene(): void {
this.scene = new THREE.Scene() this.scene = new THREE.Scene();
this.scene.fog = new THREE.FogExp2(0x000000, 0.03) this.scene.fog = new THREE.FogExp2(0x000000, 0.03);
} }
private setupCamera(): void { private setupCamera(): void {
@ -83,122 +83,131 @@ export class AudioVisualizer {
60, 60,
window.innerWidth / window.innerHeight, window.innerWidth / window.innerHeight,
0.1, 0.1,
1000 1000,
) );
this.camera.position.set(0, 5, 15) this.camera.position.set(0, 5, 20);
this.camera.lookAt(0, 0, 0) this.camera.lookAt(0, 0, 0);
} }
private setupControls(): void { private setupControls(): void {
this.controls = new OrbitControls(this.camera, this.renderer.domElement) this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05 this.controls.dampingFactor = 0.05;
this.controls.maxDistance = 50 this.controls.maxDistance = 50;
this.controls.minDistance = 5 this.controls.minDistance = 5;
this.controls.autoRotate = true this.controls.autoRotate = true;
this.controls.autoRotateSpeed = 0.5 this.controls.autoRotateSpeed = 0.5;
} }
private setupPostProcessing(): void { private setupPostProcessing(): void {
this.composer = new EffectComposer(this.renderer) this.composer = new EffectComposer(this.renderer);
const renderPass = new RenderPass(this.scene, this.camera) const renderPass = new RenderPass(this.scene, this.camera);
this.composer.addPass(renderPass) this.composer.addPass(renderPass);
const bloomPass = new UnrealBloomPass( const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight), new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // strength 1.5, // strength
0.4, // radius 0.4, // radius
0.85 // threshold 0.85, // threshold
) );
this.composer.addPass(bloomPass) this.composer.addPass(bloomPass);
} }
private setupGeometry(): void { private setupGeometry(): void {
// ── Grid ────────────────────────────────────────────────────────────── // ── Grid ──────────────────────────────────────────────────────────────
this.gridHelper = new THREE.GridHelper(60, 60, 0x00ffff, 0x003333) this.gridHelper = new THREE.GridHelper(60, 60, 0x00ffff, 0x003333);
this.gridHelper.position.y = -3 this.gridHelper.position.y = -3;
this.scene.add(this.gridHelper) this.scene.add(this.gridHelper);
// ── Icosahedron (center, bass-reactive) ─────────────────────────────── // ── Icosahedron (center, bass-reactive) ───────────────────────────────
const icoGeo = new THREE.IcosahedronGeometry(2.5, 2) const icoGeo = new THREE.IcosahedronGeometry(2.5, 2);
const icoMat = new THREE.MeshBasicMaterial({ const icoMat = new THREE.MeshBasicMaterial({
color: 0x00ffff, color: 0x00ffff,
wireframe: true, wireframe: true,
}) });
this.icosahedron = new THREE.Mesh(icoGeo, icoMat) this.icosahedron = new THREE.Mesh(icoGeo, icoMat);
this.scene.add(this.icosahedron) this.scene.add(this.icosahedron);
// ── Cubes ring (mids-reactive) ─────────────────────────────────────── // ── Cubes ring (mids-reactive) ───────────────────────────────────────
const cubeCount = 16 const cubeCount = 16;
for (let i = 0; i < cubeCount; i++) { for (let i = 0; i < cubeCount; i++) {
const size = 0.2 + Math.random() * 0.4 const size = 0.2 + Math.random() * 0.4;
const geo = new THREE.BoxGeometry(size, size, size) const geo = new THREE.BoxGeometry(size, size, size);
const hue = (i / cubeCount) * 0.3 const hue = (i / cubeCount) * 0.3;
const mat = new THREE.MeshBasicMaterial({ const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(hue, 1, 0.5), color: new THREE.Color().setHSL(hue, 1, 0.5),
wireframe: true, wireframe: true,
transparent: true, transparent: true,
opacity: 0.8, opacity: 0.8,
}) });
const cube = new THREE.Mesh(geo, mat) const cube = new THREE.Mesh(geo, mat);
const angle = (i / cubeCount) * Math.PI * 2 const angle = (i / cubeCount) * Math.PI * 2;
const radius = 8 const radius = 8;
cube.position.set( cube.position.set(
Math.cos(angle) * radius, Math.cos(angle) * radius,
(Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2,
Math.sin(angle) * radius Math.sin(angle) * radius,
) );
cube.userData.baseY = cube.position.y cube.userData.baseY = cube.position.y;
cube.userData.angle = angle cube.userData.angle = angle;
cube.userData.radius = radius cube.userData.radius = radius;
this.scene.add(cube) this.scene.add(cube);
this.cubes.push(cube) this.cubes.push(cube);
} }
// ── Torus (treble-reactive) ─────────────────────────────────────────── // ── Torus (treble-reactive) ───────────────────────────────────────────
const torusGeo = new THREE.TorusGeometry(5, 0.08, 8, 64) const torusGeo = new THREE.TorusGeometry(8, 0.08, 8, 64);
const torusMat = new THREE.MeshBasicMaterial({ color: 0xff003c, wireframe: true }) const torusMat = new THREE.MeshBasicMaterial({
this.torus = new THREE.Mesh(torusGeo, torusMat) color: 0xff003c,
this.torus.rotation.x = Math.PI / 2 wireframe: true,
this.scene.add(this.torus) });
this.torus = new THREE.Mesh(torusGeo, torusMat);
this.torus.rotation.x = Math.PI / 2;
this.scene.add(this.torus);
// ── Inner ring ─────────────────────────────────────────────────────── // ── Inner ring ───────────────────────────────────────────────────────
const ringGeo = new THREE.TorusGeometry(3.5, 0.05, 8, 48) const ringGeo = new THREE.TorusGeometry(5, 0.05, 8, 48);
const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ff88, wireframe: true }) const ringMat = new THREE.MeshBasicMaterial({
this.ring = new THREE.Mesh(ringGeo, ringMat) color: 0x00ff88,
this.ring.rotation.x = Math.PI / 2 wireframe: true,
this.scene.add(this.ring) });
this.ring = new THREE.Mesh(ringGeo, ringMat);
this.ring.rotation.x = Math.PI / 2;
this.scene.add(this.ring);
// ── Particle system ─────────────────────────────────────────────────── // ── Particle system ───────────────────────────────────────────────────
const particleCount = 2000 const particleCount = 2000;
const positions = new Float32Array(particleCount * 3) const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3) const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) { for (let i = 0; i < particleCount; i++) {
const i3 = i * 3 const i3 = i * 3;
// Distribute in a sphere // Distribute in a sphere
const radius = 10 + Math.random() * 20 const radius = 10 + Math.random() * 20;
const theta = Math.random() * Math.PI * 2 const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1) const phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta) positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta) positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi) positions[i3 + 2] = radius * Math.cos(phi);
// Cyan to magenta gradient // Cyan to magenta gradient
const t = Math.random() const t = Math.random();
const color = new THREE.Color().setHSL(0.5 + t * 0.5, 1, 0.5) const color = new THREE.Color().setHSL(0.5 + t * 0.5, 1, 0.5);
colors[i3] = color.r colors[i3] = color.r;
colors[i3 + 1] = color.g colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b colors[i3 + 2] = color.b;
} }
const particleGeo = new THREE.BufferGeometry() const particleGeo = new THREE.BufferGeometry();
particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3)) particleGeo.setAttribute(
particleGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3)) "position",
new THREE.BufferAttribute(positions, 3),
);
particleGeo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const particleMat = new THREE.PointsMaterial({ const particleMat = new THREE.PointsMaterial({
size: 0.15, size: 0.15,
@ -206,15 +215,15 @@ export class AudioVisualizer {
transparent: true, transparent: true,
opacity: 0.6, opacity: 0.6,
blending: THREE.AdditiveBlending, blending: THREE.AdditiveBlending,
}) });
this.particles = new THREE.Points(particleGeo, particleMat) this.particles = new THREE.Points(particleGeo, particleMat);
this.scene.add(this.particles) this.scene.add(this.particles);
// ── Ring group for nested rotation ──────────────────────────────────── // ── Ring group for nested rotation ────────────────────────────────────
this.ringGroup = new THREE.Group() this.ringGroup = new THREE.Group();
this.scene.add(this.ringGroup) this.scene.add(this.ringGroup);
this.ringGroup.add(this.torus) this.ringGroup.add(this.torus);
this.ringGroup.add(this.ring) this.ringGroup.add(this.ring);
// ── Frequency bands ─────────────────────────────────────────────────── // ── Frequency bands ───────────────────────────────────────────────────
this.bassBand = { this.bassBand = {
@ -222,10 +231,10 @@ export class AudioVisualizer {
getScale: () => 1 + this.bassValue / 128, getScale: () => 1 + this.bassValue / 128,
getPosition: () => 0, getPosition: () => 0,
setScale: () => { setScale: () => {
const s = 1 + this.bassValue / 128 const s = 1 + this.bassValue / 128;
this.icosahedron.scale.setScalar(s) this.icosahedron.scale.setScalar(s);
}, },
} };
this.midBand = { this.midBand = {
mesh: this.cubes[0], mesh: this.cubes[0],
@ -233,176 +242,200 @@ export class AudioVisualizer {
getPosition: () => 0, getPosition: () => 0,
setScale: () => { setScale: () => {
this.cubes.forEach((cube) => { this.cubes.forEach((cube) => {
const scale = 0.8 + (this.midsValue / 128) * 1.5 const scale = 0.8 + (this.midsValue / 128) * 1.5;
cube.scale.setScalar(scale) cube.scale.setScalar(scale);
}) });
}, },
} };
this.trebleBand = { this.trebleBand = {
mesh: this.torus, mesh: this.torus,
getScale: () => 1 + this.trebleValue / 128, getScale: () => 1 + this.trebleValue / 128,
getPosition: () => 0, getPosition: () => 0,
setScale: () => { setScale: () => {
const scale = 1 + (this.trebleValue / 128) * 0.5 const scale = 1 + (this.trebleValue / 128) * 0.5;
this.torus.scale.set(scale, scale, scale) this.torus.scale.set(scale, scale, scale);
this.ring.scale.set(scale, scale, scale) this.ring.scale.set(scale, scale, scale);
}, },
} };
} }
private setupEventListeners(): void { private setupEventListeners(): void {
window.addEventListener('resize', this.onResize) window.addEventListener("resize", this.onResize);
} }
private onResize = (): void => { private onResize = (): void => {
const w = window.innerWidth const w = window.innerWidth;
const h = window.innerHeight const h = window.innerHeight;
this.camera.aspect = w / h this.camera.aspect = w / h;
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h) this.renderer.setSize(w, h);
this.composer.setSize(w, h) this.composer.setSize(w, h);
} };
connectAnalyser(analyser: AnalyserNode): void { connectAnalyser(analyser: AnalyserNode): void {
this.analyser = analyser this.analyser = analyser;
this.frequencyData = new Uint8Array(analyser.frequencyBinCount) this.frequencyData = new Uint8Array(analyser.frequencyBinCount);
} }
private triggerGlitch(): void { private triggerGlitch(): void {
if (this.isGlitching) return if (this.isGlitching) return;
this.isGlitching = true this.isGlitching = true;
this.config.onGlitch?.() this.config.onGlitch?.();
document.body.classList.add('glitch-active') this.renderer.domElement.classList.add("glitch-active");
setTimeout(() => { setTimeout(() => {
document.body.classList.remove('glitch-active') this.renderer.domElement.classList.remove("glitch-active");
this.isGlitching = false this.isGlitching = false;
}, 450) }, 450);
} }
private animate = (): void => { private animate = (): void => {
this.animationId = requestAnimationFrame(this.animate) this.animationId = requestAnimationFrame(this.animate);
const elapsed = this.clock.getElapsedTime() const elapsed = this.clock.getElapsedTime();
this.controls.update() this.controls.update();
let bassValue = 0;
let midsValue = 0;
let trebleValue = 0;
// Update frequency data // Update frequency data
if (this.frequencyData && this.analyser) { if (this.frequencyData && this.analyser) {
this.analyser.getByteFrequencyData(this.frequencyData as Uint8Array<ArrayBuffer>) this.analyser.getByteFrequencyData(
this.frequencyData as Uint8Array<ArrayBuffer>,
);
const { bass, mids, treble } = this.getFrequencyBands() const { bass, mids, treble } = this.getFrequencyBands();
// Smooth the values // Smooth the values
this.bassValue = THREE.MathUtils.lerp(this.bassValue, bass, 0.15) bassValue = this.bassValue = THREE.MathUtils.lerp(
this.midsValue = THREE.MathUtils.lerp(this.midsValue, mids, 0.15) this.bassValue,
this.trebleValue = THREE.MathUtils.lerp(this.trebleValue, treble, 0.15) bass,
0.15,
);
midsValue = this.midsValue = THREE.MathUtils.lerp(
this.midsValue,
mids,
0.15,
);
trebleValue = this.trebleValue = THREE.MathUtils.lerp(
this.trebleValue,
treble,
0.15,
);
// Update visual elements // Update visual elements
this.bassBand.setScale(this.bassBand.getScale()) this.bassBand.setScale(this.bassBand.getScale());
this.midBand.setScale(this.midBand.getScale()) this.midBand.setScale(this.midBand.getScale());
this.trebleBand.setScale(this.trebleBand.getScale()) this.trebleBand.setScale(this.trebleBand.getScale());
// Random glitch // Random glitch
if (Math.random() < 0.003) { if (Math.random() < 0.003) {
this.triggerGlitch() this.triggerGlitch();
} }
} }
// Animate icosahedron rotation // Animate icosahedron rotation
this.icosahedron.rotation.x += 0.003 this.icosahedron.rotation.x += 0.003;
this.icosahedron.rotation.y += 0.005 this.icosahedron.rotation.y += 0.005;
// Animate color based on bass // Animate color based on bass
const hue = (elapsed * 0.05) % 1 const hue = (elapsed * 0.05) % 1;
const mat = this.icosahedron.material as THREE.MeshBasicMaterial const mat = this.icosahedron.material as THREE.MeshBasicMaterial;
mat.color.setHSL(hue, 1, 0.5) mat.color.setHSL(hue, 1, 0.5);
// Animate cubes // Animate cubes
this.cubes.forEach((cube, i) => { this.cubes.forEach((cube, i) => {
const angle = cube.userData.angle + elapsed * 0.2 const angle = cube.userData.angle + elapsed * 0.2;
const radius = cube.userData.radius + this.midsValue / 32 const radius = cube.userData.radius + this.midsValue / 32;
cube.position.x = Math.cos(angle) * radius cube.position.x = Math.cos(angle) * radius;
cube.position.z = Math.sin(angle) * radius cube.position.z = Math.sin(angle) * radius;
cube.position.y = cube.userData.baseY + Math.sin(elapsed * 2 + i) * 0.5 cube.position.y = cube.userData.baseY + Math.sin(elapsed * 2 + i) * 0.5;
cube.rotation.x += 0.01 cube.rotation.x += 0.01;
cube.rotation.y += 0.015 cube.rotation.y += 0.015;
}) });
// Animate torus ring // Animate torus ring
this.ringGroup.rotation.z = elapsed * 0.1 this.ringGroup.rotation.z = elapsed * 0.1;
// Animate particles // Animate particles
this.particles.rotation.y += 0.0003 this.particles.rotation.y += 0.0003;
this.particles.rotation.x += 0.0001 this.particles.rotation.x += 0.0001;
// Pulse ring color // Pulse ring color
const ringMat = this.ring.material as THREE.MeshBasicMaterial const ringMat = this.ring.material as THREE.MeshBasicMaterial;
ringMat.color.setHSL(0.4 + Math.sin(elapsed) * 0.1, 1, 0.5) // ringMat.color.setHSL(0.4 + Math.sin(elapsed) * 0.1, 1, 0.5);
ringMat.color.setRGB(bassValue / 255, midsValue / 255, trebleValue / 255);
this.composer.render() this.composer.render();
} };
private getFrequencyBands(): { bass: number; mids: number; treble: number } { private getFrequencyBands(): { bass: number; mids: number; treble: number } {
if (!this.frequencyData || !this.analyser) { if (!this.frequencyData || !this.analyser) {
return { bass: 0, mids: 0, treble: 0 } return { bass: 0, mids: 0, treble: 0 };
} }
const sampleRate = this.analyser.context.sampleRate const sampleRate = this.analyser.context.sampleRate;
const binCount = this.analyser.frequencyBinCount const binCount = this.analyser.frequencyBinCount;
const maxFreq = sampleRate / 2 const maxFreq = sampleRate / 2;
const getAverage = (lowHz: number, highHz: number): number => { const getAverage = (lowHz: number, highHz: number): number => {
const startBin = Math.floor((lowHz / maxFreq) * binCount) const startBin = Math.floor((lowHz / maxFreq) * binCount);
const endBin = Math.floor((highHz / maxFreq) * binCount) const endBin = Math.floor((highHz / maxFreq) * binCount);
let sum = 0 let sum = 0;
for (let i = startBin; i <= endBin && i < binCount; i++) { for (let i = startBin; i <= endBin && i < binCount; i++) {
sum += this.frequencyData![i] sum += this.frequencyData![i];
}
return sum / Math.max(1, endBin - startBin + 1)
} }
return sum / Math.max(1, endBin - startBin + 1);
};
return { return {
bass: getAverage(20, 150), bass: getAverage(20, 150),
mids: getAverage(150, 2000), mids: getAverage(150, 2000),
treble: getAverage(2000, 8000), treble: getAverage(2000, 8000),
} };
} }
public getAudioData(): { bass: number; mids: number; treble: number; volume: number } { public getAudioData(): {
const bands = this.getFrequencyBands() bass: number;
const volume = mids: number;
this.frequencyData treble: number;
? this.frequencyData.reduce((a, b) => a + b, 0) / this.frequencyData.length volume: number;
: 0 } {
return { ...bands, volume } const bands = this.getFrequencyBands();
const volume = this.frequencyData
? this.frequencyData.reduce((a, b) => a + b, 0) /
this.frequencyData.length
: 0;
return { ...bands, volume };
} }
public dispose(): void { public dispose(): void {
if (this.animationId !== null) { if (this.animationId !== null) {
cancelAnimationFrame(this.animationId) cancelAnimationFrame(this.animationId);
} }
window.removeEventListener('resize', this.onResize) window.removeEventListener("resize", this.onResize);
this.renderer.dispose() this.renderer.dispose();
this.composer.dispose() this.composer.dispose();
this.scene.traverse((object) => { this.scene.traverse((object) => {
if (object instanceof THREE.Mesh) { if (object instanceof THREE.Mesh) {
object.geometry.dispose() object.geometry.dispose();
if (Array.isArray(object.material)) { if (Array.isArray(object.material)) {
object.material.forEach((m) => m.dispose()) object.material.forEach((m) => m.dispose());
} else { } else {
object.material.dispose() object.material.dispose();
} }
} }
if (object instanceof THREE.Points) { if (object instanceof THREE.Points) {
object.geometry.dispose() object.geometry.dispose();
;(object.material as THREE.Material).dispose() (object.material as THREE.Material).dispose();
} }
}) });
this.config.container.removeChild(this.renderer.domElement) this.config.container.removeChild(this.renderer.domElement);
} }
} }

6
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.css' {
const content: string
export default content
}

View File

@ -21,6 +21,7 @@
/* Paths */ /* Paths */
"baseUrl": ".", "baseUrl": ".",
"ignoreDeprecations": "6.0",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
} }