Skip to contentCREATIVE·CODE·ART
1
1
1
1
1
1
1
1
1
three.js

Liquid Prism

Liquid Prism is a living orb of liquid light. A noise field ripples across its surface in real time, while an iridescent shader shifts its colors with every angle, like oil on water. A soft halo wraps the sphere and a drift of stardust orbits around it. Everything is computed on the GPU. Use the sliders to change distortion, frequency, iridescence, glow, and hue, then drag to orbit and scroll to zoom.

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

View the code
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

// ---------- تعریف اسلایدرها: [نام، برچسب، min، max، step، پیش‌فرض] ----------
const CFG = [
  ["speed",    "Speed",        0,    3,    0.1,  1],
  ["amp",      "Distortion",   0,    0.8,  0.01, 0.32],
  ["freq",     "Frequency",    0.5,  3,    0.05, 1.2],
  ["irid",     "Iridescence",  0,    3,    0.05, 1.4],
  ["glow",     "Glow",         0,    3,    0.05, 1],
  ["hue",      "Hue shift",    0,    1,    0.01, 0],
  ["orbit",    "Camera orbit", 0,    3,    0.05, 0.6],
  ["dust",     "Dust",         0,    6000, 100,  3500],
  ["dustsize", "Dust size",    0.5,  4,    0.1,  1.6],
];

const V = {};
CFG.forEach((c) => (V[c[0]] = c[5]));

// اگر حتی یکی از اسلایدرها در سایت تعریف شده باشد از params می‌خوانیم
const siteSliders = CFG.some((c) => typeof params[c[0]] === "number");

const read = () => ({
  speed:    params.speed    ?? V.speed,
  amp:      params.amp      ?? V.amp,
  freq:     params.freq     ?? V.freq,
  irid:     params.irid     ?? V.irid,
  glow:     params.glow     ?? V.glow,
  hue:      params.hue      ?? V.hue,
  orbit:    params.orbit    ?? V.orbit,
  dust:     params.dust     ?? V.dust,
  dustsize: params.dustsize ?? V.dustsize,
});

// ---------- پنل داخلی (فقط اگر سایت اسلایدر نساخته باشد) ----------
document.body.style.margin = "0";
document.body.style.overflow = "hidden";

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);
}

// ---------- صحنه ----------
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.domElement.style.display = "block";
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x03040a);

const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0.4, 4.6);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;
controls.autoRotate = true;
controls.minDistance = 2.4;
controls.maxDistance = 10;

// ---------- نویز سه‌بعدی (simplex) ----------
const NOISE = `
vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}
vec4 permute(vec4 x){return mod289(((x*34.0)+1.0)*x);}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}
float snoise(vec3 v){
  const vec2 C=vec2(1.0/6.0,1.0/3.0);
  const vec4 D=vec4(0.0,0.5,1.0,2.0);
  vec3 i=floor(v+dot(v,C.yyy));
  vec3 x0=v-i+dot(i,C.xxx);
  vec3 g=step(x0.yzx,x0.xyz);
  vec3 l=1.0-g;
  vec3 i1=min(g.xyz,l.zxy);
  vec3 i2=max(g.xyz,l.zxy);
  vec3 x1=x0-i1+C.xxx;
  vec3 x2=x0-i2+C.yyy;
  vec3 x3=x0-D.yyy;
  i=mod289(i);
  vec4 p=permute(permute(permute(
    i.z+vec4(0.0,i1.z,i2.z,1.0))
   +i.y+vec4(0.0,i1.y,i2.y,1.0))
   +i.x+vec4(0.0,i1.x,i2.x,1.0));
  float n_=0.142857142857;
  vec3 ns=n_*D.wyz-D.xzx;
  vec4 j=p-49.0*floor(p*ns.z*ns.z);
  vec4 x_=floor(j*ns.z);
  vec4 y_=floor(j-7.0*x_);
  vec4 x=x_*ns.x+ns.yyyy;
  vec4 y=y_*ns.x+ns.yyyy;
  vec4 h=1.0-abs(x)-abs(y);
  vec4 b0=vec4(x.xy,y.xy);
  vec4 b1=vec4(x.zw,y.zw);
  vec4 s0=floor(b0)*2.0+1.0;
  vec4 s1=floor(b1)*2.0+1.0;
  vec4 sh=-step(h,vec4(0.0));
  vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;
  vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
  vec3 p0=vec3(a0.xy,h.x);
  vec3 p1=vec3(a0.zw,h.y);
  vec3 p2=vec3(a1.xy,h.z);
  vec3 p3=vec3(a1.zw,h.w);
  vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
  p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;
  vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);
  m=m*m;
  return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
}`;

