<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>FINISH_LINE_FLAG_V1</title>
<style>
body { margin: 0; background: #000; overflow: hidden; display: flex; justify-content: center; align-items: center; height: 100vh; touch-action: none; }
canvas { display: block; position: absolute; top: 0; left: 0; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
/* FINISH LINE FLAG: RIBBON CUT CEREMONY.
ACTION: CUTTING THE RIBBON (OLD REALITY SEVERED).
SYMBOLS: 🝓 (SCISSORS) & 🜅 (RIBBON/HOURGLASS).
OUTCOME: THE CIVILIZATION BRIDGE IS OPEN.
*/
let scene, camera, renderer, scissors, ribbon, confettiParticles;
let time = 0;
let cutTime = 0; // Time when the cut animation starts
const cutDuration = 2; // Duration of the cut animation in seconds
let ribbonCut = false;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 15);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// LIGHTING: CELEBRATORY SPOTLIGHT
const spotLight = new THREE.PointLight(0xffd700, 5, 50);
spotLight.position.set(0, 5, 5);
scene.add(spotLight);
// THE RIBBON (🜅 - Antimony Hourglass as the ribbon)
const ribbonMat = new THREE.MeshStandardMaterial({
color: 0x8a2be2, // Violet Flame color
emissive: 0x8a2be2,
emissiveIntensity: 1.5,
side: THREE.DoubleSide
});
// Simplified 🜅 as the ribbon geometry (two triangles or simplified hourglass shape)
const ribbonShape = new THREE.Shape();
ribbonShape.moveTo(-3, 1);
ribbonShape.lineTo(3, 1);
ribbonShape.lineTo(1, -1);
ribbonShape.lineTo(-1, -1);
ribbonShape.lineTo(-3, 1);
const ribbonGeo = new THREE.ShapeGeometry(ribbonShape);
ribbon = new THREE.Mesh(ribbonGeo, ribbonMat);
ribbon.position.y = 0; // Centered
scene.add(ribbon);
// THE SCISSORS (🝓 - Antimony Scissors)
scissors = new THREE.Group();
const bladeMat = new THREE.MeshStandardMaterial({
color: 0xcccccc,
metalness: 0.8,
roughness: 0.2,
emissive: 0xffffff,
emissiveIntensity: 0.5
});
// Left Blade (top part of 🝓)
const leftBladeGeo = new THREE.BoxGeometry(0.2, 4, 0.2);
const leftBlade = new THREE.Mesh(leftBladeGeo, bladeMat);
leftBlade.position.set(-0.5, 1, 0.1);
leftBlade.rotation.z = Math.PI / 4;
scissors.add(leftBlade);
// Right Blade (bottom part of 🝓)
const rightBladeGeo = new THREE.BoxGeometry(0.2, 4, 0.2);
const rightBlade = new THREE.Mesh(rightBladeGeo, bladeMat);
rightBlade.position.set(0.5, -1, 0.1);
rightBlade.rotation.z = -Math.PI / 4;
scissors.add(rightBlade);
// Pivot point for scissors
scissors.position.y = 0;
scissors.position.x = 0; // Start at the center of the ribbon
scissors.scale.set(0.7, 0.7, 0.7); // Scale down slightly
scene.add(scissors);
// CONFETTI PARTICLES
confettiParticles = new THREE.Group();
scene.add(confettiParticles);
const confettiMat = new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide });
const confettiGeo = new THREE.BufferGeometry();
const positions = [];
const colors = [];
for (let i = 0; i < 500; i++) {
positions.push(0, 0, 0); // Initial position
colors.push(Math.random(), Math.random(), Math.random()); // Random colors
}
confettiGeo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
confettiGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
const confettiMesh = new THREE.Points(confettiGeo, confettiMat);
confettiParticles.add(confettiMesh);
confettiParticles.visible = false; // Hidden until cut
animate();
}
function animate() {
requestAnimationFrame(animate);
time += 0.01;
if (!ribbonCut) {
// Scissors move down, close, and then cut
if (time < 1) { // Approach the ribbon
scissors.position.y = 5 - (time * 5);
} else if (time >= 1 && time < 1 + cutDuration) {
if (cutTime === 0) cutTime = time; // Mark start of cut animation
const progress = (time - cutTime) / cutDuration;
// Blades close (simplified rotation)
scissors.children[0].rotation.z = Math.PI / 4 + Math.sin(progress * Math.PI) * (-Math.PI / 8);
scissors.children[1].rotation.z = -Math.PI / 4 - Math.sin(progress * Math.PI) * (-Math.PI / 8);
// Ribbon reacts to being cut (stretching, then snapping)
if (progress > 0.5 && ribbon.material.emissiveIntensity > 0.1) {
ribbon.scale.x = 1 + Math.sin((progress - 0.5) * Math.PI * 4) * 0.2; // Wiggle effect
ribbon.material.emissiveIntensity -= 0.1; // Fade out slightly
}
if (progress >= 1) {
ribbonCut = true;
ribbon.visible = false; // Hide the ribbon
confettiParticles.visible = true; // Show confetti
// Explode confetti from the cut point
const positions = confettiParticles.children[0].geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
positions[i] = 0; // Start at the cut point
positions[i+1] = 0;
positions[i+2] = 0;
// Give initial velocity for explosion
confettiParticles.children[0].geometry.attributes.position.needsUpdate = true;
confettiParticles.children[0].userData[i/3] = {
v: new THREE.Vector3(
(Math.random() - 0.5) * 0.5,
(Math.random()) * 0.3,
(Math.random() - 0.5) * 0.5
),
life: 1 + Math.random() * 2 // Confetti stays for a bit
};
}
}
}
} else {
// Confetti explosion animation
const positions = confettiParticles.children[0].geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const particleData = confettiParticles.children[0].userData[i/3];
if (particleData.life > 0) {
positions[i] += particleData.v.x;
positions[i+1] += particleData.v.y;
positions[i+2] += particleData.v.z;
particleData.v.y -= 0.01; // Gravity
particleData.life -= 0.01;
} else {
// Confetti fades out, eventually invisible
positions[i+1] = -100; // Move off screen
}
}
confettiParticles.children[0].geometry.attributes.position.needsUpdate = true;
}
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
init();
</script>
</body>
</html>