Skip to contentCREATIVE·CODE·ART
Canvas JS

Chaos Bloom

Chaos Bloom renders a Clifford strange attractor in real time. Thousands of particles iterate a simple chaotic equation hundreds of thousands of times per frame, and their trails pile up into silky, luminous structures. Color comes from how fast each particle moves, and the equation slowly morphs so the form is always being born again. Move the mouse to nudge the equation, or use the sliders to change shape, morph, density, persistence, and hue.

Licence: CC BY-NC-SA 4.0: remix with credit, non-commercial, share alike

View the code
// ---------- اسلایدرها: [نام، برچسب، min، max، step، پیش‌فرض] ----------
const CFG = [
  ["speed",    "Speed",       0,    3,    0.05, 1],
  ["shape",    "Shape",       1,    4,    1,    2],
  ["morph",    "Morph",       0,    1.5,  0.05, 1],
  ["density",  "Density",     10,   120,  5,    50],
  ["fade",     "Persistence", 0.5,  0.98, 0.01, 0.9],
  ["exposure", "Exposure",    0.4,  3,    0.05, 1.4],
  ["hue",      "Hue shift",   0,    1,    0.01, 0.05],
  ["zoom",     "Zoom",        0.5,  2,    0.05, 1],
  ["rotate",   "Rotate",      0,    2,    0.05, 0.3],
];

const V = {};
CFG.forEach((c) => (V[c[0]] = c[5]));
const PR = typeof params !== "undefined" && params ? params : {};
const siteSliders = CFG.some((c) => typeof PR[c[0]] === "number");
const read = () => {
  const o = {};
  CFG.forEach((c) => (o[c[0]] = typeof PR[c[0]] === "number" ? PR[c[0]] : V[c[0]]));
  return o;
};

// ---------- بوم ----------
const cv =
  document.querySelector("canvas") ||
  (() => {
    const c = document.createElement("canvas");
    document.body.appendChild(c);
    return c;
  })();
document.body.style.margin = "0";
document.body.style.background = "#03040a";
document.body.style.overflow = "hidden";
cv.style.display = "block";
const ctx = cv.getContext("2d");

// ---------- پنل داخلی (فقط اگر سایت اسلایدر نساخته باشد) ----------
if (!siteSliders) {
  const panel = document.createElement("div");
  panel.style.cssText =
    "position:fixed;top:10px;right:10px;width:220px;max-height:calc(100vh - 20px);overflow:auto;" +
    "background:rgba(8,10,20,.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);" +
    "border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:10px 12px;" +
    "font:11px system-ui,sans-serif;color:#cfd6ff;z-index:10;user-select:none";
  const inputs = {};
  CFG.forEach(([key, label, min, max, step, def]) => {
    const row = document.createElement("div");
    row.style.margin = "7px 0";
    const top = document.createElement("div");
    top.style.cssText = "display:flex;justify-content:space-between;margin-bottom:2px";
    const name = document.createElement("span");
    name.textContent = label;
    const val = document.createElement("span");
    val.style.color = "#ffb454";
    val.textContent = def;
    top.append(name, val);
    const inp = document.createElement("input");
    inp.type = "range";
    inp.min = min; inp.max = max; inp.step = step; inp.value = def;
    inp.style.cssText = "width:100%;accent-color:#ffb454;margin:0";
    inp.addEventListener("input", () => {
      V[key] = parseFloat(inp.value);
      val.textContent = inp.value;
    });
    inputs[key] = { inp, val, def };
    row.append(top, inp);
    panel.appendChild(row);
  });
  const reset = document.createElement("button");
  reset.textContent = "Reset";
  reset.style.cssText =
    "width:100%;margin-top:6px;padding:6px;border-radius:8px;" +
    "border:1px solid rgba(255,255,255,.2);background:transparent;color:#cfd6ff;cursor:pointer";
  reset.onclick = () => {
    for (const k in inputs) {
      const { inp, val, def } = inputs[k];
      inp.value = def; val.textContent = def; V[k] = def;
    }
  };
  panel.appendChild(reset);
  document.body.appendChild(panel);
}

// ---------- جاذب Clifford: چهار پیکربندی زیبا [a, b, c, d] ----------
const PRESETS = [
  [-1.4, 1.6, 1.0, 0.7],
  [-1.7, 1.3, -0.1, -1.21],
  [1.5, -1.8, 1.6, 0.9],
  [-1.8, -2.0, -0.5, -0.9],
];

// ---------- بافرها ----------
const N = 3000; // تعداد ذرات
const px = new Float32Array(N);
const py = new Float32Array(N);
for (let i = 0; i < N; i++) {
  px[i] = Math.random() * 2 - 1;
  py[i] = Math.random() * 2 - 1;
}

const buf = document.createElement("canvas");
const bctx = buf.getContext("2d");
let RW, RH, img, dens, colb;