const PAL = `
vec3 pal(float t, float h) {
  return 0.5 + 0.5 * cos(6.28318 * (t + vec3(0.0, 0.33, 0.67) + h));
}`;

// ---------- گوی سیال ----------
const orbMat = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0 },
    uAmp: { value: 0.32 },
    uFreq: { value: 1.2 },
    uIrid: { value: 1.4 },
    uGlow: { value: 1 },
    uHue: { value: 0 },
  },
  vertexShader: `
    uniform float uTime, uAmp, uFreq;
    varying vec3 vNormal;
    varying vec3 vView;
    varying float vNz;
    ${NOISE}

    float nz(vec3 p) {
      float n = snoise(p * uFreq + vec3(0.0, uTime * 0.4, uTime * 0.2));
      n += 0.35 * snoise(p * uFreq * 2.1 - vec3(uTime * 0.3));
      return n;
    }

    void main() {
      vec3 n0 = normalize(position);
      float n = nz(n0);
      vec3 pos = n0 * (1.0 + n * uAmp);

      // نرمال تحلیلی از روی جابه‌جایی دو نقطه‌ی همسایه
      vec3 up = abs(n0.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
      vec3 t = normalize(cross(n0, up));
      vec3 b = cross(n0, t);
      float e = 0.02;
      vec3 p1 = normalize(n0 + t * e);
      vec3 p2 = normalize(n0 + b * e);
      vec3 q1 = p1 * (1.0 + nz(p1) * uAmp);
      vec3 q2 = p2 * (1.0 + nz(p2) * uAmp);
      vec3 nn = normalize(cross(q1 - pos, q2 - pos));

      vNormal = normalMatrix * nn;
      vec4 mv = modelViewMatrix * vec4(pos, 1.0);
      vView = -mv.xyz;
      vNz = n;
      gl_Position = projectionMatrix * mv;
    }
  `,
  fragmentShader: `
    uniform float uTime, uIrid, uGlow, uHue;
    varying vec3 vNormal;
    varying vec3 vView;
    varying float vNz;
    ${PAL}

    void main() {
      vec3 N = normalize(vNormal);
      vec3 V = normalize(vView);
      float ndv = clamp(dot(N, V), 0.0, 1.0);
      float fres = pow(1.0 - ndv, 3.0);

      // رنگ لایه‌ی نازک: به زاویه‌ی دید و موج سطح بستگی دارد
      vec3 irid = pal(ndv * uIrid * 1.6 + vNz * 1.2 + uTime * 0.03, uHue);

      // نورها در فضای جهان ثابت‌اند
      vec3 L1 = normalize(mat3(viewMatrix) * vec3(0.6, 0.8, 0.7));
      vec3 L2 = normalize(mat3(viewMatrix) * vec3(-0.7, -0.3, -0.5));
      float d1 = max(dot(N, L1), 0.0);
      float d2 = max(dot(N, L2), 0.0);
      float s1 = pow(max(dot(reflect(-L1, N), V), 0.0), 70.0);

      // انعکاس محیط ساختگی
      vec3 R = reflect(-V, N);
      vec3 Rw = (vec4(R, 0.0) * viewMatrix).xyz;
      float e = Rw.y * 0.5 + 0.5;
      vec3 env = mix(vec3(0.015, 0.02, 0.05),
                     pal(e * 0.7 + 0.15 + uTime * 0.02, uHue),
                     pow(e, 2.2));
      float strip = pow(max(dot(Rw, normalize(vec3(-0.4, 0.6, 0.7))), 0.0), 24.0);

      vec3 col = irid * (0.08 + 0.55 * d1)
               + pal(0.5 + vNz, uHue) * 0.12 * d2
               + env * (0.35 + 0.9 * fres)
               + vec3(1.0) * s1 * 0.9
               + vec3(1.0) * strip * 0.45
               + irid * fres * uGlow * 1.3;

      col = 1.0 - exp(-col * 1.5);
      gl_FragColor = vec4(col, 1.0);
    }
  `,
});
const orb = new THREE.Mesh(new THREE.IcosahedronGeometry(1, 40), orbMat);
scene.add(orb);

