GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Cloud Animation: 9 Sky Scenes in Canvas

css cloud animation: Why is a puffy cloud's base so flat? 03 adds puffs only above the height where rising air condenses, 04 offsets hill shadows by sun

A Cloud Is Density, Not a Drawing: CSS Cloud Animation Starts by Computing How Thick the Droplets Are in Each Cell

Auto-plays · click a tile to jump to its section · all nine in one zip

All nine scenes are the same few noise functions plugged into different places. 01 and 02 handle the outline and light of a single cloud, 03 handles the height where a cloud forms, and 04 and 05 show what the ground and the wind leave on it. 06 and 07 are plane and band backdrops, 08 is a cloud dissolving, and 09 combines the calculations of 06, 02 and 07 in a single scene.

Why a cloud shrinks to one density number

Each cell of the sky holds not a cloud or no cloud but a number for how thick the droplets are. Build that number from layered noise, work out how much light the density removes along the way, and a dark core with a bright thin edge falls out by itself. A cloud animation gif replays frames drawn once, while these scenes take the time and recompute density every frame, so changing the sun height or the wind value gives a different sky.

The source is plain canvas 2D

The result is a procedural cloudscape: all nine scenes use only canvas 2D and requestAnimationFrame, with no library, WebGL, CDN or photo file, and the sky, clouds and ground are all painted by calculation. Pixels are computed on a small grid per scene (02 uses two 120×150 panels, 03 and 08 use 160×100, 05, 06 and 09 use 200×125, and 01, 04 and 07 use 240×150), then putImageData uploads the result and it is stretched to 480×300. The only random source is one seeded generator, so Math.random appears in 0 of 9 and the same code paints the same sky.

Where this differs from the neighboring posts

For terrain built from noise see canvas noise terrain, and for the baseline of canvas scene effects see canvas scene fx. The color bands of the sky live in the sunset gradient post, and the same calculation style piles up snow in the snow effect post. This post splits how one cloud forms, catches light and frays into nine parts.

CSS Cloud Animation: 9 Scenes — Cartoon Cloud Outline with Melted-Together Puffs, Silver Lining on a Cloud Facing Away from the Sun, Puffy Cloud That Flattens at the Condensation Height, Cloud Shadows Sliding over Hills, Offset by Sun Height, Wispy Cloud Stretched by Wind That Varies with Height, Sea of Clouds Seen from a Plane Window, Seamless Layered Cloud Bands, Cloud That Frays from the Edges into Dry Air and Sunrise Hero Scene Above the Clouds

01 starts with how one cloud joins together and 02 with how it catches light. 03 shows where clouds form, while 04 and 05 show what the ground and the wind leave on them. 06 and 07 are backdrop scenes, 08 is the cloud going away, and 09 gathers the earlier calculations in one view.

Area, moving frames and intensity from rendering all nine scenes

Scene Cumulative area Moving frames Mean intensity
01 Puff outline 23.1% 23/23 40.0
02 Silver lining 18.7% 23/23 18.1
03 Puffy cloud 8.3% 23/23 25.1
04 Cloud shadows 17.8% 23/23 38.0
05 Wispy cloud 29.6% 23/23 19.3
06 Sea of clouds 33.0% 23/23 24.3
07 Cloud bands 43.2% 23/23 32.2
08 Fraying cloud 14.2% 13/23 54.9
09 Sunrise hero 49.2% 23/23 18.7

Cumulative area runs from 8.3% for 03, where one cloud body rises in a narrow patch, up to 49.2% for 09. Mean intensity is highest for 08 at 54.9 and lowest for 02 at 18.1. Only 08 stops at 13/23 moving frames, and the other eight scenes moved in 23/23.

01Cartoon Cloud Outline with Melted-Together Puffs

