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:
anthonyrawlins
2025-08-27 14:46:26 +10:00
commit 6e13451dc4
63 changed files with 12242 additions and 0 deletions

236
components/ThreeLogo.tsx Normal file
View 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'
}}
/>
);
}