function setup() {
  cv.width = innerWidth;
  cv.height = innerHeight;
  const k = Math.min(1, 900 / Math.max(cv.width, cv.height));
  RW = Math.max(2, Math.floor(cv.width * k));
  RH = Math.max(2, Math.floor(cv.height * k));
  buf.width = RW;
  buf.height = RH;
  img = bctx.createImageData(RW, RH);
  dens = new Float32Array(RW * RH);
  colb = new Float32Array(RW * RH);
}
setup();
addEventListener("resize", setup);

// ---------- ماوس: معادله را کمی دستکاری می‌کند ----------
let mx = 0, my = 0, mox = 0, moy = 0;
addEventListener("pointermove", (e) => {
  mx = (e.clientX / innerWidth - 0.5) * 0.5;
  my = (e.clientY / innerHeight - 0.5) * 0.5;
});

// ---------- جدول رنگ (فقط وقتی hue عوض شود دوباره ساخته می‌شود) ----------
const lut = new Float32Array(256 * 3);
let lutHue = -1;
function buildLut(h) {
  for (let i = 0; i < 256; i++) {
    const t = (i / 255) * 0.8 + 0.1;
    for (let k = 0; k < 3; k++) {
      lut[i * 3 + k] = 0.5 + 0.5 * Math.cos(6.28318 * (t + k * 0.33 + h));
    }
  }
  lutHue = h;
}

// ---------- حلقه‌ی اجرا ----------
let T = 0, last = performance.now(), maxSm = 1;

function frame(now) {
  const dt = Math.min(0.05, (now - last) / 1000);
  last = now;
  const S = read();
  T += dt * S.speed;

  mox += (mx - mox) * 0.05;
  moy += (my - moy) * 0.05;

  const pr = PRESETS[Math.max(0, Math.min(3, Math.round(S.shape) - 1))];
  const a = pr[0] + 0.35 * S.morph * Math.sin(T * 0.31) + mox;
  const b = pr[1] + 0.35 * S.morph * Math.sin(T * 0.23 + 1.0) + moy;
  const c = pr[2] + 0.25 * S.morph * Math.sin(T * 0.17 + 2.0);
  const d = pr[3] + 0.25 * S.morph * Math.sin(T * 0.13 + 3.0);

  const ext = 1 + Math.max(Math.abs(c), Math.abs(d));
  const sc = ((0.46 * Math.min(RW, RH)) / ext) * S.zoom;
  const ang = T * 0.1 * S.rotate;
  const cr = Math.cos(ang) * sc;
  const sr = Math.sin(ang) * sc;
  const cx = RW / 2, cy = RH / 2;
  const steps = Math.round(S.density);

  // --- ذرات را حرکت بده و ردشان را انباشته کن ---
  for (let p = 0; p < N; p++) {
    let x = px[p], y = py[p];
    for (let s = 0; s < steps; s++) {
      const nx = Math.sin(a * y) + c * Math.cos(a * x);
      const ny = Math.sin(b * x) + d * Math.cos(b * y);
      const m = Math.min(1, (Math.abs(nx - x) + Math.abs(ny - y)) * 0.3);
      x = nx; y = ny;
      const ix = (cx + x * cr - y * sr) | 0;
      const iy = (cy + x * sr + y * cr) | 0;
      if (ix >= 0 && ix < RW && iy >= 0 && iy < RH) {
        const i = iy * RW + ix;
        dens[i] += 1;
        colb[i] += m;
      }
    }
    if (!isFinite(x) || Math.random() < 0.002) {
      x = Math.random() * 2 - 1;
      y = Math.random() * 2 - 1;
    }
    px[p] = x; py[p] = y;
  }

  // --- تبدیل چگالی به رنگ (تون‌مپ) ---
  if (S.hue !== lutHue) buildLut(S.hue);
  const inv = 1 / Math.max(maxSm, 1);
  const fade = S.fade;
  const expo = S.exposure;
  const out = img.data;
  const total = RW * RH;
  let mxNow = 0;

  for (let i = 0; i < total; i++) {
    const dv = dens[i];
    const o = i * 4;
    if (dv < 0.0005) {
      out[o] = 3; out[o + 1] = 4; out[o + 2] = 10; out[o + 3] = 255;
    } else {
      if (dv > mxNow) mxNow = dv;
      let t = Math.sqrt(Math.sqrt(dv * inv)) * expo;
      if (t > 1) t = 1;
      let idx = ((colb[i] / dv) * 1.8 * 255) | 0;
      if (idx > 255) idx = 255;
      const w = t * t * t * t * 70; // هسته‌های بسیار پرنور به سمت سفید می‌روند
      const L = idx * 3;
      out[o]     = 3  + lut[L]     * t * 252 + w;
      out[o + 1] = 4  + lut[L + 1] * t * 250 + w;
      out[o + 2] = 10 + lut[L + 2] * t * 245 + w;
      out[o + 3] = 255;
    }
    dens[i] = dv * fade;
    colb[i] *= fade;
  }
  maxSm = maxSm * 0.92 + mxNow * 0.08;

  bctx.putImageData(img, 0, 0);
  ctx.imageSmoothingEnabled = true;
  ctx.drawImage(buf, 0, 0, cv.width, cv.height);

  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);