Initial commit: CHORUS PING! blog
- Next.js 14 blog application with theme support - Docker containerization with volume bindings - Traefik integration with Let's Encrypt SSL - MDX support for blog posts - Theme toggle with localStorage persistence - Scheduled posts directory structure - Brand guidelines compliance with CHORUS colors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
236
components/ThreeLogo.tsx
Normal file
236
components/ThreeLogo.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
||||
|
||||
interface ThreeLogoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ThreeLogo({ className = "" }: ThreeLogoProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
// Scene / Renderer / Camera
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
|
||||
camera.position.set(0, 0, 3.0); // Move camera back to prevent clipping
|
||||
camera.lookAt(0, 0, 0); // Ensure camera looks at exact center
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setClearColor(0x000000, 0); // transparent background
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// Resize handling with proper aspect ratio
|
||||
const resize = () => {
|
||||
const { clientWidth, clientHeight } = container;
|
||||
// Ensure square aspect ratio for the logo
|
||||
const size = Math.min(clientWidth, clientHeight);
|
||||
renderer.setSize(size, size, false);
|
||||
camera.aspect = 1; // Always square
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
// Exact lighting setup from your reference logo.html
|
||||
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));
|
||||
|
||||
// Load environment map from your horizon gradient image
|
||||
const expandGradient = (img: HTMLImageElement) => {
|
||||
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 narrow gradient strip to fill the canvas
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
};
|
||||
|
||||
const envLoader = new THREE.ImageLoader();
|
||||
envLoader.load('/logos/horizon-gradient.png', (image) => {
|
||||
const tex = expandGradient(image);
|
||||
tex.mapping = THREE.EquirectangularReflectionMapping;
|
||||
|
||||
// Generate proper environment map using PMREM
|
||||
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||
const envMap = pmrem.fromEquirectangular(tex).texture;
|
||||
|
||||
scene.environment = envMap;
|
||||
pmrem.dispose();
|
||||
});
|
||||
|
||||
scene.background = null; // keep transparent
|
||||
|
||||
// Theme-aware material colors
|
||||
const getThemeMaterial = (theme: string = 'default') => {
|
||||
const colorMap = {
|
||||
'default': 0x333333,
|
||||
'protanopia': 0x1e40af, // Blue-800 for red-blind
|
||||
'deuteranopia': 0x6b21a8, // Purple-800 for green-blind
|
||||
'tritanopia': 0x991b1b, // Red-800 for blue-blind
|
||||
'achromatopsia': 0x374151, // Gray-700 for color-blind
|
||||
};
|
||||
|
||||
return new THREE.MeshPhysicalMaterial({
|
||||
color: colorMap[theme as keyof typeof colorMap] || colorMap.default,
|
||||
roughness: 0.24,
|
||||
metalness: 1.0,
|
||||
clearcoat: 0.48,
|
||||
clearcoatRoughness: 0.15,
|
||||
reflectivity: 1.2,
|
||||
sheen: 0.35,
|
||||
sheenColor: new THREE.Color(0x212121),
|
||||
sheenRoughness: 0.168,
|
||||
envMapIntensity: 1,
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize with default material
|
||||
let currentMaterial = getThemeMaterial('default');
|
||||
|
||||
// Load GLB with DRACO support
|
||||
const loader = new GLTFLoader();
|
||||
const draco = new DRACOLoader();
|
||||
draco.setDecoderPath('/draco/');
|
||||
loader.setDRACOLoader(draco);
|
||||
|
||||
let model: THREE.Object3D | null = null;
|
||||
|
||||
console.log('Loading your mobius-ring.glb...');
|
||||
loader.load(
|
||||
'/logos/mobius-ring.glb',
|
||||
(gltf) => {
|
||||
console.log('🎉 Your GLB loaded successfully!', gltf);
|
||||
model = gltf.scene;
|
||||
|
||||
// Apply the theme-aware material
|
||||
model.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
(child as THREE.Mesh).material = currentMaterial;
|
||||
}
|
||||
});
|
||||
|
||||
// Center the model exactly at origin
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
model.position.sub(center); // Move model so its center is at (0,0,0)
|
||||
|
||||
scene.add(model);
|
||||
console.log('🎯 Your Möbius GLB model added to scene and centered at origin');
|
||||
},
|
||||
(progress) => {
|
||||
if (progress.total > 0) {
|
||||
const percent = Math.round(progress.loaded / progress.total * 100);
|
||||
console.log(`GLB loading progress: ${percent}% (${progress.loaded}/${progress.total} bytes)`);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
console.error('❌ GLB load error:', err);
|
||||
|
||||
// Fallback: create a torus geometry with the theme-aware material
|
||||
console.log('Creating fallback torus geometry...');
|
||||
const fallbackGeometry = new THREE.TorusGeometry(0.6, 0.2, 16, 100);
|
||||
const fallbackMesh = new THREE.Mesh(fallbackGeometry, currentMaterial);
|
||||
fallbackMesh.position.set(0, 0, 0); // Ensure fallback is also centered
|
||||
scene.add(fallbackMesh);
|
||||
model = fallbackMesh;
|
||||
|
||||
console.log('⚠️ Fallback torus geometry created (placeholder for your GLB)');
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for accessibility theme changes
|
||||
const handleThemeChange = (event: any) => {
|
||||
const newTheme = event.detail.theme;
|
||||
const newMaterial = getThemeMaterial(newTheme);
|
||||
|
||||
if (model) {
|
||||
// Update material on all meshes
|
||||
model.traverse((child: any) => {
|
||||
if (child.isMesh) {
|
||||
child.material.dispose(); // Clean up old material
|
||||
child.material = newMaterial;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update current material reference
|
||||
currentMaterial = newMaterial;
|
||||
console.log(`🎨 Logo theme changed to: ${newTheme}`);
|
||||
};
|
||||
|
||||
window.addEventListener('accessibilityThemeChanged', handleThemeChange);
|
||||
|
||||
let raf = 0;
|
||||
const tick = () => {
|
||||
raf = requestAnimationFrame(tick);
|
||||
|
||||
if (model) {
|
||||
// Use exact rotation parameters from your reference logo.html
|
||||
model.rotation.x += 0.010; // spinSpeedX from params
|
||||
model.rotation.y += -0.010; // spinSpeedY from params
|
||||
model.rotation.z += -0.1; // spinSpeedZ from params
|
||||
}
|
||||
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
tick();
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener('resize', resize);
|
||||
window.removeEventListener('accessibilityThemeChanged', handleThemeChange);
|
||||
renderer.dispose();
|
||||
draco.dispose();
|
||||
if (container.contains(renderer.domElement)) {
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
scene.traverse((obj: any) => {
|
||||
if (obj.geometry) obj.geometry.dispose?.();
|
||||
if (obj.material) {
|
||||
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
||||
mats.forEach((m: any) => m.dispose?.());
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
aspectRatio: '1 / 1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user