import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; // Logo instances storage const logoInstances = new Map(); // Define materials for both themes and accessibility modes let darkMaterial, lightMaterial; let accessibilityMaterials = {}; let accessibilityMaterialsDark = {}; // Animation parameters (now in an object for GUI control) const params = { spinSpeedX: 0.01, // continuous X spin spinSpeedY: 0.01, // continuous X spin spinSpeedZ: 0.1, // continuous Z spin lightCount: 25, }; function expandGradient(img) { const canvas = document.createElement('canvas'); const w = 512, h = 256; // safe HDRI-like size canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); // Stretch the 1px-wide strip to fill the canvas ctx.drawImage(img, 0, 0, w, h); return new THREE.CanvasTexture(canvas); } function createMaterials() { console.log("Creating materials..."); darkMaterial = new THREE.MeshPhysicalMaterial({ color: 0x453d2e, // Sand color for dark theme roughness: 0.24, metalness: 1.0, clearcoat: 0.48, clearcoatRoughness: 0.15, reflectivity: 1.2, sheen: 0.35, sheenColor: new THREE.Color(0xc1bfb1), // Brushed Nickel sheenRoughness: 0.168, envMapIntensity: 1, }); lightMaterial = new THREE.MeshPhysicalMaterial({ color: 0x0b0213, // Dark Mulberry for light theme roughness: 0.28, metalness: 0.98, clearcoat: 0.52, clearcoatRoughness: 0.12, reflectivity: 1.0, sheen: 0.25, sheenColor: new THREE.Color(0x403730), // Walnut Brown sheenRoughness: 0.2, envMapIntensity: 0.8, }); // CHORUS 8-Color Accessibility Materials - Matching CSS system accessibilityMaterials.protanopia = new THREE.MeshPhysicalMaterial({ color: 0x2a3441, // ocean-950 (matching --chorus-danger for protanopia) roughness: 0.26, metalness: 0.95, clearcoat: 0.5, clearcoatRoughness: 0.14, reflectivity: 1.1, sheen: 0.3, sheenColor: new THREE.Color(0x473e2f), // sand-950 (matching --chorus-success for protanopia) sheenRoughness: 0.18, envMapIntensity: 0.9, }); accessibilityMaterials.deuteranopia = new THREE.MeshPhysicalMaterial({ color: 0x2a3441, // ocean-950 (matching --chorus-success for deuteranopia) roughness: 0.25, metalness: 0.96, clearcoat: 0.49, clearcoatRoughness: 0.13, reflectivity: 1.15, sheen: 0.32, sheenColor: new THREE.Color(0x2e1d1c), // coral-950 (matching --chorus-accent for deuteranopia) sheenRoughness: 0.17, envMapIntensity: 0.92, }); accessibilityMaterials.tritanopia = new THREE.MeshPhysicalMaterial({ color: 0x2e1d1c, // coral-950 (matching --chorus-info for tritanopia) roughness: 0.27, metalness: 0.94, clearcoat: 0.51, clearcoatRoughness: 0.15, reflectivity: 1.05, sheen: 0.28, sheenColor: new THREE.Color(0x2a3330), // eucalyptus-950 (matching --chorus-warning for tritanopia) sheenRoughness: 0.19, envMapIntensity: 0.88, }); accessibilityMaterials.achromatopsia = new THREE.MeshPhysicalMaterial({ color: 0x111111, // Almost black (matching --chorus-danger for achromatopsia) roughness: 0.3, metalness: 0.9, clearcoat: 0.45, clearcoatRoughness: 0.16, reflectivity: 1.0, sheen: 0.25, sheenColor: new THREE.Color(0x444444), // Dark medium (matching --chorus-info for achromatopsia) sheenRoughness: 0.2, envMapIntensity: 0.85, }); // Create dark mode variants for accessibility materials accessibilityMaterialsDark.protanopia = new THREE.MeshPhysicalMaterial({ color: 0xcbefff, // ocean-50 (matching dark mode --chorus-danger for protanopia) roughness: 0.24, metalness: 1.0, clearcoat: 0.48, clearcoatRoughness: 0.15, reflectivity: 1.2, sheen: 0.35, sheenColor: new THREE.Color(0xcee1be), // sand-50 (matching dark mode --chorus-success for protanopia) sheenRoughness: 0.168, envMapIntensity: 1, }); accessibilityMaterialsDark.deuteranopia = new THREE.MeshPhysicalMaterial({ color: 0xcbefff, // ocean-50 (matching dark mode --chorus-success for deuteranopia) roughness: 0.24, metalness: 1.0, clearcoat: 0.48, clearcoatRoughness: 0.15, reflectivity: 1.2, sheen: 0.35, sheenColor: new THREE.Color(0xffd6d6), // coral-50 (matching dark mode --chorus-accent for deuteranopia) sheenRoughness: 0.168, envMapIntensity: 1, }); accessibilityMaterialsDark.tritanopia = new THREE.MeshPhysicalMaterial({ color: 0xffd6d6, // coral-50 (matching dark mode --chorus-info for tritanopia) roughness: 0.24, metalness: 1.0, clearcoat: 0.48, clearcoatRoughness: 0.15, reflectivity: 1.2, sheen: 0.35, sheenColor: new THREE.Color(0xbacfbf), // eucalyptus-50 (matching dark mode --chorus-warning for tritanopia) sheenRoughness: 0.168, envMapIntensity: 1, }); accessibilityMaterialsDark.achromatopsia = new THREE.MeshPhysicalMaterial({ color: 0xeeeeee, // Almost white (matching dark mode --chorus-danger for achromatopsia) roughness: 0.24, metalness: 1.0, clearcoat: 0.48, clearcoatRoughness: 0.15, reflectivity: 1.2, sheen: 0.35, sheenColor: new THREE.Color(0xbbbbbb), // Light medium (matching dark mode --chorus-info for achromatopsia) sheenRoughness: 0.168, envMapIntensity: 1, }); console.log("Materials created including accessibility variants and dark mode variants"); } function initLogo(canvas) { const scene = new THREE.Scene(); // Create materials if not created yet if (!darkMaterial || !lightMaterial) { createMaterials(); } function addRandomLights() { for (let i = 0; i < params.lightCount; i++) { const color = new THREE.Color().setHSL(Math.random(), 0.84, 0.9); const light = new THREE.PointLight(color, params.lightIntensity, params.lightDistance); const angle = Math.random() * Math.PI * 2; const height = (Math.random() - 0.5) * 4; const radius = 6 + Math.random() * 4; light.position.set(Math.cos(angle) * radius, height, Math.sin(angle) * radius); scene.add(light); } } addRandomLights(); const envloader = new THREE.ImageLoader(); envloader.load('logos/horizon-gradient.png', (image) => { const tex = expandGradient(image); tex.mapping = THREE.EquirectangularReflectionMapping; // ✅ safe to feed into PMREM const pmrem = new THREE.PMREMGenerator(renderer); const envMap = pmrem.fromEquirectangular(tex).texture; scene.environment = envMap; }); // Get canvas dimensions - ensure square for proper Möbius ring rendering const computedStyle = window.getComputedStyle(canvas); let width = parseInt(computedStyle.width) || canvas.width || 200; let height = parseInt(computedStyle.height) || canvas.height || 200; // Force square dimensions for proper Möbius ring rendering const size = Math.max(width, height); width = size; height = size; // Initialize renderer const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); renderer.setClearColor(0x000000, 0); // Set up camera const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100); camera.position.set(0, 0, 1.8); camera.lookAt(0, 0, 0); // Lighting setup const light = new THREE.PointLight(0xffffff, 1.4); light.position.set(0, 4, 1); scene.add(light); const bottomLight = new THREE.PointLight(0x800080, 1.2, 12); bottomLight.position.set(0, -4, 1); scene.add(bottomLight); const leftLight = new THREE.PointLight(0x808000, 1.45, 5); leftLight.position.set(-5, 0, 4); scene.add(leftLight); scene.add(new THREE.AmbientLight(0xffffff, 0.45)); let mobius = null; // Load the Möbius ring model const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/'); loader.setDRACOLoader(dracoLoader); loader.load( "logos/mobius-ring.glb", (gltf) => { console.log("GLB loaded successfully for canvas:", canvas); mobius = gltf.scene; mobius.traverse((child) => { if (child.isMesh) { // Start with dark theme material (default) child.material = darkMaterial.clone(); } }); scene.add(mobius); // Initialize with current theme state const isDark = document.documentElement.classList.contains('dark'); updateCanvasMaterial(mobius, isDark); }, (progress) => { console.log("Loading progress:", progress); }, (err) => console.error("Error loading 3D logo:", err) ); // Animation loop function animate() { requestAnimationFrame(animate); if (mobius) { // Gentle rotation mobius.rotation.x += 0.01; mobius.rotation.y += -0.01; mobius.rotation.z += -0.1; } renderer.render(scene, camera); } animate(); // Store instance for theme updates - mobius will be set later when GLB loads const instance = { canvas, renderer, scene, camera, mobius: () => mobius, // Use function to get current mobius reference setMobius: (mobiusScene) => { mobius = mobiusScene; } // Allow setting mobius later }; logoInstances.set(canvas, instance); return instance; } function updateCanvasMaterial(mobius, isDark) { if (mobius) { // Check for accessibility theme first (both data-theme for legacy and data-accessibility for new system) const accessibilityTheme = document.documentElement.getAttribute('data-accessibility') || document.documentElement.getAttribute('data-theme'); let targetMaterial; if (accessibilityTheme && accessibilityTheme !== 'standard') { // Choose accessibility material based on light/dark mode const accessibilityMaterialSet = isDark ? accessibilityMaterialsDark : accessibilityMaterials; if (accessibilityMaterialSet[accessibilityTheme]) { targetMaterial = accessibilityMaterialSet[accessibilityTheme]; console.log(`Updating material to ${isDark ? 'dark' : 'light'} accessibility theme: ${accessibilityTheme}`); } else { // Fallback to standard materials targetMaterial = isDark ? darkMaterial : lightMaterial; console.log(`Accessibility material not found for ${accessibilityTheme}, using standard ${isDark ? 'dark' : 'light'} material`); } } else { // Standard materials targetMaterial = isDark ? darkMaterial : lightMaterial; console.log(`Updating material to standard:`, isDark ? 'dark' : 'light', 'material color:', targetMaterial.color.getHex().toString(16)); } let meshCount = 0; mobius.traverse((child) => { if (child.isMesh) { child.material = targetMaterial.clone(); meshCount++; } }); console.log(`Updated ${meshCount} mesh materials`); } } // Function to update all logo materials based on theme function updateAllLogoMaterials(isDark) { console.log(`updateAllLogoMaterials called with isDark: ${isDark}`); console.log(`Number of logo instances: ${logoInstances.size}`); logoInstances.forEach((instance, canvas) => { const mobius = instance.mobius(); console.log(`Updating logo instance for canvas:`, canvas, `mobius exists:`, !!mobius); if (mobius) { updateCanvasMaterial(mobius, isDark); } }); } // Initialize all canvas elements with chorus-logo class function initAllLogos() { document.querySelectorAll("canvas.chorus-logo").forEach((canvas) => { initLogo(canvas); }); } // Function to update logos when accessibility theme changes function updateLogoAccessibilityTheme(themeName) { console.log(`updateLogoAccessibilityTheme called with theme: ${themeName}`); logoInstances.forEach((instance, canvas) => { const mobius = instance.mobius(); if (mobius) { // Always call updateCanvasMaterial which will check for accessibility theme const isDark = document.documentElement.classList.contains('dark'); updateCanvasMaterial(mobius, isDark); } }); } // Export functions for global access window.updateAllLogoMaterials = updateAllLogoMaterials; window.updateLogoAccessibilityTheme = updateLogoAccessibilityTheme; window.initAllLogos = initAllLogos; // Initialize when DOM is loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initAllLogos(); console.log('Logo system initialized, updateAllLogoMaterials available:', typeof window.updateAllLogoMaterials); }); } else { initAllLogos(); console.log('Logo system initialized immediately, updateAllLogoMaterials available:', typeof window.updateAllLogoMaterials); }