Here's the philosophy and the single-file HTML. I read the skill's viewer.html template first and kept its layout, Anthropic styling and seed section. I swapped out the algorithm and parameters, and added the Regenerate and Download PNG buttons the skill requires. I couldn't run this in a browser here, so it's untested. Open it and check the seeds before it goes anywhere near your landing page.
Algorithmic Philosophy: Slack Water
Slack water is the pause between tides, when the sea has stopped advancing and hasn't started to retreat. This movement lives in that pause. It treats a tide pool as a computed event: a landscape of stone, a fixed volume of water sitting in its hollows, and the first low light of morning moving across both. Nothing in it is drawn. The stone is a landform that gets flooded, and every other element follows from that.
The terrain is a domain-warped field of layered noise. A second, thin noise contour is cut into it as a network of narrow fissures, the way weathering finds seams in real rock. The tide is not a height threshold. The field is rank-ordered into quantiles, so the Tide slider sets the fraction of the world that is underwater. Raising it floods the low basins first, then the fissures, then the shelves, and the pools join or separate as a topological change. Light comes from one low sun at the upper-left corner and rakes across the slopes. Wet stone darkens and borrows the sky, shallows borrow the bottom, and deep water borrows the sky almost entirely. Warmth moves the whole sky from cold pre-dawn grey-blue toward peach and gold.
Motion is limited to what water does. Caustic filaments are the contour lines of two slowly drifting phase fields, so they braid and dissolve the way refracted light does. They fade with depth and are brightest where the water is thin. Foam lace crawls along the waterline and breaks wherever the stone texture breaks it. Motes of plankton ride a curl of noise and flare as they cross the light, and rings spread from unseen disturbances, squashed into the perspective of a surface seen at a low angle. A drift speed of zero freezes the scene completely, which is useful for composing a still.
Life is placed by rules about where it can survive. Anemones need water of a certain depth and a margin of water on every side. Barnacles cluster as Gaussian colonies on rock that stays above the tide line. Starfish settle at the wet edge, five-fold symmetry from a single trigonometric lobe function. Kelp roots in the shallows and curls with the local noise. Each seed is a different shoreline with a different arrangement of life, and the same seed reproduces exactly.
Every constant here should read as the product of long, patient tuning by someone who knows this material thoroughly. That includes the fissure width, the exponents that sharpen the caustics, the weight of the wet-stone darkening, and the falloff of the dawn glow. The implementation should be meticulously crafted, painstakingly balanced, and free of visual noise. The restraint is deliberate: nothing in the frame is there by accident.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tide Pools at Dawn — Slack Water</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
<style>
/* Anthropic Brand Colors */
:root {
--anthropic-dark: #141413;
--anthropic-light: #faf9f5;
--anthropic-mid-gray: #b0aea5;
--anthropic-light-gray: #e8e6dc;
--anthropic-orange: #d97757;
--anthropic-blue: #6a9bcc;
--anthropic-green: #788c5d;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, var(--anthropic-light) 0%, #f5f3ee 100%);
min-height: 100vh;
color: var(--anthropic-dark);
}
.container { display: flex; min-height: 100vh; padding: 20px; gap: 20px; }
/* Sidebar */
.sidebar {
width: 320px;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 24px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(20, 20, 19, 0.1);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar h1 {
font-family: 'Lora', serif;
font-size: 24px;
font-weight: 500;
color: var(--anthropic-dark);
margin-bottom: 8px;
}
.sidebar .subtitle {
color: var(--anthropic-mid-gray);
font-size: 14px;
margin-bottom: 32px;
line-height: 1.4;
}
/* Control Sections */
.control-section { margin-bottom: 32px; }
.control-section h3 {
font-size: 16px;
font-weight: 600;
color: var(--anthropic-dark);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.control-section h3::before {
content: '•';
color: var(--anthropic-orange);
font-weight: bold;
}
/* Seed Controls */
.seed-input {
width: 100%;
background: var(--anthropic-light);
padding: 12px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 14px;
margin-bottom: 12px;
border: 1px solid var(--anthropic-light-gray);
text-align: center;
}
.seed-input:focus {
outline: none;
border-color: var(--anthropic-orange);
box-shadow: 0 0 0 2px rgba(217, 119, 87, 0.1);
background: white;
}
.seed-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 8px;
}
.regen-button { margin-bottom: 0; }
/* Parameter Controls */
.control-group { margin-bottom: 20px; }
.control-group label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--anthropic-dark);
margin-bottom: 8px;
}
.slider-container { display: flex; align-items: center; gap: 12px; }
.slider-container input[type="range"] {
flex: 1;
height: 4px;
background: var(--anthropic-light-gray);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--anthropic-orange);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
}
.slider-container input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
background: #c86641;
}
.slider-container input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--anthropic-orange);
border-radius: 50%;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.value-display {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--anthropic-mid-gray);
min-width: 60px;
text-align: right;
}
/* Color Pickers */
.color-group { margin-bottom: 16px; }
.color-group label {
display: block;
font-size: 12px;
color: var(--anthropic-mid-gray);
margin-bottom: 4px;
}
.color-picker-container { display: flex; align-items: center; gap: 8px; }
.color-picker-container input[type="color"] {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
cursor: pointer;
background: none;
padding: 0;
}
.color-value {
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--anthropic-mid-gray);
}
/* Buttons */
.button {
background: var(--anthropic-orange);
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.button:hover { background: #c86641; transform: translateY(-1px); }
.button:active { transform: translateY(0); }
.button.secondary { background: var(--anthropic-blue); }
.button.secondary:hover { background: #5a8bb8; }
.button.tertiary { background: var(--anthropic-green); }
.button.tertiary:hover { background: #6b7b52; }
.button-row { display: flex; gap: 8px; margin-bottom: 8px; }
.button-row .button { flex: 1; }
.hint {
font-size: 12px;
color: var(--anthropic-mid-gray);
margin-top: 10px;
line-height: 1.4;
}
/* Canvas Area */
.canvas-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
}
#canvas-container {
width: 100%;
max-width: 1000px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(20, 20, 19, 0.1);
background: white;
}
#canvas-container canvas {
display: block;
width: 100% !important;
height: auto !important;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: var(--anthropic-mid-gray);
}
@media (max-width: 600px) {
.container { flex-direction: column; }
.sidebar { width: 100%; }
.canvas-area { padding: 20px; }
}
</style>
</head>
<body>
<div class="container">
<!-- Control Sidebar -->
<div class="sidebar">
<h1>Tide Pools at Dawn</h1>
<div class="subtitle">Slack Water — low light on wet stone, a shoreline holding its breath</div>
<!-- Seed Section -->
<div class="control-section">
<h3>Seed</h3>
<input type="number" id="seed-input" class="seed-input" value="7" onchange="updateSeed()">
<div class="seed-controls">
<button class="button secondary" onclick="previousSeed()">← Prev</button>
<button class="button secondary" onclick="nextSeed()">Next →</button>
</div>
<button class="button tertiary regen-button" onclick="randomSeedAndUpdate()">↻ Random</button>
<div class="hint">Tip: use the ← → arrow keys to flip through seeds.</div>
</div>
<!-- Parameters Section -->
<div class="control-section">
<h3>Parameters</h3>
<div class="control-group">
<label>Tide Level (share of shore underwater)</label>
<div class="slider-container">
<input type="range" id="tide" min="0.25" max="0.65" step="0.01" value="0.45" oninput="updateParam('tide', this.value)">
<span class="value-display" id="tide-value">0.45</span>
</div>
</div>
<div class="control-group">
<label>Rock Grain (scale of the shore)</label>
<div class="slider-container">
<input type="range" id="rockScale" min="1.5" max="6" step="0.1" value="3.2" oninput="updateParam('rockScale', this.value)">
<span class="value-display" id="rockScale-value">3.2</span>
</div>
</div>
<div class="control-group">
<label>Dawn Warmth</label>
<div class="slider-container">
<input type="range" id="warmth" min="0" max="1" step="0.05" value="0.7" oninput="updateParam('warmth', this.value)">
<span class="value-display" id="warmth-value">0.7</span>
</div>
</div>
<div class="control-group">
<label>Tide-pool Life</label>
<div class="slider-container">
<input type="range" id="life" min="0" max="1" step="0.05" value="0.6" oninput="updateParam('life', this.value)">
<span class="value-display" id="life-value">0.6</span>
</div>
</div>
<div class="control-group">
<label>Caustic Shimmer</label>
<div class="slider-container">
<input type="range" id="shimmer" min="0" max="1.5" step="0.05" value="0.8" oninput="updateParam('shimmer', this.value)">
<span class="value-display" id="shimmer-value">0.8</span>
</div>
</div>
<div class="control-group">
<label>Drift Speed (0 freezes the scene)</label>
<div class="slider-container">
<input type="range" id="drift" min="0" max="2" step="0.1" value="1" oninput="updateParam('drift', this.value)">
<span class="value-display" id="drift-value">1</span>
</div>
</div>
</div>
<!-- Colors Section -->
<div class="control-section">
<h3>Colors</h3>
<div class="color-group">
<label>Dawn Glow</label>
<div class="color-picker-container">
<input type="color" id="color1" value="#f6b98a" onchange="updateColor('color1', this.value)">
<span class="color-value" id="color1-value">#f6b98a</span>
</div>
</div>
<div class="color-group">
<label>Shallows</label>
<div class="color-picker-container">
<input type="color" id="color2" value="#8fd1c4" onchange="updateColor('color2', this.value)">
<span class="color-value" id="color2-value">#8fd1c4</span>
</div>
</div>
<div class="color-group">
<label>Deep Water</label>
<div class="color-picker-container">
<input type="color" id="color3" value="#17435a" onchange="updateColor('color3', this.value)">
<span class="color-value" id="color3-value">#17435a</span>
</div>
</div>
<div class="color-group">
<label>Wet Rock</label>
<div class="color-picker-container">
<input type="color" id="color4" value="#3a3436" onchange="updateColor('color4', this.value)">
<span class="color-value" id="color4-value">#3a3436</span>
</div>
</div>
</div>
<!-- Actions Section -->
<div class="control-section">
<h3>Actions</h3>
<div class="button-row">
<button class="button" onclick="initializeSystem()">Regenerate</button>
<button class="button" onclick="resetParameters()">Reset</button>
</div>
<button class="button secondary" onclick="downloadPNG()">Download PNG</button>
</div>
</div>
<!-- Main Canvas Area -->
<div class="canvas-area">
<div id="canvas-container">
<div class="loading">Initializing generative art...</div>
</div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════════════
// PARAMETERS
// ═══════════════════════════════════════════════════════════════════════
let params = {
seed: 7,
tide: 0.45, // fraction of the shore that is underwater
rockScale: 3.2, // spatial frequency of the terrain
warmth: 0.7, // cold pre-dawn -> peach and gold
life: 0.6, // density of anemones, barnacles, starfish, kelp
shimmer: 0.8, // caustic intensity
drift: 1.0, // global time rate (0 freezes everything)
// [dawn glow, shallows, deep water, wet rock]
colorPalette: ['#f6b98a', '#8fd1c4', '#17435a', '#3a3436']
};
let defaultParams = { ...params, colorPalette: [...params.colorPalette] };
// Which stage of the pipeline each parameter invalidates
const DIRTY_MAP = {
tide: ['base', 'life'],
rockScale: ['field'],
warmth: ['base'],
life: ['life'],
shimmer: [],
drift: []
};
// ═══════════════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════════════
const G = 320; // resolution of the simulated shore grid
const N = G * G;
let Q, S, TX, W1, W2; // quantile height, light, texture, caustic phases
let active = new Int32Array(N);
let activeCount = 0;
let baseImg, glintImg, lifeLayer;
let motes = [], ripples = [];
let tt = 0, nextRipple = 0;
let dirty = { field: true, base: true, life: true };
const ANEMONES = [
{ body: [208, 102, 128], tip: [250, 200, 205] },
{ body: [92, 150, 104], tip: [210, 240, 190] },
{ body: [226, 132, 84], tip: [255, 220, 170] },
{ body: [140, 110, 170], tip: [225, 205, 245] }
];
const KELP = [[96, 120, 64], [142, 128, 58], [78, 110, 84], [170, 110, 70]];
const STARS = [[222, 108, 70], [232, 150, 72], [190, 70, 86], [210, 130, 110]];
// ═══════════════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════════════
function hexToRgb(h) {
const n = parseInt(h.slice(1), 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function mixc(a, b, t) {
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
}
function mulc(a, k) {
return [Math.min(255, a[0] * k), Math.min(255, a[1] * k), Math.min(255, a[2] * k)];
}
function sstep(a, b, x) {
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
return t * t * (3 - 2 * t);
}
function cellOf(x, y) {
const i = Math.min(G - 1, Math.max(0, Math.floor(x / width * G)));
const j = Math.min(G - 1, Math.max(0, Math.floor(y / height * G)));
return j * G + i;
}
function depthAt(x, y) { return params.tide - Q[cellOf(x, y)]; }
// ═══════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════
function setup() {
const canvas = createCanvas(1200, 1200);
pixelDensity(1);
canvas.parent('canvas-container');
lifeLayer = createGraphics(1200, 1200);
lifeLayer.pixelDensity(1);
baseImg = createImage(G, G);
glintImg = createImage(G, G);
initializeSystem();
document.querySelector('.loading').style.display = 'none';
}
function initializeSystem() {
randomSeed(params.seed);
noiseSeed(params.seed);
ripples = [];
motes = [];
for (let i = 0; i < 280; i++) {
motes.push({ x: random(width), y: random(height), ph: random(TAU), s: random(1, 2.6) });
}
nextRipple = tt + 0.5;
dirty.field = dirty.base = dirty.life = true;
processDirty();
}
function processDirty() {
if (dirty.field) { buildField(); dirty.base = dirty.life = true; }
if (dirty.base) buildBase();
if (dirty.life) buildLife();
dirty.field = dirty.base = dirty.life = false;
}
// ═══════════════════════════════════════════════════════════════════════
// THE SHORE: warped terrain, fissure network, quantile tide
// ═══════════════════════════════════════════════════════════════════════
function buildField() {
noiseSeed(params.seed);
if (!Q) {
Q = new Float32Array(N); S = new Float32Array(N); TX = new Float32Array(N);
W1 = new Float32Array(N); W2 = new Float32Array(N);
}
const raw = new Float32Array(N);
const s = params.rockScale;
for (let j = 0; j < G; j++) {
for (let i = 0; i < G; i++) {
const x = i / G, y = j / G, id = j * G + i;
const qx = noise(x * s * 0.7 + 5.2, y * s * 0.7 + 1.3);
const qy = noise(x * s * 0.7 + 8.3, y * s * 0.7 + 2.8);
const wx = x * s + (qx - 0.5) * 3.0;
const wy = y * s + (qy - 0.5) * 3.0;
const n1 = 0.55 * noise(wx, wy)
+ 0.30 * noise(wx * 2.1 + 9, wy * 2.1 + 4)
+ 0.15 * noise(wx * 4.3 + 3, wy * 4.3 + 7);
const n2 = noise(wx * 0.9 + 30, wy * 0.9 + 17);
const crack = Math.exp(-Math.pow((n2 - 0.5) / 0.045, 2));
raw[id] = n1 - 0.2 * crack;
TX[id] = noise(x * 38 + 50, y * 38 + 20);
W1[id] = noise(x * 12 + 70, y * 12 + 90);
W2[id] = noise(x * 8 + 120, y * 8 + 40);
}
}
// Rank-order the heights so the tide slider is a literal share of the shore.
const order = Array.from({ length: N }, (_, k) => k).sort((a, b) => raw[a] - raw[b]);
for (let r = 0; r < N; r++) Q[order[r]] = r / (N - 1);
// Raking light from the upper-left sun, taken from the smooth field
for (let j = 0; j < G; j++) {
for (let i = 0; i < G; i++) {
const i0 = Math.max(0, i - 1), i1 = Math.min(G - 1, i + 1);
const j0 = Math.max(0, j - 1), j1 = Math.min(G - 1, j + 1);
const dx = raw[j * G + i1] - raw[j * G + i0];
const dy = raw[j1 * G + i] - raw[j0 * G + i];
S[j * G + i] = Math.min(1, Math.max(0, 0.5 + (dx * 0.7 + dy * 0.7) * 30));
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// BASE IMAGE: stone, wet stone, water over stone, sky reflection
// ═══════════════════════════════════════════════════════════════════════
function buildBase() {
const pal = params.colorPalette.map(hexToRgb);
const glow = pal[0], shallow = pal[1], deep = pal[2], rock = pal[3];
const w = params.warmth, tide = params.tide;
const skyFar = mixc([128, 146, 190], [214, 148, 166], w);
const skyNear = mixc([188, 204, 216], glow, w);
const rockShade = mulc(rock, 0.55);
const rockLit = mixc(mulc(rock, 1.8), glow, 0.4 * w);
const olive = [78, 98, 66], sand = [214, 196, 166];
baseImg.loadPixels();
glintImg.loadPixels();
const bp = baseImg.pixels, gp = glintImg.pixels;
activeCount = 0;
for (let j = 0; j < G; j++) {
for (let i = 0; i < G; i++) {
const id = j * G + i, o = id * 4;
const x = i / G, y = j / G;
const d = tide - Q[id], s = S[id], tx = TX[id];
const gd = Math.min(1, Math.hypot(x - 0.05, y) / 1.25);
const sk = mixc(skyFar, skyNear, Math.pow(1 - gd, 1.3));
// stone
let rc = mixc(rockShade, rockLit, s);
const grain = 0.82 + 0.36 * tx;
rc = [rc[0] * grain, rc[1] * grain, rc[2] * grain];
let col;
if (d <= 0) {
const wet = Math.exp(d * 16);
rc = mulc(rc, 1 - 0.38 * wet);
const alg = Math.exp(d * 9) * 0.5 * (tx > 0.5 ? 1 : 0.4) * (1 - s * 0.5);
rc = mixc(rc, olive, alg);
col = mixc(rc, sk, 0.16 * wet * (0.4 + 0.6 * s));
} else {
const dn = Math.min(d / 0.22, 1);
const tint = mixc(shallow, deep, Math.pow(dn, 0.7));
const bottom = mulc(mixc(rc, sand, sstep(0.03, 0.2, d) * 0.5), 0.85);
const trans = Math.exp(-d * 10) * 0.8;
col = mixc(tint, bottom, trans);
col = mixc(col, sk, 0.16 + 0.3 * dn);
}
bp[o] = col[0]; bp[o + 1] = col[1]; bp[o + 2] = col[2]; bp[o + 3] = 255;
gp[o] = gp[o + 1] = gp[o + 2] = 0; gp[o + 3] = 255;
if (d > -0.03) active[activeCount++] = id;
}
}
baseImg.updatePixels();
glintImg.updatePixels();
}
// ═══════════════════════════════════════════════════════════════════════
// LIFE: kelp, anemones, barnacles, starfish — placed by habitat rules
// ═══════════════════════════════════════════════════════════════════════
function buildLife() {
randomSeed(params.seed * 7919 + 13);
const g = lifeLayer;
g.clear();
const lf = params.life;
if (lf <= 0) return;
const placed = [];
const free = (x, y, r) => placed.every(p => Math.hypot(p.x - x, p.y - y) > p.r + r + 4);
function pickSpot(minD, maxD, r, extra, tries = 120) {
for (let t = 0; t < tries; t++) {
const x = random(r + 10, width - r - 10), y = random(r + 10, height - r - 10);
const d = depthAt(x, y);
if (d >= minD && d <= maxD && free(x, y, r) && (!extra || extra(x, y, r))) return { x, y, d };
}
return null;
}
// kelp, rooted in the shallows
const nKelp = Math.round(26 * lf);
for (let c = 0; c < nKelp; c++) {
const spot = pickSpot(0.04, 0.3, 0, null, 60);
if (!spot) continue;
const strands = floor(random(3, 7));
for (let k = 0; k < strands; k++) {
drawKelp(g, spot.x + random(-9, 9), spot.y + random(-9, 9));
}
}
// anemones need water on every side
const nAnem = Math.round(18 * lf);
for (let c = 0; c < nAnem; c++) {
const R = random(14, 34);
const spot = pickSpot(0.04, 0.35, R, (x, y, r) =>
[[1, 0], [-1, 0], [0, 1], [0, -1]].every(v => depthAt(x + v[0] * r * 0.8, y + v[1] * r * 0.8) > 0.01));
if (!spot) continue;
placed.push({ x: spot.x, y: spot.y, r: R });
drawAnemone(g, spot.x, spot.y, R, random(ANEMONES));
}
// barnacle colonies on dry-ish stone
const nColony = Math.round(22 * lf);
for (let c = 0; c < nColony; c++) {
const spread = random(14, 38);
const spot = pickSpot(-0.5, -0.04, spread * 0.6);
if (!spot) continue;
placed.push({ x: spot.x, y: spot.y, r: spread * 0.6 });
const n = floor(random(6, 22));
for (let k = 0; k < n; k++) {
const bx = spot.x + randomGaussian() * spread * 0.5;
const by = spot.y + randomGaussian() * spread * 0.5;
if (depthAt(bx, by) > -0.03) continue;
drawBarnacle(g, bx, by, random(3, 7.5));
}
}
// starfish at the wet edge
const nStar = Math.round(6 * lf);
for (let c = 0; c < nStar; c++) {
const R = random(24, 44);
const spot = pickSpot(-0.25, 0.03, R);
if (!spot) continue;
placed.push({ x: spot.x, y: spot.y, r: R });
drawStar(g, spot.x, spot.y, R);
}
}
function drawKelp(g, x, y) {
const col = random(KELP);
const len = random(50, 120), n = 14, step = len / n;
const curl = random(-0.35, 0.35);
let a = random(TAU), px = x, py = y;
g.strokeCap(ROUND);
for (let k = 0; k < n; k++) {
const t = k / n;
a += curl * 0.3 + (noise(px * 0.01, py * 0.01) - 0.5) * 0.9;
const nx = px + Math.cos(a) * step, ny = py + Math.sin(a) * step;
g.stroke(col[0], col[1], col[2], 200 * (1 - t * 0.35));
g.strokeWeight(lerp(5.5, 1.2, t));
g.line(px, py, nx, ny);
if (k % 3 === 1) {
g.noStroke();
g.fill(col[0], col[1], col[2], 140);
g.push();
g.translate(nx, ny);
g.rotate(a + HALF_PI);
g.ellipse(0, 6, 5, 14);
g.pop();
}
px = nx; py = ny;
}
}
function drawAnemone(g, x, y, R, pal) {
const b = pal.body, tp = pal.tip;
g.noStroke();
g.fill(b[0], b[1], b[2], 55);
g.circle(x, y, R * 3);
g.strokeCap(ROUND);
const rings = [0.5, 0.78, 1.0];
for (const ring of rings) {
const count = floor(R * 2.6 * ring);
for (let k = 0; k < count; k++) {
const a = (k / count) * TAU + random(-0.05, 0.05);
const curl = random(-0.25, 0.25);
const r0 = R * 0.28 * ring, r1 = R * ring * random(0.8, 1.0);
const x0 = x + Math.cos(a) * r0, y0 = y + Math.sin(a) * r0;
const x1 = x + Math.cos(a + curl) * r1, y1 = y + Math.sin(a + curl) * r1;
g.stroke(b[0], b[1], b[2], 200);
g.strokeWeight(2.2);
g.line(x0, y0, x1, y1);
g.noStroke();
g.fill(tp[0], tp[1], tp[2], 230);
g.circle(x1, y1, 3.2);
}
}
g.noStroke();
g.fill(b[0] * 0.5, b[1] * 0.5, b[2] * 0.5, 230);
g.circle(x, y, R * 0.56);
g.fill(30, 20, 24, 220);
g.circle(x, y, R * 0.24);
}
function drawBarnacle(g, x, y, r) {
g.noStroke();
g.fill(10, 10, 12, 90);
g.circle(x + 1.2, y + 1.6, r * 2.1);
g.fill(226, 222, 208);
g.stroke(150, 146, 134);
g.strokeWeight(1);
g.circle(x, y, r * 2);
g.stroke(150, 146, 134, 80);
for (let k = 0; k < 6; k++) {
const a = (k / 6) * TAU;
g.line(x + Math.cos(a) * r * 0.5, y + Math.sin(a) * r * 0.5, x + Math.cos(a) * r * 0.95, y + Math.sin(a) * r * 0.95);
}
g.noStroke();
g.fill(200, 196, 180);
g.ellipse(x, y, r * 1.2, r * 0.9);
g.fill(58, 52, 50);
g.ellipse(x, y, r * 0.7, r * 0.45);
}
function drawStar(g, x, y, R) {
const rot = random(TAU), col = random(STARS);
const ctx = g.drawingContext;
ctx.shadowColor = 'rgba(20,10,10,0.45)';
ctx.shadowBlur = 9;
ctx.shadowOffsetY = 4;
g.fill(col[0], col[1], col[2]);
g.stroke(col[0] * 0.65, col[1] * 0.6, col[2] * 0.6);
g.strokeWeight(1.5);
g.beginShape();
for (let a = 0; a < TAU; a += 0.04) {
const r = R * (0.3 + 0.7 * Math.pow(Math.abs(Math.cos(2.5 * (a - rot))), 1.8));
g.vertex(x + Math.cos(a) * r, y + Math.sin(a) * r);
}
g.endShape(CLOSE);
ctx.shadowColor = 'rgba(0,0,0,0)';
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
g.noStroke();
for (let k = 0; k < 5; k++) {
const ang = rot + (k * TAU) / 5;
for (let s = 0.12; s < 0.92; s += 0.09) {
g.fill(255, 235, 200, 170);
g.circle(x + Math.cos(ang) * R * s, y + Math.sin(ang) * R * s, lerp(3, 1.2, s));
}
}
g.fill(255, 230, 190, 200);
g.circle(x, y, 5);
}
// ═══════════════════════════════════════════════════════════════════════
// ANIMATION: caustics + foam, plankton, ripples, dawn light
// ═══════════════════════════════════════════════════════════════════════
function updateGlint() {
glintImg.loadPixels();
const px = glintImg.pixels;
const t = tt * 1.3, sh = params.shimmer, w = params.warmth, tide = params.tide;
const sr = 255, sg = 238 - 30 * w, sb = 215 - 55 * w;
for (let k = 0; k < activeCount; k++) {
const id = active[k];
const d = tide - Q[id];
let inten = 0, foam = 0;
if (d > 0) {
const dn = Math.min(d / 0.28, 1);
const a = 1 - Math.abs(Math.sin(W1[id] * 22 + t));
const b = 1 - Math.abs(Math.sin(W2[id] * 17 - t * 0.8 + 1.7));
const a2 = a * a, a4 = a2 * a2, a8 = a4 * a4;
const b2 = b * b, b4 = b2 * b2, b8 = b4 * b4;
const vis = (1 - 0.6 * dn) * Math.min(1, d / 0.04 + 0.15);
inten = (a8 + 0.8 * b8) * sh * vis * 0.7;
}
if (d > -0.012 && d < 0.02) {
const f = Math.max(0, 1 - Math.abs(d - 0.004) / 0.016);
foam = f * (0.35 + 0.65 * TX[id]) * (0.75 + 0.25 * Math.sin(t * 1.0 + W1[id] * 30)) * 0.55;
}
const o = id * 4;
px[o] = Math.min(255, sr * inten + 255 * foam);
px[o + 1] = Math.min(255, sg * inten + 250 * foam);
px[o + 2] = Math.min(255, sb * inten + 240 * foam);
}
glintImg.updatePixels();
}
function drawMotes() {
const tide = params.tide, sp = 0.45 * params.drift;
strokeCap(ROUND);
for (const m of motes) {
const a = noise(m.x * 0.003, m.y * 0.003, tt * 0.15) * TAU * 2;
m.x += Math.cos(a) * sp;
m.y += Math.sin(a) * sp;
if (m.x < 0) m.x += width; if (m.x > width) m.x -= width;
if (m.y < 0) m.y += height; if (m.y > height) m.y -= height;
const d = tide - Q[cellOf(m.x, m.y)];
if (d < 0.03) continue;
const tw = 80 + 120 * Math.pow(Math.sin(tt * 2 + m.ph), 2);
stroke(255, 236, 205, tw * Math.min(1, d * 6));
strokeWeight(m.s);
point(m.x, m.y);
}
}
function drawRipples() {
if (tt > nextRipple && params.drift > 0) {
for (let t = 0; t < 30; t++) {
const x = random(width), y = random(height);
if (depthAt(x, y) > 0.06) {
ripples.push({ x, y, t0: tt, maxR: random(40, 110) });
break;
}
}
nextRipple = tt + random(0.8, 2.2) / Math.max(params.drift, 0.2);
}
noFill();
for (let i = ripples.length - 1; i >= 0; i--) {
const rp = ripples[i];
const life = (tt - rp.t0) / 3.2;
if (life > 1) { ripples.splice(i, 1); continue; }
for (let k = 0; k < 3; k++) {
const r = rp.maxR * (life - k * 0.08);
if (r <= 0) continue;
stroke(255, 235, 215, (1 - life) * 90);
strokeWeight(1.5);
ellipse(rp.x, rp.y, r * 2, r * 1.24);
}
}
}
function dawnLight() {
const ctx = drawingContext, w = params.warmth;
const gl = hexToRgb(params.colorPalette[0]);
ctx.save();
ctx.globalCompositeOperation = 'screen';
const gx = width * 0.04, gy = -height * 0.04;
const gr = ctx.createRadialGradient(gx, gy, 0, gx, gy, width * 1.05);
gr.addColorStop(0, `rgba(${gl[0]},${gl[1]},${gl[2]},${0.10 + 0.35 * w})`);
gr.addColorStop(1, `rgba(${gl[0]},${gl[1]},${gl[2]},0)`);
ctx.fillStyle = gr;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = 'multiply';
const vg = ctx.createRadialGradient(width / 2, height / 2, width * 0.35, width / 2, height / 2, width * 0.8);
vg.addColorStop(0, 'rgba(255,255,255,1)');
vg.addColorStop(1, 'rgba(150,150,170,1)');
ctx.fillStyle = vg;
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
function draw() {
processDirty();
tt += 0.016 * params.drift;
image(baseImg, 0, 0, width, height);
image(lifeLayer, 0, 0);
updateGlint();
blendMode(ADD);
image(glintImg, 0, 0, width, height);
drawMotes();
drawRipples();
blendMode(BLEND);
dawnLight();
}
// ═══════════════════════════════════════════════════════════════════════
// UI CONTROL HANDLERS
// ═══════════════════════════════════════════════════════════════════════
function updateParam(name, value) {
params[name] = parseFloat(value);
document.getElementById(name + '-value').textContent = String(+params[name].toFixed(2));
(DIRTY_MAP[name] || []).forEach(k => { dirty[k] = true; });
}
function updateColor(colorId, value) {
const idx = parseInt(colorId.replace('color', ''), 10) - 1;
params.colorPalette[idx] = value;
document.getElementById(colorId + '-value').textContent = value;
dirty.base = true;
}
function downloadPNG() {
saveCanvas('tide-pools-seed-' + params.seed, 'png');
}
// ═══════════════════════════════════════════════════════════════════════
// SEED CONTROL FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════
function updateSeedDisplay() {
document.getElementById('seed-input').value = params.seed;
}
function updateSeed() {
const input = document.getElementById('seed-input');
const newSeed = parseInt(input.value);
if (newSeed && newSeed > 0) {
params.seed = newSeed;
initializeSystem();
} else {
updateSeedDisplay();
}
}
function previousSeed() {
params.seed = Math.max(1, params.seed - 1);
updateSeedDisplay();
initializeSystem();
}
function nextSeed() {
params.seed = params.seed + 1;
updateSeedDisplay();
initializeSystem();
}
function randomSeedAndUpdate() {
params.seed = Math.floor(Math.random() * 999999) + 1;
updateSeedDisplay();
initializeSystem();
}
function resetParameters() {
const keepSeed = params.seed;
params = { ...defaultParams, colorPalette: [...defaultParams.colorPalette], seed: keepSeed };
['tide', 'rockScale', 'warmth', 'life', 'shimmer', 'drift'].forEach(name => {
document.getElementById(name).value = params[name];
document.getElementById(name + '-value').textContent = String(+params[name].toFixed(2));
});
params.colorPalette.forEach((c, i) => {
document.getElementById('color' + (i + 1)).value = c;
document.getElementById('color' + (i + 1) + '-value').textContent = c;
});
updateSeedDisplay();
initializeSystem();
}
// Arrow keys flip through seeds (ignored while typing in the seed box)
window.addEventListener('keydown', function (e) {
if (e.target && e.target.tagName === 'INPUT' && e.target.type !== 'range') return;
if (e.key === 'ArrowRight') nextSeed();
if (e.key === 'ArrowLeft') previousSeed();
});
window.addEventListener('load', function () {
updateSeedDisplay();
});
</script>
</body>
</html>
Notes for your landing page
- Seeds: Prev, Next, Random, the jump box and the ← → keys all work. Each seed gives a new shoreline, and the same seed always reproduces it.
- Sliders:
- Tide Level is the share of the shore that's underwater, so low values give separate pools and high values merge them.
- Rock Grain sets the scale of the terrain.
- Dawn Warmth moves the sky from cold grey-blue to peach and gold.
- Tide-pool Life controls how many anemones, barnacles, starfish and kelp appear.
- Caustic Shimmer sets the intensity of the light filaments.
- Drift Speed at 0 freezes the scene, which is useful for a still.
- Palette: The four color pickers are Dawn Glow, Shallows, Deep Water and Wet Rock. You can match them to your brand.
- Performance: The shore simulates on a 320×320 grid and is scaled up to 1200px. If it feels heavy on weaker devices, lower
Gat the top of the script. Moving the Rock Grain slider rebuilds the terrain, so it will lag a little while you drag it. - Embedding: The CDN scripts (p5.js and Google Fonts) need network access. For production, host p5 yourself.
Real run recorded with claude-code / claude-sonnet-5-5. Output is shown verbatim, unmodified.
它做什么
让模型先写一份「算法哲学」(4-6 段的生成美学宣言),再用 p5.js 把它实现出来:带种子的随机数、噪声场、粒子等系统,最终是一个自包含的单文件 HTML 页面。
工作方式
- 模型先输出 Markdown 形式的哲学文本,并从你的需求里提取一个隐含的主题。
- 读取自带的
templates/viewer.html,保留它的版式:种子面板(上一个 / 下一个 / 随机 / 跳转)、参数面板、可选的颜色选择器,以及重置 / 重新生成 / 下载 PNG。 - 把模板里的示例算法和滑块替换成贴合这套哲学的版本。相同的种子永远生成相同的画面。
适合场景
- 给落地页、作品集做主视觉、背景或探索性作品
- 通过切换种子,向客户展示多个变体
- 学习带种子的生成艺术是怎么组织代码的
模板用的是 Anthropic 风格的配色和字体,需要自己的品牌风格时请自行调整。
纯指令和模板文件。自带的两个文件(HTML 查看器模板、JS 参考)不会被 Skill 自己执行,没有安装脚本,也没有 shell 命令。生成的页面会从 cdnjs.cloudflare.com 加载 p5.js、从 Google Fonts 加载 Poppins / Lora 字体,所以需要联网才能显示。我们的试用环境里模型没有浏览器,生成的 HTML 没有真正打开验证过;用之前请自己打开,测一遍种子和滑块。