// ---------- هاله‌ی نورانی دور گوی ----------
const haloMat = new THREE.ShaderMaterial({
  transparent: true,
  depthWrite: false,
  side: THREE.BackSide,
  blending: THREE.AdditiveBlending,
  uniforms: {
    uTime: { value: 0 },
    uGlow: { value: 1 },
    uHue: { value: 0 },
  },
  vertexShader: `
    varying vec3 vN;
    void main() {
      vN = normalize(normalMatrix * normal);
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float uTime, uGlow, uHue;
    varying vec3 vN;
    ${PAL}
    void main() {
      float t = clamp(-dot(vN, vec3(0.0, 0.0, 1.0)), 0.0, 1.0);
      float i = pow(t, 2.0);
      vec3 c = pal(0.25 + uTime * 0.02 + t * 0.4, uHue);
      gl_FragColor = vec4(c * i * uGlow * 1.2, clamp(i * uGlow, 0.0, 1.0));
    }
  `,
});
const halo = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), haloMat);
scene.add(halo);

// ---------- غبار ستاره‌ای ----------
const DUST_MAX = 6000;
const dPos = new Float32Array(DUST_MAX * 3);
const dRand = new Float32Array(DUST_MAX);
for (let i = 0; i < DUST_MAX; i++) {
  const v = new THREE.Vector3().randomDirection();
  const rad = 1.9 + Math.pow(Math.random(), 1.5) * 4.5;
  dPos.set([v.x * rad, v.y * rad * 0.6, v.z * rad], i * 3);
  dRand[i] = Math.random();
}
const dustGeo = new THREE.BufferGeometry();
dustGeo.setAttribute("position", new THREE.BufferAttribute(dPos, 3));
dustGeo.setAttribute("aRand", new THREE.BufferAttribute(dRand, 1));

const dustMat = new THREE.ShaderMaterial({
  transparent: true,
  depthWrite: false,
  blending: THREE.AdditiveBlending,
  uniforms: {
    uTime: { value: 0 },
    uSize: { value: 1.6 },
    uHue: { value: 0 },
    uPixel: { value: renderer.getPixelRatio() },
  },
  vertexShader: `
    uniform float uTime, uSize, uHue, uPixel;
    attribute float aRand;
    varying vec3 vColor;
    ${PAL}
    void main() {
      vec3 p = position;
      // چرخش مداری: نزدیک‌تر سریع‌تر
      float r2 = length(p.xz);
      float ang = uTime * 0.25 / (0.5 + r2);
      float c = cos(ang), s = sin(ang);
      p.xz = mat2(c, -s, s, c) * p.xz;
      p.y += sin(uTime * 0.5 + aRand * 20.0) * 0.08;

      vec4 mv = modelViewMatrix * vec4(p, 1.0);
      gl_Position = projectionMatrix * mv;
      float tw = 0.6 + 0.4 * sin(uTime * 2.0 + aRand * 30.0);
      gl_PointSize = uSize * uPixel * (0.5 + aRand) * (8.0 / -mv.z) * tw;
      vColor = pal(aRand * 0.4 + 0.55, uHue) * (0.5 + 0.8 * tw);
    }
  `,
  fragmentShader: `
    varying vec3 vColor;
    void main() {
      float d = length(gl_PointCoord - 0.5);
      float a = smoothstep(0.5, 0.0, d);
      a = a * a;
      gl_FragColor = vec4(vColor, a);
    }
  `,
});
const dust = new THREE.Points(dustGeo, dustMat);
dust.frustumCulled = false;
scene.add(dust);

// ---------- حلقه‌ی اجرا ----------
const clock = new THREE.Clock();
let time = 0; // زمان جمع می‌شود تا تغییر سرعت پرش نداشته باشد

renderer.setAnimationLoop(() => {
  const S = read();
  time += clock.getDelta() * S.speed;

  const o = orbMat.uniforms;
  o.uTime.value = time;
  o.uAmp.value = S.amp;
  o.uFreq.value = S.freq;
  o.uIrid.value = S.irid;
  o.uGlow.value = S.glow;
  o.uHue.value = S.hue;

  const h = haloMat.uniforms;
  h.uTime.value = time;
  h.uGlow.value = S.glow;
  h.uHue.value = S.hue;
  halo.scale.setScalar(1.28 + S.amp * 1.1);

  const d = dustMat.uniforms;
  d.uTime.value = time;
  d.uSize.value = S.dustsize;
  d.uHue.value = S.hue;
  dustGeo.setDrawRange(0, Math.min(DUST_MAX, Math.round(S.dust)));

  controls.autoRotate = S.orbit > 0;
  controls.autoRotateSpeed = S.orbit;
  controls.update();
  renderer.render(scene, camera);
});

addEventListener("resize", () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});