cloud-engine
Version:
CloudEngine: layered cloud SVG engine with React component CloudMaker.
488 lines (482 loc) • 19.4 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// cloud_maker.js
var cloud_maker_exports = {};
__export(cloud_maker_exports, {
createCloudEngine: () => createCloudEngine
});
function mulberry32(a) {
return function() {
let t = a += 1831565813;
t = Math.imul(t ^ t >>> 15, 1 | t);
t ^= t + Math.imul(t ^ t >>> 7, 61 | t);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
function createCloudEngine(opts = {}) {
const o = {
width: 1200,
height: 380,
layers: 7,
segments: 450,
baseAmplitude: 16,
baseFrequency: 0.03,
baseRandom: 6,
layerAmplitudeStep: 2.6,
layerFrequencyStep: 4e-3,
layerRandomStep: 1.2,
layerVerticalSpacing: 16,
secondaryWaveFactor: 0.45,
baseColor: "#ffffff",
layerColors: [],
blur: 2.2,
seed: 1337,
// New physicality controls
waveForm: "sincos",
// 'sin' | 'cos' | 'sincos'
noiseSmoothness: 0.45,
// 0..1 moving-average smoothing on noise
amplitudeJitter: 0,
// 0..1 multiplicative jitter on amplitude
amplitudeJitterScale: 0.25,
// 0..1 fraction of segments for jitter correlation length
curveType: "spline",
// 'linear' | 'spline'
curveTension: 0.85,
// 0..1, higher means smoother curves
peakStability: 1,
// 0..1, 1 = fully co-moving noise/jitter (no peak jiggle)
peakNoiseDamping: 1,
// 0..1, reduce noise & amp jitter near wave peaks
peakNoisePower: 4,
// >=1, shaping power for damping falloff
peakHarmonicDamping: 1,
// 0..1, reduce secondary harmonic near peaks
useSharedBaseline: true,
// if true, all layers close to the same baseline to avoid checkerboard overlaps
// Creation-time variation only
morphStrength: 0,
// 0..1, set 0 to freeze shape post creation
morphPeriodSec: 18,
// seconds for a full morph loop
amplitudeEnvelopeStrength: 0.3,
// 0..1 envelope modulation at creation
amplitudeEnvelopeCycles: 4,
// number of envelope cycles across width
peakRoundness: 0.3,
// 0..1 extra local smoothing at crests/troughs
peakRoundnessPower: 2,
// >=1 tightness of peak-local smoothing
amplitudeLayerCycleVariance: 0.2,
// 0..1 per-layer amplitude scale variance per cycle
...opts
};
const center = o.width / 2;
const rand = mulberry32(o.seed >>> 0 || 1);
const phases = Array.from({ length: o.layers }, () => rand() * Math.PI * 2);
const smoothArray = (arr, window2) => {
if (window2 <= 1) return arr.slice();
const half = Math.floor(window2 / 2);
const out = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
let sum = 0, count = 0;
for (let j = -half; j <= half; j++) {
const k = Math.min(arr.length - 1, Math.max(0, i + j));
sum += arr[k];
count++;
}
out[i] = sum / count;
}
return out;
};
const noiseWindow = Math.max(1, Math.round(o.noiseSmoothness * o.segments * 0.15));
const ampWindow = Math.max(1, Math.round(Math.max(0.01, o.amplitudeJitterScale) * o.segments));
const hash32 = (n) => {
let x = n | 0;
x ^= x << 13;
x ^= x >>> 17;
x ^= x << 5;
return x >>> 0;
};
const buildFields = (seedA, seedB) => {
const rA = mulberry32(seedA >>> 0);
const rB = mulberry32(seedB >>> 0);
const envelopePhases = Array.from({ length: o.layers }, () => rA() * Math.PI * 2);
const ampLayerScale = Array.from({ length: o.layers }, () => {
const v = (rA() - 0.5) * 2;
const s = 1 + (o.amplitudeLayerCycleVariance || 0) * v;
return Math.max(0.3, s);
});
const rawNoisesA = Array.from({ length: o.layers }, () => Array.from({ length: o.segments + 1 }, () => rA() - 0.5));
const rawNoisesB = Array.from({ length: o.layers }, () => Array.from({ length: o.segments + 1 }, () => rB() - 0.5));
const noisesA = rawNoisesA.map((layerNoise) => smoothArray(layerNoise, noiseWindow));
const noisesB = rawNoisesB.map((layerNoise) => smoothArray(layerNoise, noiseWindow));
const rawAmpModsA = Array.from({ length: o.layers }, () => Array.from({ length: o.segments + 1 }, () => rA() * 2 - 1));
const rawAmpModsB = Array.from({ length: o.layers }, () => Array.from({ length: o.segments + 1 }, () => rB() * 2 - 1));
const ampModsA = rawAmpModsA.map((arr) => smoothArray(arr, ampWindow));
const ampModsB = rawAmpModsB.map((arr) => smoothArray(arr, ampWindow));
const normalize = (arrs) => {
for (let i = 0; i < arrs.length; i++) {
const a = arrs[i];
let maxAbs = 0;
for (let v of a) maxAbs = Math.max(maxAbs, Math.abs(v));
const s = maxAbs > 0 ? 1 / maxAbs : 1;
for (let k = 0; k < a.length; k++) a[k] = a[k] * s;
}
};
normalize(ampModsA);
normalize(ampModsB);
return { noisesA, noisesB, ampModsA, ampModsB, envelopePhases, ampLayerScale };
};
let cachedCycle = -1;
let cachedFields = buildFields(o.seed >>> 0 || 1, (o.seed >>> 0 || 1) ^ 2654435769);
const ensureFields = (cycleIndex) => {
if (cycleIndex === cachedCycle) return;
const base = o.seed >>> 0 || 1;
const seedA = base ^ hash32(cycleIndex + 1013904223);
const seedB = base ^ hash32(cycleIndex + 1 + 1013904223);
cachedFields = buildFields(seedA, seedB);
cachedCycle = cycleIndex;
};
const colorAt = (i) => o.layerColors && o.layerColors.length ? o.layerColors[Math.min(i, o.layerColors.length - 1)] : mixToWhite(o.baseColor, Math.min(0.08 * i, 0.6));
const wrapIndex = (idx, len) => {
let i = idx % len;
if (i < 0) i += len;
return i;
};
const sampleWrapped = (arr, x) => {
const len = arr.length;
const i0 = Math.floor(x);
const i1 = i0 + 1;
const t = x - i0;
const v0 = arr[wrapIndex(i0, len)];
const v1 = arr[wrapIndex(i1, len)];
return v0 * (1 - t) + v1 * t;
};
const sampleMorph = (arrA, arrB, x, m) => {
return (1 - m) * sampleWrapped(arrA, x) + m * sampleWrapped(arrB, x);
};
const params = (i) => ({
// Top-line baseline controls the vertical placement of the wave crest for this layer
baseLine: o.height - i * o.layerVerticalSpacing,
amp: o.baseAmplitude + i * o.layerAmplitudeStep,
freq: o.baseFrequency + i * o.layerFrequencyStep,
rndF: o.baseRandom + i * o.layerRandomStep,
phi: phases[i],
// Use one of the precomputed noise fields as the static per-layer field
noise: cachedFields.noisesA[i]
});
const pathFor = (i, phase, morphT = 0) => {
const p = params(i);
const fillBase = o.useSharedBaseline ? o.height : p.baseLine;
const pts = [];
for (let k = 0; k <= o.segments; k++) {
const x = k / o.segments * o.width;
const dist = Math.abs(x - center);
const baseArg = p.freq * (dist - phase) + p.phi;
let wave = 0;
if (o.waveForm === "sin") {
wave = Math.sin(baseArg);
} else if (o.waveForm === "cos") {
wave = Math.cos(baseArg);
} else {
const base = Math.sin(baseArg);
const harmonic = Math.sin(2 * baseArg + p.phi * 0.3);
const peakProximity2 = Math.pow(Math.abs(Math.cos(baseArg)), Math.max(1, o.peakNoisePower));
const harmonicScale = 1 - o.peakHarmonicDamping * (1 - peakProximity2);
wave = base + o.secondaryWaveFactor * harmonicScale * harmonic;
}
const peakProximity = Math.pow(Math.abs(Math.cos(baseArg)), Math.max(1, o.peakNoisePower));
const noiseScale = 1 - o.peakNoiseDamping * (1 - peakProximity);
const radialIdx = (dist - phase) * (o.segments / Math.max(1, o.width));
const m = Math.max(0, Math.min(1, o.morphStrength)) * (0.5 - 0.5 * Math.cos(2 * Math.PI * morphT));
const stableNoise = sampleMorph(cachedFields.noisesA[i], cachedFields.noisesB[i], radialIdx, m);
const stableAmpMod = sampleMorph(cachedFields.ampModsA[i], cachedFields.ampModsB[i], radialIdx, m);
const mixedNoise = ((1 - o.peakStability) * (p.noise[k] || 0) + o.peakStability * stableNoise) * noiseScale;
const mixedAmpMod = ((1 - o.peakStability) * (cachedFields.ampModsA[i][k] || 0) + o.peakStability * stableAmpMod) * noiseScale;
const envStrength = o.amplitudeEnvelopeStrength * (0.9 + 0.2 * (i * 16807 % 11) / 10);
const env2 = 1 + envStrength * Math.sin(
2 * Math.PI * o.amplitudeEnvelopeCycles * (dist - phase) / Math.max(1, o.width) + (cachedFields.envelopePhases[i] + i * 0.37)
);
const baseAmp = p.amp * (cachedFields.ampLayerScale[i] || 1) * env2;
const ampMult = 1 + o.amplitudeJitter * mixedAmpMod;
const w = baseAmp * ampMult * wave + mixedNoise * p.rndF;
pts.push({ x, y: p.baseLine - w });
}
if (o.curveType === "linear") {
let d2 = `M 0 ${rnd(fillBase)}`;
d2 += ` L ${rnd(pts[0].x)} ${rnd(pts[0].y)}`;
for (let idx = 1; idx < pts.length; idx++) {
const pt = pts[idx];
d2 += ` L ${rnd(pt.x)} ${rnd(pt.y)}`;
}
return d2 + ` L ${o.width} ${rnd(fillBase)} Z`;
}
if (o.peakRoundness > 0) {
const power = Math.max(1, o.peakRoundnessPower || 1);
const rounded = new Array(pts.length);
for (let k = 0; k < pts.length; k++) {
const x = pts[k].x;
const dist = Math.abs(x - center);
const baseArg = p.freq * (dist - phase) + p.phi;
const peakWeight = Math.pow(1 - Math.abs(Math.cos(baseArg)), power);
const w = Math.max(0, Math.min(1, o.peakRoundness * peakWeight));
const yPrev = pts[Math.max(0, k - 1)].y;
const yCurr = pts[k].y;
const yNext = pts[Math.min(pts.length - 1, k + 1)].y;
const neighborhoodAvg = (yPrev + yCurr + yNext) / 3;
const yRounded = yCurr * (1 - w) + neighborhoodAvg * w;
rounded[k] = { x, y: yRounded };
}
for (let k = 0; k < pts.length; k++) pts[k] = rounded[k];
}
const t = Math.max(0, Math.min(1, o.curveTension));
let d = `M 0 ${rnd(fillBase)} L ${rnd(pts[0].x)} ${rnd(pts[0].y)}`;
for (let s = 0; s < pts.length - 1; s++) {
const p0 = pts[s - 1] || pts[s];
const p1 = pts[s];
const p2 = pts[s + 1];
const p3 = pts[s + 2] || p2;
const cp1x = p1.x + (p2.x - p0.x) / 6 * t;
const cp1y = p1.y + (p2.y - p0.y) / 6 * t;
const cp2x = p2.x - (p3.x - p1.x) / 6 * t;
const cp2y = p2.y - (p3.y - p1.y) / 6 * t;
d += ` C ${rnd(cp1x)} ${rnd(cp1y)} ${rnd(cp2x)} ${rnd(cp2y)} ${rnd(p2.x)} ${rnd(p2.y)}`;
}
return d + ` L ${o.width} ${rnd(fillBase)} Z`;
};
const pathsAt = (phase = 0, morphT = 0, cycleIndex = 0) => {
ensureFields(Math.max(0, Math.floor(cycleIndex)));
return Array.from({ length: o.layers }, (_, i) => ({
d: pathFor(i, phase, morphT),
fill: colorAt(i),
opacity: +(1 - i * 0.12).toFixed(2)
}));
};
const svgAt = (phase = 0) => {
const paths = pathsAt(phase).map((p) => `<path d="${p.d}" fill="${p.fill}" fill-opacity="${p.opacity}"/>`).join("\n ");
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${o.width} ${o.height}">
<defs>
<filter id="cloud-blur" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="${o.blur}"/>
</filter>
</defs>
<g filter="url(#cloud-blur)">
${paths}
</g>
</svg>`;
};
return { pathsAt, svgAt, width: o.width, height: o.height, blur: o.blur, config: o };
}
var rnd, mixToWhite;
var init_cloud_maker = __esm({
"cloud_maker.js"() {
"use strict";
rnd = (n) => n;
mixToWhite = (hex, t) => {
let h = hex.replace("#", "");
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
const n = parseInt(h, 16), r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
const m = (v) => Math.round(v + (255 - v) * t).toString(16).padStart(2, "0");
return `#${m(r)}${m(g)}${m(b)}`;
};
}
});
// CloudBackdrop.tsx
init_cloud_maker();
import { useEffect, useMemo, useRef } from "react";
// cloudDefaults.json
var cloudDefaults_default = {
width: 800,
height: 458,
layers: 6,
segments: 450,
baseColor: "#ffffff",
speed: 34,
seed: 1337,
blur: 0,
waveForm: "sincos",
noiseSmoothness: 0.45,
amplitudeJitter: 0,
amplitudeJitterScale: 0.25,
additiveBlending: false,
curveType: "spline",
curveTension: 0.85,
peakStability: 1,
peakNoiseDamping: 1,
peakNoisePower: 4,
peakHarmonicDamping: 1,
useSharedBaseline: true,
morphStrength: 0,
morphPeriodSec: 18,
amplitudeEnvelopeStrength: 0.36,
amplitudeEnvelopeCycles: 2,
peakRoundness: 0.8,
peakRoundnessPower: 10,
staticPeaks: true,
sunsetMode: true,
sunsetPeriodSec: 12,
paletteIndex: 4,
hueShift: 0,
saturation: 1,
lightness: 0.02,
contrast: 0.04,
altHueDelta: -30,
altSatScale: 1.41
};
// CloudBackdrop.tsx
import { jsx, jsxs } from "react/jsx-runtime";
var CloudMaker = ({
width = cloudDefaults_default.width,
height = cloudDefaults_default.height,
layers = cloudDefaults_default.layers,
segments = cloudDefaults_default.segments,
baseColor = cloudDefaults_default.baseColor,
layerColors = [],
layerOpacities,
speed = cloudDefaults_default.speed,
seed = cloudDefaults_default.seed,
blur = cloudDefaults_default.blur,
waveForm = cloudDefaults_default.waveForm,
noiseSmoothness = cloudDefaults_default.noiseSmoothness,
amplitudeJitter = cloudDefaults_default.amplitudeJitter,
amplitudeJitterScale = cloudDefaults_default.amplitudeJitterScale,
additiveBlending = cloudDefaults_default.additiveBlending,
curveType = cloudDefaults_default.curveType,
curveTension = cloudDefaults_default.curveTension,
peakStability = cloudDefaults_default.peakStability,
peakNoiseDamping = cloudDefaults_default.peakNoiseDamping,
peakNoisePower = cloudDefaults_default.peakNoisePower,
peakHarmonicDamping = cloudDefaults_default.peakHarmonicDamping,
useSharedBaseline = cloudDefaults_default.useSharedBaseline,
morphStrength = cloudDefaults_default.morphStrength,
morphPeriodSec = cloudDefaults_default.morphPeriodSec,
amplitudeEnvelopeStrength = cloudDefaults_default.amplitudeEnvelopeStrength,
amplitudeEnvelopeCycles = cloudDefaults_default.amplitudeEnvelopeCycles,
peakRoundness = cloudDefaults_default.peakRoundness,
peakRoundnessPower = cloudDefaults_default.peakRoundnessPower,
seamlessLoop = true,
animate = true,
phase = 0,
morphT = 0,
cycleIndex = 0,
className,
style,
fit = "stretch",
background = false
}) => {
const engine = useMemo(
() => createCloudEngine({ width, height, layers, segments, baseColor, layerColors, layerOpacities, seed, blur, waveForm, noiseSmoothness, amplitudeJitter, amplitudeJitterScale, curveType, curveTension, peakStability, peakNoiseDamping, peakNoisePower, peakHarmonicDamping, useSharedBaseline, morphStrength, morphPeriodSec, amplitudeEnvelopeStrength, amplitudeEnvelopeCycles, peakRoundness, peakRoundnessPower }),
[width, height, layers, segments, baseColor, layerColors, layerOpacities, seed, blur, waveForm, noiseSmoothness, amplitudeJitter, amplitudeJitterScale, curveType, curveTension, peakStability, peakNoiseDamping, peakNoisePower, peakHarmonicDamping, useSharedBaseline, morphStrength, morphPeriodSec, amplitudeEnvelopeStrength, amplitudeEnvelopeCycles, peakRoundness, peakRoundnessPower]
);
const initial = useMemo(() => engine.pathsAt(phase, morphT, cycleIndex), [engine, phase, morphT, cycleIndex]);
const refs = useRef([]);
useEffect(() => {
if (!animate) return;
if (typeof window === "undefined") return;
let raf = 0;
const t0 = performance.now();
const loop = (t) => {
const elapsedSec = (t - t0) / 1e3;
const phase2 = speed * elapsedSec;
const period = Math.max(1e-4, engine.config.morphPeriodSec);
const morphT2 = elapsedSec / period % 1;
const cycleIndex2 = seamlessLoop ? 0 : Math.floor(elapsedSec / period);
engine.pathsAt(phase2, morphT2, cycleIndex2).forEach((p, i) => refs.current[i]?.setAttribute("d", p.d));
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [engine, speed, animate, seamlessLoop]);
const preserve = fit === "stretch" ? "none" : fit === "slice" ? "xMidYMid slice" : "xMidYMid meet";
return /* @__PURE__ */ jsxs(
"svg",
{
viewBox: `0 0 ${engine.width} ${engine.height}`,
preserveAspectRatio: preserve,
width: "100%",
height: "100%",
className,
style,
suppressHydrationWarning: true,
"aria-hidden": true,
children: [
typeof background === "string" && /* @__PURE__ */ jsx("rect", { x: 0, y: 0, width: "100%", height: "100%", fill: background }),
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("filter", { id: "cloud-blur", x: "-20%", y: "-20%", width: "140%", height: "140%", children: /* @__PURE__ */ jsx("feGaussianBlur", { stdDeviation: engine.blur }) }) }),
/* @__PURE__ */ jsx("g", { filter: "url(#cloud-blur)", style: { mixBlendMode: additiveBlending ? "screen" : "normal" }, children: initial.map((p, i) => /* @__PURE__ */ jsx(
"path",
{
ref: (el) => {
if (el) refs.current[i] = el;
},
d: p.d,
fill: p.fill,
fillOpacity: layerOpacities?.[i] ?? (additiveBlending ? Math.max(0.06, (i + 1) / (engine.config.layers || initial.length) * 0.7) : p.opacity)
},
i
)) })
]
}
);
};
var CloudBackdrop_default = CloudMaker;
// index.ts
init_cloud_maker();
function renderSvg(config = {}, opts = {}) {
const { createCloudEngine: createCloudEngine2 } = (init_cloud_maker(), __toCommonJS(cloud_maker_exports));
const engine = createCloudEngine2(config);
return engine.svgAt(opts.phase ?? 0, opts.morphT ?? 0, opts.cycleIndex ?? 0);
}
var presets = {
default: {
width: 1200,
height: 380,
layers: 7,
segments: 450,
baseColor: "#ffffff",
seed: 1337,
blur: 2.2,
waveForm: "sincos",
noiseSmoothness: 0.45,
amplitudeJitter: 0,
amplitudeJitterScale: 0.25,
curveType: "spline",
curveTension: 0.85,
peakStability: 1,
peakNoiseDamping: 1,
peakNoisePower: 4,
peakHarmonicDamping: 1,
useSharedBaseline: true,
morphStrength: 0,
morphPeriodSec: 18,
amplitudeEnvelopeStrength: 0.7,
amplitudeEnvelopeCycles: 10,
peakRoundness: 0.8,
peakRoundnessPower: 10
}
};
export {
CloudBackdrop_default as CloudMaker,
cloudDefaults_default as cloudDefaults,
createCloudEngine,
presets,
renderSvg
};