commit 5376aba3967231352f3439f34667a3e224c73a51
parent f031238aa3e1046066ca697fd2f0b78b036eec41
Author: DAEMON <zer0sec.xp@icloud.com>
Date: Mon, 5 Oct 2026 15:34:54 +0100
Update
Diffstat:
8 files changed, 239 insertions(+), 13 deletions(-)
diff --git a/src/components/FuzzField.astro b/src/components/FuzzField.astro
@@ -0,0 +1,11 @@
+<canvas class="fuzz-field" data-fuzz aria-hidden="true"></canvas>
+
+<script>
+ import { initFuzz } from '../scripts/fuzz';
+
+ // Bind any unbound [data-fuzz] canvases. Called on every Astro page-load
+ // so view-transitions don't leave new canvases uninitialized.
+ document.addEventListener('astro:page-load', initFuzz);
+ if (document.readyState !== 'loading') initFuzz();
+ else document.addEventListener('DOMContentLoaded', initFuzz, { once: true });
+</script>
diff --git a/src/pages/hacktricks/[...slug].astro b/src/pages/hacktricks/[...slug].astro
@@ -1,5 +1,5 @@
---
-import SignalField from '../../components/SignalField.astro';
+import FuzzField from '../../components/FuzzField.astro';
import { render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import HackTricksCredit from '../../components/HackTricksCredit.astro';
@@ -33,8 +33,10 @@ const toc = headings.filter((heading) => heading.depth === 2 || heading.depth ==
<a href={url(`hacktricks/section/${entry.data.sectionSlug}`)} style="color:var(--gold)">{entry.data.sectionSlug}</a>
</nav>
- <header class="sheet-head signal-hero" data-pagefind-meta="category:HackTricks">
- <SignalField />
+ <header class="sheet-head plate signal-hero" data-pagefind-meta="category:HackTricks">
+ <FuzzField />
+ <span class="grain grain--local" aria-hidden="true"></span>
+ <span class="scanlines" aria-hidden="true"></span>
<p class="eyebrow" style="color:var(--gold)">// HackTricks · {sectionTitle}</p>
<h1 class="sheet-title" data-pagefind-meta="title">{entry.data.title}</h1>
</header>
diff --git a/src/pages/internal/[...slug].astro b/src/pages/internal/[...slug].astro
@@ -1,5 +1,5 @@
---
-import SignalField from '../../components/SignalField.astro';
+import FuzzField from '../../components/FuzzField.astro';
import { render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import Icon from '../../components/Icon.astro';
@@ -42,8 +42,10 @@ const leaf = entry.id.split('/').pop()!.replace(/^index$/, d.sectionSlug);
: <><a href={url(`internal/${d.sectionSlug}`)} style="color: var(--pine)">{d.sectionSlug}</a><span class="sep">/</span><span class="muted">{leaf}</span></>}
</nav>
- <header class="sheet-head signal-hero" data-pagefind-meta="category:Internal">
- <SignalField />
+ <header class="sheet-head plate signal-hero" data-pagefind-meta="category:Internal">
+ <FuzzField />
+ <span class="grain grain--local" aria-hidden="true"></span>
+ <span class="scanlines" aria-hidden="true"></span>
<p class="eyebrow" style="color: var(--pine)">// Internal · {sectionTitle}</p>
<h1 class="sheet-title" data-pagefind-meta="title">{d.title}</h1>
</header>
diff --git a/src/pages/payloads/[...slug].astro b/src/pages/payloads/[...slug].astro
@@ -1,5 +1,5 @@
---
-import SignalField from '../../components/SignalField.astro';
+import FuzzField from '../../components/FuzzField.astro';
import { render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import PayloadCredit from '../../components/PayloadCredit.astro';
@@ -37,8 +37,10 @@ const leaf = entry.id.split('/').pop()!.replace(/^index$/, d.topic);
: <><a href={url(`payloads/${d.topicSlug}`)} style="color: var(--iris)">{d.topicSlug}</a><span class="sep">/</span><span class="muted">{leaf}</span></>}
</nav>
- <header class="sheet-head signal-hero" data-pagefind-meta="category:Payloads">
- <SignalField />
+ <header class="sheet-head plate signal-hero" data-pagefind-meta="category:Payloads">
+ <FuzzField />
+ <span class="grain grain--local" aria-hidden="true"></span>
+ <span class="scanlines" aria-hidden="true"></span>
<p class="eyebrow" style="color: var(--iris)">// Payloads · {topicTitle}</p>
<h1 class="sheet-title" data-pagefind-meta="title">{d.title}</h1>
</header>
diff --git a/src/pages/sheets/[...slug].astro b/src/pages/sheets/[...slug].astro
@@ -1,5 +1,5 @@
---
-import SignalField from '../../components/SignalField.astro';
+import FuzzField from '../../components/FuzzField.astro';
import { render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import Icon from '../../components/Icon.astro';
@@ -40,8 +40,10 @@ const pdfHref = d.pdf ? url(`pdfs/${d.pdf}`) : null;
<span class="muted">{entry.id.split('/').pop()}</span>
</nav>
- <header class="sheet-head signal-hero" data-pagefind-meta={`category:${cat.title}`}>
- <SignalField />
+ <header class="sheet-head plate signal-hero" data-pagefind-meta={`category:${cat.title}`}>
+ <FuzzField />
+ <span class="grain grain--local" aria-hidden="true"></span>
+ <span class="scanlines" aria-hidden="true"></span>
<p class="eyebrow" style={`--acc: var(--${cat.accent});`}>
<span class="eyebrow__n">{cat.tag}</span>
<span class="eyebrow__mark">^:</span>
diff --git a/src/scripts/fuzz.ts b/src/scripts/fuzz.ts
@@ -0,0 +1,188 @@
+// The signal field — the site's signature canvas texture, shared by the home
+// hero and every section banner. Six pixel-sorted streak bands drifting on an
+// additive blend plus bright filaments, looping seamlessly every nine seconds.
+//
+// Ported from daemon-sec.xyz's FuzzField. Four things are load-bearing and
+// break the look if changed: the loop-global phase (so two plates on one page
+// stay in step), the DPR backing (at 1× the streaks are 1–2px and upscaling
+// blurs the pixel-sorted look), the erasure scrim, and the synchronous frame
+// zero (rAF never fires in a hidden tab / print / screenshot).
+
+/** Band geometry — six streaks, three hues between them. `hue` is a *slot*
+ * (1 = iris, 2 = foam, 3 = love), not a colour: which colour a slot resolves
+ * to, and how it composites, is read from CSS at draw time so the field
+ * follows the mode along with everything else. */
+const BANDS = [
+ { cy: 0.10, ch: 0.20, hue: 1, drift: 1, cyc: 1, dens: 12 },
+ { cy: 0.34, ch: 0.24, hue: 2, drift: -1, cyc: 2, dens: 15 },
+ { cy: 0.22, ch: 0.14, hue: 3, drift: 2, cyc: 1, dens: 9 },
+ { cy: 0.60, ch: 0.22, hue: 2, drift: 1, cyc: 1, dens: 13 },
+ { cy: 0.80, ch: 0.16, hue: 1, drift: -1, cyc: 2, dens: 10 },
+ { cy: 0.92, ch: 0.14, hue: 3, drift: 1, cyc: 2, dens: 9 },
+] as const;
+
+interface Palette {
+ blend: GlobalCompositeOperation;
+ bands: [string, string, string];
+ filaments: [string, string, string];
+ gain: number;
+}
+
+const FALLBACK: Palette = {
+ blend: 'lighter',
+ bands: ['196,167,231', '156,207,216', '235,111,146'],
+ filaments: ['224,222,244', '156,207,216', '196,167,231'],
+ gain: 1,
+};
+
+/**
+ * Resolve the palette off the canvas itself, so a plate inside a scope that
+ * pins the night palette draws the dark field even while the page around it is
+ * on paper. Channels come back space-separated from CSS and the 2D context
+ * wants them comma-separated, hence the normalise.
+ */
+function readPalette(cv: HTMLCanvasElement): Palette {
+ const cs = getComputedStyle(cv);
+ const channels = (name: string, fallback: string) => {
+ const v = cs.getPropertyValue(name).trim();
+ return v ? v.replace(/\s+/g, ',') : fallback;
+ };
+ const blend = cs.getPropertyValue('--fuzz-blend').trim();
+ const gain = parseFloat(cs.getPropertyValue('--fuzz-gain'));
+ return {
+ blend: (blend || FALLBACK.blend) as GlobalCompositeOperation,
+ bands: [
+ channels('--fuzz-band-1', FALLBACK.bands[0]),
+ channels('--fuzz-band-2', FALLBACK.bands[1]),
+ channels('--fuzz-band-3', FALLBACK.bands[2]),
+ ],
+ filaments: [
+ channels('--fuzz-fil-1', FALLBACK.filaments[0]),
+ channels('--fuzz-fil-2', FALLBACK.filaments[1]),
+ channels('--fuzz-fil-3', FALLBACK.filaments[2]),
+ ],
+ gain: Number.isFinite(gain) ? gain : FALLBACK.gain,
+ };
+}
+
+/** One frame of the field at `phase` ∈ [0,1). Every band's drift and breathe is
+ * a whole number of cycles per loop, so the 9s loop seams cleanly and any two
+ * plates on the page stay in step. */
+function frame(cv: HTMLCanvasElement, phase: number, amt: number, pal: Palette): void {
+ const host = cv.parentElement;
+ const w = (cv.clientWidth || host?.clientWidth || 0) | 0;
+ const h = (cv.clientHeight || host?.clientHeight || 0) | 0;
+ if (!w || !h) return;
+
+ const dpr = Math.min(2, devicePixelRatio || 1);
+ const bw = Math.round(w * dpr);
+ const bh = Math.round(h * dpr);
+ if (cv.width !== bw || cv.height !== bh) { cv.width = bw; cv.height = bh; }
+
+ const ctx = cv.getContext('2d');
+ if (!ctx) return;
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+
+ const frac = (x: number) => x - Math.floor(x);
+ const rnd = (s: number) => frac(Math.sin(s * 127.1) * 43758.5453);
+
+ ctx.clearRect(0, 0, w, h);
+ ctx.globalCompositeOperation = pal.blend;
+
+ for (let bi = 0; bi < BANDS.length; bi++) {
+ const b = BANDS[bi];
+ const cy = b.cy * h;
+ const ch = b.ch * h;
+ for (let y = Math.max(0, Math.round(cy - ch)); y < Math.min(h, cy + ch); y += 2) {
+ const fall = 1 - Math.abs(y - cy) / ch;
+ const breathe = 0.5 + 0.5 * Math.sin(2 * Math.PI * (b.cyc * phase + y * 0.004 + bi * 0.3));
+ const env = fall * (0.4 + 0.6 * breathe);
+ if (env <= 0.02) continue;
+ const n = Math.round(env * b.dens * amt);
+ for (let i = 0; i < n; i++) {
+ const s = y * 7.3 + i * 13.7 + bi * 101.3;
+ const x = frac(rnd(s) + phase * b.drift) * (w + 420) - 210;
+ const len = (18 + rnd(s + 1) * 340) * (0.4 + env);
+ const hx = 0.22 + 0.78 * Math.pow(Math.sin(Math.PI * ((x / w) * (1 + (bi % 3)) + phase + rnd(bi * 9))), 2);
+ const a = (0.06 + rnd(s + 2) * 0.34) * env * hx * 1.7 * pal.gain;
+ ctx.fillStyle = `rgba(${pal.bands[b.hue - 1]},${a.toFixed(3)})`;
+ ctx.fillRect(x, y, len, rnd(s + 3) > 0.84 ? 2 : 1);
+ }
+ }
+ }
+
+ for (let i = 0; i < 54; i++) {
+ const s = i * 37.1;
+ const y = Math.round((rnd(s) * h) / 2) * 2;
+ const cyc = 1 + (i % 2);
+ const x = frac(rnd(s + 5) + phase * cyc) * (w + 700) - 350;
+ const a = (0.1 + rnd(s + 6) * 0.28) * amt * pal.gain;
+ ctx.fillStyle = `rgba(${pal.filaments[i % 3]},${a.toFixed(3)})`;
+ ctx.fillRect(x, y, 120 + rnd(s + 7) * 520, 1);
+ }
+
+ // Scrim by erasure: destination-out bands at top and bottom so type near the
+ // plate edges always sits on clean ink. The percentage clamp matters — a
+ // fixed 40px erased most of the short plates.
+ ctx.globalCompositeOperation = 'destination-out';
+ const solid = Math.min(40, h * 0.14);
+ const ramp = Math.min(26, h * 0.1);
+ ctx.fillStyle = 'rgba(0,0,0,1)';
+ ctx.fillRect(0, 0, w, solid);
+ ctx.fillRect(0, h - solid, w, solid);
+ let g = ctx.createLinearGradient(0, solid, 0, solid + ramp);
+ g.addColorStop(0, 'rgba(0,0,0,1)');
+ g.addColorStop(1, 'rgba(0,0,0,0)');
+ ctx.fillStyle = g;
+ ctx.fillRect(0, solid, w, ramp);
+ g = ctx.createLinearGradient(0, h - solid - ramp, 0, h - solid);
+ g.addColorStop(0, 'rgba(0,0,0,0)');
+ g.addColorStop(1, 'rgba(0,0,0,1)');
+ ctx.fillStyle = g;
+ ctx.fillRect(0, h - solid - ramp, w, ramp);
+ ctx.globalCompositeOperation = 'source-over';
+}
+
+/** Density scale. The main site ships at 6; the constant exists for tuning. */
+const PRESS = 6;
+/** The frame reduced motion keeps as the permanent texture — mid-loop, where
+ * the bands are at their fullest rather than at a seam. */
+const STILL = 0.3;
+
+/** Bind every unbound `[data-fuzz]` canvas on the page and start its loop.
+ * Idempotent — a second call skips already-bound canvases, so it is safe to
+ * call on every `astro:page-load`. */
+export function initFuzz(): void {
+ const canvases = document.querySelectorAll<HTMLCanvasElement>('[data-fuzz]:not([data-bound])');
+ canvases.forEach((cv) => {
+ cv.dataset.bound = '1';
+ const amt = Math.max(0.2, PRESS / 5);
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches || document.documentElement.dataset.motion === 'reduce';
+
+ let pal = readPalette(cv);
+
+ const still = () => frame(cv, STILL, amt, pal);
+ frame(cv, reduced ? STILL : (performance.now() / 9000) % 1, amt, pal);
+
+ const ro = new ResizeObserver(() => { if (reduced) still(); });
+ ro.observe(cv.parentElement ?? cv);
+
+ const onMode = () => { pal = readPalette(cv); if (reduced) still(); };
+ addEventListener('daemonmodechange', onMode);
+
+ let raf = 0;
+ if (!reduced) {
+ const loop = (now: number) => {
+ if (!document.hidden && document.documentElement.dataset.motion !== 'reduce') frame(cv, (now / 9000) % 1, amt, pal);
+ raf = requestAnimationFrame(loop);
+ };
+ raf = requestAnimationFrame(loop);
+ }
+
+ document.addEventListener('astro:before-swap', () => {
+ cancelAnimationFrame(raf);
+ ro.disconnect();
+ removeEventListener('daemonmodechange', onMode);
+ }, { once: true });
+ });
+}
diff --git a/src/styles/chrome.css b/src/styles/chrome.css
@@ -56,7 +56,8 @@
isolation: isolate;
overflow: hidden;
}
-.signal-field {
+.signal-field,
+[data-fuzz] {
position: absolute;
inset: 0;
z-index: -1;
diff --git a/src/styles/global.css b/src/styles/global.css
@@ -889,6 +889,24 @@ main, .site-header, .site-footer { position: relative; z-index: 2; }
/* Same measure as the body below it, so the title, the rule under the
eyebrow and the prose all start and end on the same two lines. */
.sheet-head { margin-top: 1.6rem; max-width: 1080px; }
+/* When sheet-head doubles as a dark plate hero (fuzz field visible), it
+ needs padding so text breathes inside the dark block and a min-height so
+ the field has room to render. `max-width: none` and negative margins
+ counteract the `.wrap` gutter so the plate runs edge-to-edge like banners. */
+.sheet-head.plate {
+ max-width: none;
+ margin-top: 0;
+ margin-left: calc(-1 * var(--gutter, 1.25rem));
+ margin-right: calc(-1 * var(--gutter, 1.25rem));
+ padding: 3rem var(--gutter, 1.25rem) 2.5rem;
+ min-height: 10rem;
+ border-top: 1px solid var(--rule);
+}
+.sheet-head.plate .sheet-title,
+.sheet-head.plate .sheet-desc,
+.sheet-head.plate .sheet-meta,
+.sheet-head.plate .eyebrow { position: relative; z-index: 2; }
+
.breadcrumb {
font-family: var(--font-mono); font-size: var(--step-micro);
letter-spacing: var(--track-micro); text-transform: uppercase;