2c8cd11153
The hero is now layered like Treasures' HeroGallery: - CampaignHeroBackground (new) — full-bleed banner image from the currently-spotlit campaign, crossfading over ~1.5 s and panning left. Warm tint + film grain overlay so foreground text stays legible. - HeroGlobe — pushed to the right edge with a larger radius, slightly translucent so the photo bleeds through. Hearts replace the old dots for marker symbols; clicking one selects that campaign. - HeroCampaignSpotlight (new) — minimal text overlay anchored to the bottom-left of the hero container (title, summary, avatar + author, location, progress bar with goal, 'View' link). No card chrome. Land polygons are now the full Natural Earth 110m fidelity (~10.5k vertices) instead of being heavily Douglas-Peucker'd, so coastlines look organic rather than chunky. Back-hemisphere rings are now properly hidden by walking each edge and either dropping back-side vertices outright or interpolating to the sphere limb where a ring crosses it — fixes the 'phantom continents through the front' bug. Rings additionally fade in/out over a narrow z-band near the limb instead of popping at z = 0. Markers also have proper z-fade and pull off-canvas when on the back so they can't intercept clicks they aren't visible for. Selected markers scale 1.35x with a stronger glow so the user can tell which campaign the spotlight refers to. Other cleanup: - formatCampaignAmount + formatSatsShort move out of CampaignCard.tsx into src/lib/formatCampaignAmount.ts so CampaignCard stops failing the react-refresh/only-export-components lint. - Hero CTAs drop the 'Unstoppable fundraising on Nostr' pill and the em dash from the supporting copy. - New keyframes (heroPanLeft / heroPanRight) for the slow Ken-Burns pan on the background photos, with prefers-reduced-motion respected.
121 lines
3.6 KiB
JavaScript
121 lines
3.6 KiB
JavaScript
// Build a heavily-simplified land-polygon dataset for the hero globe.
|
|
//
|
|
// Input: Natural Earth 110m countries TopoJSON
|
|
// (https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json)
|
|
//
|
|
// Output: src/lib/landPolygons.ts — an array of rings (each ring is a flat
|
|
// array [lng0, lat0, lng1, lat1, ...]) representing landmasses.
|
|
//
|
|
// Run with: node scripts/build-land-polygons.mjs
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const INPUT = process.argv[2] ?? '/tmp/opencode/countries-110m.json';
|
|
const OUTPUT = path.join(REPO_ROOT, 'src/lib/landPolygons.ts');
|
|
|
|
const topo = JSON.parse(fs.readFileSync(INPUT, 'utf8'));
|
|
const layer = topo.objects.countries;
|
|
const transform = topo.transform;
|
|
|
|
/** Decode a topojson arc into absolute [lng, lat] pairs. */
|
|
function decodeArc(arc) {
|
|
const out = [];
|
|
let x = 0;
|
|
let y = 0;
|
|
for (const [dx, dy] of arc) {
|
|
x += dx;
|
|
y += dy;
|
|
out.push([
|
|
x * transform.scale[0] + transform.translate[0],
|
|
y * transform.scale[1] + transform.translate[1],
|
|
]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const arcs = topo.arcs.map(decodeArc);
|
|
|
|
/** Resolve a topojson arc index (negative means reversed) into points. */
|
|
function resolveArc(i) {
|
|
if (i < 0) {
|
|
const arc = arcs[~i];
|
|
return arc.slice().reverse();
|
|
}
|
|
return arcs[i];
|
|
}
|
|
|
|
/** Build a ring from an array of arc indices. */
|
|
function buildRing(arcIndices) {
|
|
const ring = [];
|
|
for (let i = 0; i < arcIndices.length; i++) {
|
|
const seg = resolveArc(arcIndices[i]);
|
|
// Skip the duplicated joining point between consecutive arcs.
|
|
if (i === 0) ring.push(...seg);
|
|
else ring.push(...seg.slice(1));
|
|
}
|
|
return ring;
|
|
}
|
|
|
|
const rings = [];
|
|
for (const feature of layer.geometries) {
|
|
if (feature.type === 'Polygon') {
|
|
for (const arcIndices of feature.arcs) {
|
|
rings.push(buildRing(arcIndices));
|
|
}
|
|
} else if (feature.type === 'MultiPolygon') {
|
|
for (const polygon of feature.arcs) {
|
|
for (const arcIndices of polygon) {
|
|
rings.push(buildRing(arcIndices));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use the full Natural Earth 110m resolution. We skip Douglas-Peucker
|
|
// entirely so coastlines look organic at hero scale rather than blocky.
|
|
// We still quantize to 0.1° (well below the rendered pixel size on a
|
|
// ~600 px globe) which is a free ~30 % byte saving with no visible loss.
|
|
const MIN_VERTS = 3;
|
|
|
|
const simplifiedRings = [];
|
|
for (const ring of rings) {
|
|
if (ring.length < MIN_VERTS) continue;
|
|
const flat = [];
|
|
for (const [lng, lat] of ring) {
|
|
flat.push(Math.round(lng * 10) / 10, Math.round(lat * 10) / 10);
|
|
}
|
|
simplifiedRings.push(flat);
|
|
}
|
|
|
|
const totalCoords = simplifiedRings.reduce((sum, r) => sum + r.length / 2, 0);
|
|
|
|
const banner = `/**
|
|
* Simplified land polygons for the hero globe.
|
|
*
|
|
* Generated from Natural Earth 110m country boundaries via
|
|
* \`scripts/build-land-polygons.mjs\`. Each entry is a flat \`[lng, lat, lng,
|
|
* lat, ...]\` ring. We keep the data inline (rather than fetching a TopoJSON
|
|
* blob at runtime) so the hero renders instantly, with no network jitter and
|
|
* no extra runtime dependency.
|
|
*
|
|
* Do not edit by hand — re-run the script to regenerate.
|
|
*/
|
|
`;
|
|
|
|
const body = `export const LAND_RINGS: readonly (readonly number[])[] = [\n${
|
|
simplifiedRings.map((r) => ` [${r.join(',')}],`).join('\n')
|
|
}\n];\n`;
|
|
|
|
fs.writeFileSync(OUTPUT, banner + body);
|
|
|
|
console.log(
|
|
`Wrote ${OUTPUT}`,
|
|
`\n rings: ${simplifiedRings.length}`,
|
|
`\n vertices: ${totalCoords}`,
|
|
`\n bytes: ${fs.statSync(OUTPUT).size}`,
|
|
);
|