- Add complete Iconography section with Coolicons v4.1 integration - Implement theme-adaptive icons (black for light mode, white for dark mode) - Add Visual Aid modal dialog for accessibility settings - Replace theme toggle with semantic moon/sun icons - Add personality trait icons with appropriate semantic choices - Fix code block theming to respect light/dark mode toggle - Include comprehensive icon categories: Interface, File/Data, Communication, Navigation - Add detailed implementation guides for HTML, SVG, and Tailwind - Create accessibility-aware color system with vision deficiency support - Add Inconsolata and Inter Tight fonts for complete typography system 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
239 lines
7.4 KiB
HTML
239 lines
7.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>CHORUS</title>
|
|
<style>
|
|
body { margin: 0; overflow: hidden; background-color: #5E6367; }
|
|
canvas { display: block; }
|
|
</style>
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&family=Luckiest+Guy&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
|
|
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.19/+esm"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
</head>
|
|
<body>
|
|
<script type="module">
|
|
import GUI from 'lil-gui';
|
|
|
|
import * as THREE from 'three';
|
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
|
|
|
|
// Initialize CSS2DRenderer
|
|
const labelRenderer = new CSS2DRenderer();
|
|
labelRenderer.setSize(window.innerWidth, window.innerHeight);
|
|
labelRenderer.domElement.style.position = 'absolute';
|
|
labelRenderer.domElement.style.top = '0px';
|
|
labelRenderer.domElement.style.pointerEvents = 'none'; // allows clicks to pass through
|
|
document.body.appendChild(labelRenderer.domElement);
|
|
|
|
|
|
// === Animation parameters ===
|
|
const spinSpeed = 0.003; // radians per frame
|
|
|
|
// 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,
|
|
};
|
|
|
|
|
|
let scene, camera, renderer, mobius, clock;
|
|
let baseRotationY = 0;
|
|
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
scene = new THREE.Scene();
|
|
|
|
const material = new THREE.MeshPhysicalMaterial({
|
|
color: 0xFFFFFF,
|
|
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,
|
|
});
|
|
|
|
// Add scattered lights
|
|
addRandomLights();
|
|
|
|
// Generate a synthetic cube environment
|
|
const size = 512;
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
// Gradient: sunset horizon (bottom warm, top cool)
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
|
gradient.addColorStop(0, '#001133'); // top dark blue
|
|
gradient.addColorStop(0.4, '#223366'); // mid blue
|
|
gradient.addColorStop(0.5, '#ff8844'); // orange near horizon
|
|
gradient.addColorStop(0.51, '#000000'); // black horizon
|
|
gradient.addColorStop(0.8, '#105010'); // green base
|
|
gradient.addColorStop(1, '#000000'); // black base
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
|
|
// Apply as environment
|
|
scene.environment = texture;
|
|
scene.background = null; // keep transparent
|
|
|
|
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);
|
|
}
|
|
|
|
const envloader = new THREE.ImageLoader();
|
|
envloader.load('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;
|
|
});
|
|
|
|
|
|
|
|
const textDiv = document.createElement('div');
|
|
textDiv.textContent = "CHORUS";
|
|
textDiv.style.color = "#FFFFFF";
|
|
textDiv.style.fontSize = "96px";
|
|
textDiv.style.fontFamily = "Exo, sans-serif";
|
|
textDiv.style.textAlign = "left";
|
|
|
|
// Create CSS2DObject and position it at the origin
|
|
const textLabel = new CSS2DObject(textDiv);
|
|
textLabel.position.set(0, 0, 0); // exact origin
|
|
scene.add(textLabel);
|
|
|
|
camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 100);
|
|
camera.position.set(0, 0, 1.8);
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
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); // (color, intensity, distance)
|
|
bottomLight.position.set(0, -4, 1); // directly under the model
|
|
scene.add(bottomLight);
|
|
|
|
const leftLight = new THREE.PointLight(0x808000, 1.45, 5); // (color, intensity, distance)
|
|
leftLight.position.set(-5, 0, 4); // top left of the model
|
|
scene.add(leftLight);
|
|
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.45));
|
|
|
|
const loader = new GLTFLoader();
|
|
|
|
const dracoLoader = new DRACOLoader();
|
|
dracoLoader.setDecoderPath( 'https://www.gstatic.com/draco/v1/decoders/' );
|
|
loader.setDRACOLoader( dracoLoader );
|
|
loader.load(
|
|
"./mobius-ring.glb", // ensure mobius.glb is in the same folder as this HTML
|
|
(gltf) => {
|
|
mobius = gltf.scene;
|
|
mobius.traverse((child) => {
|
|
if (child.isMesh) {
|
|
child.material = material;
|
|
}
|
|
});
|
|
scene.add(mobius);
|
|
},
|
|
undefined,
|
|
(err) => console.error("Error loading model:", err)
|
|
);
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setClearColor(0x000000, 0); // transparent background
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
clock = new THREE.Clock();
|
|
window.addEventListener("resize", onWindowResize);
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
labelRenderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
|
|
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
if (mobius) {
|
|
const elapsed = clock.getElapsedTime();
|
|
|
|
// Continuous spin
|
|
mobius.rotation.x += params.spinSpeedX;
|
|
mobius.rotation.y += params.spinSpeedY;
|
|
mobius.rotation.z += params.spinSpeedZ;
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
labelRenderer.render(scene, camera);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|