Zum Inhalt

perlin-studio

Parametric 3D/4D Perlin animations in the browser — worker-accelerated, video export, seamless loop. Fully client-side, no dependencies, no build.

License: AGPL-3.0 No build step No dependencies Runtime: Browser

Launch the web app   Live embedding demo

Perlin Studio — Lava preset, 1200×630

Grown out of two older hobby projects (_archiv/texturizer.ipynb from early 2023 + _archiv/perlin_noise/ from May 2023) into a focused browser tool. Designed for static hosting, embedding on a homepage, and as the basis for an Obsidian plugin.


Quickstart

# Variant A — just double-click
open index.html       # macOS
start index.html      # Windows
xdg-open index.html   # Linux

# Variant B — local server (handy for hot-reload during development)
python3 -m http.server 8000
# → http://localhost:8000

Hit ▶ or Space, click a preset chip, or move any slider — the live preview updates after every input.

Features

  • Live preview of parametric Perlin animations (fBm with octaves, persistence, lacunarity, seed)
  • Low-resolution "pixel" look — a small Width/Height upscales into crisp pixel blocks for a deliberate retro / Minecraft feel
  • Six color modes: RGB (per channel, with optional per-channel multi-sample for nebula / film-grain looks), Gradient (8 presets + a custom-stops editor with per-stop hex input and paste-a-palette), HSV, Posterize, Cosine (seven palettes after Inigo Quilez, a + b·cos(2π(c·t+d)), fully editable), Flow (renders the noise as a vector field — hue = local gradient direction)
  • Cellular noise — Voronoi cells, cell borders and smooth blobs next to Perlin, seeded, tileable and seamlessly loopable like the rest
  • Relief lighting — treats the noise as a height field and lights it (light direction and height, ambient, highlight) on top of any colour mode
  • Domain warping — pre-displaces the sample point by a smoother noise field. Switches Perlin output from "Perlin" to marble veins, ink in water, alien terrain
  • Layer compositing — a second noise field can be blended on top of the main one with multiply / add / screen / darken / lighten / difference modes and a mix slider
  • Image mask — load a PNG / JPG and use its alpha (or luminance as fallback) as a per-pixel mask. PNG export keeps the transparency
  • Tileable — Tile X / Tile Y make the noise wrap modulo a configurable cell period, so the output repeats seamlessly as a background texture
  • Seamless loop — 4D Perlin on a circular path in (z, w), closes mathematically without any "breathing" artifact
  • Video export — MP4/H.264 where the browser supports it (Safari, iOS, Chrome ≥ 126), WebM otherwise (Firefox) — at arbitrary length/framerate via MediaRecorder
  • PNG snapshot of the current frame
  • Preset system with 10 built-ins (default, lava, aurora, clouds, terrain, circuit, marble, nebula, flow, composite) and JSON import / export
  • Worker-accelerated — main thread stays free, frame-skip without drift, wall-clock time as animation basis
  • Optional GPU renderer — a WebGL2 port of the same noise (Renderer → GPU); same image, orders of magnitude faster at large sizes
  • Dark + light theme, follows the OS by default; a sun/moon toggle in the topbar pins an explicit choice across reloads

Every image is a single frame from a built-in preset, rendered by the tool itself — and reproducible from the presets via scripts/render-readme-images.js. Together they show the range of colour modes and features.

Aurora preset — HSV mode, cyan-green-blue bands Nebula preset — per-channel RGB noise, vivid magenta Marble preset — domain warp pulls the field into stone veins
Aurora · HSV mode Nebula · per-channel RGB noise Marble · domain warp
Flow preset — noise rendered as a vector field Composite preset — two noise layers blended on a magma gradient Terrain preset — posterized forest gradient, contour-map look
Flow · flow field Composite · Layer 2 blend Terrain · Posterize mode

Live loops

Three seamless 2.5-second loops, rendered from the built-in presets via scripts/render-readme-videos.js. The WebMs live under docs/examples/ and are directly linkable from the live demo (handy for embedding into other docs).

Lava · Fire gradient Aurora · HSV mode Terrain · Posterize · Forest gradient

