Parametric 3D/4D Perlin animations in the browser — vanilla JS, no build, seamless loop, WebM export. Worker-accelerated. https://jkaindl.codeberg.page/perlin-studio/
  • JavaScript 43.1%
  • Swift 21.9%
  • Jupyter Notebook 12.6%
  • CSS 11.1%
  • HTML 8%
  • Other 3.3%
Find a file
2026-06-15 21:54:52 +02:00
.forgejo/workflows chore: close 6 comply-or-explain convention gaps (web-static · native) 2026-06-15 13:54:02 +02:00
.gitea/issue_template v0.6.0: full translation to English 2026-05-27 17:15:21 +02:00
_archiv initial commit 2026-05-20 17:23:18 +02:00
assets build: render the gallery from the real presets (full-pipeline tool) 2026-06-02 20:16:53 +02:00
dist feat: refresh built-in presets for more impact 2026-06-02 20:10:20 +02:00
docs docs(plan): Phase 1 implementation plan — web video export 2026-06-15 17:56:02 +02:00
examples docs: add <perlin-noise> embedding demo page 2026-05-28 13:23:58 +02:00
ios fix(ios,macos): pin pixel buffers for the lifetime of their CGContext 2026-06-10 17:05:36 +02:00
macos chore(macos): use 'jkaindl' as the notarytool keychain profile 2026-06-15 14:08:09 +02:00
scripts feat(export): export panel markup (resolution + background) + gate perlin-export.js 2026-06-15 18:09:09 +02:00
.editorconfig v0.6.0: full translation to English 2026-05-27 17:15:21 +02:00
.gitignore docs: comprehensive illustrated User Guide 2026-06-02 21:13:19 +02:00
AGENTS.md docs(export): changelog + AGENTS for perlin-export module 2026-06-15 18:38:09 +02:00
CHANGELOG.md docs(export): changelog + AGENTS for perlin-export module 2026-06-15 18:38:09 +02:00
CLA.md docs: dual-licensing model + Contributor License Agreement 2026-06-01 17:33:42 +02:00
CONTRIBUTING.md docs: dual-licensing model + Contributor License Agreement 2026-06-01 17:33:42 +02:00
index.html style(export): align §09 controls with panel house style 2026-06-15 18:14:45 +02:00
LICENSE Lizenz auf AGPL-3.0 umstellen, Codeberg-Veröffentlichung vorbereiten 2026-05-27 11:55:33 +02:00
LICENSE-DOCS chore: close 6 comply-or-explain convention gaps (web-static · native) 2026-06-15 13:54:02 +02:00
LICENSING.md docs: dual-licensing model + Contributor License Agreement 2026-06-01 17:33:42 +02:00
manifest.webmanifest v0.6.0: full translation to English 2026-05-27 17:15:21 +02:00
perlin-element.js feat: add <perlin-noise> renderer-only custom element 2026-05-28 13:23:08 +02:00
perlin-engine.js feat: visible transport state + single primary + first-paint/worker feedback 2026-06-02 15:02:53 +02:00
perlin-export.js feat(export): add compositeOverColor + hexToRgb (pure) 2026-06-15 18:04:28 +02:00
perlin-presets.js feat: refresh built-in presets for more impact 2026-06-02 20:10:20 +02:00
perlin-studio.js fix(export): transparent export uses alpha-capable WebM (no silent black flatten) + stale comment/CSS cleanup 2026-06-15 18:45:41 +02:00
perlin.js v0.11.0: image mask + layer compositing 2026-05-27 23:24:05 +02:00
README.md fix: record MP4/H.264 so Safari and iOS can capture (was WebM-only) 2026-06-04 15:11:09 +02:00
SECURITY.md v0.6.0: full translation to English 2026-05-27 17:15:21 +02:00
styles.css Merge feat/web-video-export: web video export (mask + background compositing, Phase 1) 2026-06-15 21:54:52 +02:00

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.

What it does

  • 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
  • Five 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, Flow (renders the noise as a vector field — hue = local gradient direction)
  • 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
  • TileableTile 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
  • 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

Full reference of every parameter and control: docs/USAGE.md.

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: jkaindl.codeberg.page/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/ARCHITECTURE.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-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/
│   ├── ARCHITECTURE.md    # technical docs
│   ├── USAGE.md           # full user guide
│   └── 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

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:

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

Still on the wishlist:

  • Obsidian pluginmain.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.