홈 / Skills / 웹·UI 디자인 / 알고리즘 아트 Algorithmic Art
웹·UI 디자인

알고리즘 아트 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 참고 코드)은 스킬이 직접 실행하지 않으며 설치 스크립트나 셸 명령도 없습니다. 생성된 페이지는 cdnjs.cloudflare.com에서 p5.js를, Google Fonts에서 Poppins / Lora를 불러오므로 표시하려면 인터넷 연결이 필요합니다. 시험 실행 환경의 모델에는 브라우저가 없어 생성된 HTML을 실제로 열어 확인하지 않았습니다. 어디에 쓰기 전에 직접 열어 시드와 슬라이더를 확인하세요.