Usage

New here? Start with Getting started. Every parameter and control: control reference.

Short version of the most important controls:

Element Function
Left sidebar All parameters (resolution, noise, loop, color, presets, export)
Large canvas Live preview (always matches the current slider/input state)
▶ / Space Play / pause
⏮ Slider back to 0
t slider Time scrub (loop: [0, 1), otherwise free)
fps / frames Only used for video export

Embedding

Drop the looping animation onto any page with the <perlin-noise> custom element — renderer only, no controls:

<script src="perlin.js"></script>
<script src="perlin-presets.js"></script>
<script src="perlin-engine.js"></script>
<script src="perlin-element.js"></script>

<perlin-noise preset="aurora" autoplay></perlin-noise>

Configure it with a built-in preset name, a src pointing at a preset JSON exported from the studio, or the params JS property. It fills its container responsively (render resolution capped via max-resolution, default 1280), respects prefers-reduced-motion, and pauses while off-screen. Works from file:// too — except src, which needs an HTTP server (use preset or .params locally).

▶ See it live: pages.jkaindl.de/perlin-studio/examples/embed.html — source in examples/embed.html.

Architecture in one picture

Main thread (UI)              Web Worker
┌─────────────────┐           ┌────────────────┐
│ RAF loop        │ params/t  │ makePermutation│
│ gatherParams()  │──────────▶│ fbm3/fbm4      │
│                 │           │ colorize()     │
│ putImageData    │ ImageData │                │
│ (zero-copy)     │◀──────────│ postMessage    │
│                 │           │  (transferable)│
└─────────────────┘           └────────────────┘

Full technical docs (math, pipeline, edge cases): docs/explanation/architecture.md.

No build step

There is nothing to compile, bundle, or install. The seven scripts you see in Project layout are the shipped artifact: plain ES5-compatible JavaScript, each attaching one window global, loaded in order by index.html. No package.json, no node_modules, no transpiler, no framework.

That is a deliberate design constraint, not a gap:

  • It runs from file://. Double-clicking index.html gives you the full tool — no server, no toolchain, no network. The Perlin worker is loaded from an inline Blob (MTPerlin.WORKER_SOURCE), with an automatic main-thread fallback where a browser's CSP blocks it.
  • It deploys anywhere static. rsync the directory onto any webhost and it works. No CI step stands between the source and what visitors run.
  • It stays embeddable. <perlin-noise> drops into any page with four <script> tags — no build tooling imposed on whoever embeds it.
  • It stays readable. What you read in the repo is exactly what runs in the browser; a stack trace points at real source lines, not at bundle output.

The cost is a deliberate one: no TypeScript, no npm ecosystem, and hand-rolled solutions where a dependency would be quicker. Quality is gated by scripts/check-syntax.mjs (parses every deployable script) and scripts/smoke-test.js (Perlin math invariants) rather than by a compiler — both run before the site is published.

If you want to change this, talk to the maintainer first — see AGENTS.md.

Browser compatibility

Browser Status
Chrome / Edge (Chromium) ≥ 90 ✅ full incl. worker; exports MP4/H.264 (Chrome ≥ 126) or VP9 WebM
Firefox ≥ 90 ✅ full; exports WebM (VP8/VP9) — no MP4 muxer
Safari ≥ 14 ✅ live preview; exports MP4/H.264 (Safari ≥ 14.1 / iOS ≥ 14.5); some versions block the worker under file:// → falls back to main thread
Mobile Safari / Chrome Mobile ✅ live preview; MP4 export (iOS ≥ 14.5); may require a longer touch gesture

The app is responsive and works on mobile — the worker path keeps the animation smooth even at low resolutions.

Project layout