On a landing page's first screen this gives a weather illustration whose 11 circles melt into one outline that breathes. smin blends two distances within SMOOTH_K of 18px, so the midpoint of two circles 8px apart moves from a plain minimum of 4 to −0.5 and gaps up to 9px (SMOOTH_K / 2) fill in. Each puff radius is a spring with PUFF_STIFFNESS 60 and PUFF_DAMPING 5, so one kick swells it by up to +9.97px before it overshoots by −3.39px on the way back, with a damping ratio of 0.323.

Cartoon Cloud Outline with Melted-Together Puffs
Open in a new tab

01 · Cartoon Cloud Outline with Melted-Together Puffs — Measured area 23.124% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SMOOTH_KPUFF_STIFFNESSPUFF_DAMPING
    function smin(a, b, k) {
        const h = Math.max(k - Math.abs(a - b), 0) / k;
        return Math.min(a, b) - h * h * k / 4;
    }
    // Distance (px, negative inside) from screen point (x, y) to the cloud outline: puff distances blended one after another with a smooth minimum
    function cloudDistance(x, y, k) {
        let d = 1e9;
        for (let c = 0; c < 2; c += 1) {
            const cc = cloudCenter(c, sceneT);
            let dc = 1e9;
            for (let i = 0; i < puffs.length; i += 1) {
                const p = puffs[i];
                if (p.cloud !== c) {
                    continue;
                }
                const di = Math.hypot(x - cc[0] - p.ox, y - cc[1] - p.oy) - p.r;
                dc = k > 0 ? smin(dc, di, k) : Math.min(dc, di);
            }
            d = Math.min(d, dc);
        }
        return d;
    }

02Silver Lining on a Cloud Facing Away from the Sun

Behind a product introduction page's dusk background, lightSum walks 6 steps from each cell toward the sun gathering density, and e^(−ABSORB × S) cuts the light so a density sum of 1 leaves only 0.407 of it. In the 2-second capture the mean transmission inside the cloud (density above 0.85) is 0.135. On the right panel, which has a sky-light floor, thin edges read 0.879 in brightness against 0.742 in the core.

Silver Lining on a Cloud Facing Away from the Sun
Open in a new tab

02 · Silver Lining on a Cloud Facing Away from the Sun — Measured area 18.733% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LIGHT_STEPSAMBIENT_LIGHTFORWARD_POWER
    // Density sum S gathered by walking from the cell toward the sun (includes half a step for the cell itself)
    function lightSum(gx, gy, steps) {
        const x = (gx + 0.5) * CELL;
        const y = (gy + 0.5) * CELL;
        const dx = SUN_X - x;
        const dy = SUN_Y - y;
        const len = Math.hypot(dx, dy) || 1;
        const ux = dx / len * LIGHT_STEP_PX / CELL;
        const uy = dy / len * LIGHT_STEP_PX / CELL;
        let s = dens02.v[gy * GRID_W + gx] * 0.5;
        for (let k = 1; k <= steps; k += 1) {
            s += sampleField(dens02, gx + ux * k, gy + uy * k);
        }
        return s * LIGHT_STEP_FRAC;
    }

03Puffy Cloud That Flattens at the Condensation Height

A weather app's afternoon-shower card is where this fits. The 70 air parcels rise on buoyancy, and only those above CONDENSE_H of 112px leave a puff through stampPuff, so the base stays on the 110px row for the full 10 seconds while the width near the top spreads from 105px at 2 seconds to 261px at 10 seconds. Above the condensation height a parcel cools by only MOIST_LAPSE 0.04 and keeps climbing, and once its upward speed drops under SPREAD_SPEED 14 it is pushed away from the center.

Puffy Cloud That Flattens at the Condensation Height
Open in a new tab

