Compare commits
15 commits
698ac14dd7
...
ae60a7c856
| Author | SHA1 | Date | |
|---|---|---|---|
| ae60a7c856 | |||
| 27e743bffd | |||
| f533e9933b | |||
| 993cedf0ed | |||
| 73170a29e5 | |||
| d71a7a214d | |||
| 45e84c1856 | |||
| 867a8c1762 | |||
| cfc5983f41 | |||
| 8bcd2486e3 | |||
| 91546ab393 | |||
| be6e9ea13d | |||
| f112e26784 | |||
| f8113f7a94 | |||
| 24db26177e |
9 changed files with 317 additions and 50 deletions
283
src/components/islands/ParticleField.tsx
Normal file
283
src/components/islands/ParticleField.tsx
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
// ---- Perlin noise helpers ---------------------------------------------------
|
||||||
|
|
||||||
|
function fade(t: number) { return t * t * t * (t * (t * 6 - 15) + 10); }
|
||||||
|
function lerp(a: number, b: number, t: number) { return a + t * (b - a); }
|
||||||
|
function grad(h: number, x: number, y: number) {
|
||||||
|
h &= 7;
|
||||||
|
const u = h < 4 ? x : y, v = h < 4 ? y : x;
|
||||||
|
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
|
||||||
|
}
|
||||||
|
function buildPermutation(): number[] {
|
||||||
|
const a = Array.from({ length: 256 }, (_, i) => i);
|
||||||
|
for (let i = 255; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[a[i], a[j]] = [a[j], a[i]];
|
||||||
|
}
|
||||||
|
return [...a, ...a];
|
||||||
|
}
|
||||||
|
function noise2(P: number[], x: number, y: number): number {
|
||||||
|
const X = Math.floor(x) & 255, Y = Math.floor(y) & 255;
|
||||||
|
x -= Math.floor(x); y -= Math.floor(y);
|
||||||
|
const u = fade(x), v = fade(y);
|
||||||
|
const aa = P[X] + Y, bb = P[X + 1] + Y;
|
||||||
|
return lerp(
|
||||||
|
lerp(grad(P[aa], x, y), grad(P[bb], x - 1, y), u),
|
||||||
|
lerp(grad(P[aa + 1], x, y - 1), grad(P[bb + 1], x - 1, y - 1), u),
|
||||||
|
v,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function fbm(P: number[], x: number, y: number, octaves = 4): number {
|
||||||
|
let value = 0, amp = 0.5, freq = 1, max = 0;
|
||||||
|
for (let i = 0; i < octaves; i++) {
|
||||||
|
value += noise2(P, x * freq, y * freq) * amp;
|
||||||
|
max += amp;
|
||||||
|
amp *= 0.5;
|
||||||
|
freq *= 2;
|
||||||
|
}
|
||||||
|
return value / max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Config — all tunable values in one place ------------------------------
|
||||||
|
|
||||||
|
const PARTICLE_CONFIG = {
|
||||||
|
count: 40, // number of particles
|
||||||
|
reach: 600, // max connection distance (px)
|
||||||
|
flowSpeed: 1.0, // field drift speed (1–10 scale)
|
||||||
|
fieldScale: 1, // noise zoom level
|
||||||
|
dimAtMin: 0.3, // opacity reduction at field minimum (0–1)
|
||||||
|
|
||||||
|
particleSpeed: 0.14,
|
||||||
|
particleMinR: 0.8,
|
||||||
|
particleMaxR: 1.6,
|
||||||
|
|
||||||
|
fieldOffscreenW: 80,
|
||||||
|
fieldOffscreenH: 52,
|
||||||
|
fieldUpdateMs: 50,
|
||||||
|
fieldOctaves: 4,
|
||||||
|
|
||||||
|
timeConstantX: 0.000014 * 30,
|
||||||
|
timeConstantY: 0.0000085 * 20,
|
||||||
|
|
||||||
|
// Warm neutral palette — fg-on-dark-2/3 register (#AE9B7F range)
|
||||||
|
lineColor: "174,140,100",
|
||||||
|
glowColorInner: "200,170,130",
|
||||||
|
glowColorMid: "155,120,82",
|
||||||
|
|
||||||
|
// Dot colour at fv=0 (dim) and per-unit boost at fv=1 (bright)
|
||||||
|
dotBaseR: 174, dotBaseG: 145, dotBaseB: 110,
|
||||||
|
dotBoostR: 30, dotBoostG: 35, dotBoostB: 40,
|
||||||
|
|
||||||
|
// Glow: base radius multiplier (relative to particle r); field peaks expand by +1.5×
|
||||||
|
glowScale: 1.2,
|
||||||
|
|
||||||
|
// Master opacity applied to the entire field (0–1); 0.5 = half brightness
|
||||||
|
masterOpacity: 0.4,
|
||||||
|
// Max brightness boost on particles near the cursor (0–1); 0.30 = 30% increase
|
||||||
|
cursorBoostMax: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Types -----------------------------------------------------------------
|
||||||
|
|
||||||
|
interface Particle { x: number; y: number; vx: number; vy: number; r: number; }
|
||||||
|
|
||||||
|
// ---- Component -------------------------------------------------------------
|
||||||
|
|
||||||
|
export default function ParticleField() {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
// Re-bind as non-nullable so nested functions can reference them safely
|
||||||
|
const cvs: HTMLCanvasElement = canvas;
|
||||||
|
const c: CanvasRenderingContext2D = ctx;
|
||||||
|
|
||||||
|
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
const cfg = PARTICLE_CONFIG;
|
||||||
|
|
||||||
|
const fieldCache = new Float32Array(cfg.fieldOffscreenW * cfg.fieldOffscreenH);
|
||||||
|
|
||||||
|
const P = buildPermutation();
|
||||||
|
let particles: Particle[] = [];
|
||||||
|
const mouse = { x: -9999, y: -9999 };
|
||||||
|
let elapsed = 0;
|
||||||
|
let lastTs: number | null = null;
|
||||||
|
let fieldAge = 9999;
|
||||||
|
let rafId = 0;
|
||||||
|
let W = 0, H = 0;
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
W = cvs.width = window.innerWidth;
|
||||||
|
H = cvs.height = window.innerHeight;
|
||||||
|
syncCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeParticle(): Particle {
|
||||||
|
return {
|
||||||
|
x: Math.random() * W,
|
||||||
|
y: Math.random() * H,
|
||||||
|
vx: (Math.random() - 0.5) * cfg.particleSpeed,
|
||||||
|
vy: (Math.random() - 0.5) * cfg.particleSpeed,
|
||||||
|
r: cfg.particleMinR + Math.random() * (cfg.particleMaxR - cfg.particleMinR),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCount() {
|
||||||
|
while (particles.length < cfg.count) particles.push(makeParticle());
|
||||||
|
while (particles.length > cfg.count) particles.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sampleField(x: number, y: number): number {
|
||||||
|
const oW = cfg.fieldOffscreenW, oH = cfg.fieldOffscreenH;
|
||||||
|
const cx = Math.max(0, Math.min(oW - 1, Math.round((x / W) * (oW - 1))));
|
||||||
|
const cy = Math.max(0, Math.min(oH - 1, Math.round((y / H) * (oH - 1))));
|
||||||
|
return fieldCache[cy * oW + cx];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldToOpacity(fv: number): number {
|
||||||
|
const minMult = 1 - cfg.dimAtMin;
|
||||||
|
return minMult + (1 - minMult) * fv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateField() {
|
||||||
|
const { fieldOffscreenW: oW, fieldOffscreenH: oH, fieldScale,
|
||||||
|
fieldOctaves, timeConstantX, timeConstantY } = cfg;
|
||||||
|
const scale = fieldScale * 0.0018;
|
||||||
|
const ox = elapsed * timeConstantX;
|
||||||
|
const oy = elapsed * timeConstantY;
|
||||||
|
for (let py = 0; py < oH; py++) {
|
||||||
|
for (let px = 0; px < oW; px++) {
|
||||||
|
const raw = fbm(P, px * scale + ox, py * scale + oy, fieldOctaves);
|
||||||
|
fieldCache[py * oW + px] = Math.max(0, Math.min(1, raw * 1.8 + 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
const { reach, lineColor, glowColorInner, glowColorMid,
|
||||||
|
dotBaseR, dotBaseG, dotBaseB, dotBoostR, dotBoostG, dotBoostB,
|
||||||
|
glowScale, masterOpacity, cursorBoostMax } = cfg;
|
||||||
|
const reach2 = reach * reach;
|
||||||
|
|
||||||
|
c.clearRect(0, 0, W, H);
|
||||||
|
c.globalAlpha = masterOpacity;
|
||||||
|
|
||||||
|
for (const p of particles) {
|
||||||
|
p.x += p.vx; p.y += p.vy;
|
||||||
|
if (p.x < 0 || p.x > W) p.vx *= -1;
|
||||||
|
if (p.y < 0 || p.y > H) p.vy *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fv = particles.map(p => sampleField(p.x, p.y));
|
||||||
|
const fo = fv.map(v => fieldToOpacity(v));
|
||||||
|
|
||||||
|
for (let i = 0; i < particles.length; i++) {
|
||||||
|
for (let j = i + 1; j < particles.length; j++) {
|
||||||
|
const a = particles[i], b = particles[j];
|
||||||
|
const dx = a.x - b.x, dy = a.y - b.y;
|
||||||
|
const d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 >= reach2) continue;
|
||||||
|
const dist = Math.sqrt(d2);
|
||||||
|
const t01 = dist / reach;
|
||||||
|
const expFade = (1 - t01) ** 3;
|
||||||
|
const lineMult = Math.sqrt(fo[i] * fo[j]);
|
||||||
|
const alpha = expFade * lineMult * 0.85;
|
||||||
|
if (alpha < 0.005) continue;
|
||||||
|
c.beginPath();
|
||||||
|
c.moveTo(a.x, a.y);
|
||||||
|
c.lineTo(b.x, b.y);
|
||||||
|
c.strokeStyle = `rgba(${lineColor},${alpha * 0.7})`;
|
||||||
|
c.lineWidth = 0.5;
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < particles.length; i++) {
|
||||||
|
const p = particles[i];
|
||||||
|
const f = fv[i];
|
||||||
|
const opMult = fo[i];
|
||||||
|
const dx = p.x - mouse.x, dy = p.y - mouse.y;
|
||||||
|
const md = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const proximity = md < reach ? 1 - md / reach : 0;
|
||||||
|
const cursorBoost = cursorBoostMax * proximity ** 3;
|
||||||
|
const gA = Math.max(0.02, opMult * 0.85 * (1 + cursorBoost));
|
||||||
|
const glowR = p.r * (glowScale + f * 1.5);
|
||||||
|
|
||||||
|
const grd = c.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR * 3);
|
||||||
|
grd.addColorStop(0, `rgba(${glowColorInner},${gA * 0.55})`);
|
||||||
|
grd.addColorStop(0.35, `rgba(${glowColorMid},${gA * 0.22})`);
|
||||||
|
grd.addColorStop(1, "rgba(0,0,0,0)");
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(p.x, p.y, glowR * 3, 0, Math.PI * 2);
|
||||||
|
c.fillStyle = grd;
|
||||||
|
c.fill();
|
||||||
|
|
||||||
|
const dR = Math.round(dotBaseR + f * dotBoostR);
|
||||||
|
const dG = Math.round(dotBaseG + f * dotBoostG);
|
||||||
|
const dB = Math.round(dotBaseB + f * dotBoostB);
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(p.x, p.y, p.r * (0.8 + f * 0.5), 0, Math.PI * 2);
|
||||||
|
c.fillStyle = `rgba(${dR},${dG},${dB},${gA})`;
|
||||||
|
c.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
c.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop(ts: number) {
|
||||||
|
if (lastTs === null) lastTs = ts;
|
||||||
|
const dt = ts - lastTs;
|
||||||
|
lastTs = ts;
|
||||||
|
elapsed += dt * (cfg.flowSpeed / 3);
|
||||||
|
fieldAge += dt;
|
||||||
|
if (fieldAge >= cfg.fieldUpdateMs) {
|
||||||
|
updateField();
|
||||||
|
fieldAge = 0;
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => { mouse.x = e.clientX; mouse.y = e.clientY; };
|
||||||
|
const onMouseLeave = () => { mouse.x = -9999; mouse.y = -9999; };
|
||||||
|
const onResize = () => resize();
|
||||||
|
|
||||||
|
resize();
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
document.addEventListener("mouseleave", onMouseLeave);
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
|
if (reduce) {
|
||||||
|
updateField();
|
||||||
|
draw();
|
||||||
|
} else {
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
|
document.removeEventListener("mouseleave", onMouseLeave);
|
||||||
|
window.removeEventListener("resize", onResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: 0,
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import "~/styles/colors_and_type.css";
|
||||||
import "~/styles/site.css";
|
import "~/styles/site.css";
|
||||||
import TopBar from "~/components/organisms/TopBar.astro";
|
import TopBar from "~/components/organisms/TopBar.astro";
|
||||||
import SiteFoot from "~/components/organisms/SiteFoot.astro";
|
import SiteFoot from "~/components/organisms/SiteFoot.astro";
|
||||||
import Mark from "~/components/atoms/Mark.astro";
|
import ParticleField from "~/components/islands/ParticleField";
|
||||||
|
|
||||||
type CurrentSection = "home" | "blog" | "art" | "code";
|
type CurrentSection = "home" | "blog" | "art" | "code";
|
||||||
|
|
||||||
|
|
@ -50,13 +50,7 @@ const canonicalHref = canonical ?? new URL(Astro.url.pathname, Astro.site).toStr
|
||||||
<meta property="og:url" content={canonicalHref} />
|
<meta property="og:url" content={canonicalHref} />
|
||||||
</head>
|
</head>
|
||||||
<body class="dark">
|
<body class="dark">
|
||||||
<!-- sprinkled accent marks — loose, non-rigid placement, per page-of-page overrides via slot -->
|
<ParticleField client:idle />
|
||||||
<slot name="marks">
|
|
||||||
<Mark kind="ringdots" width={84} top="14%" left="3%" rotate={-5} />
|
|
||||||
<Mark kind="pipes" width={28} top="73%" left="6%" rotate={8} />
|
|
||||||
<Mark kind="dots" width={52} top="26%" right="3.5%" />
|
|
||||||
<Mark kind="concentric" width={42} bottom="9%" right="7%" />
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<div class="stage">
|
<div class="stage">
|
||||||
<TopBar current={current} showCoord={showCoordInTopbar} />
|
<TopBar current={current} showCoord={showCoordInTopbar} />
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,10 @@ const dateFmt = new Intl.DateTimeFormat("en-AU", { month: "long", year: "numeric
|
||||||
description={description ?? standfirst}
|
description={description ?? standfirst}
|
||||||
current="blog"
|
current="blog"
|
||||||
>
|
>
|
||||||
<Fragment slot="marks">
|
|
||||||
<Mark kind="ringdots" width={70} top="16%" left="4%" rotate={-6} />
|
|
||||||
<Mark kind="dots" width={48} top="58%" right="3.5%" />
|
|
||||||
<Mark kind="pipes" width={26} bottom="14%" left="6%" rotate={5} />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
<header class="posthead col">
|
<header class="posthead col">
|
||||||
|
<Mark kind="ringdots" width={70} top="-12px" left="-16px" rotate={-6} />
|
||||||
|
<Mark kind="dots" width={48} top="-12px" right="-12px" />
|
||||||
{(tag || series) && (
|
{(tag || series) && (
|
||||||
<span class="kicker">
|
<span class="kicker">
|
||||||
{tag && <span class="tag">{tag}</span>}
|
{tag && <span class="tag">{tag}</span>}
|
||||||
|
|
@ -53,6 +49,7 @@ const dateFmt = new Intl.DateTimeFormat("en-AU", { month: "long", year: "numeric
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="postnav col-wide">
|
<nav class="postnav col-wide">
|
||||||
|
<Mark kind="pipes" width={26} bottom="-8px" left="-8px" rotate={5} />
|
||||||
<a class="back" href={blog("/")}>
|
<a class="back" href={blog("/")}>
|
||||||
<span class="lbl">← Back to writing</span>
|
<span class="lbl">← Back to writing</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -66,6 +63,7 @@ const dateFmt = new Intl.DateTimeFormat("en-AU", { month: "long", year: "numeric
|
||||||
.col-wide { max-width: 940px; margin-left: auto; margin-right: auto; }
|
.col-wide { max-width: 940px; margin-left: auto; margin-right: auto; }
|
||||||
|
|
||||||
.posthead {
|
.posthead {
|
||||||
|
position: relative;
|
||||||
padding: clamp(40px, 8vh, 104px) 0 clamp(26px, 4vh, 44px);
|
padding: clamp(40px, 8vh, 104px) 0 clamp(26px, 4vh, 44px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +201,7 @@ const dateFmt = new Intl.DateTimeFormat("en-AU", { month: "long", year: "numeric
|
||||||
:global(body:not(.dark)) .prose :global(blockquote) { color: var(--fg-1); }
|
:global(body:not(.dark)) .prose :global(blockquote) { color: var(--fg-1); }
|
||||||
|
|
||||||
.postnav {
|
.postnav {
|
||||||
|
position: relative;
|
||||||
padding: clamp(40px, 6vh, 72px) 0 clamp(20px, 3vh, 32px);
|
padding: clamp(40px, 6vh, 72px) 0 clamp(20px, 3vh, 32px);
|
||||||
margin-top: clamp(30px, 5vh, 56px);
|
margin-top: clamp(30px, 5vh, 56px);
|
||||||
border-top: 1px solid var(--hairline-on-dark);
|
border-top: 1px solid var(--hairline-on-dark);
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,10 @@ import { home } from "~/lib/urls";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Not yet here — Josh Bairstow" description="The page you were looking for doesn't exist yet.">
|
<BaseLayout title="Not yet here — Josh Bairstow" description="The page you were looking for doesn't exist yet.">
|
||||||
<Fragment slot="marks">
|
|
||||||
<Mark kind="ringdots" width={92} top="18%" left="6%" rotate={-8} />
|
|
||||||
<Mark kind="dots" width={50} top="64%" right="6%" />
|
|
||||||
<Mark kind="pipes" width={28} bottom="18%" left="40%" rotate={10} />
|
|
||||||
<Mark kind="concentric" width={44} bottom="22%" right="14%" />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<main class="missing">
|
<main class="missing">
|
||||||
|
<Mark kind="ringdots" width={92} top="-14px" left="-16px" rotate={-8} />
|
||||||
|
<Mark kind="dots" width={50} top="-12px" right="-12px" />
|
||||||
|
<Mark kind="concentric" width={44} bottom="-14px" right="-12px" />
|
||||||
<p class="num">404</p>
|
<p class="num">404</p>
|
||||||
<h1>Not yet here<span class="accent">.</span></h1>
|
<h1>Not yet here<span class="accent">.</span></h1>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
|
|
@ -29,6 +25,7 @@ import { home } from "~/lib/urls";
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.missing {
|
.missing {
|
||||||
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,15 @@ import TideStudyCanvas from "~/components/islands/TideStudyCanvas";
|
||||||
description="A real-time field of warm rings, seeded each morning by the tide chart off Manly."
|
description="A real-time field of warm rings, seeded each morning by the tide chart off Manly."
|
||||||
current="art"
|
current="art"
|
||||||
>
|
>
|
||||||
<Fragment slot="marks">
|
|
||||||
<Mark kind="dots" width={50} top="20%" right="4%" />
|
|
||||||
<Mark kind="pipes" width={26} bottom="16%" left="4%" rotate={6} />
|
|
||||||
<Mark kind="concentric" width={38} top="70%" right="8%" />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<main class="gallery">
|
<main class="gallery">
|
||||||
<section class="work">
|
<section class="work">
|
||||||
<TideStudyCanvas client:load />
|
<TideStudyCanvas client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="caption">
|
<aside class="caption">
|
||||||
|
<Mark kind="dots" width={50} top="-14px" right="-12px" />
|
||||||
|
<Mark kind="pipes" width={26} bottom="-8px" left="-8px" rotate={6} />
|
||||||
|
<Mark kind="concentric" width={38} top="40%" right="-14px" />
|
||||||
<span class="idx">Work 07 / 12</span>
|
<span class="idx">Work 07 / 12</span>
|
||||||
<p class="eyebrow series-line">Tide studies · generative</p>
|
<p class="eyebrow series-line">Tide studies · generative</p>
|
||||||
<h1>Tide Study <em>No. 7</em></h1>
|
<h1>Tide Study <em>No. 7</em></h1>
|
||||||
|
|
@ -125,6 +122,7 @@ import TideStudyCanvas from "~/components/islands/TideStudyCanvas";
|
||||||
}
|
}
|
||||||
|
|
||||||
.caption {
|
.caption {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,14 +59,8 @@ for (const post of posts) {
|
||||||
description="Short pieces on craft, the ocean, and the quiet discipline of making small software."
|
description="Short pieces on craft, the ocean, and the quiet discipline of making small software."
|
||||||
current="blog"
|
current="blog"
|
||||||
>
|
>
|
||||||
<Fragment slot="marks">
|
|
||||||
<Mark kind="dots" width={54} top="18%" right="4%" />
|
|
||||||
<Mark kind="concentric" width={40} top="64%" left="3%" rotate={6} />
|
|
||||||
<Mark kind="pipes" width={26} bottom="12%" right="9%" />
|
|
||||||
<Mark kind="ringdots" width={72} bottom="26%" left="46%" rotate={-4} />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<header class="masthead">
|
<header class="masthead">
|
||||||
|
<Mark kind="ringdots" width={72} top="-12px" right="-14px" rotate={-4} />
|
||||||
<div>
|
<div>
|
||||||
<Eyebrow>Journal · field notes</Eyebrow>
|
<Eyebrow>Journal · field notes</Eyebrow>
|
||||||
<h1 class="title">Writing<span style="color:var(--accent-deep)">.</span></h1>
|
<h1 class="title">Writing<span style="color:var(--accent-deep)">.</span></h1>
|
||||||
|
|
@ -109,6 +103,8 @@ for (const post of posts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="archive">
|
<section class="archive">
|
||||||
|
<Mark kind="dots" width={54} top="-14px" right="-10px" />
|
||||||
|
<Mark kind="concentric" width={40} bottom="-12px" left="-12px" rotate={6} />
|
||||||
{
|
{
|
||||||
yearGroups.map(([year, items]) => (
|
yearGroups.map(([year, items]) => (
|
||||||
<>
|
<>
|
||||||
|
|
@ -135,6 +131,7 @@ for (const post of posts) {
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.masthead {
|
.masthead {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.1fr 0.9fr;
|
grid-template-columns: 1.1fr 0.9fr;
|
||||||
gap: clamp(30px, 6vw, 80px);
|
gap: clamp(30px, 6vw, 80px);
|
||||||
|
|
@ -256,6 +253,7 @@ for (const post of posts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.archive {
|
.archive {
|
||||||
|
position: relative;
|
||||||
padding: clamp(34px, 6vh, 72px) 0 0;
|
padding: clamp(34px, 6vh, 72px) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,8 @@ import { blog, home } from "~/lib/urls";
|
||||||
description="Experiments and small tools. Coming online piece by piece."
|
description="Experiments and small tools. Coming online piece by piece."
|
||||||
current="code"
|
current="code"
|
||||||
>
|
>
|
||||||
<Fragment slot="marks">
|
|
||||||
<Mark kind="pipes" width={30} top="22%" left="6%" rotate={-4} />
|
|
||||||
<Mark kind="dots" width={48} top="68%" right="6%" />
|
|
||||||
<Mark kind="concentric" width={40} bottom="12%" left="44%" />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<header class="masthead">
|
<header class="masthead">
|
||||||
|
<Mark kind="pipes" width={30} top="-10px" left="-10px" rotate={-4} />
|
||||||
<div>
|
<div>
|
||||||
<Eyebrow>Workshop · in progress</Eyebrow>
|
<Eyebrow>Workshop · in progress</Eyebrow>
|
||||||
<h1 class="title">Code<span class="accent">.</span></h1>
|
<h1 class="title">Code<span class="accent">.</span></h1>
|
||||||
|
|
@ -30,6 +25,8 @@ import { blog, home } from "~/lib/urls";
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="placeholder">
|
<section class="placeholder">
|
||||||
|
<Mark kind="dots" width={48} top="-14px" right="-8px" />
|
||||||
|
<Mark kind="concentric" width={40} bottom="-12px" left="-8px" />
|
||||||
<p>
|
<p>
|
||||||
In the meantime, the writing index is the closest thing to a working log:
|
In the meantime, the writing index is the closest thing to a working log:
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -45,6 +42,7 @@ import { blog, home } from "~/lib/urls";
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.masthead {
|
.masthead {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.1fr 0.9fr;
|
grid-template-columns: 1.1fr 0.9fr;
|
||||||
gap: clamp(30px, 6vw, 80px);
|
gap: clamp(30px, 6vw, 80px);
|
||||||
|
|
@ -82,6 +80,7 @@ import { blog, home } from "~/lib/urls";
|
||||||
:global(body:not(.dark)) .masthead .blurb { color: var(--fg-2); }
|
:global(body:not(.dark)) .masthead .blurb { color: var(--fg-2); }
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
|
position: relative;
|
||||||
padding: clamp(34px, 6vh, 72px) 0 0;
|
padding: clamp(34px, 6vh, 72px) 0 0;
|
||||||
border-top: 1px solid var(--hairline-on-dark);
|
border-top: 1px solid var(--hairline-on-dark);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -31,26 +31,21 @@ const indexItems = [
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Josh Bairstow" current="home" showCoordInTopbar={true}>
|
<BaseLayout title="Josh Bairstow" current="home" showCoordInTopbar={true}>
|
||||||
<Fragment slot="marks">
|
|
||||||
<Mark kind="ringdots" width={84} top="14%" left="3%" rotate={-5} />
|
|
||||||
<Mark kind="pipes" width={28} top="73%" left="6%" rotate={8} />
|
|
||||||
<Mark kind="dots" width={52} top="26%" right="3.5%" />
|
|
||||||
<Mark kind="concentric" width={42} bottom="9%" right="7%" />
|
|
||||||
<Mark kind="ringdots" width={66} bottom="20%" left="44%" rotate={-3} />
|
|
||||||
</Fragment>
|
|
||||||
|
|
||||||
<main class="home" data-layout="stack">
|
<main class="home" data-layout="stack">
|
||||||
<section class="intro">
|
<section class="intro">
|
||||||
|
<Mark kind="ringdots" width={84} top="-10px" left="-24px" rotate={-5} />
|
||||||
<Eyebrow>engineer errant · Sydney</Eyebrow>
|
<Eyebrow>engineer errant · Sydney</Eyebrow>
|
||||||
<h2 class="statement">
|
<h2 class="statement">
|
||||||
Deliberate work.<br /><em>Restless inquiry</em><span class="accent">.</span>
|
Deliberate work.<br /><em>Restless inquiry</em><span class="accent">.</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Software, pondering, and the occasional ink mark in my quiet corner of the internet.
|
Software, pondering, and the occasional ink mark in a quiet corner of the internet.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
|
<Mark kind="dots" width={52} top="-16px" right="-12px" />
|
||||||
|
<Mark kind="pipes" width={28} bottom="-8px" left="-6px" rotate={8} />
|
||||||
<HeroZone client:load />
|
<HeroZone client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -68,6 +63,7 @@ const indexItems = [
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="index">
|
<section class="index">
|
||||||
|
<Mark kind="concentric" width={42} bottom="-14px" right="-10px" />
|
||||||
<Eyebrow>Index</Eyebrow>
|
<Eyebrow>Index</Eyebrow>
|
||||||
<nav>
|
<nav>
|
||||||
{indexItems.map((item) => (
|
{indexItems.map((item) => (
|
||||||
|
|
@ -100,6 +96,7 @@ const indexItems = [
|
||||||
}
|
}
|
||||||
.intro {
|
.intro {
|
||||||
grid-area: intro;
|
grid-area: intro;
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -143,6 +140,7 @@ const indexItems = [
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
grid-area: hero;
|
grid-area: hero;
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
/* Styles for .hzframe + its children come from the React island's class names */
|
/* Styles for .hzframe + its children come from the React island's class names */
|
||||||
|
|
@ -215,6 +213,7 @@ const indexItems = [
|
||||||
|
|
||||||
.index {
|
.index {
|
||||||
grid-area: index;
|
grid-area: index;
|
||||||
|
position: relative;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.index :global(.eyebrow) {
|
.index :global(.eyebrow) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ body:not(.dark)::before { opacity: 0.045; mix-blend-mode: normal; }
|
||||||
--mark-rest: 0.16;
|
--mark-rest: 0.16;
|
||||||
}
|
}
|
||||||
.mark {
|
.mark {
|
||||||
position: fixed; pointer-events: none; z-index: 0;
|
position: absolute; pointer-events: none; z-index: 0;
|
||||||
filter: var(--mark-filter-dark);
|
filter: var(--mark-filter-dark);
|
||||||
opacity: var(--mark-rest);
|
opacity: var(--mark-rest);
|
||||||
transition: opacity var(--dur-slow) var(--ease-out);
|
transition: opacity var(--dur-slow) var(--ease-out);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue