Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -942,37 +942,4 @@ body {
margin-left: 0.25em;
}

/* Hero visual placement: anchor the image to bottom-right and allow it to extend past the section */
.hero-visual {
position: absolute;
right: 0;
bottom: 0;
z-index: 10;
width: min(85vw, 950px);
max-width: none;
transform: translateX(0) translateY(0);
}

@media (min-width: 768px) {
.hero-visual {
right: -2.5rem; /* push slightly into the right gutter */
bottom: -3rem; /* push slightly below the fold */
width: min(85vw, 950px);
}
}

@media (min-width: 1024px) {
.hero-visual {
right: -4rem;
bottom: -5rem;
width: min(90vw, 1200px);
}
}

/* ensure the image inside the container preserves its object-fit and does not add extra padding */
.hero-visual img,
.hero-visual .next-image {
object-fit: contain;
object-position: right bottom;
padding: 0;
}
20 changes: 4 additions & 16 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import Link from 'next/link';
import type { CSSProperties } from 'react';
import Navbar from "@/components/Navbar";
import HeroGlobe from "@/components/HeroGlobe";
import { useTranslations } from "@/lib/i18n";

function Typewriter({ text, className = "", speed = 45 }: { text: string; className?: string; speed?: number }) {
Expand Down Expand Up @@ -139,7 +140,7 @@
const tFooter = useTranslations("footer");

// Features data and reveal animation refs
const features = [

Check warning on line 143 in app/page.tsx

View workflow job for this annotation

GitHub Actions / build (20.x)

'features' is assigned a value but never used
{
title: tHome("whyTectonicFeatures.triggerRedemptions.title"),
emoji: '⚡',
Expand Down Expand Up @@ -269,24 +270,11 @@

</div>

<div className="relative mx-auto mt-8 w-full max-w-sm md:mt-0 md:absolute hero-visual md:w-[min(85vw,950px)] hero-visual-lg lg:w-[min(90vw,1200px)] lg:max-w-none">
<div className="relative aspect-[4/3] w-full drop-shadow-2xl md:aspect-[5/4]">
<Image
src={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/tectonic-hero.png`}
alt="Tectonic hero illustration"
fill
priority
sizes="(max-width: 768px) 90vw, (max-width: 1024px) 85vw, 90vw"
className="object-contain object-right-bottom p-0 scale-x-[-1]"
/>
</div>
<div className="mx-auto w-full max-w-[320px] sm:max-w-[400px] lg:max-w-[500px]">
<HeroGlobe />
</div>
</div>

{/* Scroll Indicator */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
<div className="text-orange-600">↓</div>
</div>
</section>

{/* Features Section (styled to match provided design) */}
Expand Down Expand Up @@ -594,7 +582,7 @@
<div className="flex flex-col items-center text-center">
<div className="logo-hover-wrap mb-4 flex flex-col items-center gap-3 text-slate-900">
<Image src={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/Logo.svg`} alt="Tectonic logo" width={72} height={72} className="logo-hover-zoom h-16 w-auto object-contain" />
<span className="text-2xl font-black tracking-[0.04em] bg-gradient-to-b from-[#c38b44] to-[#7e4420] bg-clip-text text-transparent drop-shadow-[0_2px_4px_rgba(0,0,0,0.4)] leading-none">TECTONIC</span>
<span className="text-2xl font-black tracking-[0.04em] bg-gradient-to-b from-[#c38b44] to-[#7e4420] bg-clip-text text-transparent leading-none">TECTONIC</span>
</div>
<p className="max-w-xs text-sm leading-6 text-slate-600">
{tFooter("desc")}
Expand Down
232 changes: 232 additions & 0 deletions components/HeroGlobe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"use client";

import { useEffect, useRef } from "react";
import { decodeDots, HUBS } from "@/lib/world-land";

/* --- TYPES --- */

interface HeroGlobeProps {
/** Applied to the wrapping element. The canvas fills it and stays square. */
className?: string;
/** Degrees of rotation per second. */
speed?: number;
/** Longitude at the centre of the globe when the animation starts. */
startLongitude?: number;
}

/* --- GEOMETRY --- */

const DEG = Math.PI / 180;
const VIEW_LATITUDE = 22; // tilt: how far above the equator the camera sits
const DEPTH_BANDS = 4; // dots are batched into this many depth buckets per frame

/** [lon, lat] in degrees to a unit vector with z through the north pole. */
function toVector(lon: number, lat: number): [number, number, number] {
const p = lat * DEG;
const l = lon * DEG;
const c = Math.cos(p);
return [c * Math.cos(l), c * Math.sin(l), Math.sin(p)];
}

/** Flatten [lon, lat] pairs to [x0, y0, z0, x1, y1, z1, ...]. */
function toBuffer(points: Array<[number, number]>): Float32Array {
const out = new Float32Array(points.length * 3);
for (let i = 0; i < points.length; i++) {
const [x, y, z] = toVector(points[i][0], points[i][1]);
out[i * 3] = x;
out[i * 3 + 1] = y;
out[i * 3 + 2] = z;
}
return out;
}

/* --- COMPONENT --- */

export default function HeroGlobe({
className = "",
speed = 2,
startLongitude = -32,
}: HeroGlobeProps) {
const wrapRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

useEffect(() => {
const wrap = wrapRef.current;
const canvas = canvasRef.current;
if (!wrap || !canvas) return;

const ctx = canvas.getContext("2d");
if (!ctx) return;

const land = toBuffer(decodeDots());
const hubs = HUBS.map(([lon, lat]) => toVector(lon, lat));

const S0 = Math.sin(VIEW_LATITUDE * DEG);
const C0 = Math.cos(VIEW_LATITUDE * DEG);

let size = 0;
let frame = 0;
let running = false;
let last = 0;
let spin = -startLongitude * DEG;

function render() {
if (!size) return;
const mid = size / 2;
const r = mid - 2;
const sa = Math.sin(spin);
const ca = Math.cos(spin);

ctx!.clearRect(0, 0, size, size);

/* Spin about the polar axis, tilt to the viewing latitude, drop the depth
component. Dots are collected into depth bands so each band is one fill
instead of one fill per dot. */
const bands: Array<Array<number>> = [];
for (let b = 0; b < DEPTH_BANDS; b++) bands.push([]);

for (let i = 0; i < land.length; i += 3) {
const vx = land[i];
const vy = land[i + 1];
const vz = land[i + 2];
const rx = vx * ca - vy * sa;
const depth = rx * C0 + vz * S0;
if (depth <= 0.02) continue;
const band = Math.min(DEPTH_BANDS - 1, Math.floor(depth * DEPTH_BANDS));
bands[band].push(
mid + r * (vx * sa + vy * ca),
mid - r * (vz * C0 - rx * S0),
);
}

for (let b = 0; b < DEPTH_BANDS; b++) {
const pts = bands[b];
if (!pts.length) continue;
const t = (b + 0.5) / DEPTH_BANDS;
const dot = r * (0.0055 + 0.0055 * t);
ctx!.beginPath();
for (let i = 0; i < pts.length; i += 2) {
ctx!.moveTo(pts[i] + dot, pts[i + 1]);
ctx!.arc(pts[i], pts[i + 1], dot, 0, Math.PI * 2);
}
ctx!.fillStyle = `rgba(138,79,34,${(0.2 + 0.55 * t).toFixed(3)})`;
ctx!.fill();
}

// hub network: project the visible hubs, then link the near pairs
const shown: Array<[number, number]> = [];
for (const [vx, vy, vz] of hubs) {
const rx = vx * ca - vy * sa;
if (rx * C0 + vz * S0 <= 0.08) continue;
shown.push([mid + r * (vx * sa + vy * ca), mid - r * (vz * C0 - rx * S0)]);
}

ctx!.lineCap = "round";
ctx!.beginPath();
for (let i = 0; i < shown.length; i++) {
for (let j = i + 1; j < shown.length; j++) {
if (Math.hypot(shown[i][0] - shown[j][0], shown[i][1] - shown[j][1]) < r) {
ctx!.moveTo(shown[i][0], shown[i][1]);
ctx!.lineTo(shown[j][0], shown[j][1]);
}
}
}
ctx!.lineWidth = Math.max(r / 260, 0.5);
ctx!.strokeStyle = "rgba(194,65,12,0.3)";
ctx!.stroke();

ctx!.beginPath();
for (const [x, y] of shown) {
ctx!.moveTo(x + r * 0.012, y);
ctx!.arc(x, y, r * 0.012, 0, Math.PI * 2);
}
ctx!.fillStyle = "rgba(180,83,9,0.85)";
ctx!.fill();

// the silhouette - the curve the old asset never closed
ctx!.beginPath();
ctx!.arc(mid, mid, r, 0, Math.PI * 2);
ctx!.lineWidth = Math.max(r / 200, 0.6);
ctx!.strokeStyle = "rgba(138,79,34,0.28)";
ctx!.stroke();
}

function tick(now: number) {
if (!running) return;
const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;
last = now;
spin -= speed * DEG * dt;
render();
frame = requestAnimationFrame(tick);
}

function start() {
if (running) return;
running = true;
last = 0;
frame = requestAnimationFrame(tick);
}

function stop() {
running = false;
cancelAnimationFrame(frame);
}

function resize() {
const next = Math.round(wrap!.clientWidth);
if (!next) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
size = next;
canvas!.width = Math.round(next * dpr);
canvas!.height = Math.round(next * dpr);
canvas!.style.width = `${next}px`;
canvas!.style.height = `${next}px`;
ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
render();
}

const motion = window.matchMedia("(prefers-reduced-motion: reduce)");

// Only animate while the globe is actually on screen and motion is welcome.
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !motion.matches) start();
else stop();
},
{ threshold: 0 },
);

function onMotionChange() {
if (motion.matches) {
stop();
render();
} else {
start();
}
}

const ro = new ResizeObserver(resize);
resize();
ro.observe(wrap);
io.observe(wrap);
motion.addEventListener("change", onMotionChange);

return () => {
stop();
ro.disconnect();
io.disconnect();
motion.removeEventListener("change", onMotionChange);
};
}, [speed, startLongitude]);

return (
<div ref={wrapRef} className={`relative aspect-square w-full ${className}`}>
<canvas
ref={canvasRef}
className="block h-full w-full"
role="img"
aria-label="A slowly rotating globe, its landmasses drawn as a field of dots, with a network of connected hubs"
/>
</div>
);
}
2 changes: 1 addition & 1 deletion components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export default function Navbar() {
className="logo-hover-zoom h-9 w-auto object-contain sm:h-10"
priority
/>
<span className="hidden text-xl leading-none font-black tracking-[0.04em] bg-gradient-to-b from-[#c38b44] to-[#7e4420] bg-clip-text text-transparent drop-shadow-[0_2px_4px_rgba(0,0,0,0.4)] sm:block">
<span className="hidden text-xl leading-none font-black tracking-[0.04em] bg-gradient-to-b from-[#c38b44] to-[#7e4420] bg-clip-text text-transparent sm:block">
TECTONIC
</span>
</Link>
Expand Down
Loading
Loading