03 · Puffy Cloud That Flattens at the Condensation Height — Measured area 8.347% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CONDENSE_HMOIST_LAPSESPREAD_ACCEL
    function buoyancy(p) { return (p.temp - envTemp(p.h)) * BUOYANCY; }
    function stepParcel(p, dt) {
        const a = buoyancy(p) - DRAG * p.vh;
        p.vh += a * dt;
        const dh = p.vh * dt;
        p.temp -= dh > 0 ? coolBy(p.h, dh) : (dh * DRY_LAPSE);
        p.h += dh;
        let ax = Math.sin(sceneT * 1.7 + p.seed) * 4 - DRAG * p.vx;
        if (p.h > CONDENSE_H && p.vh < SPREAD_SPEED) {
            ax += (p.x >= SOURCE_X ? 1 : -1) * SPREAD_ACCEL;
        }
        p.vx += ax * dt;
        p.x += p.vx * dt;
        p.age += dt;
        if (p.h < 0) {
            p.h = 0;
            p.vh = 0;
        }
        if (p.age > 9 || p.x < -20 || p.x > W + 20 || (p.h < 4 && p.vh <= 0 && p.age > 1)) {
            spawn(p, 0);
        }
    }

04Cloud Shadows Sliding over Hills, Offset by Sun Height

A travel page's meadow hero is where this fits. shadowSourceX climbs from a ground point toward the sun up to the cloud layer with x + (CLOUD_H − h) / tan(elevation), so at a 50° sun a 34px spot takes the shadow of a cloud 149.4px to the right and a 126px hill takes the shadow of one 72.2px to the right. sunFacing takes the dot product of the slope normal and the sun direction, which splits mean brightness into 0.882 on sunward slopes and 0.515 on slopes facing away.

Cloud Shadows Sliding over Hills, Offset by Sun Height
Open in a new tab

04 · Cloud Shadows Sliding over Hills, Offset by Sun Height — Measured area 17.765% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SUN_ELEV_DEGCLOUD_HshadowSourceX
    // Spot reached by climbing from ground point (x, h) toward the sun up to the cloud layer height: x + (CLOUD_H − h) / tan(elevation)
    function shadowSourceX(x, h) { return x + (CLOUD_H - h) / Math.tan(toRad(SUN_ELEV_DEG)); }
    // Dot product of the slope normal and the sun direction: 1 faces the sun head-on, 0 or less is a slope facing away
    function sunFacing(x) {
        const s = terrainSlope(x);
        const v = sunVec();
        return (-s * v[0] + v[1]) / Math.hypot(s, 1);
    }

05Wispy Cloud Stretched by Wind That Varies with Height

A meditation app's start screen sky is where this fits. windAt multiplies the height ratio hr by SHEAR, so wind grows from 30px per second at the bottom to 48px per second at the top (1.6 times), and wispPhase shifts the x it reads by that wind while stretching only the horizontal axis 6 times. Two sheets that rewind every 16 seconds are mixed half a cycle apart so the push does not grow without end, and at the middle of the phase (8 seconds) the strands lean 25.6°.

Wispy Cloud Stretched by Wind That Varies with Height
Open in a new tab

05 · Wispy Cloud Stretched by Wind That Varies with Height — Measured area 29.586% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WIND_PXPHASE_SwindAt
    // Wind (px per second) at height ratio hr
    function windAt(hr) { return WIND_PX * (1 + SHEAR * hr); }
    // Density of one phase sheet: shift the x being read back by tp seconds of wind at that height, then stretch only the horizontal axis by STRETCH
    function wispPhase(x, y, tp, salt) {
        const hr = 1 - y / H;
        const u = (x - tp * windAt(hr)) / (NOISE_PX * STRETCH);
        const v = y / NOISE_PX + salt;
        return layeredNoise(u, v, 4, 55);
    }
    // Wisp density at cell (x, y): two phase sheets mixed with triangle weights, fading in over height ratio 0.28 to 0.52 so only the high sky above keeps it
    function wispDensity(x, y, t) {
        const f = t / PHASE_S - Math.floor(t / PHASE_S);
        const g = (f + 0.5) - Math.floor(f + 0.5);
        const wa = 1 - Math.abs(2 * f - 1);
        const n = wispPhase(x, y, f * PHASE_S, 0) * wa + wispPhase(x, y, g * PHASE_S, 17.3) * (1 - wa);
        const hr = 1 - y / H;
        return band(0.52, 0.66, n) * band(0.28, 0.52, hr);
    }

