首頁 / Skills / 網頁與介面設計 / 演算法藝術 Algorithmic Art
網頁與介面設計

演算法藝術 Algorithmic Art

AnthropicApache-2.0Skill 語言: 英文
授權已核對含腳本
演示真實執行紀錄 · claude-sonnet-5-5 · 2026年10月
I'm a designer building a landing page for a tide-pool aquarium. Use the algorithmic-art skill to make me an interactive generative piece inspired by "tide pools at dawn" – I want to be able to flip through seeds and tweak a few sliders. Please output the algorithmic philosophy and the full single-file HTML in your reply (no file tools are available).
Skill: algorithmic-art
Read: templates/viewer.html

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 G at 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 頁面。

運作方式

  1. 模型先輸出 Markdown 形式的哲學文字,並從你的需求中擷取一個隱含的主題。
  2. 讀取內附的 templates/viewer.html,保留其版面:種子面板(上一個 / 下一個 / 隨機 / 跳轉)、參數面板、可選的顏色選擇器,以及重設 / 重新生成 / 下載 PNG。
  3. 把模板中的範例演算法與滑桿換成符合這套哲學的版本。相同的種子一定會產生相同的畫面。

適合情境

  • 為著陸頁、作品集製作主視覺、背景或探索性作品
  • 透過切換種子,向客戶展示多個變體
  • 學習帶種子的生成藝術如何組織程式碼

模板採用 Anthropic 風格的配色與字型,若需要自己的品牌風格請自行調整。

說明與風險

純指令與模板檔案。內附的兩個檔案(HTML 檢視器模板、JS 參考)不會由 Skill 自己執行,沒有安裝腳本,也沒有 shell 指令。產生的頁面會從 cdnjs.cloudflare.com 載入 p5.js、從 Google Fonts 載入 Poppins / Lora 字型,因此需要連網才能顯示。我們的試用環境中模型沒有瀏覽器,產生的 HTML 並未實際開啟驗證;使用前請自行開啟,測試種子與滑桿。