Globe Study
Globe Study is a React and Tailwind CSS background component written for NedDev and yours to ship. Copy it and paste it into your project.
NedDev
backgrounds
What it is
Globe Study is a React and Tailwind CSS background component written for NedDev and yours to ship. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component was written for this catalogue. It is yours to ship in client work and in products you sell, under the terms of use.
Globe Study
import { useMemo, type CSSProperties } from "react";
// Verbatim (trimmed to the 'globe' study) from MengTo/threeui, MIT licensed:
// src/shaders/text-path-studies/sources/text-on-a-path-ii.html
const GLOBE_STUDY_SOURCE = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Text on a Path II — Globe</title>
<style>
:root{
--bg:#08090a;
--line:rgba(255,255,255,.028);
--line-strong:rgba(255,255,255,.05);
--fig:rgba(255,255,255,.24);
--title:#f2f3f5;
--copy:rgba(255,255,255,.46);
--sans:-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:var(--bg);
font-family:var(--sans);
color:var(--copy);
-webkit-font-smoothing:antialiased;
overflow:hidden;
-webkit-user-select:none;user-select:none;
}
.frame{height:100%;display:flex;flex-direction:column;padding:clamp(18px,2.2vw,34px)}
header{
display:flex;align-items:baseline;justify-content:space-between;gap:24px;flex:0 0 auto;
font-size:clamp(9.5px,.78vw,12px);font-weight:500;line-height:1;letter-spacing:.15em;
padding-bottom:clamp(14px,1.6vw,22px);
border-bottom:1px solid var(--line);
}
.ttl{white-space:nowrap;color:rgba(255,255,255,.78)}
.ttl b{font-weight:600;color:rgba(255,255,255,.92)}
.ttl span{color:rgba(255,255,255,.30);margin-left:1.5em}
.nav{color:rgba(255,255,255,.30);white-space:nowrap}
.grid{
position:relative;
flex:1 1 auto;min-height:0;
display:grid;
grid-template-columns:repeat(3,1fr);
grid-auto-rows:1fr;
}
.fig{
position:relative;min-width:0;min-height:0;
display:flex;flex-direction:column;justify-content:center;
padding:clamp(14px,1.35vw,22px) clamp(16px,1.7vw,28px) clamp(12px,1.2vw,20px);
}
.fig::before{
content:"";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--line);
}
.fig:nth-child(3n+1)::before{display:none}
.fig:nth-child(n+4)::after{
content:"";position:absolute;left:0;right:0;top:0;height:1px;background:var(--line);
}
.fignum{
font-size:clamp(9px,.72vw,11.5px);font-weight:500;letter-spacing:.17em;
color:var(--fig);flex:0 0 auto;
}
.art{
flex:1 1 auto;min-height:0;max-height:min(100%, 52vh);
margin:clamp(4px,.5vw,10px) 0 clamp(10px,1.1vw,18px);
display:flex;align-items:center;justify-content:center;
container-type:size;
}
.plate{
position:relative;width:min(100cqw,100cqh);height:min(100cqw,100cqh);
cursor:crosshair;touch-action:none;
background:radial-gradient(ellipse 60% 58% at 50% 48%, rgba(255,255,255,.038), rgba(255,255,255,0) 70%);
}
h3{
font-size:clamp(13px,1.03vw,16px);font-weight:500;letter-spacing:-.01em;
color:var(--title);margin-bottom:.42em;flex:0 0 auto;
}
.fig p{
font-size:clamp(12px,.95vw,15px);line-height:1.58;color:var(--copy);
max-width:34ch;flex:0 0 auto;
}
.plate canvas{
position:absolute;inset:0;width:100%;height:100%;display:block;
transform:translateZ(0);contain:strict;
}
@media (max-width:900px){
.grid{display:block;overflow-y:auto;-webkit-overflow-scrolling:touch}
.fig{display:block;padding:26px 4px 30px}
.fig::before{display:none}
.fig:nth-child(n+2)::after{content:"";position:absolute;left:0;right:0;top:0;height:1px;background:var(--line)}
.art{height:min(78vw,420px);max-height:none;margin:14px 0 20px}
.plate{width:min(100cqw,100cqh);height:min(100cqw,100cqh)}
.fig p{max-width:none}
.nav{display:none}
}
</style>
</head>
<body>
<div class="frame">
<header>
<div class="ttl"><b>TEXT ON A PATH</b><span>SIX MORE STUDIES IN MOTION</span></div>
<div class="nav">GLOBE</div>
</header>
<div class="grid" id="grid">
<section class="fig">
<div class="fignum">FIG 06</div>
<div class="art"><div class="plate"><canvas id="globe"></canvas></div></div>
<h3>Globe</h3>
<p>Land drawn as type along the latitude lines. Drag, scroll and click to pin.</p>
</section>
</div>
</div>
<script>
(function(){
"use strict";
var FACE = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
var INK = '226,228,233';
var LAND_B64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPcBAOD/HwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA//+P//f/LwgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4/v/4/////wcAAAAEAPABAAAAfAAAAAAAAAAAAAAAAAAAAADg9w/4/////wEAAP4AAAAAAAAA+AAAAAAAAAAAAAAAAAAAAIAG+Of//////wAAAHwGAAAAAAAAAAMAAAAAAAAAAAAAAACABwAc/4P//////wAAADAAAAAAQAAAAD4AAAAAAAAAAAAAAAAAfMbDcQAA/v///wAAAAAAAADABwAA//8HAMAPAAAAAAAAAABgAAAAAAAA/P///wAAAAAAAABwAADg//8AAAAAAAAAAAAAAADwG457dwcA8P//HwAAAAAAAAAYAAD///9/eAAAAAAAAAAAAAD4/g0H/w8A8P//PwAAAAAAAAAOgOv/////fwD/AAAAAAA/AAAA/B84/v8A8P//LwAAAAD4AAAA4PP//////////wAAAOD///H/+D/3cPgDoP//DwAAAID/BwAAx/v/////////P4AA/P///////////////////////w8AAOcBAAgAAAAAAAAAAAAAgP///////////////////////wcAACYAAAQAAAAAAAAAAAAAgf///////////////////////wMM8AAAAAAAAAAAAAAAAAAAIID/////////e+wPwH8AgA8AAP/5z/////////////////8/ANH///////9/AIALgD8AAAAAwH/+//////////////////9/APj///////8fADwAAD8AAAAA8D/+//////////////////sPAPC/+f////8fAPwAADgAAAAA8D/+////////////////D/wBAMCfAP////8PAPwYAAAAAAAA8H/4//////////////9/DgcAAAAcAOD///8/APw/AAAAAAAQAB7w//////////////8BgAMAAAACAMD/////Afh/AAAAAAA4gBz+/////////////38A4AMAAMAAAMD/////B/z/AAAAAABwwAb+/////////////x8A8AEAAAAAAAD/////P///AwAAAADmgOH//////////////z8A4AAAAAAAAAD+////P/7/BwAAAAD28P////////////////8D4AAAAAAAAAD8////f/7/BwAAAADz+f////////////////8HIAAAAAAAAAD6////////BAAAAABw/v////////////////8EAAAAAAAAAADo//////8jHgAAAACA//////////////////8MAAAAAAAAAADQ//////8OPgAAAADw//////////////////8AAAAAAAAAAADg//////+PIAAAAADA////v////////////38EAAAAAAAAAADg////////AAAAAACA//v/zD/8/////////z8AAAAAAAAAAADg//////8bAAAAAACA//N/gD///////////x8GAAAAAAAAAADw//////8AAAAAAAD+B8c/AD/+/////////wcPAAAAAAAAAADg//////8AAAAAAAD+gx4/DH74/////////wABAAAAAAAAAADg/////x8AAAAAAAD+gbCn///8////////fQABAAAAAAAAAADg/////w8AAAAAAAD/gCDn///4//////9/MgADAAAAAAAAAADA/////w8AAAAAAAD+AADm/3/4//////8/cIABAAAAAAAAAADA/////wcAAAAAAAA44AHC///5////////4+ABAAAAAAAAAACA/////wcAAAAAAACI/wEA4P//////////4OgAAAAAAAAAAAAA/////wMAAAAAAAD4/wAA4P//////////ADYAAAAAAAAAAAAA/P///wEAAAAAAAD+/wEA8P//////////AQcAAAAAAAAAAAAA+P//fwAAAAAAAAD//w8P8P//////////AQEAAAAAAAAAAAAAyP//fwAAAAAAAAD//3//////////////AQAAAAAAAAAAAAAA0P+PYQAAAAAAAAD//////z//////////AwAAAAAAAAAAAAAAoP8HwAAAAAAAAMD/////83/+////////AQAAAAAAAAAAAAAAIP8DwAAAAAAAAOD/////5//I////////AAAAAAAAAAAAAAAAQP4DgAAAAAAAAPD/////z/+A////////AAAAAAAAAAAAAAAAAPwDAAIAAAAAAPD/////z/8ZwP////9/AQAAAAAAAAAAAAAAAPgDQAAAAAAAAPj/////j/9/gP////8fAQAAAAAAAAAAAAAAAPADEAMAAAAAAPz/////v///AP9//P8DAAAAAAAAAAAAAAAAAPADAwwAAAAAAPj/////P/9/APw//B8AAAAAAAAAAAAIAAAAAPCHA8AAAAAAAPj/////P/4/APwP+J8BAAAAAAAAAAAAAAAAAMD/A0YEAAAAAPj/////f/4fAPwH+B8AAwAAAAAAAAAAAAAAAAD/AQAAAAAAAPj/////f/wHAPgD8D8AAwAAAAAAAAAAAAAAAADgHwAAAAAAAPj///////wDAPgAwH8AAQAAAAAAAAAAAAAAAADAHwAAAAAAAPz//////30AAPAAwH8AAAAAAAAAAAAAAAAAAAAAHAAAAAAAAPj//////wsAAPAAgH4AAQAAAAAAAAAAAAAAAAAAGEAAAAAAAPj//////wMBAPAAgHwAAAAAAAAAAAAAAAAAAAAAGPAhAAAAAPD///////cBAOAAgDiABAAAAAAAAAAAAAAAAAAAIPl/AAAAAOD///////8AAGABABBAFAAAAAAAAAAAAAAAAAAAgP7/AQAAAMD///////8AAAABgAAAHAAAAAAAAAAAAAAAAAAAAPz/AQAAAID///////8AAAABAAEgCAAAAAAAAAAAAAAAAAAAAPz/HwAAAAD/8P///38AAAAAAANgAAAAAAAAAAAAAAAAAAAAAPz/fwAAAAAAoP///z8AAAAAYAd4AAAAAAAAAAAAAAAAAAAAAPz/fwAAAAAAAP///x8AAAAAwAY8AAAAAAAAAAAAAAAAAAAAAP7//wAAAAAAAP///w8AAAAAgAc+AAAAAAAAAAAAAAAAAAAAAP///wAAAAAAAP///wcAAAAAgIM/TwAAAAAAAAAAAAAAAAAAAP///wEAAAAAgP///wMAAAAAAIc/QAQAAAAAAAAAAAAAAAAAgP///w8AAAAAgP///wEAAAAAAA6fAUQAAAAAAAAAAAAAAAAAAP////8AAAAAAP///wAAAAAAAB6ewuwDAgAAAAAAAAAAAAAAgP////8DAAAAAP7//wAAAAAAABwAAvAPAgAAAAAAAAAAAAAAgP////8PAAAAAPz/fwAAAAAAABAAAMCfAQAAAAAAAAAAAAAAAP////8PAAAAAPz//wAAAAAAAOADAIA/MAAAAAAAAAAAAAAAAP7///8PAAAAAPz/fwAAAAAAAAAPAMBngAAAAAAAAAAAAAAAAP7///8PAAAAAPz//wAAAAAAAAAACABAAAAAAAAAAAAAAAAAAPz///8HAAAAAPj//wAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAPz///8DAAAAAPj//wAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAPj///8BAAAAAPz//4AAAAAAAAAAAB8GAAAAAAAAAAAAAAAAAPj///8BAAAAAPz//8EAAAAAAAAAIB8OAAAAAAAAAAAAAAAAAPD///8BAAAAAP7//+AAAAAAAAAA+B8OACAAAAAAAAAAAAAAAMD///8BAAAAAP7/f/gAAAAAAAAA/H8eAAAAAAAAAAAAAAAAAID///8AAAAAAP7/H/gAAAAAAAAA/P8fAABAAAAAAAAAAAAAAAD///8AAAAAAPz/D3AAAAAAAAAA/v8/AAAAAAAAAAAAAAAAAAD///8AAAAAAPj/D3gAAAAAAADA//9/AAgAAAAAAAAAAAAAAAD//38AAAAAAPj/DzgAAAAAAADw////AAAAAAAAAAAAAAAAAAD//x8AAAAAAPD/DzgAAAAAAAD4////AQAAAAAAAAAAAAAAAAD//wMAAAAAAPD/DzgAAAAAAAD4////AwAAAAAAAAAAAAAAAID//wEAAAAAAPD/AwAAAAAAAAD4////AwAAAAAAAAAAAAAAAID//wEAAAAAAPD/AwAAAAAAAAD4////BwAAAAAAAAAAAAAAAID//wEAAAAAAOD/AwAAAAAAAAD4////BwAAAAAAAAAAAAAAAID//wAAAAAAAOD/AQAAAAAAAADw////BwAAAAAAAAAAAAAAAID//wAAAAAAAMD/AAAAAAAAAADw////AwAAAAAAAAAAAAAAAID/fwAAAAAAAIB/AAAAAAAAAADgf/z/AwAAAAAAAAAAAAAAAID/PwAAAAAAAIA/AAAAAAAAAADgB/D/AQAAAAAAAAAAAAAAAMD/HQAAAAAAAIABAAAAAAAAAADwAND/AQAAAAAAAAAAAAAAAMD/AwAAAAAAAAAAAAAAAAAAAAAAAID/AAAIAAAAAAAAAAAAAMD/BwAAAAAAAAAAAAAAAAAAAAAAAAD/AAAQAAAAAAAAAAAAAOD/AwAAAAAAAAAAAAAAAAAAAAAAAAA+AABwAAAAAAAAAAAAAOA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAOA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAOAPAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAGAAAAAAAAAAAAAMAPAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAADAAAAAAAAAAAAAPAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMABAAAAAAAAAAAAAPADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOABAAAAAAAAAAAAAPAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPADAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAPABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPCBAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAA+AABAAJ8//j8PAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAPD/fwD///////8/AAAAAAAAAAAAAAAAAIA+AAAAAAAAAAAAPP///8D/////////HwAAAAAAAAAAAAAAAIA9AAAAAAAA8Pz/////P/j//////////wMAAAAAAAAAAMAAAPB9AAAAAID/////////P/7///////////8BAAAAAAAAAOABAwB/AAAAAPD///////////////////////8AAAAAAFACPoD///9/AAAAAPD//////////////////////x8AAAAA+P////////8HAAAAAP///////////////////////wcAAAAA/v///////wMAAAAA/v///////////////////////wcAAAD8/////////w8AAA7w/////////////////////////w8AAMAB/////////wMAgB84/////////////////////////wEAAAAA/P///////3/w4AcA/////////////////////////wAAAADg//////////8/gM///////////////////////////wMAAADg/////////////f///////////////////////////z8A7wMA/v////////////////////////////////////////8/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
var SOFT = 0.88;
var INK64 = [];
for (var q0=0;q0<64;q0++) INK64.push('rgba(' + INK + ',' + (q0/63*SOFT).toFixed(4) + ')');
function ink(a){ return INK64[a <= 0 ? 0 : a >= 1 ? 63 : (a*63)|0]; }
function Surface(el){
this.el = el; this.ctx = el.getContext('2d');
this.w = 0; this.h = 0; this.dpr = 1;
this.resize();
}
Surface.prototype.resize = function(){
var r = this.el.getBoundingClientRect();
if (!r.width || !r.height) return false;
var dpr = Math.min(1.5, window.devicePixelRatio || 1);
var w = Math.round(r.width*dpr), h = Math.round(r.height*dpr);
if (w === this.el.width && h === this.el.height && this.w === r.width) return false;
this.el.width = w; this.el.height = h;
this.w = r.width; this.h = r.height; this.dpr = dpr;
this.ctx.setTransform(dpr,0,0,dpr,0,0);
return true;
};
Object.defineProperty(Surface.prototype, 'u', {
get: function(){ return this.w < this.h ? this.w : this.h; }
});
Surface.prototype.base = function(){
this.ctx.setTransform(this.dpr,0,0,this.dpr,0,0);
};
var QA = Math.PI*2/64;
function qang(a){ return Math.round(a/QA)*QA; }
function local(canvas, e){
var r = canvas.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
/* ---------- 06 / GLOBE ---------- */
var Globe = (function(){
var cv = document.getElementById('globe');
var s = new Surface(cv);
var MW = 288, MH = 144, land;
(function(){
var bin = atob(LAND_B64);
land = new Uint8Array(bin.length);
for (var i=0;i<bin.length;i++) land[i] = bin.charCodeAt(i);
})();
function isLand(lon, lat){
var gx = Math.floor((lon+180)/360*MW), gy = Math.floor((90-lat)/180*MH);
if (gx<0||gx>=MW||gy<0||gy>=MH) return false;
var b = gy*MW+gx;
return (land[b>>3] >> (b&7)) & 1;
}
var PHRASE = "everypointonthisballisapathbacktoanotherone";
var nodes = [];
(function(){
var LAT_STEP = 3.05, k = 0, run = 0, sea3 = 0;
for (var lat = -86; lat <= 86; lat += LAT_STEP){
var rl = Math.cos(lat*Math.PI/180);
var n = Math.max(1, Math.round(98*rl));
for (var i=0;i<n;i++){
var lon = -180 + 360*i/n;
var l = isLand(lon, lat);
if (!l && (sea3++ % 2)) continue;
var letter = 0;
if (l && (run++ % 2 === 0)) letter = PHRASE.charAt(k++ % PHRASE.length);
nodes.push({
lat: lat*Math.PI/180, lon: lon*Math.PI/180,
land: l, c: letter
});
}
}
})();
var spin = 2.1, vel = 0.16, hover = false, drag = null, tilt = -0.36, vtilt = 0;
var land8 = [];
var zoom = 1, zoomT = 1, pins = [], look = null;
var sea = [], soil = [];
cv.addEventListener('pointerenter', function(){ hover = true; });
cv.addEventListener('pointerleave', function(){ hover = false; });
cv.addEventListener('pointerdown', function(e){
drag = local(cv, e); drag.moved = false;
cv.setPointerCapture(e.pointerId);
});
cv.addEventListener('pointermove', function(e){
var p = local(cv, e);
look = p;
if (!drag) return;
vel = (p.x - drag.x) / s.u * 9;
vtilt = -(p.y - drag.y) / s.u * 6;
tilt = Math.max(-1.15, Math.min(1.15, tilt + vtilt*0.016));
drag = p; drag.moved = true;
});
function release(e){
if (drag && !drag.moved){
var g = unproject(drag.x, drag.y);
if (g){ pins.push({lat:g.lat, lon:g.lon, t:performance.now()}); if (pins.length>7) pins.shift(); }
}
drag = null;
}
cv.addEventListener('pointerup', release);
cv.addEventListener('pointercancel', function(){ drag = null; });
cv.addEventListener('wheel', function(e){
e.preventDefault();
zoomT = Math.max(0.85, Math.min(2.6, zoomT * Math.exp(-e.deltaY*0.0016)));
}, {passive:false});
var view = {cx:0, cy:0, R:1, cs:1, sn:0, ct:1, st:0};
function unproject(px, py){
var x1 = (px - view.cx)/view.R, y2 = (view.cy - py)/view.R;
var q = 1 - x1*x1 - y2*y2;
if (q <= 0.002) return null;
var z2 = Math.sqrt(q);
var y0 = y2*view.ct + z2*view.st;
var z1 = -y2*view.st + z2*view.ct;
var x0 = x1*view.cs + z1*view.sn;
var z0 = -x1*view.sn + z1*view.cs;
return { lat: Math.asin(Math.max(-1, Math.min(1, y0))), lon: Math.atan2(z0, x0) };
}
function draw(now, dt){
var ctx = s.ctx;
ctx.clearRect(0,0,s.w,s.h);
zoom += (zoomT - zoom) * Math.min(1, dt/180);
if (!drag){
var idle = hover ? 0.045 : 0.16;
vel += (idle - vel) * Math.min(1, dt/900);
vtilt *= Math.pow(0.90, dt/16);
tilt += vtilt*dt/1000;
tilt += (-0.36 - tilt) * Math.min(1, dt/4000);
}
spin += vel * dt/1000;
var cx = s.w/2, cy = s.h/2 + s.u*0.035;
var R = s.u*0.318*zoom;
var fs = s.u*0.0275*Math.pow(zoom, 0.72);
var cs = Math.cos(spin), sn = Math.sin(spin);
var ct = Math.cos(tilt), st = Math.sin(tilt);
view.cx = cx; view.cy = cy; view.R = R; view.cs = cs; view.sn = sn; view.ct = ct; view.st = st;
var lx = -1e9, ly = -1e9, lr = s.u*0.20, lr2 = lr*lr;
if (look && !drag){ lx = look.x; ly = look.y; }
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
sea.length = 0; soil.length = 0;
for (var i=0;i<nodes.length;i++){
var nd = nodes[i];
var cl = Math.cos(nd.lat);
var x0 = cl*Math.cos(nd.lon), y0 = Math.sin(nd.lat), z0 = cl*Math.sin(nd.lon);
var x1 = x0*cs - z0*sn, z1 = x0*sn + z0*cs;
var y2 = y0*ct - z1*st, z2 = y0*st + z1*ct;
if (z2 <= 0.02) continue;
var px = cx + x1*R, py = cy - y2*R;
var dx = px-lx, dy = py-ly;
var glow = (dx*dx + dy*dy < lr2) ? (1 - Math.sqrt(dx*dx+dy*dy)/lr) : 0;
if (!nd.land){ sea.push(px, py, Math.min(0.999, z2 + glow*0.55)); continue; }
if (!nd.c){ soil.push(px, py, Math.min(0.999, z2 + glow*0.55)); continue; }
var tx0 = -Math.sin(nd.lon), tz0 = Math.cos(nd.lon);
var tx1 = tx0*cs - tz0*sn, tz1 = tx0*sn + tz0*cs;
var ang = qang(Math.atan2(tz1*st, tx1));
var b = Math.min(7, Math.max(0, ((Math.min(0.999, z2 + glow*0.6))*7.99)|0));
(land8[b] || (land8[b] = [])).push(px, py, ang, nd.c, 0);
}
var dmin = Math.max(0.7, s.u*0.0029);
function dots(list, base, gain, grow){
for (var lvl=0; lvl<6; lvl++){
var z = (lvl+0.5)/6, dsz = dmin*grow*(0.55 + 0.75*z);
ctx.fillStyle = ink(base + gain*z);
ctx.beginPath();
for (var q=0;q<list.length;q+=3){
var lv = list[q+2] >= 1 ? 5 : (list[q+2]*6)|0;
if (lv !== lvl) continue;
ctx.rect(list[q]-dsz/2, list[q+1]-dsz/2, dsz, dsz);
}
ctx.fill();
}
}
dots(sea, 0.10, 0.22, 1.0);
dots(soil, 0.34, 0.46, 1.7);
for (var bi=0; bi<8; bi++){
var arr = land8[bi];
if (!arr || !arr.length) continue;
var zb = (bi+0.5)/8;
ctx.font = 'bold ' + (fs*(0.42 + 0.58*zb)).toFixed(2) + 'px ' + FACE;
ctx.fillStyle = ink(0.28 + 0.72*Math.pow(zb, 0.6));
for (var t=0;t<arr.length;t+=5){
ctx.save();
ctx.translate(arr[t], arr[t+1]);
ctx.rotate(arr[t+2]);
ctx.fillText(arr[t+3], 0, 0);
ctx.restore();
}
arr.length = 0;
}
for (var pi=0; pi<pins.length; pi++){
var pn = pins[pi];
var pcl = Math.cos(pn.lat);
var ax = pcl*Math.cos(pn.lon), ay = Math.sin(pn.lat), az = pcl*Math.sin(pn.lon);
var bx1 = ax*cs - az*sn, bz1 = ax*sn + az*cs;
var by2 = ay*ct - bz1*st, bz2 = ay*st + bz1*ct;
if (bz2 <= 0.02) continue;
var ppx = cx + bx1*R, ppy = cy - by2*R;
var age = (now - pn.t)/1000;
var pop = Math.min(1, age/0.22);
var rr2 = s.u*0.016*(0.4 + 0.6*pop)*(0.55 + 0.45*bz2);
ctx.beginPath();
ctx.arc(ppx, ppy, rr2, 0, Math.PI*2);
ctx.strokeStyle = ink(0.30 + 0.55*bz2);
ctx.lineWidth = Math.max(0.7, s.u*0.0022);
ctx.stroke();
ctx.beginPath();
ctx.arc(ppx, ppy, Math.max(0.7, rr2*0.22), 0, Math.PI*2);
ctx.fillStyle = ink(0.45 + 0.55*bz2);
ctx.fill();
if (age < 0.9){
var w2 = 1 - age/0.9;
ctx.beginPath();
ctx.arc(ppx, ppy, rr2 + (1-w2)*s.u*0.05, 0, Math.PI*2);
ctx.strokeStyle = ink(0.55*w2*w2);
ctx.lineWidth = Math.max(0.6, s.u*0.0016);
ctx.stroke();
}
}
}
return { s:s, draw:draw };
})();
/* ---------- loop ---------- */
var studies = [Globe];
var prev = performance.now();
var visible = [true];
function checkVisible(){
var vh = window.innerHeight;
for (var i=0;i<studies.length;i++){
var r = studies[i].s.el.getBoundingClientRect();
visible[i] = r.bottom > -80 && r.top < vh + 80;
}
}
function frame(now){
var dt = Math.min(64, now - prev); prev = now;
for (var i=0;i<studies.length;i++){
var st = studies[i], sf = st.s;
if (!sf.w || !visible[i]) continue;
sf.base();
st.draw(now, dt);
}
requestAnimationFrame(frame);
}
addEventListener('scroll', checkVisible, true);
requestAnimationFrame(frame);
function onResize(){
for (var i=0;i<studies.length;i++){
studies[i].s.resize();
if (studies[i].reset) studies[i].reset();
}
checkVisible();
}
window.addEventListener('resize', onResize);
if (window.ResizeObserver) new ResizeObserver(onResize).observe(document.getElementById('grid'));
onResize();
})();
</script>
</body>
</html>
`;
export type GlobeStudyProps = {
mode?: "dark" | "light";
scale?: number;
opacity?: number;
hue?: number;
saturation?: number;
brightness?: number;
className?: string;
style?: CSSProperties;
};
export const GLOBE_STUDY_DEFAULTS = {
mode: "dark",
scale: 1,
opacity: 1,
hue: 0,
saturation: 1,
brightness: 1,
} as const;
function clamp(value: number, minimum: number, maximum: number) {
return Math.min(maximum, Math.max(minimum, value));
}
function focusStyles(mode: "dark" | "light") {
const selected = 1;
const surface = mode === "light" ? "#f3f5f8" : "#08090a";
const themeStyles =
mode === "light"
? `
:root {
color-scheme: light;
--bg: #f3f5f8;
--line: rgba(20, 24, 32, .055);
--line-strong: rgba(20, 24, 32, .10);
--fig: rgba(20, 24, 32, .42);
--title: #171922;
--copy: rgba(20, 24, 32, .62);
}
`
: ":root { color-scheme: dark; }";
return `<style id="threeui-study-focus">
${themeStyles}
html, body, .frame {
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
}
body { margin: 0 !important; background: ${surface} !important; }
.frame { padding: 0 !important; background: ${surface} !important; }
header { display: none !important; }
.grid {
display: block !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
}
.fig { display: none !important; }
.fig:nth-child(${selected}) {
display: flex !important;
width: 100% !important;
height: 100% !important;
padding: 0 !important;
}
.fig::before, .fig::after, .fignum, .fig h3, .fig p { display: none !important; }
.art {
display: flex !important;
width: 100% !important;
height: 100% !important;
max-height: none !important;
margin: 0 !important;
align-items: center !important;
justify-content: center !important;
}
.plate {
width: min(100cqw, 100cqh) !important;
height: min(100cqw, 100cqh) !important;
}
</style>`;
}
const AUTHORED_FIGURE_INK = "var INK = '226,228,233';";
function replaceRequired(source: string, authored: string, focused: string) {
if (!source.includes(authored)) {
throw new Error(`Globe study source adapter could not find: ${authored}`);
}
return source.replace(authored, focused);
}
function focusedDocument(mode: "dark" | "light") {
const source =
mode === "light"
? replaceRequired(
GLOBE_STUDY_SOURCE,
AUTHORED_FIGURE_INK,
"var INK = '38,40,48';",
)
: GLOBE_STUDY_SOURCE;
return source
.replace(/<title>[\s\S]*?<\/title>/i, `<title>Globe — ThreeUI</title>`)
.replace("</head>", `${focusStyles(mode)}
</head>`);
}
export default function GlobeStudy({
mode = GLOBE_STUDY_DEFAULTS.mode,
scale = GLOBE_STUDY_DEFAULTS.scale,
opacity = GLOBE_STUDY_DEFAULTS.opacity,
hue = GLOBE_STUDY_DEFAULTS.hue,
saturation = GLOBE_STUDY_DEFAULTS.saturation,
brightness = GLOBE_STUDY_DEFAULTS.brightness,
className,
style,
}: GlobeStudyProps) {
const safeMode = mode === "light" ? "light" : "dark";
const doc = useMemo(() => focusedDocument(safeMode), [safeMode]);
const boundedScale = clamp(scale, 0.65, 1.5);
const boundedOpacity = clamp(opacity, 0.1, 1);
const boundedHue = clamp(hue, -180, 180);
const boundedSaturation = clamp(saturation, 0, 2);
const boundedBrightness = clamp(brightness, 0.4, 1.8);
const filter =
boundedHue === 0 && boundedSaturation === 1 && boundedBrightness === 1
? undefined
: `hue-rotate(${boundedHue}deg) saturate(${boundedSaturation}) brightness(${boundedBrightness})`;
return (
<div
className={["text-path-study", `text-path-study--${safeMode}`, className]
.filter(Boolean)
.join(" ")}
data-mode={safeMode}
style={{
opacity: boundedOpacity,
filter,
width: "100%",
height: "100%",
...style,
}}
>
<iframe
className="text-path-study-frame"
data-mode={safeMode}
title="Globe interactive canvas study"
sandbox="allow-scripts"
srcDoc={doc}
style={{
width: "100%",
height: "100%",
border: "none",
display: "block",
transform: boundedScale === 1 ? undefined : `scale(${boundedScale})`,
}}
/>
</div>
);
}