06Sea of Clouds Seen from a Plane Window

An airfare search page's first-screen background is where this fits. planeDistance finds the distance to the cloud plane as CAM_HEIGHT 40 divided by the slope of a pixel below the horizon, so screen y 300 reads a spot 44 away and y 120 reads one 4000 away. Farther rows use fewer noise layers (4 down to 2) and mix in fog by 1 − e^(−z × FOG), so rows near y 120 melt into 0.988 sky color.

Sea of Clouds Seen from a Plane Window
Open in a new tab

06 · Sea of Clouds Seen from a Plane Window — Measured area 33.032% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CAM_HEIGHTFOCAL_PXplaneDistance
    function buildSea(t) {
        const travel = t * FLY_SPEED;
        for (let gy = 0; gy < GRID_H; gy += 1) {
            const y = (gy + 0.5) * CELL;
            const z = planeDistance(y);
            for (let gx = 0; gx < GRID_W; gx += 1) {
                const i = gy * GRID_W + gx;
                seaZ[i] = z;
                if (z < 0) {
                    continue;
                }
                const X = ((gx + 0.5) * CELL - W / 2) / FOCAL_PX * z;
                const oct = octavesFor(z);
                const n = seaHeight(X, z + travel, oct);
                seaN[i] = n;
                seaLit[i] = clamp(0.5 + (n - seaHeight(X, z + travel + LIGHT_PROBE, oct)) * 6, 0, 1);
            }
        }
    }

07Seamless Layered Cloud Bands

A website header's flowing background is where this fits. The four layer outlines come from periodic noise (PERIOD of 8 cells) in bandProfile, so after flowing 8 cells the values return to the start, with depth 1 moving 89.6px per second and depth 4 moving 12.8px per second while the sky-color share rises from 0 to 0.72. A full lap takes 5 seconds for the nearest layer and 20 seconds for the farthest.

Seamless Layered Cloud Bands
Open in a new tab

07 · Seamless Layered Cloud Bands — Measured area 43.224% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LAYER_COUNTSKY_MIXperiodicLayered
    // Distance flowed (grid cells) by the layer at depth, taken modulo PERIOD so the number does not grow without end
    function layerOffset(depth, t) {
        const s = t * layerSpeed(depth);
        return s - Math.floor(s / PERIOD) * PERIOD;
    }
    // Top-line height (0 to 1) of a layer: a sum of periodic layered noise
    function bandProfile(u, depth) { return periodicLayered(u, depth * 3.1, PERIOD, 4, 77); }

08Cloud That Frays from the Edges into Dry Air

A blog post header showing a change of season is where this fits. stepGrid spreads density to neighbors and cuts evaporation harder for outer cells with many empty neighbors, so during one dry second (0.5 to 1.5 seconds) outer cells lose 0.334 on average against 0.156 inside, which is 2.1 times faster. When humidity rises from 0.12 to 0.92 between 4.6 and 5.4 seconds, cells with a core weight above 0.9 refill first, averaging 0.591 at 5.6 seconds against 0.093 for cells weighted 0.2 to 0.5.

Cloud That Frays from the Edges into Dry Air
Open in a new tab

08 · Cloud That Frays from the Edges into Dry Air — Measured area 14.21% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