.
├── index.html             # UI markup
├── styles.css             # theme (dark + light, system-following with topbar toggle override)
├── perlin.js              # math/API (3D + 4D noise, color modes, worker source)
├── perlin-presets.js      # gradients, built-in presets, resolveParams (shared)
├── perlin-engine.js       # headless render engine (worker + RAF loop)
├── perlin-gl.js           # optional WebGL2 renderer (port of the noise maths)
├── perlin-studio.js       # controller (UI, recording, presets, events)
├── perlin-element.js      # <perlin-noise> embeddable custom element
├── manifest.webmanifest   # PWA manifest (installable)
├── assets/                # static assets (favicon, PWA icons, README images)
├── examples/
│   └── embed.html         # <perlin-noise> embedding demo
├── docs/                  # documentation, see docs/README.md (Diátaxis)
│   ├── tutorials/         # getting started
│   ├── how-to/            # task guides, troubleshooting
│   ├── reference/         # controls, colour modes, preset format, element
│   ├── explanation/       # architecture, design choices
│   ├── internal/          # notes for contributors and agents
│   └── examples/          # rendered WebM loops (for README + embedding)
├── scripts/
│   ├── render-readme-images.js  # renders assets/*.png from perlin.js
│   └── render-readme-videos.js  # renders docs/examples/*.webm (Node → ffmpeg)
├── _archiv/               # historical predecessors (READ-ONLY)
│   ├── perlin_noise/      # Python original
│   ├── texturizer.ipynb   # Texturizer notebook
│   └── old_texturizer_combined/   # first browser version, incl. Texturizer
├── LICENSE                # AGPL-3.0
├── CHANGELOG.md
├── CONTRIBUTING.md
├── SECURITY.md            # threat model + reporting
└── AGENTS.md              # context file for AI assistants

Documentation

The documentation is split by what you need it for (index):

Contributing

Issues and PRs welcome. Developer quickstart: CONTRIBUTING.md.

Security-related reports — please don't file them publicly. See SECURITY.md.

AI assistants (Claude Code, etc.) get their orientation in AGENTS.md.

Roadmap

Shipped since the original list:

  • [x] Gradient editor — custom color stops (since v0.8.0)
  • [x] Domain warping — Inigo-Quilez-style sample-point displacement (v0.8.0)
  • [x] Per-channel noise for RGB mode (v0.9.0)
  • [x] Flow-field colour mode — render the noise as a vector field (v0.9.0)
  • [x] Tileable noise — Tile X / Tile Y make the output repeat (v0.10.0)
  • [x] Image mask — load a PNG / JPG, render noise inside the alpha (v0.11.0)
  • [x] Layer system — composite two noise fields with a blend mode (v0.11.0)
  • [x] Web Component — embeddable <perlin-noise> renderer element (v0.12.0)

Still on the wishlist:

  • [ ] Obsidian plugin — main.ts with a modal + canvas embed (separate repo)
  • [ ] <perlin-studio> element — the full editor (controls + canvas) as a single embeddable tag, building on the shared PerlinEngine

Considered and explicitly deferred:

  • ~~GIF export~~ — WebM is technically superior in every dimension (smaller, higher quality, alpha-friendly), and gif.js would be the project's first JavaScript dependency. The Discord/Slack reach gain doesn't justify breaking the "no dependencies" principle.

License

Perlin Studio is dual-licensed — see LICENSING.md for the full picture:

  • Open source (default): GNU AGPL-3.0-or-later — copyleft with the network-use clause. If you host the tool (or a fork) so others can use it over the network, the source of your variant must also be available under AGPL.
  • Commercial license (on request): for uses the AGPL does not fit — e.g. a proprietary/closed-source product or an Apple App Store build, which is incompatible with the AGPL. Details in LICENSING.md; contributions are governed by the CLA.

The Perlin permutation table and the "Improved Noise" algorithm come from Ken Perlin's freely published Java reference (2002), reimplemented here in JS.

Arbeitsregeln (AGENTS.md)

Workspace-Standards (maintainer-lokal): Die verbindliche Leitkonvention steht in _docs/CONVENTIONS.md im Multi-Projekt-Workspace des Maintainers, ../_docs relativ zu diesem Repo — nicht Teil dieses Repos, ignorieren falls im Klon nicht vorhanden. Modell comply-or-explain. Dokumentierte Abweichungen siehe Abschnitt Abweichungen von der Leitkonvention.

