02Dot-matrix globe filled with points
Points are spread evenly across the sphere and spun, and changing the seed redraws a new point layout.
Best forA section showing global visitor counts, a data center introduction page
Fibonacci sphereseeded randomdepth sort

<main class="stage">
<div class="gd" id="gd">
<p class="gd__hint">
<svg class="gd__ico" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24" opacity=".2"/><path d="M228 96h-42.81a126 126 0 0 0-10.51-32h36.24a103.87 103.87 0 0 0-30-28H75.08a103.87 103.87 0 0 0-30 28h36.24a126 126 0 0 0-10.51 32H28v64h42.3a126 126 0 0 0 10.51 32H44.92a103.87 103.87 0 0 0 30 28h105.84a103.87 103.87 0 0 0 30-28h-35.89a126 126 0 0 0 10.51-32H228Zm-68.75 64h-62.5a102.26 102.26 0 0 1 0-64h62.5a102.26 102.26 0 0 1 0 64"/></g></svg>
<span>Change the seed and the dot layout is re-drawn deterministically</span>
</p>
<div class="gd__field" id="field" role="application" aria-label="Dot-matrix globe test board" tabindex="0">
<canvas class="gd__cv" id="cv"></canvas>
</div>
<div class="gd__bar">
<label class="gd__dial">Seed
<input type="number" id="seed" min="1" max="9999" step="1" value="42" aria-label="Seed number">
</label>
<button class="gd__shuffle" id="shuffle" type="button">
<svg class="gd__ico" viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M32 72h32l16 24-13 9-11-17H32a8 8 0 0 1 0-16" opacity=".2"/><path d="M237.66 178.34a8 8 0 0 1 0 11.32l-24 24A8 8 0 0 1 200 208V184h-29.41a40 40 0 0 1-33.94-18.75L102.71 115.5A24 24 0 0 0 82.75 104H32a8 8 0 0 1 0-16h50.75a40 40 0 0 1 33.94 18.75l33.94 49.75a24 24 0 0 0 19.96 11.5H200v-24a8 8 0 0 1 13.66-5.66ZM32 152h50.75a24 24 0 0 0 19.96-11.5l8.11-11.87 9.51 13.93-6.9 10.11a40 40 0 0 1-33.94 18.72H32a8 8 0 0 1 0-16m150.34-82.34a8 8 0 0 0 13.66 5.66V96h-34.46a40 40 0 0 1-33.94-18.75l-6.9-10.11 9.51-13.93 8.11 11.87A24 24 0 0 0 158.29 80H196V56a8 8 0 0 0-13.66-5.66" fill-rule="evenodd"/></g></svg>
<span>Randomize</span>
</button>
<b class="gd__val" id="seedOut" aria-hidden="true">Seed <span class="gd__valNum" id="seedNum">42</span><span class="gd__valUnit"> · 850 dots</span></b>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 도트매트릭스 지구본 — 움직임은 전부 캔버스 rAF(Fibonacci sphere 분포 + depth sort + 시드 재생성).
// CSS 는 무대·판·시드 컨트롤만. 파스텔 팔레트 — vivid 리터럴 0(BD26).
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-mint;
@include stage($stage-ink);
.gd {
// 점 3계열 — 전부 저채도 파스텔(채도 .12~.30)
--gd-hi: #d8e8dc; // 밝은 크림그린 — 강조점
--gd-mid: #9fb8ab; // 회록 파스텔 — 다수점
--gd-lo: #6a8279; // 어두운 회록 — 뒤쪽 점
--gd-core: #1d2a24; // 코어 — 잉크보다 한 단계 밝은 저채표 톤
display: flex; flex-direction: column; gap: $sp-2;
width: 100%; max-width: 424px;
}
.gd__hint {
display: flex; align-items: center; gap: $sp-1; margin: 0;
font-weight: 700; font-size: clamp(10px, 2.8vw, 12px); line-height: 1.35;
color: $subject-cream;
}
.gd__ico { width: 16px; height: 16px; flex: 0 0 auto; color: $stage-yellow; }
.gd__field {
position: relative; height: 170px; // 시드 막대가 있어 300px 무대 안에 다 들어오게
border-radius: $r-card; overflow: hidden;
background: $stage-ink;
box-shadow: inset 0 0 0 1px rgba($subject-cream, .14);
}
.gd__field:focus-visible { outline: 2px solid $stage-yellow; outline-offset: 2px; }
.gd__cv { display: block; width: 100%; height: 100%; }
.gd__bar { display: flex; align-items: center; gap: $sp-3; flex-wrap: wrap; }
.gd__dial {
display: flex; align-items: center; gap: $sp-2;
font-weight: 700; font-size: 11px; color: $subject-cream;
}
.gd__dial input {
width: 72px; margin: 0;
padding: $sp-1 $sp-2; border: 0; border-radius: $r-sm;
background: rgba($subject-cream, .12); color: $subject-cream;
font-weight: 700; font-size: 12px; font-family: inherit;
}
.gd__dial input:focus-visible { outline: 2px solid $stage-yellow; outline-offset: 2px; }
.gd__shuffle {
display: flex; align-items: center; gap: $sp-1;
padding: $sp-1 $sp-3; border: 0; border-radius: $r-pill;
background: rgba($subject-cream, .12); color: $subject-cream;
font-weight: 700; font-size: 11px; font-family: inherit; cursor: pointer;
}
.gd__shuffle:hover { background: rgba($subject-cream, .2); }
.gd__shuffle:focus-visible { outline: 2px solid $stage-yellow; outline-offset: 2px; }
.gd__val { color: $subject-mint; font-size: 11px; font-weight: 700; }
@media (max-width: 340px) {
.gd { gap: $sp-1; }
.gd__field { height: 96px; }
.gd__hint { font-size: 10px; }
.gd__bar { gap: $sp-2; }
.gd__dial input { width: 56px; }
}
// reduce — 루프가 안 돈다. 시드 입력은 여전히 결정적으로 한 장을 다시 그린다.
@media (prefers-reduced-motion: reduce) {
.gd__field { cursor: default !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.gd {
--gd-hi: #d8e8dc;
--gd-mid: #9fb8ab;
--gd-lo: #6a8279;
--gd-core: #1d2a24;
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 424px;
}
.gd__hint {
display: flex;
align-items: center;
gap: 4px;
margin: 0;
font-weight: 700;
font-size: clamp(10px, 2.8vw, 12px);
line-height: 1.35;
color: #fff7e6;
}
.gd__ico {
width: 16px;
height: 16px;
flex: 0 0 auto;
color: #ffd23f;
}
.gd__field {
position: relative;
height: 170px;
border-radius: 16px;
overflow: hidden;
background: #17141a;
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.14);
}
.gd__field:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: 2px;
}
.gd__cv {
display: block;
width: 100%;
height: 100%;
}
.gd__bar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.gd__dial {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
font-size: 11px;
color: #fff7e6;
}
.gd__dial input {
width: 72px;
margin: 0;
padding: 4px 8px;
border: 0;
border-radius: 8px;
background: rgba(255, 247, 230, 0.12);
color: #fff7e6;
font-weight: 700;
font-size: 12px;
font-family: inherit;
}
.gd__dial input:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: 2px;
}
.gd__shuffle {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border: 0;
border-radius: 999px;
background: rgba(255, 247, 230, 0.12);
color: #fff7e6;
font-weight: 700;
font-size: 11px;
font-family: inherit;
cursor: pointer;
}
.gd__shuffle:hover {
background: rgba(255, 247, 230, 0.2);
}
.gd__shuffle:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: 2px;
}
.gd__val {
color: #4cd4a6;
font-size: 11px;
font-weight: 700;
}
@media (max-width: 340px) {
.gd {
gap: 4px;
}
.gd__field {
height: 96px;
}
.gd__hint {
font-size: 10px;
}
.gd__bar {
gap: 8px;
}
.gd__dial input {
width: 56px;
}
}
@media (prefers-reduced-motion: reduce) {
.gd__field {
cursor: default !important;
}
}// Dot-matrix globe — Fibonacci sphere 로 구 표면에 점을 고르게 뿌린다.
// Fibonacci sphere: i 번째 점의 위도는 acos(1 - 2(i+0.5)/N), 경도는 황금각(golden angle) 만큼씩 돈다.
// 같은 Seed = 같은 그림. 시드는 난수 계열(mulberry32)을 고르고, 그 계열이 점의 반경·밝기·톤을 뽑는다.
var root = document.getElementById('gd');
var field = document.getElementById('field');
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');
var seedIn = document.getElementById('seed');
var shuffleBtn = document.getElementById('shuffle');
var seedOut = document.getElementById('seedNum');
var TAU = Math.PI * 2;
var GOLDEN = Math.PI * (3 - Math.sqrt(5)); // 황금각 약 2.39996rad
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var W = 0, H = 0, dpr = 1;
// 결정적 난수 — mulberry32. 같은 시드면 같은 수열을 돌려준다
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
var N = 850; // 점 개수
var dots = []; // { x,y,z(단위구), r(반경 배율), tone(0..1), jitter(위상) }
var seedVal = 42;
var rotY = 0;
var last = 0, raf = 0;
function buildDots() {
var rnd = mulberry32(seedVal);
dots = [];
for (var i = 0; i < N; i++) {
// Fibonacci sphere — 균등 분포. 황금각 회전이 나선 잎차례를 만든다
var y = 1 - 2 * (i + 0.5) / N;
var rLat = Math.sqrt(1 - y * y);
var phi = i * GOLDEN;
var jitterR = rnd();
dots.push({
x: Math.cos(phi) * rLat,
y: y,
z: Math.sin(phi) * rLat,
r: 0.6 + jitterR * 0.9, // 반경 다양성도 시드가 정한다
tone: rnd(),
jt: rnd() * TAU // 반짝임 위상
});
}
seedOut.textContent = String(seedVal);
}
function fit() {
var r = field.getBoundingClientRect();
dpr = window.devicePixelRatio || 1;
W = r.width; H = r.height;
cv.width = Math.round(W * dpr);
cv.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function draw(now) {
ctx.clearRect(0, 0, W, H);
var cx = W / 2, cy = H / 2;
var R = Math.min(W, H) * 0.40;
var cs = getComputedStyle(root);
var C_hi = cs.getPropertyValue('--gd-hi').trim();
var C_mid = cs.getPropertyValue('--gd-mid').trim();
var C_lo = cs.getPropertyValue('--gd-lo').trim();
var C_core = cs.getPropertyValue('--gd-core').trim();
// 바닥 코어 — 점 구름의 중심을 은은히 밝힌다(구체가 아니라 점의 집단임을 읽게)
ctx.beginPath();
ctx.arc(cx, cy, R, 0, TAU);
ctx.fillStyle = C_core;
ctx.fill();
var cyr = Math.cos(rotY), syr = Math.sin(rotY);
var t = now * 0.001;
// depth sort — 먼 점(z<0, 뒤)부터 그려야 앞 점이 위에 얹힌다
var order = [];
for (var i = 0; i < N; i++) {
var d = dots[i];
var lx = d.x * cyr + d.z * syr;
var lz = -d.x * syr + d.z * cyr;
order.push({ p: d, lx: lx, lz: lz });
}
order.sort(function (a, b) { return a.lz - b.lz; });
for (var k = 0; k < N; k++) {
var o = order[k];
var depth = (o.lz + 1) / 2; // 0(뒤)..1(앞)
if (depth < 0.08) continue; // 맨 뒤는 림 바깥 실루엣만 남긴다
var px = cx + o.lx * R;
var py = cy - o.p.y * R;
// 원근 없는 정사도라 깊이는 크기가 아니라 밝기·알파로만 녹인다
var alpha = 0.16 + depth * 0.84;
var tw = 0.75 + 0.25 * Math.sin(t * 2 + o.p.jt); // 반짝임 — 시간에 따라 살아 있는다
ctx.globalAlpha = alpha * tw;
ctx.fillStyle = o.p.tone > 0.82 ? C_hi : (o.p.tone > 0.45 ? C_mid : C_lo);
ctx.beginPath();
ctx.arc(px, py, o.p.r * (0.55 + depth * 0.5), 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function frame(now) {
var dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016;
last = now;
rotY += dt * 0.35;
draw(now);
raf = requestAnimationFrame(frame);
}
function applySeed(v) {
seedVal = Math.max(1, Math.min(9999, Math.floor(v) || 1));
seedIn.value = String(seedVal);
buildDots();
if (reduce) draw(0);
}
seedIn.addEventListener('input', function () { applySeed(Number(seedIn.value)); });
shuffleBtn.addEventListener('click', function () {
applySeed(1 + Math.floor(Math.random() * 9999));
});
field.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
applySeed(1 + Math.floor(Math.random() * 9999));
}
});
window.addEventListener('resize', function () { fit(); draw(0); });
fit();
buildDots();
if (reduce) {
draw(0); // reduce — 루프 없이 시드의 한 장만
} else {
raf = requestAnimationFrame(frame);
}// GlobeDotMatrix — vanilla 판(run/304/demo/02-globe-dot-matrix/index.html) 의 React 포트.
// Fibonacci sphere 로 구 표면에 점을 고르게 뿌린 도트 지구본. 같은 시드 = 같은 그림(mulberry32).
// 움직임은 캔버스 rAF 루프가 전부 그리므로 @keyframes 미리보기가 없다 — props 는 CSS 변수로 무대 격식에만 내려간다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./GlobeDotMatrix.module.scss";
export type GlobeDotMatrixProps = {
duration?: string;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Box<T> = { current: T };
type KeyEvt = { key: string; preventDefault(): void };
type InputEvt = { target: { value: string } };
type Dot = { x: number; y: number; z: number; r: number; tone: number; jt: number };
const TAU = Math.PI * 2;
const GOLDEN = Math.PI * (3 - Math.sqrt(5)); // 황금각 약 2.39996rad
const N = 850; // 점 개수
const FIELD_LABEL = "도트매트릭스 지구본 실험판";
// 결정적 난수 — mulberry32. 같은 시드면 같은 수열을 돌려준다
function mulberry32(a: number): () => number {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
const t = Math.imul(a ^ (a >>> 15), 1 | a);
const u = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((u ^ (u >>> 14)) >>> 0) / 4294967296;
};
}
export default function GlobeDotMatrix({
duration = "2s",
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#4cd4a6",
}: GlobeDotMatrixProps) {
const fieldRef = useRef<HTMLDivElement | null>(null);
const cvRef = useRef<HTMLCanvasElement | null>(null);
const rootRef = useRef<HTMLDivElement | null>(null);
const seedRef = useRef<HTMLInputElement | null>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const sizeRef = useRef({ w: 0, h: 0 }) as Box<{ w: number; h: number }>;
const dotsRef = useRef<Dot[]>([]) as Box<Dot[]>;
const seedValRef = useRef(42) as Box<number>;
const rotRef = useRef(0) as Box<number>;
const lastRef = useRef(0) as Box<number>;
const rafRef = useRef(0) as Box<number>;
const [seedNum, setSeedNum] = useState(42);
const vars: Vars = { "--duration": duration, "--easing": easing, "--color": color };
function reduced(): boolean {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function buildDots(): void {
const rnd = mulberry32(seedValRef.current);
const dots = dotsRef.current;
dots.length = 0;
for (let i = 0; i < N; i++) {
// Fibonacci sphere — 균등 분포. 황금각 회전이 나선 잎차례를 만든다
const y = 1 - 2 * (i + 0.5) / N;
const rLat = Math.sqrt(1 - y * y);
const phi = i * GOLDEN;
const jitterR = rnd();
dots.push({
x: Math.cos(phi) * rLat,
y: y,
z: Math.sin(phi) * rLat,
r: 0.6 + jitterR * 0.9, // 반경 다양성도 시드가 정한다
tone: rnd(),
jt: rnd() * TAU // 반짝임 위상
});
}
setSeedNum(seedValRef.current);
}
function fit(): void {
const field = fieldRef.current;
const cv = cvRef.current;
const ctx = ctxRef.current;
if (!field || !cv || !ctx) return;
const r = field.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
sizeRef.current.w = r.width;
sizeRef.current.h = r.height;
cv.width = Math.round(r.width * dpr);
cv.height = Math.round(r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function draw(now: number): void {
const ctx = ctxRef.current;
const root = rootRef.current;
if (!ctx || !root) return;
const W = sizeRef.current.w, H = sizeRef.current.h;
ctx.clearRect(0, 0, W, H);
const cx = W / 2, cy = H / 2;
const R = Math.min(W, H) * 0.40;
const cs = getComputedStyle(root);
const C_hi = cs.getPropertyValue("--gd-hi").trim();
const C_mid = cs.getPropertyValue("--gd-mid").trim();
const C_lo = cs.getPropertyValue("--gd-lo").trim();
const C_core = cs.getPropertyValue("--gd-core").trim();
// 바닥 코어 — 점 구름의 중심을 은은히 밝힌다(구체가 아니라 점의 집단임을 읽게)
ctx.beginPath();
ctx.arc(cx, cy, R, 0, TAU);
ctx.fillStyle = C_core;
ctx.fill();
const rotY = rotRef.current;
const cyr = Math.cos(rotY), syr = Math.sin(rotY);
const t = now * 0.001;
// depth sort — 먼 점(z<0, 뒤)부터 그려야 앞 점이 위에 얹힌다
const order: { p: Dot; lx: number; lz: number }[] = [];
const dots = dotsRef.current;
for (let i = 0; i < dots.length; i++) {
const d = dots[i];
const lx = d.x * cyr + d.z * syr;
const lz = -d.x * syr + d.z * cyr;
order.push({ p: d, lx, lz });
}
order.sort((a, b) => a.lz - b.lz);
for (let k = 0; k < order.length; k++) {
const o = order[k];
const depth = (o.lz + 1) / 2; // 0(뒤)..1(앞)
if (depth < 0.08) continue; // 맨 뒤는 림 바깥 실루엣만 남긴다
const px = cx + o.lx * R;
const py = cy - o.p.y * R;
const alpha = 0.16 + depth * 0.84;
const tw = 0.75 + 0.25 * Math.sin(t * 2 + o.p.jt); // 반짝임
ctx.globalAlpha = alpha * tw;
ctx.fillStyle = o.p.tone > 0.82 ? C_hi : (o.p.tone > 0.45 ? C_mid : C_lo);
ctx.beginPath();
ctx.arc(px, py, o.p.r * (0.55 + depth * 0.5), 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function frame(now: number): void {
const dt = lastRef.current ? Math.min((now - lastRef.current) / 1000, 0.05) : 0.016;
lastRef.current = now;
rotRef.current += dt * 0.35;
draw(now);
rafRef.current = requestAnimationFrame(frame);
}
function applySeed(v: number): void {
seedValRef.current = Math.max(1, Math.min(9999, Math.floor(v) || 1));
if (seedRef.current) seedRef.current.value = String(seedValRef.current);
buildDots();
if (reduced()) draw(0);
}
function onSeedInput(event: InputEvt): void {
applySeed(Number(event.target.value));
}
function onShuffle(): void {
applySeed(1 + Math.floor(Math.random() * 9999));
}
function onFieldKey(event: KeyEvt): void {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onShuffle();
}
}
useEffect(() => {
const cv = cvRef.current;
if (!cv) return;
ctxRef.current = cv.getContext("2d");
if (!ctxRef.current) return;
fit();
buildDots();
const onResize = (): void => { fit(); draw(0); };
window.addEventListener("resize", onResize);
if (reduced()) {
draw(0); // reduce — 루프 없이 시드의 한 장만
} else {
lastRef.current = 0;
rafRef.current = requestAnimationFrame(frame);
}
return () => {
window.removeEventListener("resize", onResize);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.root} ref={rootRef} style={vars}>
<p className={styles.hint}>
<svg className={styles.ico} viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24" opacity=".2"/><path d="M228 96h-42.81a126 126 0 0 0-10.51-32h36.24a103.87 103.87 0 0 0-30-28H75.08a103.87 103.87 0 0 0-30 28h36.24a126 126 0 0 0-10.51 32H28v64h42.3a126 126 0 0 0 10.51 32H44.92a103.87 103.87 0 0 0 30 28h105.84a103.87 103.87 0 0 0 30-28h-35.89a126 126 0 0 0 10.51-32H228Zm-68.75 64h-62.5a102.26 102.26 0 0 1 0-64h62.5a102.26 102.26 0 0 1 0 64"/></g></svg>
<span>시드를 바꾸면 점 배치가 결정적으로 다시 뽑힙니다</span>
</p>
<div
className={styles.field}
ref={fieldRef}
role="application"
aria-label={FIELD_LABEL}
tabIndex={0}
onKeyDown={onFieldKey}
>
<canvas className={styles.cv} ref={cvRef}></canvas>
</div>
<div className={styles.bar}>
<label className={styles.dial}>시드
<input type="number" ref={seedRef} min={1} max={9999} step={1} defaultValue={42} aria-label="시드 번호" onChange={onSeedInput} />
</label>
<button className={styles.shuffle} type="button" onClick={onShuffle}>
<svg className={styles.ico} viewBox="0 0 256 256" aria-hidden="true"><g fill="currentColor"><path d="M32 72h32l16 24-13 9-11-17H32a8 8 0 0 1 0-16" opacity=".2"/><path d="M237.66 178.34a8 8 0 0 1 0 11.32l-24 24A8 8 0 0 1 200 208V184h-29.41a40 40 0 0 1-33.94-18.75L102.71 115.5A24 24 0 0 0 82.75 104H32a8 8 0 0 1 0-16h50.75a40 40 0 0 1 33.94 18.75l33.94 49.75a24 24 0 0 0 19.96 11.5H200v-24a8 8 0 0 1 13.66-5.66ZM32 152h50.75a24 24 0 0 0 19.96-11.5l8.11-11.87 9.51 13.93-6.9 10.11a40 40 0 0 1-33.94 18.72H32a8 8 0 0 1 0-16m150.34-82.34a8 8 0 0 0 13.66 5.66V96h-34.46a40 40 0 0 1-33.94-18.75l-6.9-10.11 9.51-13.93 8.11 11.87A24 24 0 0 0 158.29 80H196V56a8 8 0 0 0-13.66-5.66" fillRule="evenodd"/></g></svg>
<span>무작위</span>
</button>
<b className={styles.val} aria-hidden="true">시드 <span className={styles.valNum}>{seedNum}</span><span className={styles.valUnit}> · 점 850개</span></b>
</div>
</div>
</main>
);
}02 Dot-matrix globe filled with points — Measured area 10.422% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 3D Globe Website: 9 Canvas Orbit Scenes
The post this demo lives in
3D Globe Website: 9 Canvas Orbit ScenesA 3d globe website usually loads Three.js. These nine scenes run with no Three.js — just Canvas 2D math — and the widest See all demos in this post
If you like this effect - similar demos
Creative UI02 Soft Mesh with Drifting 3x3 Control PointsMesh Gradient Background: 9 Canvas Animations
Creative UI01 An Ordered Dither That Stamps a Moving Gradient in Two Colors with a Bayer MatrixCSS Dither: 9 Scenes Drawn by Threshold Math
Creative UI04 Ordered Dither That Shows Light and Shade With Only Two GlyphsASCII Art JavaScript: 9 Canvas Glyph Scenes