EVAP_RATEEMPTY_LEVELSPIKE_CAP_S
                const avg = (l + r + u + dn) / 4;
                let empty = 0;
                for (let oy = -1; oy <= 1; oy += 1) {
                    for (let ox = -1; ox <= 1; ox += 1) {
                        if (ox === 0 && oy === 0) {
                            continue;
                        }
                        const x = gx + ox;
                        const y = gy + oy;
                        const nb = (x < 0 || y < 0 || x >= GRID_W || y >= GRID_H) ? 0 : v[y * GRID_W + x];
                        if (nb < EMPTY_LEVEL) {
                            empty += 1;
                        }
                    }
                }
                let nv = v[i] + DIFFUSE * dt * (avg - v[i]);
                nv -= EVAP_RATE * (1 - hum) * (EDGE_BASE + empty / 8) * dryMix(gx, gy) * dt;
                nv += FILL_RATE * hum * core08[i] * (1 - v[i]) * dt;
                next08[i] = (nv < CUT_LEVEL && nv < v[i]) ? 0 : clamp(nv, 0, 2);
            }
        }
        v.set(next08);
    }

09Sunrise Hero Scene Above the Clouds

A portfolio first screen is where this fits: on the plane of 06, sunBlock collects the cloud height that rises above the light ray over SUN_STEPS 5 steps toward the sun, and sunTransmit turns it into e^(−ABSORB_09 × S / 22). With the sun at 0° the top 20% of cloud tops transmit 0.841 against 0.339 in the bottom 20% of valleys, so light reaches the tops first. At 12° the two values are 0.996 and 0.967, nearly equal.

Sunrise Hero Scene Above the Clouds
Open in a new tab

09 · Sunrise Hero Scene Above the Clouds — Measured area 49.187% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SUN_STEPSAMBIENT_09sunTransmit
    // Blocking S gathered by walking from top point (X, Z, h) toward the sun (+Z, rising by tan of the elevation): only the cloud that stands above the ray is added
    function sunBlock(X, Z, h, elevDeg) {
        const rise = Math.tan(toRad(elevDeg)) * SUN_STEP;
        let s = 0;
        for (let k = 1; k <= SUN_STEPS; k += 1) {
            const ray = h + rise * k;
            const top = topHeight(X, Z + SUN_STEP * k, 2);
            if (top > ray) {
                s += top - ray;
            }
        }
        return s;
    }
    function sunTransmit(X, Z, h, elevDeg) { return Math.exp(-ABSORB_09 * sunBlock(X, Z, h, elevDeg) / TOP_HEIGHT); }

When Drawing Cloud Shadows, What Do You Add to the Cloud Position So the Shadow Lands Right on the Ground?

A shadow placed directly below its cloud lands in the wrong spot. Scene 04 says the shadow at ground point (x, h) comes from the cloud at x + (CLOUD_H − h) / tan(elevation), so the cloud x has to be offset by a distance that the sun height decides.

Higher hills sit closer to the shadow's cloud

At the same 50° sun, a 34px ground point gets a shadow 149.4px from its cloud, an 80px point gets 110.8px and a 126px point gets 72.2px. The closer a hilltop is to the cloud layer, the shorter the height it has to climb toward the sun.

Measure the sun angle from the ground

SUN_ELEV_DEG is the angle measured from the ground. A smaller angle makes tan smaller, and since the offset is a division its distance grows, so reading it as an angle measured from overhead would throw the offset distance off.

Laying it over a background

For a cloud background, compute on a grid smaller than the screen and stretch it as 04 does, which keeps the pixel count down. For a travel page's meadow hero, match the hill height h(x) to the terrain art of the page and keep the status line top left and the pause button top right.

Make Density with Noise, Walk Toward the Sun to Trim the Light, Push It with Wind: The Order in Which One Noise Sheet Becomes a Cloud

Four steps turn noise into a cloud: make the density, walk toward the sun to cut the light, push it with wind, then compute on a small grid and stretch it.

Step What it does Names used Scenes
1 Make density Sum layered noise, doubling frequency and halving strength per layer, and fit it to 0 to 1 layeredNoise 02 to 09
2 Trim light Walk toward the sun summing density and cut it with an exponential lightSum, sunBlock 02, 09
3 Push with wind Shift the coordinate being read by the distance flowed so the cloud moves windAt, layerOffset 05, 07
4 Stretch Build pixels on a small grid and enlarge them to 480×300 makePix, putPx, blit 01 to 09