This file is aimed at AI assistants (Claude Code, Cursor, Aider, …) that work on this repo. Humans contributing here will find what they need in README.md, CONTRIBUTING.md, and docs/.

Project character

Perlin Studio is a single-page browser tool for parametric 3D/4D Perlin animations. Vanilla JS, no dependencies, no backend, no build step. Works by double-clicking index.html (file://) and on any static webhost.

Two native companions reuse the same noise: Perlin Live Wallpaper (macos/, Swift + Metal, public AppKit only) and Perlin Live Photo (ios/, SwiftUI + Metal, reuses the macOS portable core by file reference).

Zero-dependency / no-build is a deliberate feature (PROF-WEB-01): the tool must stay statically deployable and embeddable as a Web Component. Don't migrate to TypeScript, a bundler, or a framework without discussing it with the maintainer first — Vanilla-JS constancy is the point.

Architecture principles

  • Five plain scripts, each attaching a window global, loaded in order (file://-safe, no ES modules): perlin.js → perlin-presets.js → perlin-engine.js → the consumers perlin-studio.js and perlin-element.js; perlin-export.js is pure compositing math. perlin-gl.js is an optional WebGL2 port of the noise maths that PerlinEngine can route frames through.
  • Render/animation logic lives in PerlinEngine, not duplicated in the studio or the <perlin-noise> element.
  • No dependencies without a specific reason. A 12-KB library for one feature is OK, but discuss with the maintainer first.
  • Defensive programming only at external boundaries (user input, file upload). Trust internal data flow.
  • Comments explain why, not what. Code should be readable without comments; comments cover the non-obvious, edge cases, math background.
  • _archiv/ is off-limits for active changes. It documents the project's genealogy and may only be added to, never edited or deleted.

Context index

  • Before reading or changing perlin*.js (the GLSL port perlin-gl.js included) or scripts/smoke-test.js: read .claude/rules/perlin-core.md (math bug classes, animation step, worker rules, GPU parity). It also loads by itself in Claude Code.
  • Before adding or moving a file, or when a term is unclear (fBm, loopProgress, safeProgress, scaleT): read docs/internal/architecture.md (module layering, file layout, glossary).
  • Before asking why something is the way it is (zero-dep, deploy guards, v tag prefix, hero.png over budget, single remote): read docs/internal/decisions.md.
  • Before changing the math, the worker pipeline or the loop mechanics: read docs/explanation/architecture.md.
  • Before changing the UI: read docs/internal/ui-spec.md; for the embeddable element: docs/internal/web-component-spec.md.
  • Before a macOS or iOS release or notarization: read docs/how-to/install-the-macos-app.md, macos/README.md, ios/README.md.
  • Before writing or moving user documentation: read docs/README.md (the Diátaxis index: tutorial, how-to, reference, explanation — a page belongs in exactly one).
  • Before changing what the repo delivers (export formats, example loops, the standalone bundle): read ANGEBOT.md.

Commands

There is no full test suite. Instead:

  1. Syntax gate — parses every deployable script (external perlin*.js plus inline <script> blocks in index.html / examples/) without executing it (PROF-WEB-03): node scripts/check-syntax.mjs
  2. Math smoke tests for Perlin changes: node scripts/smoke-test.js. Covers permutation determinism, value range of perlin3/perlin4, loop identity at progress=0 vs progress=1 (bytewise and bit-exact), Z/W axis independence, and colorize range. For ad-hoc scripts the same pattern works (global.window = {} + require perlin.js).
  3. Path hygiene gate — no absolute maintainer paths in tracked Markdown: node scripts/check-no-abs-paths.mjs
  4. GPU parity (by hand, after touching perlin-gl.js or the maths in perlin.js; needs Playwright, see the script header): node scripts/check-gl-parity.mjs — renders each case on the CPU and the GPU path in a real browser and compares. Exit 2 = skipped.
  5. Manual browser test — python3 -m http.server and click around. The permission setup may forbid starting an HTTP server from the repo's location; if so, ask the user to test it themselves.

Deploy: bash scripts/deploy-page.sh runs gates 1–3, snapshots committed HEAD via git archive and rsyncs it to pages.jkaindl.de/perlin-studio/. It needs real rsync 3.x (brew install rsync; macOS ships openrsync). The script carries four macOS deploy guards (CORE-OPS-01); don't remove them.

macOS app: cd macos && swift build && swift run, headless check swift run PerlinLiveWallpaper --selftest, package with ./scripts/build-app.sh [--notarize]. Version bump from the repo root before tagging: bash scripts/bump-version.sh X.Y.Z.

Conventions

  • Everything is English: UI strings (including new buttons/sections), code comments, identifiers, commit messages, PR descriptions, issue templates, docs. Math terms (Octaves, Persistence, Lacunarity, fBm) keep their standard form. No emojis in code/docs unless the user explicitly asks.
  • Conventional Commits (feat/fix/docs/chore/refactor/test(scope): …); commit body may be German (CORE-GIT-04).
  • One remote: Forgejo (origin = git@git.jkaindl.de:jkaindl/perlin-studio.git), no mirror since 2026-09-05 (CORE-GIT-01).
  • Tags: SemVer with v-prefix (vX.Y.Z), a documented deviation from CORE-GIT-03.
  • Stage only touched files, never git add -A (CORE-GIT-06).
  • AI-assisted commits carry the trailer Co-Authored-By: Claude Opus <version> (1M context) <noreply@anthropic.com> (CORE-GIT-05).
  • Update CHANGELOG.md for every user-visible feature/fix (keep-a-changelog).
  • Mind the flow: subtle math bugs look visually normal until the loop stops closing or edges start flickering (details in .claude/rules/perlin-core.md).

Memory

  • Project memory: ~/.claude/projects/-Users-johannes-Projects-jkaindl-apps-perlin-studio/memory/MEMORY.md — the index loaded each session (CORE-AGENT-02).
  • Coding-Cockpit (SSOT): $VAULT/25_Coding/perlin-studio/perlin-studio.md (CORE-META-14, maintainer-local; $VAULT = the maintainer's Obsidian vault) — status · tasks · decisions · history. Specs, plans and task reports live in its _SDD/, not in the repo.
  • Session handoff: .remember/ (gitignored, CORE-AGENT-03).
  • Specs, plans and design briefs live in the cockpit _SDD/, never in docs/ (the old docs/superpowers/ and design docs moved there on 2026-10-26).
  • Never in the repo: absolute paths outside the repo (/Users/…, vault paths) — use placeholders ($VAULT/…, ~/…, repo-relative).

Abweichungen von der Leitkonvention

Profile: web-static · native (+ KERN). Abgeglichen gegen _docs/CONVENTIONS.md (comply-or-explain). Das Warum steht in docs/internal/decisions.md.

  • PROF-NAT-02 (adaptiert) — Kein package.json: das Web-Frontend ist no-build/zero-dep (PROF-WEB-01), es gibt keine maschinenlesbare Web-Version (die Studio-Historie lebt im CHANGELOG.md). Die einzige versionierte native Artefakt-Quelle ist macos/packaging/Info.plist; scripts/bump-version.sh hält dort CFBundleShortVersionString + CFBundleVersion self-consistent. Es gibt keinen zweiten Versionsort, daher entfällt die package.json-Synchronisation.
  • image-budget (assets/hero.png) (bewusste Ausnahme) — hero.png ist 1200×630, weil index.html es als og:image mit dieser Größe ausweist, und liegt damit bei ~563 KB über dem 400-KB-Budget (verlustfrei gemessen: 832 → 576 KB mit oxipng -o max -Z, mehr gibt das Rauschen nicht her). Die sechs Galeriebilder liegen unter dem Budget (560 px, oxipng im Render-Skript). Der Linter wiegt assets/ heute nicht (_images_dir()); sobald er es tut, meldet er hero.png — dann ist diese Zeile der Grund.
  • PROF-WEB-04 (N/A) — Die installierbare PWA hat bewusst keinen Service-Worker / Offline-Cache; es existiert keine SW-Cache-Version zum Bumpen.

Quelldateien