From 48df332993e5406b2101238388a78b382feaf6ce Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 13 May 2026 20:10:20 -0500 Subject: [PATCH 1/4] feat(only the globe spins) --- app/components/sponsors/ReunionTower.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/components/sponsors/ReunionTower.tsx b/app/components/sponsors/ReunionTower.tsx index d7b3dd3..b7b3749 100644 --- a/app/components/sponsors/ReunionTower.tsx +++ b/app/components/sponsors/ReunionTower.tsx @@ -162,7 +162,7 @@ function SponsorPlane({ // ── Tower model with sponsor logos in globe triangles ──────── function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerSceneProps) { const { scene } = useGLTF(MODEL_PATH); - const groupRef = useRef(null); + const globeRef = useRef(null); const smoothRotation = useRef(0); const autoAngle = useRef(0); @@ -179,6 +179,15 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr return { normScale: s, centerY: center.y * s }; }, [clonedScene]); + // Find the PlatonicSphere node (the actual globe ball) to rotate independently + useEffect(() => { + clonedScene.traverse((child) => { + if (child.name === "PlatonicSphere") { + globeRef.current = child; + } + }); + }, [clonedScene]); + // Extract globe triangle faces for sponsor placement const sponsorFaces = useMemo(() => { // Update world matrices before scanning @@ -194,7 +203,7 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr ); useFrame((_, delta) => { - if (!groupRef.current) return; + if (!globeRef.current) return; autoAngle.current += delta * 0.3; const scrollAngle = (scrollProgressRef.current ?? 0) * Math.PI * 2; @@ -202,11 +211,12 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr const target = autoAngle.current + scrollAngle + dragAngle; smoothRotation.current += (target - smoothRotation.current) * 0.08; - groupRef.current.rotation.y = smoothRotation.current; + // Only rotate the PlatonicSphere node — the tower body stays still + globeRef.current.rotation.y = smoothRotation.current; }); return ( - + ); -} +} \ No newline at end of file From d7b01b197b678bba9d38c1106bfa474be0f810ad Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 13 May 2026 20:53:36 -0500 Subject: [PATCH 2/4] feat(logos spin again and increases zoom) --- app/components/sponsors/ReunionTower.tsx | 50 ++++++++++++++---------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/app/components/sponsors/ReunionTower.tsx b/app/components/sponsors/ReunionTower.tsx index b7b3749..6fd3a73 100644 --- a/app/components/sponsors/ReunionTower.tsx +++ b/app/components/sponsors/ReunionTower.tsx @@ -163,6 +163,7 @@ function SponsorPlane({ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerSceneProps) { const { scene } = useGLTF(MODEL_PATH); const globeRef = useRef(null); + const sponsorGroupRef = useRef(null); const smoothRotation = useRef(0); const autoAngle = useRef(0); @@ -175,7 +176,7 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr const center = new THREE.Vector3(); box.getSize(size); box.getCenter(center); - const s = 200 / size.y; + const s = 350 / size.y; return { normScale: s, centerY: center.y * s }; }, [clonedScene]); @@ -211,8 +212,13 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr const target = autoAngle.current + scrollAngle + dragAngle; smoothRotation.current += (target - smoothRotation.current) * 0.08; - // Only rotate the PlatonicSphere node — the tower body stays still - globeRef.current.rotation.y = smoothRotation.current; + + // 1. Inside part (sponsors) rotates in the original direction + if (sponsorGroupRef.current) { + sponsorGroupRef.current.rotation.y = smoothRotation.current; + } + // 2. Globe rotates in the opposite direction at half speed + globeRef.current.rotation.y = -smoothRotation.current * 0.5; }); return ( @@ -222,16 +228,18 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr scale={[normScale, normScale, normScale]} position={[0, -centerY, 0]} /> - {/* Sponsor logos placed in globe triangle gaps */} - {sponsorFaces.map((face, i) => { - const sponsor = validSponsors[i % validSponsors.length]; - if (!sponsor?.logo) return null; - return ( - - - - ); - })} + {/* Sponsor group handles the primary rotation */} + + {sponsorFaces.map((face, i) => { + const sponsor = validSponsors[i % validSponsors.length]; + if (!sponsor?.logo) return null; + return ( + + + + ); + })} + ); } @@ -242,17 +250,17 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr function CameraRig({ scrollProgressRef }: { scrollProgressRef: React.RefObject }) { const { camera } = useThree(); const smoothZ = useRef(20); - const smoothY = useRef(97); - const smoothLookY = useRef(94); + const smoothY = useRef(155); + const smoothLookY = useRef(150); useFrame(() => { const p = scrollProgressRef.current ?? 0; - // p=0: lookAt Y=94, visible top = 94+10.4 = 104.4, globe top Y=100 → 4.4 units headroom - // p=1: pulled back to see full tower - const targetZ = 20 + p * 170; - const targetY = 97 - p * 67; - const targetLookY = 94 - p * 84; + // Values recalculated for a base scale of 320 (1.6x the original 200) + // targetZ at p=1 moves from 190 to 304 to maintain vertical framing while increasing width + const targetZ = 20 + p * 284; + const targetY = 155 - p * 107; + const targetLookY = 150 - p * 134; smoothZ.current += (targetZ - smoothZ.current) * 0.06; smoothY.current += (targetY - smoothY.current) * 0.06; @@ -289,7 +297,7 @@ export default function ReunionTower({ stencil: false, depth: true, }} - camera={{ position: [0, 97, 20], fov: 55, near: 0.1, far: 1000 }} + camera={{ position: [0, 155, 20], fov: 55, near: 0.1, far: 2000 }} style={{ background: "transparent", touchAction: "pan-y" }} frameloop="always" > From 2384a2c3fc5d0179770fc8f27300039104a303de Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Wed, 13 May 2026 23:48:04 -0500 Subject: [PATCH 3/4] feat(better sponsor distribution) --- app/components/sponsors/ReunionTower.tsx | 153 ++++++++--------------- 1 file changed, 49 insertions(+), 104 deletions(-) diff --git a/app/components/sponsors/ReunionTower.tsx b/app/components/sponsors/ReunionTower.tsx index 6fd3a73..41ba61d 100644 --- a/app/components/sponsors/ReunionTower.tsx +++ b/app/components/sponsors/ReunionTower.tsx @@ -25,102 +25,35 @@ interface TowerSceneProps { sponsors: SponsorData[]; } -// ── Extract triangle faces from globe region of model ─────── -function extractGlobeTriangles( - scene: THREE.Object3D, - scale: number, - centerY: number -): TriFace[] { +// ── Procedurally generate band positions around the globe ── +function generateBandPositions(sponsorsCount: number): TriFace[] { + const bands = 4; + const globeCenterY = 125; // Centered near the top of the 350-unit tall model + const radius = 42; // Constant radius for cylindrical layout const faces: TriFace[] = []; - const box = new THREE.Box3().setFromObject(scene); - const modelHeight = (box.max.y - box.min.y) * scale; - // Globe is top ~30% of the tower - const globeThresholdY = (box.min.y + (box.max.y - box.min.y) * 0.65) * scale - centerY; - - scene.traverse((child) => { - if (!(child instanceof THREE.Mesh)) return; - const geo = child.geometry; - if (!geo || !geo.attributes.position) return; - - const pos = geo.attributes.position; - const index = geo.index; - const worldMatrix = child.matrixWorld.clone(); - - const triCount = index ? index.count / 3 : pos.count / 3; - const vA = new THREE.Vector3(); - const vB = new THREE.Vector3(); - const vC = new THREE.Vector3(); - - for (let i = 0; i < triCount; i++) { - if (index) { - vA.fromBufferAttribute(pos, index.getX(i * 3)); - vB.fromBufferAttribute(pos, index.getX(i * 3 + 1)); - vC.fromBufferAttribute(pos, index.getX(i * 3 + 2)); - } else { - vA.fromBufferAttribute(pos, i * 3); - vB.fromBufferAttribute(pos, i * 3 + 1); - vC.fromBufferAttribute(pos, i * 3 + 2); - } - - // Transform to world space then scale - vA.applyMatrix4(worldMatrix).multiplyScalar(scale); - vB.applyMatrix4(worldMatrix).multiplyScalar(scale); - vC.applyMatrix4(worldMatrix).multiplyScalar(scale); - - // Offset Y to match model positioning - vA.y -= centerY; - vB.y -= centerY; - vC.y -= centerY; - - const center = new THREE.Vector3() - .addVectors(vA, vB) - .add(vC) - .divideScalar(3); - - // Only faces in globe region - if (center.y < globeThresholdY) continue; - - // Compute face normal - const edge1 = new THREE.Vector3().subVectors(vB, vA); - const edge2 = new THREE.Vector3().subVectors(vC, vA); - const normal = new THREE.Vector3().crossVectors(edge1, edge2).normalize(); - - // Face size (area) - const area = new THREE.Vector3().crossVectors(edge1, edge2).length() * 0.5; - - // Skip tiny degenerate triangles - if (area < 0.01) continue; - - faces.push({ center, normal, size: Math.sqrt(area) * 0.6 }); + + const sponsorsPerBand = Math.ceil(sponsorsCount / bands); + const yOffsets = [-12, -4, 4, 12]; // Vertical distribution + + for (let b = 0; b < bands; b++) { + const y = globeCenterY + yOffsets[b]; + const r = radius; + + for (let i = 0; i < sponsorsPerBand; i++) { + if (faces.length >= sponsorsCount) break; + + const angle = (i / sponsorsPerBand) * Math.PI * 2; + const x = Math.cos(angle) * r; + const z = Math.sin(angle) * r; + + const center = new THREE.Vector3(x, y, z); + const normal = new THREE.Vector3(x, 0, z).normalize(); + + // Use a consistent size for all procedural "faces" + faces.push({ center, normal, size: 22 }); } - }); - - return faces; -} - -// ── Select well-spaced triangles for sponsor placement ────── -function selectSponsorFaces(faces: TriFace[], count: number): TriFace[] { - if (faces.length === 0) return []; - - // Sort by size descending — prefer larger triangles - const sorted = [...faces].sort((a, b) => b.size - a.size); - - const selected: TriFace[] = []; - const minDist = 1.2; // minimum distance between selected faces - - for (const face of sorted) { - if (selected.length >= count) break; - - // Check distance to already selected faces - const tooClose = selected.some( - (s) => s.center.distanceTo(face.center) < minDist - ); - if (tooClose) continue; - - selected.push(face); } - - return selected; + return faces; } // ── Single sponsor logo plane ─────────────────────────────── @@ -132,7 +65,7 @@ function SponsorPlane({ logoUrl: string; }) { const texture = useLoader(THREE.TextureLoader, logoUrl); - const meshRef = useRef(null); + const groupRef = useRef(null); const quaternion = useMemo(() => { const q = new THREE.Quaternion(); @@ -141,13 +74,27 @@ function SponsorPlane({ return q; }, [face.normal]); + const aspect = texture.image ? texture.image.width / texture.image.height : 1; + const logoWidth = face.size * 0.15 * aspect; + const logoHeight = face.size * 0.15; + + const bgWidth = face.size * 0.18 * aspect; + const bgHeight = face.size * 0.18; + return ( - - + {/* White background rectangle */} + + + + + {/* Logo plane */} + + - + + ); } @@ -191,11 +139,8 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr // Extract globe triangle faces for sponsor placement const sponsorFaces = useMemo(() => { - // Update world matrices before scanning - clonedScene.updateMatrixWorld(true); - const allFaces = extractGlobeTriangles(clonedScene, normScale, centerY); - return selectSponsorFaces(allFaces, sponsors.length); - }, [clonedScene, normScale, centerY, sponsors.length]); + return generateBandPositions(sponsors.length); + }, [sponsors.length]); // Filter sponsors to ones with valid logos const validSponsors = useMemo( @@ -229,7 +174,7 @@ function TowerModel({ scrollProgressRef, dragOffsetRef, sponsors }: TowerScenePr position={[0, -centerY, 0]} /> {/* Sponsor group handles the primary rotation */} - + {sponsorFaces.map((face, i) => { const sponsor = validSponsors[i % validSponsors.length]; if (!sponsor?.logo) return null; From 91a33bada636672ad58a93dcddcd6e181f99c1fa Mon Sep 17 00:00:00 2001 From: Noel Varghese <91435868+NoelVarghese2006@users.noreply.github.com> Date: Sat, 16 May 2026 13:40:24 -0500 Subject: [PATCH 4/4] curved sponsor + pink background (for debugging purposes) --- app/components/sponsors/ReunionTower.tsx | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/components/sponsors/ReunionTower.tsx b/app/components/sponsors/ReunionTower.tsx index 41ba61d..25f7e17 100644 --- a/app/components/sponsors/ReunionTower.tsx +++ b/app/components/sponsors/ReunionTower.tsx @@ -7,6 +7,8 @@ import * as THREE from "three"; const MODEL_PATH = "/models/reunion-tower.glb"; +const GLOBE_RADIUS = 36; + // ── Types ─────────────────────────────────────────────────── interface TriFace { center: THREE.Vector3; @@ -29,7 +31,7 @@ interface TowerSceneProps { function generateBandPositions(sponsorsCount: number): TriFace[] { const bands = 4; const globeCenterY = 125; // Centered near the top of the 350-unit tall model - const radius = 42; // Constant radius for cylindrical layout + const radius = GLOBE_RADIUS; // Constant radius for cylindrical layout const faces: TriFace[] = []; const sponsorsPerBand = Math.ceil(sponsorsCount / bands); @@ -81,20 +83,26 @@ function SponsorPlane({ const bgWidth = face.size * 0.18 * aspect; const bgHeight = face.size * 0.18; + // Calculate angular spans for the curved surface (chord length / radius approx) + const thetaLength = logoWidth / GLOBE_RADIUS; + const thetaStart = -thetaLength / 2; + const bgThetaLength = bgWidth / GLOBE_RADIUS; + const bgThetaStart = -bgThetaLength / 2; + return ( - {/* White background rectangle */} - - - + {/* Test background curved segment (Pink) */} + + + - {/* Logo plane */} - - + {/* Logo curved segment */} + + {/* Sponsor group handles the primary rotation */} - + {sponsorFaces.map((face, i) => { const sponsor = validSponsors[i % validSponsors.length]; if (!sponsor?.logo) return null;