Why layered noise gives a cloud its grain

One layer of noise makes only smooth hills. Stack layers with twice the frequency and half the strength, divide by the strength sum, and fine grain sits on large lumps while values stay between 0 and 1.

Why light is summed and cut by an exponential

With transmission e^(−ABSORB × S), light that passes density 0.5 twice equals light that passes density 1 once. Because only a sum over a set number of steps is needed, six steps toward the sun per cell are enough to leave dark cores and bright thin edges.

Why compute on a small grid and stretch

Pixel work scales with the cell count. Computing on small grids (the largest is 240×150) and stretching with drawImage lets the enlargement make the soft cloud edge.

Where Clouds Break: a Loop That Snaps at the Seam, Light That Dies to Black on the Side Facing Away from the Sun and Clouds That Jump After a Hidden Tab

A loop that snaps at the seam

The two test strips on top of 07 feed the same u to two different noises. Even after taking the travelled distance modulo PERIOD, plain noise reads a different lattice value where u wraps from 8 to 0, so the gap between u 7.999 and 0 over 64 height rows is 0.175 on average and 0.553 at most, while periodic noise gives 0 and 0. On the 17px test strip the plain one jumps 5px at the seam and the periodic one jumps 0px, and seamJump is a test and measurement function that no scene on screen calls.

    // Failing copy: reads the same spot with non-periodic layered noise (feeds in u taken modulo PERIOD as is)
    function bandProfileNaive(u, depth) {
        const m = u - Math.floor(u / PERIOD) * PERIOD;
        return layeredNoise(m, depth * 3.1, 4, 77);
    }
    // Seam value gap: the difference between values read just before PERIOD (PERIOD − 0.001) and at 0
    function seamJump(read, depth) {
        return Math.abs(read(PERIOD - 0.001, depth) - read(0, depth));
    }

Light that dies to black on the side facing away from the sun

The left panel of 02 is painted by transmission alone, so inside a cloud facing away from the sun the light falls exponentially to the floor. The right panel passes AMBIENT_LIGHT 0.6 to paintPanel02 as a sky-light floor, and at the same density the darkest cloud cell goes from 0.029 to 0.629 while cells under 0.15 brightness fall from 80.4% to 0%. 09 uses the same fix as AMBIENT_09, where the brightness share of a sunless valley is 0.88 with the floor and 0.2 without.

                const dv = dens02.v[i];
                if (dv > 0) {
                    const L = trans02[i] * (BASE_LIGHT + forwardPhase(x, y));
                    const cl = [
                        SUN_TINT[0] * L * 1.15 + AMBIENT_TINT[0] * ambient,
                        SUN_TINT[1] * L * 1.15 + AMBIENT_TINT[1] * ambient,
                        SUN_TINT[2] * L * 1.15 + AMBIENT_TINT[2] * ambient,
                    ];
                    c = mix(c, cl, 1 - Math.exp(-dv * OPACITY));
                }

Clouds that jump after a hidden tab

Scene 08 stages a 1.2-second frame at 2 seconds into the demo on purpose. The fixed copy cuts it to SPIKE_CAP_S 0.05 seconds, a single step with a diffusion share of 0.04, while the failing copy spends 1.2 seconds in one step with a share of 0.96, so cells whose density changed by more than 0.2 are 0% against 6.82% of the grid. The failing copy then has more empty cells and evaporates faster, so cloud cover drifts apart to 22.4 against 17.4% at 4 seconds and 18.2 against 12.1% at 5 seconds before meeting again at 26.7 against 26.6% from 6 seconds on.

    function update(dt) {
        const hum = humidityAt(sceneT);
        mixT08 = sceneT;
        if (lastT08 < SPIKE_AT && sceneT >= SPIKE_AT) {
            stepGrid(fix08, Math.min(HIDDEN_FRAME_S, SPIKE_CAP_S), hum);
            stepGrid(fail08, HIDDEN_FRAME_S, hum);
        }
        lastT08 = sceneT;
        stepGrid(fix08, dt, hum);
        stepGrid(fail08, dt, hum);
    }

