Globe Study
Globe Study é um componente de fundo para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.
NedDev
backgrounds
O que é
Globe Study é um componente de fundo para React e Tailwind CSS, escrito para a NedDev e teu para usares. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Não precisa de pacotes além do que o projeto já tem
Licença
Este componente foi escrito para este catálogo. É teu para usares em trabalho de cliente e em produtos que vendas, nos termos de uso.
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>
);
}