Files
chorus-services/modules/teaser/public/logos/logo-test.html
tony c8fb816775 feat: Add CHORUS teaser website with mobile-responsive design
- Created complete Next.js 15 teaser website with CHORUS brand styling
- Implemented mobile-responsive 3D logo (128px mobile, 512px desktop)
- Added proper Exo font loading via Next.js Google Fonts for iOS/Chrome compatibility
- Built comprehensive early access form with GDPR compliance and rate limiting
- Integrated PostgreSQL database with complete schema for lead capture
- Added scroll indicators that auto-hide when scrolling begins
- Optimized mobile modal forms with proper scrolling and submit button access
- Deployed via Docker Swarm with Traefik SSL termination at chorus.services
- Includes database migrations, consent tracking, and email notifications

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 13:57:30 +10:00

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>