This release transforms PING into a sophisticated newspaper-style digital publication with enhanced readability and professional presentation. Major Features: - New FeaturedPostHero component with full-width newspaper design - Completely redesigned homepage with responsive newspaper grid layout - Enhanced PostCard component with refined typography and spacing - Improved mobile-first responsive design (mobile → tablet → desktop → 2XL) - Archive section with multi-column layout for deeper content discovery Technical Improvements: - Enhanced blog post validation and error handling in lib/blog.ts - Better date handling and normalization for scheduled posts - Improved Dockerfile with correct content volume mount paths - Fixed port configuration (3025 throughout stack) - Updated Tailwind config with refined typography and newspaper aesthetics - Added getFeaturedPost() function for hero selection UI/UX Enhancements: - Professional newspaper-style borders and dividers - Improved dark mode styling throughout - Better content hierarchy and visual flow - Enhanced author bylines and metadata presentation - Refined color palette with newspaper sophistication Documentation: - Added DESIGN_BRIEF_NEWSPAPER_LAYOUT.md detailing design principles - Added TESTING_RESULTS_25_POSTS.md with test scenarios This release establishes PING as a premium publication platform for AI orchestration and contextual intelligence thought leadership. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
237 lines
7.7 KiB
TypeScript
237 lines
7.7 KiB
TypeScript
'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',
|
|
opacity: 0.5
|
|
}}
|
|
/>
|
|
);
|
|
} |