652 lines
28 KiB
HTML
652 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>yigid.net</title>
|
||
<meta name="description" content="Root domain of a private network run by yigit balaban. Nothing is served here.">
|
||
<meta name="author" content="Yigit BALABAN">
|
||
<link rel="canonical" href="https://yigid.net/">
|
||
<meta name="color-scheme" content="dark light">
|
||
<meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)">
|
||
<meta name="theme-color" content="#cecece" media="(prefers-color-scheme: light)">
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='5' fill='%23ffc100'/%3E%3C/svg%3E">
|
||
|
||
<!-- open graph -->
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:site_name" content="yigid.net">
|
||
<meta property="og:title" content="yigid.net">
|
||
<meta property="og:description" content="Root domain of a private network run by yigit balaban. Nothing is served here.">
|
||
<meta property="og:url" content="https://yigid.net/">
|
||
<meta name="twitter:card" content="summary">
|
||
|
||
<!-- crosslinks to yigid.dev -->
|
||
<link rel="author" href="https://yigid.dev">
|
||
<link rel="me" href="https://yigid.dev">
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@graph": [
|
||
{
|
||
"@type": "WebSite",
|
||
"@id": "https://yigid.net/#website",
|
||
"url": "https://yigid.net/",
|
||
"name": "yigid.net",
|
||
"description": "Root domain of a private network run by yigit balaban.",
|
||
"inLanguage": "en",
|
||
"author": { "@id": "https://yigid.dev/#person" },
|
||
"publisher": { "@id": "https://yigid.dev/#person" }
|
||
},
|
||
{
|
||
"@type": "Person",
|
||
"@id": "https://yigid.dev/#person",
|
||
"name": "Yigit BALABAN",
|
||
"alternateName": ["Ferit Yigit Balaban", "Ferit Yiğit Balaban", "Yigid Balaban", "yigit balaban", "yiğit balaban"],
|
||
"url": "https://yigid.dev",
|
||
"sameAs": [
|
||
"https://github.com/itsyigid",
|
||
"https://medium.com/@balaban",
|
||
"https://x.com/itsyigid",
|
||
"https://www.linkedin.com/in/yigid",
|
||
"https://instagram.com/itsyigid",
|
||
"https://git.yigid.dev/yigid"
|
||
]
|
||
}
|
||
]
|
||
}
|
||
</script>
|
||
<style>
|
||
/* ── yigid.dev DS — fonts.css (self-hosted; drop the woff2 files into ./fonts) ── */
|
||
@font-face { font-family: "Lexend"; src: local("Lexend"), url("fonts/Lexend.woff2") format("woff2"); font-weight: 400 600; font-display: swap; }
|
||
@font-face { font-family: "Raleway"; src: local("Raleway"), url("fonts/Raleway.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||
@font-face { font-family: "Raleway"; src: local("Raleway Italic"), url("fonts/Raleway-Italic.woff2") format("woff2"); font-weight: 400; font-style: italic; font-display: swap; }
|
||
@font-face { font-family: "Fira Code"; src: local("Fira Code"), url("fonts/FiraCode-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
|
||
|
||
/* ── yigid.dev DS — colors.css, typography.css, spacing.css ── */
|
||
:root {
|
||
--c-white: #ffffff;
|
||
--c-black: #000000;
|
||
--c-gray-0: #1a1a1a;
|
||
--c-gray-1: #cecece;
|
||
--c-amber: #ffc100;
|
||
--c-blue: #004099;
|
||
|
||
/* dark is the default, as on yigid.dev */
|
||
--bg-color: var(--c-gray-0);
|
||
--fg-color: var(--c-white);
|
||
--fg-color-sec: rgba(255, 255, 255, 0.85);
|
||
--fg-color-thr: rgba(255, 255, 255, 0.70);
|
||
--spec-color: var(--c-amber);
|
||
--border-hairline: rgba(255, 255, 255, 0.14);
|
||
|
||
--font-display: "Lexend", sans-serif;
|
||
--font-body: "Raleway", sans-serif;
|
||
--font-mono: "Fira Code", ui-monospace, monospace;
|
||
--fw-regular: 400;
|
||
--fw-medium: 500;
|
||
--fw-semibold: 600;
|
||
--lh-tight: 1.2;
|
||
--lh-body: 1.6;
|
||
|
||
--space-1: 0.25rem;
|
||
--space-2: 0.5rem;
|
||
--space-3: 1rem;
|
||
--space-4: 1.5rem;
|
||
--space-5: 2rem;
|
||
--space-6: 3rem;
|
||
}
|
||
@media (prefers-color-scheme: light) {
|
||
:root {
|
||
--bg-color: var(--c-gray-1);
|
||
--fg-color: var(--c-black);
|
||
--fg-color-sec: rgba(0, 0, 0, 0.85);
|
||
--fg-color-thr: rgba(0, 0, 0, 0.70);
|
||
--spec-color: var(--c-blue);
|
||
--border-hairline: rgba(0, 0, 0, 0.14);
|
||
}
|
||
}
|
||
|
||
/* this sheet's names for the DS tokens */
|
||
:root {
|
||
--paper: var(--bg-color);
|
||
--ink: var(--fg-color);
|
||
--ink-2: var(--fg-color-thr);
|
||
--ink-3: color-mix(in srgb, var(--fg-color) 35%, transparent); /* faintest step; not in the DS */
|
||
--rule: var(--border-hairline);
|
||
--signal: var(--spec-color);
|
||
--mono: var(--font-mono);
|
||
--sans: var(--font-display);
|
||
--body: var(--font-body);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html, body { height: 100%; }
|
||
body {
|
||
background: var(--paper);
|
||
color: var(--ink);
|
||
font: 13px/var(--lh-body) var(--mono);
|
||
-webkit-font-smoothing: antialiased;
|
||
padding: 20px;
|
||
}
|
||
a { color: inherit; text-decoration: underline; text-decoration-color: var(--signal); text-underline-offset: 3px; text-decoration-thickness: 1px; }
|
||
a:hover { color: var(--signal); }
|
||
::selection { background: var(--signal); color: var(--paper); }
|
||
|
||
/* ── the sheet ─────────────────────────────────────── */
|
||
.sheet {
|
||
min-height: calc(100vh - 40px);
|
||
border: 1px solid var(--ink);
|
||
display: grid;
|
||
grid-template-columns: 18px 1fr 18px;
|
||
grid-template-rows: 18px 1fr 18px;
|
||
}
|
||
.ref {
|
||
display: flex; justify-content: space-around; align-items: center;
|
||
font-size: 9px; color: var(--ink-2); letter-spacing: .1em;
|
||
}
|
||
.ref.h { grid-column: 2; }
|
||
.ref.v { flex-direction: column; grid-row: 2; }
|
||
.ref.top { grid-row: 1; border-bottom: 1px solid var(--ink); }
|
||
.ref.bot { grid-row: 3; border-top: 1px solid var(--ink); }
|
||
.ref.left { grid-column: 1; border-right: 1px solid var(--ink); }
|
||
.ref.right { grid-column: 3; border-left: 1px solid var(--ink); }
|
||
.ref span { position: relative; }
|
||
.ref.h span::before, .ref.v span::before { content: ""; position: absolute; background: var(--ink-3); }
|
||
.ref.h span::before { width: 1px; height: 4px; left: 50%; }
|
||
.ref.top span::before { top: -6px; } .ref.bot span::before { bottom: -6px; }
|
||
.ref.v span::before { height: 1px; width: 4px; top: 50%; }
|
||
.ref.left span::before { left: -6px; } .ref.right span::before { right: -6px; }
|
||
|
||
.field {
|
||
grid-column: 2; grid-row: 2;
|
||
display: grid;
|
||
grid-template-columns: minmax(300px, 11fr) 13fr;
|
||
grid-template-rows: 1fr auto;
|
||
background-image:
|
||
radial-gradient(circle, var(--rule) 1px, transparent 1.2px);
|
||
background-size: 24px 24px;
|
||
background-position: 12px 12px;
|
||
}
|
||
|
||
/* ── left column: words ────────────────────────────── */
|
||
.words {
|
||
padding: clamp(24px, 3.5vw, 48px);
|
||
display: flex; flex-direction: column; gap: var(--space-4);
|
||
grid-row: 1 / 3;
|
||
}
|
||
.mark {
|
||
font-family: var(--sans);
|
||
font-weight: var(--fw-semibold);
|
||
font-size: clamp(44px, 6vw, 84px);
|
||
line-height: .9;
|
||
letter-spacing: -.045em;
|
||
}
|
||
.mark .dot { color: var(--signal); }
|
||
/* the dot ticks with the wall clock: lit on the second, dark for the last 40% of it */
|
||
.mark .dot.tick { animation: tick 1s step-end infinite; }
|
||
@keyframes tick { 0% { opacity: 1; } 60% { opacity: 0; } }
|
||
@media (prefers-reduced-motion: reduce) { .mark .dot.tick { animation: none; } }
|
||
.lede {
|
||
font-family: var(--sans);
|
||
font-weight: var(--fw-regular);
|
||
font-size: clamp(17px, 1.6vw, 21px);
|
||
line-height: var(--lh-tight);
|
||
letter-spacing: -.01em;
|
||
max-width: 26ch;
|
||
margin-top: 18px;
|
||
}
|
||
.lede em { font-style: normal; color: var(--ink-2); }
|
||
/* redactions carry no text — there is nothing under the bar to select or view-source */
|
||
.x {
|
||
position: relative;
|
||
display: inline-block; width: calc(var(--w, 6) * 1ch); height: 1.05em;
|
||
background: var(--ink); vertical-align: -.18em;
|
||
cursor: help; outline: none;
|
||
transform-origin: 50% 100%;
|
||
}
|
||
.x::after {
|
||
content: attr(data-say);
|
||
position: absolute; left: 50%; bottom: calc(100% + 7px); z-index: 2;
|
||
transform: translate(-50%, 4px); opacity: 0;
|
||
padding: 4px 7px 3px; border: 1px solid var(--ink); background: var(--paper);
|
||
font: 11px/1 var(--mono); color: var(--ink); white-space: nowrap;
|
||
pointer-events: none;
|
||
transition: opacity .16s, transform .16s;
|
||
}
|
||
.x:hover::after, .x:focus-visible::after, .x.say::after { opacity: 1; transform: translate(-50%, 0); }
|
||
.x:focus-visible { box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--signal); }
|
||
.x.shrug { animation: shrug .62s cubic-bezier(.3, .7, .4, 1); }
|
||
.x.no { animation: no .5s ease-in-out; }
|
||
.x.hop { animation: hop .55s cubic-bezier(.3, .7, .4, 1); }
|
||
.x.flinch{ animation: flinch .45s ease-out; }
|
||
@keyframes shrug {
|
||
0% { transform: none; }
|
||
18% { transform: translateY(-4px) scale(1.08, .86); }
|
||
38% { transform: translateY(0) scale(.96, 1.06); }
|
||
54% { transform: translateY(-2px) rotate(-3deg); }
|
||
72% { transform: rotate(2deg); }
|
||
100% { transform: none; }
|
||
}
|
||
@keyframes no {
|
||
0%, 100% { transform: none; }
|
||
20% { transform: translateX(-3px) rotate(-1deg); }
|
||
40% { transform: translateX(3px) rotate(1deg); }
|
||
60% { transform: translateX(-2px); }
|
||
80% { transform: translateX(1px); }
|
||
}
|
||
@keyframes hop {
|
||
0%, 100% { transform: none; }
|
||
30% { transform: translateY(-6px) scale(.94, 1.08); }
|
||
55% { transform: translateY(0) scale(1.1, .88); }
|
||
75% { transform: scale(.98, 1.03); }
|
||
}
|
||
@keyframes flinch {
|
||
0%, 100% { transform: none; }
|
||
25% { transform: scale(.82, .9) translateY(1px); }
|
||
60% { transform: scale(1.04, .98); }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) { .x.shrug, .x.no, .x.hop, .x.flinch { animation: none; } .x::after { transition: none; } }
|
||
|
||
.notes, .legend { border-top: 1px solid var(--ink); max-width: 54ch; }
|
||
.notes h2, .legend h2, .tb h2 {
|
||
font-size: 9px; font-weight: 400; letter-spacing: .14em; text-transform: uppercase;
|
||
color: var(--ink-2);
|
||
}
|
||
.notes h2, .legend h2 { padding: 8px 0; border-bottom: 1px solid var(--rule); }
|
||
.notes ol { list-style: none; counter-reset: n; }
|
||
.notes li {
|
||
counter-increment: n; display: grid; grid-template-columns: 3ch 1fr;
|
||
padding: 7px 0; border-bottom: 1px solid var(--rule); color: var(--fg-color-sec);
|
||
font: 14.5px/1.5 var(--body);
|
||
}
|
||
.notes li::before { content: counter(n) "."; color: var(--ink); font-family: var(--mono); font-size: 13px; }
|
||
.notes a { color: var(--ink); }
|
||
|
||
.legend table { width: 100%; border-collapse: collapse; }
|
||
.legend td { padding: 7px 0; border-bottom: 1px solid var(--rule); color: var(--ink-2); vertical-align: top; }
|
||
.legend td:first-child { color: var(--ink); padding-right: 20px; white-space: nowrap; }
|
||
.legend td:last-child { text-align: right; font-size: 10px; letter-spacing: .12em; text-transform: uppercase; }
|
||
.legend .pub { color: var(--ink); }
|
||
|
||
/* the classification stamp */
|
||
.stamp {
|
||
position: absolute; right: 36px; top: -76px;
|
||
padding: 9px 14px 8px;
|
||
border: 2px solid var(--signal);
|
||
box-shadow: inset 0 0 0 2px var(--paper), inset 0 0 0 3px var(--signal);
|
||
color: var(--signal);
|
||
font: 700 13px/1.15 var(--sans); letter-spacing: .1em; text-transform: uppercase;
|
||
text-align: center;
|
||
transform: rotate(-6deg);
|
||
opacity: .9;
|
||
mix-blend-mode: multiply;
|
||
pointer-events: none; user-select: none;
|
||
}
|
||
.stamp small { display: block; font: 400 8.5px/1.4 var(--mono); letter-spacing: .08em; margin-top: 4px; }
|
||
@media (prefers-color-scheme: dark) { .stamp { mix-blend-mode: normal; opacity: .85; } }
|
||
|
||
/* ── right column: drawing ─────────────────────────── */
|
||
.drawing {
|
||
position: relative;
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: 24px;
|
||
min-height: 420px;
|
||
}
|
||
.drawing svg { width: 100%; max-width: 760px; max-height: calc(100vh - 200px); height: auto; overflow: visible; }
|
||
.drawing svg { -webkit-user-select: none; user-select: none; }
|
||
svg text { font-family: var(--mono); fill: var(--ink); stroke: var(--paper); stroke-width: 3px; paint-order: stroke; stroke-linejoin: round; }
|
||
svg .sub { fill: var(--ink-2); }
|
||
svg .faint { fill: var(--ink-3); }
|
||
.callout {
|
||
position: absolute; top: 24px; right: 24px;
|
||
font-size: 9px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-2);
|
||
text-align: right;
|
||
}
|
||
.callout b { display: block; font-weight: 400; color: var(--ink); letter-spacing: .05em; font-size: 11px; text-transform: none; }
|
||
|
||
/* ── title block ───────────────────────────────────── */
|
||
.tb {
|
||
position: relative;
|
||
grid-column: 2; grid-row: 2;
|
||
justify-self: end; align-self: end;
|
||
display: grid;
|
||
grid-template-columns: repeat(4, auto);
|
||
gap: 1px;
|
||
background: var(--ink);
|
||
border-top: 1px solid var(--ink);
|
||
border-left: 1px solid var(--ink);
|
||
min-width: min(100%, 560px);
|
||
}
|
||
.tb > div:not(.stamp) { padding: 6px 12px 7px; background: var(--paper); }
|
||
.tb .wide { grid-column: span 2; }
|
||
.tb p { font-size: 12px; white-space: nowrap; }
|
||
.tb .ob { color: var(--ink-3); }
|
||
|
||
/* ── small screens ─────────────────────────────────── */
|
||
@media (max-width: 900px) {
|
||
body { padding: 12px; }
|
||
.sheet { min-height: calc(100vh - 24px); grid-template-columns: 14px 1fr 14px; grid-template-rows: 14px 1fr 14px; }
|
||
.field { grid-template-columns: 1fr; grid-template-rows: auto auto auto auto; }
|
||
.words { grid-row: auto; padding: 28px 20px 8px; }
|
||
.drawing { grid-row: 2; padding: 8px 4px 24px; min-height: 0; }
|
||
.callout { position: static; text-align: left; padding: 0 16px; }
|
||
.drawing { flex-direction: column; align-items: stretch; gap: 8px; }
|
||
.tb { grid-column: 1; grid-row: 4; justify-self: stretch; min-width: 0; grid-template-columns: 1fr 1fr; border-left: 0; }
|
||
.tb p { white-space: normal; }
|
||
.stamp { top: auto; bottom: calc(100% + 28px); right: 20px; }
|
||
}
|
||
@media (max-width: 520px) { .ref span:nth-child(n+5) { display: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="sheet">
|
||
<div class="ref h top"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span></div>
|
||
<div class="ref v left"><span>1</span><span>2</span><span>3</span><span>4</span></div>
|
||
<div class="ref v right"><span>1</span><span>2</span><span>3</span><span>4</span></div>
|
||
<div class="ref h bot"><span>A</span><span>B</span><span>C</span><span>D</span><span>E</span><span>F</span></div>
|
||
|
||
<div class="field">
|
||
<section class="words">
|
||
<header>
|
||
<h1 class="mark">yigid<span class="dot">.</span>net</h1>
|
||
<p class="lede">Root of a private network. <em>Nothing is served here.</em></p>
|
||
</header>
|
||
|
||
<div class="notes">
|
||
<h2>Notes</h2>
|
||
<ol>
|
||
<li><span>This page is the only public thing at this address.</span></li>
|
||
<li><span>Hosts and services under it sit on a private network across <i class="x" style="--w:2" aria-label="redacted"></i> sites and don’t resolve publicly.</span></li>
|
||
<li><span>Access is by <i class="x" style="--w:9" aria-label="redacted"></i> only. There is no sign-up.</span></li>
|
||
<li><span>For the person, see <a href="https://yigid.dev">yigid.dev</a>.</span></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<div class="legend">
|
||
<h2>Namespace</h2>
|
||
<table>
|
||
<tr><td>yigid.net</td><td>this page</td><td class="pub">public</td></tr>
|
||
<tr><td>gw.yigid.net</td><td>gateway</td><td class="pub">public</td></tr>
|
||
<tr><td>*.<i class="x" style="--w:1" aria-label="redacted"></i>.yigid.net</td><td><i class="x" style="--w:7" aria-label="redacted"></i></td><td>private</td></tr>
|
||
<tr><td>*.yigid.net</td><td>services</td><td>private</td></tr>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<figure class="drawing" aria-label="Schematic: machines meshed together inside a boundary, one public node on its edge, and you outside it.">
|
||
<div class="callout">Fig. 1 — schematic, illustrative<b id="fig-note">viewed from the outside</b></div>
|
||
<svg id="net" viewBox="0 50 640 450" role="img"></svg>
|
||
</figure>
|
||
|
||
<footer class="tb">
|
||
<div><h2>Drawing</h2><p>network schematic</p></div>
|
||
<div><h2>Owner</h2><p><a href="https://yigid.dev" rel="author me">yigit balaban</a></p></div>
|
||
<div><h2>Sheet</h2><p>1 of 1</p></div>
|
||
<div><h2>Rev</h2><p>2026.10</p></div>
|
||
<div class="wide"><h2>Intrigued? Text me</h2><p>hey <span class="ob">[at]</span> yigid <span class="ob">[dot]</span> dev</p></div>
|
||
<div class="wide"><h2>P1 — outages & abuse</h2><p><a href="mailto:p1@yigid.net">p1@yigid.net</a></p></div>
|
||
<div class="stamp" aria-hidden="true">Top secret//comint//noforn<small>CLASSIFIED BY: Y.B. · DECLASSIFY ON: 20991231</small></div>
|
||
</footer>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
/*
|
||
* Optional: point this at any service that only resolves inside the mesh
|
||
* (e.g. "https://something.yigid.net/"). If the request gets *any* answer,
|
||
* the visitor is on the private network and the drawing redraws them inside.
|
||
* Leave null to always draw the visitor outside.
|
||
*/
|
||
const PROBE = null;
|
||
|
||
const NS = "http://www.w3.org/2000/svg";
|
||
const svg = document.getElementById("net");
|
||
const C = { x: 320, y: 280 }, R = 200;
|
||
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||
|
||
function el(tag, attrs, parent = svg) {
|
||
const n = document.createElementNS(NS, tag);
|
||
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
||
parent.appendChild(n);
|
||
return n;
|
||
}
|
||
function txt(s, attrs, parent) { const t = el("text", attrs, parent); t.textContent = s; return t; }
|
||
const polar = (a, r) => ({ x: C.x + r * Math.cos(a), y: C.y + r * Math.sin(a) });
|
||
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
||
|
||
// deterministic layout — the drawing is the same for everyone
|
||
function rng(seed) {
|
||
return () => {
|
||
seed |= 0; seed = seed + 0x6d2b79f5 | 0;
|
||
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
||
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
||
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
||
};
|
||
}
|
||
|
||
function draw(inside) {
|
||
svg.replaceChildren();
|
||
|
||
// boundary: the overlay
|
||
el("circle", { cx: C.x, cy: C.y, r: R, fill: "none", stroke: "var(--ink)", "stroke-width": 1 });
|
||
el("circle", { cx: C.x, cy: C.y, r: R + 7, fill: "none", stroke: "var(--ink-3)", "stroke-width": .5, "stroke-dasharray": "1 3" });
|
||
el("path", { id: "rim", fill: "none", d: `M ${C.x - R - 14} ${C.y} a ${R + 14} ${R + 14} 0 1 1 0 .01` });
|
||
const rimText = txt("", { "font-size": 8, "letter-spacing": "2", class: "sub" });
|
||
const tp = el("textPath", { href: "#rim", startOffset: "2%" }, rimText);
|
||
tp.textContent = "PRIVATE · ████████████ · ███████████████████ · ████ ·";
|
||
|
||
// centre crosshair, like any good drawing
|
||
el("path", { d: `M${C.x - 6} ${C.y}h12M${C.x} ${C.y - 6}v12`, stroke: "var(--ink-3)", "stroke-width": .75 });
|
||
|
||
// clients
|
||
const r0 = rng(0x79676964);
|
||
const nodes = [];
|
||
let guard = 0;
|
||
while (nodes.length < 11 && guard++ < 4000) {
|
||
const a = r0() * Math.PI * 2, d = Math.sqrt(r0()) * (R - 34);
|
||
const p = polar(a, d);
|
||
if (dist(p, C) < 26) continue;
|
||
if (nodes.every(n => dist(n, p) > 58)) nodes.push(p);
|
||
}
|
||
|
||
const r1 = rng(0x2a);
|
||
const svcs = nodes.filter((_, i) => i % 3 !== 1).map(h => {
|
||
const a = Math.atan2(h.y - C.y, h.x - C.x) + (r1() - .5) * 1.6;
|
||
return { x: h.x + Math.cos(a) * 18, y: h.y + Math.sin(a) * 18, h };
|
||
});
|
||
const labelled = [[nodes[4], -9], [nodes[7], -9], [svcs[1], 14]].filter(([p]) => p);
|
||
// rough box each redacted label occupies
|
||
const boxes = labelled.map(([p, dy]) => p.x > C.x
|
||
? { x0: p.x, x1: p.x + 110, y0: p.y + dy - 10, y1: p.y + dy + 4 }
|
||
: { x0: p.x - 110, x1: p.x, y0: p.y + dy - 10, y1: p.y + dy + 4 });
|
||
|
||
// the visitor
|
||
const you = inside ? (() => {
|
||
let best = null;
|
||
for (let i = 0; i < 400; i++) {
|
||
const p = polar(r0() * Math.PI * 2, Math.sqrt(r0()) * (R - 40));
|
||
// keep clear of nodes, services, and the redacted labels (plus room for "you")
|
||
const hitsBox = boxes.some(b => p.x > b.x0 - 34 && p.x < b.x1 + 8 && p.y > b.y0 - 8 && p.y < b.y1 + 8);
|
||
const m = hitsBox ? 0 : Math.min(...[...nodes, ...svcs].map(n => dist(n, p)));
|
||
if (!best || m > best.m) best = { ...p, m };
|
||
}
|
||
return best;
|
||
})()
|
||
: polar(Math.PI * .78, R + 74);
|
||
|
||
// mesh: each node to its nearest neighbours
|
||
const edges = new Map();
|
||
const all = inside ? [...nodes, you] : nodes;
|
||
all.forEach((n, i) => {
|
||
all.map((m, j) => [j, dist(n, m)]).filter(([j]) => j !== i)
|
||
.sort((a, b) => a[1] - b[1]).slice(0, 3)
|
||
.forEach(([j]) => edges.set([i, j].sort((a, b) => a - b).join("-"), [all[Math.min(i, j)], all[Math.max(i, j)]]));
|
||
});
|
||
const meshG = el("g", { stroke: "var(--ink)", "stroke-width": .6, opacity: .55 });
|
||
for (const [a, b] of edges.values()) el("line", { x1: a.x, y1: a.y, x2: b.x, y2: b.y }, meshG);
|
||
|
||
// control plane: on the boundary, coordinating but never carrying traffic
|
||
const gw = polar(-Math.PI * .23, R);
|
||
const ctl = el("g", { stroke: "var(--ink-2)", "stroke-width": .5, "stroke-dasharray": "2 4", opacity: .55 });
|
||
all.forEach(n => el("line", { x1: gw.x, y1: gw.y, x2: n.x, y2: n.y }, ctl));
|
||
el("rect", { x: gw.x - 7, y: gw.y - 7, width: 14, height: 14, transform: `rotate(45 ${gw.x} ${gw.y})`, fill: "var(--paper)", stroke: "var(--ink)", "stroke-width": 1.25 });
|
||
el("rect", { x: gw.x - 2.5, y: gw.y - 2.5, width: 5, height: 5, transform: `rotate(45 ${gw.x} ${gw.y})`, fill: "var(--ink)" });
|
||
el("path", { d: `M${gw.x + 12} ${gw.y - 10} l26 -26 h40`, fill: "none", stroke: "var(--ink)", "stroke-width": .75 });
|
||
txt("gw.yigid.net", { x: gw.x + 42, y: gw.y - 40, "font-size": 11 });
|
||
txt("public gateway", { x: gw.x + 42, y: gw.y - 26, "font-size": 8.5, class: "sub" });
|
||
|
||
// services, hanging off a few hosts
|
||
svcs.forEach(s => {
|
||
el("line", { x1: s.h.x, y1: s.h.y, x2: s.x, y2: s.y, stroke: "var(--ink)", "stroke-width": .75 });
|
||
el("rect", { x: s.x - 3, y: s.y - 3, width: 6, height: 6, fill: "var(--paper)", stroke: "var(--ink)", "stroke-width": .9 });
|
||
});
|
||
|
||
// nodes
|
||
nodes.forEach(n => {
|
||
el("circle", { cx: n.x, cy: n.y, r: 7, fill: "var(--paper)" });
|
||
el("circle", { cx: n.x, cy: n.y, r: 3.6, fill: "var(--ink)" });
|
||
});
|
||
|
||
// labels — redacted, naturally
|
||
const label = (p, suffix, dx, dy, anchorRight) => {
|
||
const w = 16 + Math.round(r1() * 18);
|
||
const g = el("g", { transform: `translate(${p.x + dx} ${p.y + dy})` });
|
||
const t = txt(suffix, { x: anchorRight ? -0 : w + 2, y: 0, "font-size": 8.5, class: "sub", "text-anchor": anchorRight ? "end" : "start" }, g);
|
||
if (anchorRight) {
|
||
requestAnimationFrame(() => {
|
||
const tw = t.getComputedTextLength();
|
||
el("rect", { x: -tw - w - 2, y: -7, width: w, height: 8, fill: "var(--ink)" }, g);
|
||
});
|
||
} else el("rect", { x: 0, y: -7, width: w, height: 8, fill: "var(--ink)" }, g);
|
||
};
|
||
labelled.forEach(([p, dy], i) =>
|
||
label(p, ".yigid.net", p.x > C.x ? 10 : -10, dy, p.x <= C.x));
|
||
|
||
// you
|
||
const youG = el("g");
|
||
if (!inside) {
|
||
// the line stops at the boundary — no route
|
||
const hit = polar(Math.atan2(you.y - C.y, you.x - C.x), R + 7);
|
||
el("line", { x1: you.x, y1: you.y, x2: hit.x, y2: hit.y, stroke: "var(--signal)", "stroke-width": 1 }, youG);
|
||
const ang = Math.atan2(you.y - C.y, you.x - C.x) + Math.PI / 2;
|
||
const tx = Math.cos(ang) * 7, ty = Math.sin(ang) * 7;
|
||
el("line", { x1: hit.x - tx, y1: hit.y - ty, x2: hit.x + tx, y2: hit.y + ty, stroke: "var(--signal)", "stroke-width": 2 }, youG);
|
||
txt("no route", { x: hit.x - 56, y: hit.y + 2, "font-size": 8.5, fill: "var(--signal)", style: "fill:var(--signal)" }, youG);
|
||
}
|
||
const pulse = el("circle", { cx: you.x, cy: you.y, r: 6, fill: "none", stroke: "var(--signal)", "stroke-width": 1 }, youG);
|
||
el("circle", { cx: you.x, cy: you.y, r: 4, fill: "var(--signal)" }, youG);
|
||
txt("you", { x: you.x + (inside ? 10 : -12), y: you.y + (inside ? 4 : 18), "font-size": 11, "text-anchor": inside ? "start" : "end", style: "fill:var(--signal)" }, youG);
|
||
if (!inside) txt("public internet", { x: you.x - 12, y: you.y + 30, "font-size": 8.5, "text-anchor": "end", class: "sub" }, youG);
|
||
|
||
// traffic: packets wandering the mesh
|
||
const list = [...edges.values()];
|
||
const packets = Array.from({ length: 5 }, () => ({
|
||
dot: el("circle", { r: 1.8, fill: "var(--signal)" }),
|
||
e: list[Math.floor(r1() * list.length)], t: r1(), dir: r1() < .5 ? 1 : -1, v: .004 + r1() * .004
|
||
}));
|
||
const place = p => {
|
||
const [a, b] = p.e, t = p.dir > 0 ? p.t : 1 - p.t;
|
||
p.dot.setAttribute("cx", a.x + (b.x - a.x) * t);
|
||
p.dot.setAttribute("cy", a.y + (b.y - a.y) * t);
|
||
};
|
||
packets.forEach(place);
|
||
|
||
if (reduce) return;
|
||
let frame = 0;
|
||
cancelAnimationFrame(draw.raf);
|
||
(function tick() {
|
||
frame++;
|
||
for (const p of packets) {
|
||
p.t += p.v;
|
||
if (p.t >= 1) {
|
||
// hop onward from wherever we arrived
|
||
const at = p.dir > 0 ? p.e[1] : p.e[0];
|
||
const next = list.filter(e => (e[0] === at || e[1] === at) && e !== p.e);
|
||
p.e = next.length ? next[Math.floor(Math.random() * next.length)] : p.e;
|
||
p.dir = p.e[0] === at ? 1 : -1;
|
||
p.t = 0;
|
||
}
|
||
place(p);
|
||
}
|
||
const k = (frame % 150) / 150;
|
||
pulse.setAttribute("r", 5 + k * 14);
|
||
pulse.setAttribute("opacity", 1 - k);
|
||
draw.raf = requestAnimationFrame(tick);
|
||
})();
|
||
}
|
||
|
||
function setState(inside) {
|
||
document.getElementById("fig-note").textContent = inside ? "viewed from the inside" : "viewed from the outside";
|
||
draw(inside);
|
||
}
|
||
|
||
setState(false);
|
||
|
||
// redactions react when poked — each bar draws from its own shuffled deck,
|
||
// so the order differs per bar and nothing repeats until the deck runs out
|
||
const SAYS = [
|
||
"¯\\_(ツ)_/¯", "need-to-know", "nope", "nice try", "no comment", "classified",
|
||
"ಠ_ಠ", "(¬_¬)", "(・_・;)", "who’s asking?", "eyes only", "not today",
|
||
"403", "can’t say", "move along", "ask again later", "( ˘▽˘)っ♨", "[DATA EXPUNGED]",
|
||
"above your clearance", "¯\\_(°ペ)_/¯", "it’s a secret", "(⌐■_■)", "sealed", "shh",
|
||
];
|
||
const MOVES = ["shrug", "no", "hop", "flinch"];
|
||
const shuffle = a => {
|
||
for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
|
||
return a;
|
||
};
|
||
function deck(items) {
|
||
let pile = [], last;
|
||
return () => {
|
||
if (!pile.length) {
|
||
pile = shuffle(items.slice());
|
||
if (pile[pile.length - 1] === last) pile.unshift(pile.pop()); // no repeat across reshuffles
|
||
}
|
||
return (last = pile.pop());
|
||
};
|
||
}
|
||
document.querySelectorAll(".x").forEach(x => {
|
||
const say = deck(SAYS), move = deck(MOVES);
|
||
x.tabIndex = 0;
|
||
x.setAttribute("role", "img");
|
||
x.dataset.say = say();
|
||
const poke = () => {
|
||
x.dataset.say = say();
|
||
x.classList.remove(...MOVES); void x.offsetWidth; x.classList.add(move());
|
||
};
|
||
x.addEventListener("mouseenter", poke);
|
||
x.addEventListener("focus", poke);
|
||
x.addEventListener("click", () => {
|
||
poke();
|
||
x.classList.add("say");
|
||
clearTimeout(x.t); x.t = setTimeout(() => x.classList.remove("say"), 1400);
|
||
});
|
||
x.addEventListener("animationend", () => x.classList.remove(...MOVES));
|
||
});
|
||
|
||
// start the dot's blink on the next whole second, so it ticks with the clock
|
||
const dot = document.querySelector(".mark .dot");
|
||
dot.style.animationDelay = `-${Date.now() % 1000}ms`;
|
||
dot.classList.add("tick");
|
||
|
||
// for the ones who open devtools
|
||
console.log("%c" + [
|
||
" _ _ __ __ ",
|
||
" __ __(_)__ _(_)__/ / ___ ___ / /_",
|
||
" / // / / _ `/ / _ / / _ \\/ -_) __/",
|
||
" \\_, /_/\\_, /_/\\_,_(_)_//_/\\__/\\__/ ",
|
||
"/___/ /___/",
|
||
].join("\n"), "font-family: 'Fira Code', ui-monospace, monospace; color: #ffc100; line-height: 1.2;");
|
||
console.log("if you've come all this way, I think we should be friends.\nwrite me → hey@yigid.dev!");
|
||
|
||
if (PROBE) {
|
||
const ctrl = new AbortController();
|
||
setTimeout(() => ctrl.abort(), 2500);
|
||
fetch(PROBE, { mode: "no-cors", cache: "no-store", signal: ctrl.signal })
|
||
.then(() => setState(true))
|
||
.catch(() => {});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|