Custom Code Experiment •
Globe
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Glitch globe loader</title>
<style>
:root {
box-sizing: border-box;
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
--page: #EDEDED;
--ring-track: rgba(0, 0, 0, 0.12);
--accent: #FFC21A;
--void: #0A0A0A;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--page: #141414;
--ring-track: rgba(255, 255, 255, 0.12);
}
}
:root[data-theme="dark"] {
--page: #141414;
--ring-track: rgba(255, 255, 255, 0.12);
}
*, *::before, *::after { box-sizing: inherit; }
html { height: 100%; scroll-padding-top: env(safe-area-inset-top, 0px); }
body {
margin: 0;
height: 100%;
background: var(--page);
display: grid;
place-items: center;
}
.orb {
position: relative;
width: min(260px, 70vw);
aspect-ratio: 1;
}
/* Animated yellow ring */
.ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.ring circle { fill: none; stroke-width: 3; }
.ring .track { stroke: var(--ring-track); }
.ring .arc {
stroke: var(--accent);
stroke-linecap: round;
stroke-dasharray: 90 400;
animation: sweep 1.6s cubic-bezier(.6,.1,.3,.9) infinite;
transform-origin: 50% 50%;
}
@keyframes sweep {
0% { stroke-dasharray: 20 400; transform: rotate(0deg); }
50% { stroke-dasharray: 160 400; }
100% { stroke-dasharray: 20 400; transform: rotate(360deg); }
}
/* Circular viewport */
.window {
position: absolute;
inset: 12px;
border-radius: 50%;
overflow: hidden;
background: radial-gradient(circle at 50% 45%, #262626 0%, var(--void) 70%);
box-shadow: inset 0 0 40px rgba(0,0,0,.8), inset 0 0 0 1px rgba(255,255,255,.2);
}
.window canvas { display: block; width: 100%; height: 100%; }
/* faint star field */
.window::before {
content: "";
position: absolute;
inset: 0;
opacity: .25;
background-image:
radial-gradient(circle at 20% 30%, #fff 0 0.6px, transparent 1px),
radial-gradient(circle at 70% 60%, #fff 0 0.5px, transparent 1px),
radial-gradient(circle at 45% 85%, #fff 0 0.7px, transparent 1.2px);
background-size: 37px 41px, 53px 47px, 61px 67px;
pointer-events: none;
}
/* scanlines */
.window::after {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,.28) 2px 3px);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.ring .arc { animation-duration: 4s; }
}
</style>
</head>
<body>
<div class="orb" role="status" aria-label="Loading">
<svg class="ring" viewBox="0 0 100 100" aria-hidden="true">
<circle class="track" cx="50" cy="50" r="48"/>
<circle class="arc" cx="50" cy="50" r="48"/>
</svg>
<div class="window">
<canvas id="globe" aria-hidden="true"></canvas>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('globe');
const ctx = canvas.getContext('2d');
const buf = document.createElement('canvas');
const bctx = buf.getContext('2d');
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const WHITE = '#FFFFFF';
const GRID = '#8C8C8C';
const STAND = '#BDBDBD';
const GHOST_A = '#8C8C8C', GHOST_B = '#E0E0E0';
let W = 0, H = 0, dpr = 1;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
const r = canvas.getBoundingClientRect();
W = Math.round(r.width * dpr); H = Math.round(r.height * dpr);
canvas.width = buf.width = W;
canvas.height = buf.height = H;
}
new ResizeObserver(resize).observe(canvas);
resize();
// ---------- Globe geometry (lon/lat, degrees) ----------
const D = Math.PI / 180;
const sph = (lon, lat, r = 1) => [
r * Math.cos(lat * D) * Math.cos(lon * D),
r * Math.sin(lat * D),
-r * Math.cos(lat * D) * Math.sin(lon * D)
];
// Simplified continent outlines
const LAND = [
// North America
[[-166,68],[-156,71],[-140,70],[-125,70],[-110,73],[-95,72],[-82,69],[-80,63],[-94,59],[-90,56],[-80,52],[-78,58],[-70,60],[-62,58],[-56,52],[-60,47],[-66,45],[-70,42],[-76,38],[-76,35],[-81,31],[-80,26],[-82,29],[-85,30],[-90,29],[-97,27],[-97,22],[-92,19],[-88,21],[-87,16],[-83,15],[-83,10],[-78,8],[-80,7],[-85,10],[-92,15],[-100,17],[-105,20],[-112,28],[-117,32],[-122,37],[-124,42],[-124,48],[-130,54],[-135,58],[-145,60],[-152,59],[-158,56],[-165,54],[-160,58],[-164,63],[-166,68]],
// Greenland
[[-73,78],[-60,82],[-30,83],[-20,80],[-20,72],[-25,68],[-40,65],[-44,60],[-50,64],[-55,70],[-60,76],[-73,78]],
// South America
[[-80,8],[-77,9],[-72,12],[-62,11],[-52,5],[-50,0],[-44,-2],[-35,-5],[-35,-9],[-39,-15],[-40,-22],[-48,-26],[-53,-34],[-58,-38],[-63,-41],[-65,-46],[-68,-51],[-70,-55],[-74,-52],[-74,-45],[-73,-37],[-71,-30],[-70,-18],[-76,-14],[-81,-6],[-80,0],[-78,3],[-77,7],[-80,8]],
// Africa
[[-17,21],[-13,28],[-9,32],[-6,36],[3,37],[10,37],[11,33],[20,31],[25,32],[32,31],[34,28],[38,22],[43,12],[51,12],[51,10],[47,4],[41,-2],[40,-10],[40,-16],[35,-24],[32,-29],[27,-34],[20,-35],[18,-32],[14,-24],[12,-17],[13,-10],[9,-1],[9,4],[5,6],[-4,5],[-8,4],[-13,8],[-17,14],[-17,21]],
// Eurasia
[[-9,43],[-6,36],[3,41],[12,44],[18,40],[23,37],[28,41],[36,36],[35,28],[43,13],[52,16],[59,22],[51,27],[48,30],[57,26],[62,25],[67,24],[73,20],[77,8],[80,15],[87,22],[92,21],[98,16],[98,8],[104,1],[103,10],[109,12],[108,20],[117,23],[122,30],[121,37],[122,40],[128,39],[131,43],[141,52],[137,55],[143,59],[156,61],[163,62],[170,66],[180,68],[180,70],[160,70],[140,72],[113,74],[100,78],[80,73],[70,73],[60,70],[45,68],[40,65],[30,70],[18,70],[10,63],[5,60],[8,57],[9,54],[4,52],[-2,48],[-5,48],[-2,44],[-9,43]],
// Great Britain
[[-5,50],[1,51],[2,53],[-2,56],[-2,58],[-5,58],[-6,55],[-3,54],[-5,52],[-5,50]],
// Japan
[[130,31],[135,34],[140,35],[142,40],[141,45],[140,41],[136,36],[131,34],[130,31]],
// Borneo, Sumatra
[[109,1],[117,7],[119,1],[116,-4],[110,-3],[109,1]],
[[95,5],[100,1],[106,-6],[102,-4],[95,5]],
// Madagascar
[[44,-25],[47,-25],[50,-15],[49,-12],[44,-17],[44,-25]],
// Australia
[[114,-22],[114,-34],[118,-35],[124,-33],[131,-31],[138,-35],[141,-38],[147,-39],[150,-37],[153,-30],[153,-25],[146,-19],[142,-11],[141,-17],[136,-12],[131,-11],[126,-14],[122,-18],[114,-22]],
// New Zealand
[[172,-34],[178,-38],[174,-41],[167,-46],[172,-41],[172,-34]],
// Antarctica
[[-180,-78],[-150,-76],[-120,-73],[-90,-72],[-60,-64],[-45,-78],[-20,-72],[0,-70],[30,-69],[60,-67],[90,-66],[120,-66],[150,-69],[170,-77],[180,-78]]
];
// Break a lon/lat path into short steps so it hugs the sphere
function densify(path) {
const out = [];
for (let i = 0; i < path.length - 1; i++) {
const [a0, b0] = path[i], [a1, b1] = path[i + 1];
const n = Math.max(1, Math.ceil(Math.hypot(a1 - a0, b1 - b0) / 3));
for (let k = 0; k < n; k++) out.push([a0 + (a1 - a0) * k / n, b0 + (b1 - b0) * k / n]);
}
out.push(path[path.length - 1]);
return out;
}
const globe = []; // polylines in globe-local coords (these spin)
// graticule: parallels every 30°, meridians every 30°
for (let lat = -60; lat <= 60; lat += 30) {
const pts = [];
for (let lon = -180; lon <= 180; lon += 6) pts.push(sph(lon, lat));
globe.push({ pts, color: GRID, w: lat === 0 ? 1.1 : 0.8, dim: lat !== 0 });
}
for (let lon = -180; lon < 180; lon += 30) {
const pts = [];
for (let lat = -90; lat <= 90; lat += 6) pts.push(sph(lon, lat));
globe.push({ pts, color: GRID, w: 0.8, dim: true });
}
for (const path of LAND) {
globe.push({ pts: densify(path).map(([lo, la]) => sph(lo, la, 1.002)), color: WHITE, w: 1.5, land: true });
}
// Desk-globe stand (fixed; does not spin). Axis tilted 23.4° like Earth's.
const TILT = 23.4 * D;
const stand = [];
const axisRot = ([x, y, z]) => [x * Math.cos(TILT) - y * Math.sin(TILT), x * Math.sin(TILT) + y * Math.cos(TILT), z];
{
// Half meridian ring holding the globe, in the axis plane
for (const r of [1.1, 1.16]) {
const pts = [];
for (let a = -100; a <= 100; a += 4) pts.push(axisRot([-r * Math.cos(a * D), r * Math.sin(a * D), 0]));
stand.push({ pts, color: STAND, w: 1.4 });
}
// Ring end caps
for (const a of [-100, 100]) {
stand.push({ pts: [axisRot([-1.1 * Math.cos(a*D), 1.1 * Math.sin(a*D), 0]), axisRot([-1.16 * Math.cos(a*D), 1.16 * Math.sin(a*D), 0])], color: STAND, w: 1.2 });
}
// Axle pins through the poles
stand.push({ pts: [axisRot([0, 1.0, 0]), axisRot([0, 1.16, 0])], color: STAND, w: 1.6 });
stand.push({ pts: [axisRot([0, -1.0, 0]), axisRot([0, -1.16, 0])], color: STAND, w: 1.6 });
// Neck from the ring down to the base
const ringBottom = axisRot([-1.16 * Math.cos(-45 * D), 1.16 * Math.sin(-45 * D), 0]);
stand.push({ pts: [ringBottom, [ringBottom[0], -1.28, 0], [0, -1.32, 0]], color: STAND, w: 1.4 });
// Base: stacked ellipses
for (const [y, r] of [[-1.32, 0.35], [-1.4, 0.5], [-1.44, 0.5]]) {
const pts = [];
for (let a = 0; a <= 360; a += 10) pts.push([r * Math.cos(a * D), y, r * Math.sin(a * D)]);
stand.push({ pts, color: STAND, w: 1.2 });
}
for (let a = 0; a < 360; a += 45) {
stand.push({ pts: [[0.5 * Math.cos(a*D), -1.4, 0.5 * Math.sin(a*D)], [0.5 * Math.cos(a*D), -1.44, 0.5 * Math.sin(a*D)]], color: STAND, w: 0.8, dim: true });
}
}
// ---------- Transforms & projection ----------
const CENTER_Y = -0.17;
function toWorld(p, spin, isGlobe) {
let [x, y, z] = p;
if (isGlobe) {
// spin about the globe's own axis, then tilt the axis
const c = Math.cos(spin), s = Math.sin(spin);
[x, z] = [x * c + z * s, -x * s + z * c];
[x, y, z] = axisRot([x, y, z]);
}
return [x, y, z];
}
function project([x, y, z], view, jitter, bob) {
y = y - CENTER_Y + bob;
if (jitter) { x += (Math.random()-.5)*jitter; y += (Math.random()-.5)*jitter; }
const cy = Math.cos(view.yaw), sy = Math.sin(view.yaw);
const x1 = x * cy + z * sy, z1 = -x * sy + z * cy;
const cx = Math.cos(view.pitch), sx = Math.sin(view.pitch);
const y1 = y * cx - z1 * sx, z2 = y * sx + z1 * cx;
const d = 5, persp = d / (d - z2);
const scale = W * 0.27;
return [W/2 + x1 * scale * persp, H/2 - y1 * scale * persp, z2];
}
// Stroke segment by segment so the far side of the globe fades out properly
function strokeSet(polys, isGlobe, spin, view, color, alpha, ox, oy, jitter, bob) {
for (const poly of polys) {
const P = poly.pts.map(p => project(toWorld(p, spin, isGlobe), view, jitter, bob));
bctx.strokeStyle = color || poly.color;
bctx.lineWidth = poly.w * dpr;
for (let i = 0; i < P.length - 1; i++) {
const z = (P[i][2] + P[i + 1][2]) / 2;
let a;
if (isGlobe) {
// front hemisphere bright, back hemisphere faint
const front = Math.min(1, Math.max(0, (z + 0.25) / 0.5));
a = poly.land ? 0.12 + 0.88 * front : 0.1 + 0.6 * front;
} else {
a = 0.45 + 0.55 * Math.min(1, Math.max(0, (z + 1.2) / 2.4));
}
bctx.globalAlpha = alpha * a * (poly.dim ? 0.6 : 1);
bctx.beginPath();
bctx.moveTo(P[i][0] + ox, P[i][1] + oy);
bctx.lineTo(P[i + 1][0] + ox, P[i + 1][1] + oy);
bctx.stroke();
}
}
bctx.globalAlpha = 1;
}
function drawScene(spin, view, color, alpha, ox, oy, jitter, bob) {
strokeSet(stand, false, spin, view, color, alpha, ox, oy, jitter, bob);
strokeSet(globe, true, spin, view, color, alpha, ox, oy, jitter, bob);
}
// ---------- Loop ----------
let glitch = 0, glitchStrength = 0;
let spin = 0, last = performance.now();
let frozenSpin = null;
function frame(now) {
if (W === 0 || H === 0) {
resize();
if (W === 0 || H === 0) { last = now; requestAnimationFrame(frame); return; }
}
const dt = Math.min(50, now - last) / 1000; last = now;
spin += dt * (reduced ? 0.35 : 1.2);
if (!reduced && glitch <= 0 && Math.random() < 0.025) {
glitch = 4 + Math.floor(Math.random() * 10);
glitchStrength = 0.4 + Math.random() * 0.6;
if (Math.random() < 0.4) frozenSpin = spin + (Math.random() - .5) * 1.4;
}
const g = glitch > 0;
if (g) glitch--; else frozenSpin = null;
const t = now / 1000;
const bob = Math.sin(t * 2.2) * 0.03;
const view = { yaw: -0.35 + Math.sin(t * 0.5) * 0.15, pitch: 0.22 + Math.sin(t * 0.9) * 0.05 };
const s = frozenSpin ?? spin;
bctx.clearRect(0, 0, W, H);
bctx.lineCap = 'round';
bctx.globalCompositeOperation = 'lighter';
if (g) {
const off = (4 + Math.random() * 8) * dpr * glitchStrength;
drawScene(s, view, GHOST_A, 0.7, -off, 0, 0.03 * glitchStrength, bob);
drawScene(s, view, GHOST_B, 0.6, off, (Math.random()-.5)*2*dpr, 0.03 * glitchStrength, bob);
}
drawScene(s, view, null, g ? 0.85 : 1, 0, 0, g ? 0.02 : (reduced ? 0 : 0.004), bob);
bctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, W, H);
if (g) {
let y = 0;
while (y < H) {
const h = Math.min(H - y, Math.max(2, Math.floor((4 + Math.random() * 30) * dpr)));
const shift = Math.random() < 0.35 ? (Math.random() - .5) * 40 * dpr * glitchStrength : 0;
ctx.drawImage(buf, 0, y, W, h, shift, y, W, h);
y += h;
}
const blocks = Math.floor(Math.random() * 4);
for (let i = 0; i < blocks; i++) {
ctx.fillStyle = [WHITE, GHOST_A, GHOST_B][i % 3];
ctx.globalAlpha = 0.18 + Math.random() * 0.3;
ctx.fillRect(Math.random() * W, Math.random() * H, (10 + Math.random() * 60) * dpr, (1 + Math.random() * 3) * dpr);
}
ctx.globalAlpha = 1;
} else {
ctx.drawImage(buf, 0, 0);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>