You Might Also Like
Canvas Noise Terrain Generator — Free Perlin FBM Heightmap Snippet
Canvas Noise Terrain Generator · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Canvas Noise Terrain Generator — Perlin Noise and Elevation Banding

This snippet generates a topographic-map-style terrain from nothing but math — a from-scratch Perlin-style gradient noise implementation, stacked into fractal Brownian motion, colored by elevation band, and rasterized directly into ImageData.
Gradient noise, not value noise
noise2D() implements classic Perlin noise: a shuffled permutation table (seedNoise()) assigns each integer lattice point a pseudo-random gradient direction, and the noise value at any point is a smoothed (fade()-eased) interpolation between the dot products of the surrounding lattice gradients and the offset vectors to them. This produces smooth, organic-looking randomness — unlike naive per-pixel random values, which look like static rather than terrain.
Fractal Brownian Motion builds detail at multiple scales
A single noise octave produces smooth, blobby shapes with no fine detail. fbm() samples the same noise function multiple times at doubling frequency and halving amplitude, summing the results — broad, low-frequency octaves establish sweeping hills and valleys, while higher-frequency, lower-amplitude octaves layer in coastline roughness and small-scale variation on top. This is exactly the technique real terrain-generation and cloud-rendering systems use to avoid the "obviously smooth math function" look.
Elevation bands, not a raw gradient
Rather than mapping height directly to a smooth color gradient, colorFor() steps through a fixed sequence of elevation thresholds (deep water, shallow water, beach, grassland, forest, rock, snow) and linearly interpolates color only *within* each band. That banding, plus interpolation at the boundaries, is what makes the noise field read as a legible topographic map rather than an abstract color blob.
Rasterizing at reduced resolution
Sampling the noise function at every device pixel would be unnecessarily slow for a real-time regenerate button. Instead the field is sampled every few CSS pixels and each sample fills a small block in the pixel buffer — a resolution/performance trade-off similar to the one in canvas metaball blobs.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain how the permutation table and fade/lerp/grad functions together implement classic Perlin noise, and why summing multiple octaves at doubling frequency (fractal Brownian motion) produces more convincing terrain than a single noise call. It's a great snippet to extend with an assistant — ask for a version with animated, continuously scrolling terrain, a 3D-shaded relief look using a simple normal-based lighting calculation on the heightmap, or ridged/turbulent noise variants (abs(noise) instead of raw noise) for mountain-range-style terrain.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "procedural noise terrain generator" in plain HTML, CSS, and JavaScript using only the Canvas 2D API and a hand-rolled Perlin noise implementation — no noise or terrain-generation library.
Requirements:
- Implement classic 2D gradient (Perlin) noise from scratch: build a shuffled 256-entry permutation table (reshuffled via Fisher-Yates on a "regenerate" action), and a noise2D(x, y) function using the standard fade/lerp/grad approach — smoothed interpolation between dot products of pseudo-random gradients at surrounding integer lattice points.
- Implement fractal Brownian motion (fbm) on top of that noise function: sum multiple octaves of noise at increasing frequency (doubling each octave) and decreasing amplitude (halving each octave), normalized by total amplitude, with the octave count exposed as a live UI slider.
- Expose a "scale" slider that controls the frequency at which the noise field is sampled per pixel (lower scale = larger, smoother zoomed-in features; higher scale = smaller, busier features).
- Map the resulting 0-1 height value to a fixed sequence of elevation-band colors (e.g. deep water, shallow water, sand/beach, grassland, forest, rock, snow), interpolating color smoothly within each band as height crosses its threshold range, so the result reads as a topographic map rather than an abstract gradient.
- Rasterize the terrain by sampling the noise field on a coarser grid than actual device pixels (e.g. every few CSS pixels) and filling each grid cell's corresponding block of pixels directly into an ImageData buffer, then pushing it to the canvas with putImageData for performance.
- Include a "Regenerate" button that reshuffles the permutation table and re-renders an entirely new terrain layout using the same scale/octave settings, and re-render whenever the scale or octave sliders change. Handle window resize by recomputing canvas dimensions (accounting for devicePixelRatio) and regenerating the terrain to fit.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA colored terrain heightmap renders immediately.
- 2Adjust ScaleLower values zoom into broader, smoother terrain features.
- 3Adjust OctavesMore octaves add finer, rougher detail on top of the base shape.
- 4Click RegenerateA new random permutation table produces an entirely new landscape.
- 5Read the elevation bandsDeep water, shallow water, beach, grass, forest, rock, and snow.
- 6Resize the windowThe terrain regenerates to fill the new canvas size.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Simple per-pixel random values produce visual static with no spatial coherence between neighboring pixels. Perlin (gradient) noise instead assigns pseudo-random gradient directions to a lattice of integer points and smoothly interpolates between them, so nearby points produce similar, correlated values — that spatial smoothness is what makes it look like organic terrain rather than TV static.
A single noise octave produces smooth, rounded, blobby shapes with no fine texture at any scale. fbm() sums several octaves at doubling frequency and halving amplitude, so low-frequency octaves set the broad hills and valleys while higher-frequency octaves add progressively finer roughness on top — the combination is what gives the terrain both large-scale structure and small-scale detail simultaneously.
Regenerate calls seedNoise(), which reshuffles the 256-entry permutation table using Math.random(). Since every noise lookup depends on that table to determine gradient directions at each lattice point, a freshly shuffled table produces an entirely different noise field and therefore an entirely different terrain layout, even though the fbm and coloring logic are unchanged.
colorFor() intentionally steps through a fixed list of elevation bands (water, beach, grass, forest, rock, snow) rather than mapping height to a single smooth color ramp. Interpolating color only within each band, and switching bands at fixed height thresholds, is what produces the legible, map-like banding instead of an abstract, unreadable color blend.
Yes — sample the noise field with an offset that increases each frame (e.g. fbm((x + t) * scale, y * scale, octaves) with t incrementing slowly) and call generate() inside a requestAnimationFrame loop instead of only on slider/button events. Because noise2D is a pure function of its input coordinates, shifting those coordinates over time produces a smoothly scrolling terrain with no other changes needed.