In a real hidden tab the frame loop stops, and on return lastTs is reset to −1 and the first step is cut once more by DT_MAX 0.05 seconds. SPIKE_CAP_S and DT_MAX share the value 0.05 but are separate constants. Unlock the archive with the password n97f273v and open the index.html in folders 07, 02 and 08, pausing with the button to see where the broken and fixed copies split apart.

Raise the Absorption and How Much Darker Does the Base Get? Absorption, Sun Height and Wind Difference by Height Compared

Name Changed value Feel
Higher absorption 02 ABSORB 0.9 → 1.6 Light through a density sum of 1 drops from 41% to 20%, so mean transmission inside the cloud falls from 0.14 to 0.04, near-black cells on the no-ambient copy grow from 80% to 91% of the cloud, and thin-edge brightness drops from 0.88 to 0.79, leaving a thinner silver lining
A lower sun 04 SUN_ELEV_DEG 50 → 20 The tangent of the elevation drops from 1.19 to 0.36, so shadows land 236 to 489px from their clouds instead of 72 to 149px, brightness on sunward and shaded slopes moves from 0.88 and 0.52 to 0.53 and 0.12, and ground that gets no sun grows from 0.2% to 15%
Stronger wind change with height 05 SHEAR 0.6 → 1.4 Wind at the top goes from 1.6 to 2.4 times the bottom (48 to 72px per second), so at mid phase (8 seconds) the strands lean from 25.6° to 48.2° and the gap between the strands at height ratio 0.3 and at the top widens from 100.8 to 235.2px

Numbers to check before changing a value

ABSORB in 02 sits inside an exponent, so raising it to 1.6 makes the transmission inside the cloud fall by multiplication. SUN_ELEV_DEG in 04 is an angle from the ground, so lowering it to 20 shrinks tan and grows the result of the division. SHEAR in 05 multiplies the height ratio, so at 1.4 the wind at the top is 2.4 times the bottom.

One Still Sky Under Reduced Motion: Accessibility Through a Sentence Describing the Sky and a Pause Button

A status sentence and a pause button

The canvas of each of the nine scenes has role="img" and an aria-label describing the scene, and the status line announces the current sky as a sentence of the form cloud amount 40%, sun height 50°, wind to the right. The pause button flips aria-pressed and stops the loop when pressed with Space or Enter, and its focus shows as a 3px outline. During the demo the status line is aria-live off and switches to polite after the first input.

Reduced motion keeps one still sky

With prefers-reduced-motion: reduce all nine scenes stop the loop and draw one representative moment, which is 1.2 seconds for 01, 2 for 02, 8 for 03, 3 for 04, 8 for 05, 2 for 06, 3 for 07, 4 for 08 and 8 for 09. No drifting, billowing or fraying remains, and turning the setting off starts the loop again, as the prefers-reduced-motion reference describes.

CSS Cloud Animation FAQ: CSS-Only Clouds, Load on Phones and Using It as a Background

Can a cloud animation website get the same scenes with css alone

Stacked gradients give a still cloud with css alone, but these scenes compute density and light per cell, so canvas and js fit. You can change constants such as ABSORB, SUN_ELEV_DEG and SHEAR to get a different sky without knowing the theory.

Does it run on a phone

The canvas stays at 480×300 internally, devicePixelRatio is capped at 2, and the pixel work runs on grids of at most 240×150. If it still feels heavy, cut the cell count first.

Can I use it as a page background

06, 07 and 09 suit header and hero backgrounds, and the loop stops while the tab is hidden. Where text is read, put one semi-transparent panel over the clouds to keep the contrast.

Enter the archive password

The password is inside this article. You will find it as you read.