97

Royal Siamese Betta Splendens

Hydrodynamic fish simulation of Betta splendens featuring multi-layered volumetric flowing veil fins, iridescent cobalt/magenta chromatic gradients, carangiform spinal wave propagation, and delicate ray ribbing.

Playground
60 FPS Canvas 2D
Click + Drag to interact with field
</>

Full Executable Algorithm Code

// 075 - Royal Siamese Betta Splendens (creatures)
// 1:1 Original algorithm engine source
function createSiameseBetta() {
  return {
    setup() {
    },
    render(context, timeState, params) {
      const { ctx, width, height } = context;
      const swimSpeed = Number(params.swimCadence ?? 0.85);
      const finFlow = Number(params.veilSpread ?? 1.1);
      const iridescence = Number(params.iridescenceSheen ?? 1);
      const t = timeState.time * swimSpeed;
      ctx.fillStyle = "#02060d";
      ctx.fillRect(0, 0, width, height);
      const cx = width * 0.48;
      const cy = height * 0.5;
      const maxR = Math.min(width, height) * 0.44;
      ctx.save();
      ctx.translate(cx, cy);
      const royalHue = 215;
      const magentaHue = 325;
      for (let p = 0; p < 16; p++) {
        const pa = (p * 45 + t * 20) * (Math.PI / 180);
        const pr = maxR * (0.3 + 0.6 * (p * 37 % 100) / 100);
        const px = Math.cos(pa) * pr;
        const py = Math.sin(pa * 1.5) * (pr * 0.45);
        ctx.fillStyle = hsla(royalHue + 20, 80, 75, 0.15);
        ctx.beginPath();
        ctx.arc(px, py, 1.2, 0, Math.PI * 2);
        ctx.fill();
      }
      const spineJoints = 20;
      const spinePoints = [];
      const bodyLen = maxR * 0.95;
      for (let j = 0; j <= spineJoints; j++) {
        const frac = j / spineJoints;
        const amp = Math.pow(frac, 1.5) * (maxR * 0.16);
        const wave = Math.sin(t * 3.2 - frac * Math.PI * 2.2);
        const sx = -bodyLen * 0.45 + frac * bodyLen * 0.8;
        const sy = wave * amp;
        let bWidth = 0;
        if (frac < 0.25) {
          bWidth = Math.sin(frac / 0.25 * (Math.PI * 0.5)) * (maxR * 0.13);
        } else {
          bWidth = Math.cos((frac - 0.25) / 0.75 * (Math.PI * 0.5)) * (maxR * 0.13);
        }
        spinePoints.push({ x: sx, y: sy, angle: 0, width: Math.max(2, bWidth) });
      }
      for (let j = 0; j <= spineJoints; j++) {
        const next = spinePoints[Math.min(spineJoints, j + 1)];
        const prev = spinePoints[Math.max(0, j - 1)];
        spinePoints[j].angle = Math.atan2(next.y - prev.y, next.x - prev.x);
      }
      const tailRoot = spinePoints[spineJoints];
      const finRays = 32;
      const tailLen = maxR * 0.85 * finFlow;
      for (let layer = 0; layer < 3; layer++) {
        const layerAlpha = (0.2 + layer * 0.18) * iridescence;
        const layerOffset = (layer - 1) * 0.15;
        for (let r = 0; r < finRays; r++) {
          const rFrac = r / (finRays - 1);
          const fanAngle = tailRoot.angle + (rFrac - 0.5) * Math.PI * 0.85 + layerOffset;
          const rayLag = rFrac * 0.8;
          const rayWave1 = Math.sin(t * 3.5 - rayLag * 2.5) * (maxR * 0.12);
          const rayWave2 = Math.cos(t * 2.8 - rayLag * 1.8) * (maxR * 0.08);
          const rLen = tailLen * (0.7 + 0.3 * Math.sin(rFrac * Math.PI)) * (1 + 0.1 * Math.sin(t * 2 + r));
          const p1x = tailRoot.x;
          const p1y = tailRoot.y;
          const cp1x = p1x + Math.cos(fanAngle) * (rLen * 0.4) + rayWave1;
          const cp1y = p1y + Math.sin(fanAngle) * (rLen * 0.4) + rayWave2;
          const p2x = p1x + Math.cos(fanAngle) * rLen + rayWave1 * 1.6;
          const p2y = p1y + Math.sin(fanAngle) * rLen + rayWave2 * 1.6;
          ctx.beginPath();
          ctx.moveTo(p1x, p1y);
          ctx.quadraticCurveTo(cp1x, cp1y, p2x, p2y);
          const finHue = royalHue + rFrac * 80 + layer * 25;
          ctx.strokeStyle = hsla(finHue, 95, 62, layerAlpha);
          ctx.lineWidth = 1.2 + (1 - rFrac) * 1.5;
          ctx.stroke();
          if (r % 2 === 0) {
            ctx.fillStyle = hsla(magentaHue - rFrac * 60, 90, 55, 0.04 * iridescence);
            ctx.fill();
          }
        }
      }
      const dorsalStart = Math.floor(spineJoints * 0.3);
      const dorsalEnd = Math.floor(spineJoints * 0.85);
      for (let i = dorsalStart; i <= dorsalEnd; i++) {
        const pt = spinePoints[i];
        const dFrac = (i - dorsalStart) / (dorsalEnd - dorsalStart);
        const dLen = maxR * 0.55 * Math.sin(dFrac * Math.PI) * finFlow;
        const dAng = pt.angle - Math.PI * 0.55 + Math.sin(t * 3 - i * 0.3) * 0.25;
        const tipX = pt.x + Math.cos(dAng) * dLen;
        const tipY = pt.y + Math.sin(dAng) * dLen;
        ctx.beginPath();
        ctx.moveTo(pt.x, pt.y - pt.width * 0.8);
        ctx.quadraticCurveTo(pt.x + Math.cos(dAng) * (dLen * 0.5), pt.y + Math.sin(dAng) * (dLen * 0.5) - 10, tipX, tipY);
        ctx.strokeStyle = hsla(royalHue + dFrac * 60, 90, 65, 0.35 * iridescence);
        ctx.lineWidth = 1.2;
        ctx.stroke();
      }
      const analStart = Math.floor(spineJoints * 0.35);
      const analEnd = Math.floor(spineJoints * 0.95);
      for (let i = analStart; i <= analEnd; i++) {
        const pt = spinePoints[i];
        const aFrac = (i - analStart) / (analEnd - analStart);
        const aLen = maxR * 0.62 * Math.sin(aFrac * Math.PI) * finFlow;
        const aAng = pt.angle + Math.PI * 0.55 + Math.sin(t * 3 - i * 0.3) * 0.25;
        const tipX = pt.x + Math.cos(aAng) * aLen;
        const tipY = pt.y + Math.sin(aAng) * aLen;
        ctx.beginPath();
        ctx.moveTo(pt.x, pt.y + pt.width * 0.8);
        ctx.quadraticCurveTo(pt.x + Math.cos(aAng) * (aLen * 0.5), pt.y + Math.sin(aAng) * (aLen * 0.5) + 10, tipX, tipY);
        ctx.strokeStyle = hsla(magentaHue - aFrac * 50, 92, 60, 0.35 * iridescence);
        ctx.lineWidth = 1.2;
        ctx.stroke();
      }
      ctx.beginPath();
      const headPt = spinePoints[0];
      ctx.moveTo(headPt.x, headPt.y);
      for (let j = 1; j <= spineJoints; j++) {
        const pt = spinePoints[j];
        const normX = -Math.sin(pt.angle);
        const normY = Math.cos(pt.angle);
        ctx.lineTo(pt.x + normX * pt.width, pt.y + normY * pt.width);
      }
      for (let j = spineJoints; j >= 0; j--) {
        const pt = spinePoints[j];
        const normX = -Math.sin(pt.angle);
        const normY = Math.cos(pt.angle);
        ctx.lineTo(pt.x - normX * pt.width, pt.y - normY * pt.width);
      }
      ctx.closePath();
      const bodyGrad = ctx.createLinearGradient(spinePoints[0].x, 0, spinePoints[spineJoints].x, 0);
      bodyGrad.addColorStop(0, "#0c1a38");
      bodyGrad.addColorStop(0.3, hsla(royalHue, 90, 35, 0.95));
      bodyGrad.addColorStop(0.7, hsla(magentaHue, 85, 30, 0.95));
      bodyGrad.addColorStop(1, "#050a17");
      ctx.fillStyle = bodyGrad;
      ctx.fill();
      ctx.strokeStyle = hsla(royalHue + 20, 100, 75, 0.85 * iridescence);
      ctx.lineWidth = 1.4;
      ctx.stroke();
      for (let j = 2; j < spineJoints - 4; j += 2) {
        const pt = spinePoints[j];
        const scX = pt.x;
        const scY = pt.y - pt.width * 0.3;
        ctx.beginPath();
        ctx.ellipse(scX, scY, 3.5, 2, pt.angle, 0, Math.PI * 2);
        ctx.fillStyle = hsla(royalHue + 40, 100, 80, 0.5 * iridescence);
        ctx.fill();
      }
      const pelvicPt = spinePoints[3];
      const pelvLen = maxR * 0.7 * finFlow;
      const pelvWave = Math.sin(t * 3.5) * 12;
      ctx.beginPath();
      ctx.moveTo(pelvicPt.x, pelvicPt.y + pelvicPt.width * 0.7);
      ctx.bezierCurveTo(pelvicPt.x + 10, pelvicPt.y + pelvLen * 0.4, pelvicPt.x - 15 + pelvWave, pelvicPt.y + pelvLen * 0.7, pelvicPt.x - 5 + pelvWave, pelvicPt.y + pelvLen);
      ctx.strokeStyle = hsla(magentaHue + 20, 100, 80, 0.85);
      ctx.lineWidth = 1.6;
      ctx.stroke();
      const pectPt = spinePoints[2];
      const pectAngle = pectPt.angle - 0.4 + Math.sin(t * 5) * 0.35;
      const pectLen = maxR * 0.28;
      ctx.save();
      ctx.translate(pectPt.x, pectPt.y);
      ctx.rotate(pectAngle);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.bezierCurveTo(pectLen * 0.5, -pectLen * 0.4, pectLen * 0.9, -pectLen * 0.3, pectLen, 0);
      ctx.bezierCurveTo(pectLen * 0.8, pectLen * 0.3, pectLen * 0.4, pectLen * 0.3, 0, 0);
      ctx.fillStyle = "rgba(56, 189, 248, 0.25)";
      ctx.fill();
      ctx.strokeStyle = hsla(royalHue + 20, 95, 80, 0.75);
      ctx.lineWidth = 1;
      ctx.stroke();
      ctx.restore();
      const eyePt = spinePoints[1];
      const eyeX = eyePt.x - 4;
      const eyeY = eyePt.y - 3;
      ctx.beginPath();
      ctx.arc(eyeX, eyeY, 4.2, 0, Math.PI * 2);
      ctx.fillStyle = "#020610";
      ctx.fill();
      ctx.strokeStyle = hsla(45, 100, 65, 0.9);
      ctx.lineWidth = 1.2;
      ctx.stroke();
      ctx.beginPath();
      ctx.arc(eyeX, eyeY, 2.4, 0, Math.PI * 2);
      ctx.fillStyle = "#f59e0b";
      ctx.fill();
      ctx.beginPath();
      ctx.arc(eyeX, eyeY, 1.2, 0, Math.PI * 2);
      ctx.fillStyle = "#000000";
      ctx.fill();
      ctx.fillStyle = "#ffffff";
      ctx.beginPath();
      ctx.arc(eyeX - 0.8, eyeY - 0.8, 0.7, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }
  };
}

// Default parameters from content metadata
const defaultParams = [
  {
    "key": "swimCadence",
    "label": "Swimming Cadence",
    "type": "range",
    "min": 0.4,
    "max": 1.8,
    "step": 0.05,
    "defaultValue": 0.85,
    "description": "Carangiform body undulation speed"
  },
  {
    "key": "veilSpread",
    "label": "Veil Fin Volume",
    "type": "range",
    "min": 0.6,
    "max": 1.6,
    "step": 0.1,
    "defaultValue": 1.1,
    "description": "Flowing caudal and dorsal fin veil amplitude"
  },
  {
    "key": "iridescenceSheen",
    "label": "Scales Iridescence",
    "type": "range",
    "min": 0.4,
    "max": 1.6,
    "step": 0.1,
    "defaultValue": 1,
    "description": "Structural color reflection intensity"
  }
];

if (!window.__art_instances) window.__art_instances = {};
if (!window.__art_instances['siamese-betta']) {
  const inst = typeof createSiameseBetta === 'function' ? createSiameseBetta() : null;
  if (inst && inst.setup) {
    inst.setup({ ctx, width, height, dpr: 1, aspectRatio: width / height }, defaultParams);
  }
  window.__art_instances['siamese-betta'] = inst;
}

const instance = window.__art_instances['siamese-betta'];
if (instance && instance.render) {
  instance.render(
    { ctx, width, height, dpr: 1, aspectRatio: width / height },
    { time, deltaTime: dt, frameCount: Math.floor(time * 60), fps: 60 },
    defaultParams
  );
}
Edit in Interactive Playground Zero Dependencies • Standalone Canvas 2D
ƒ

Mathematical Formulation

high
Analytical Equation
yspine(s,t)=s1.5Asin(ksωt),pray(u)=ptail+L[cosθrsinθr]+wveil(t)y_{\text{spine}}(s, t) = s^{1.5} \cdot A \sin(ks - \omega t), \quad \mathbf{p}_{\text{ray}}(u) = \mathbf{p}_{\text{tail}} + L \begin{bmatrix} \cos\theta_r \\ \sin\theta_r \end{bmatrix} + \mathbf{w}_{\text{veil}}(t)
Click to expand
Compact Formula
spine = (s/N)^1.5 * amp * sin(ks - ωt), fin_veil = cubicBezier(p0, cp1, cp2, p3)

Mathematical Tags

#betta #fish #siamese-fighting-fish #creatures #fins #iridescent #aquatic #marine
Author: Math Art Core Target: 60 FPS

Export & Embed: Royal Siamese Betta Splendens

4K PNG Snapshot

High-resolution single frame render

WebM Video (5s Loop)

60 FPS browser-captured stream

Standalone JS Script

Complete executable Canvas 2D algorithm

HTML Iframe Embed

<iframe src="https://art.fazleyrabbi.xyz/embed/siamese-betta" width="500" height="500" frameborder="0" loading="lazy"></iframe>
ESC
↑↓ Navigate Select
101 Mathematical Artworks

Support the Project

Keep mathematical creative coding alive & open source

Or via Direct Payoneer ($0 Fee)
Payoneer Customer ID: $0 Fee Direct
24076084

💡 Payoneer app: Go to Pay → Pay to recipient → Enter ID 24076084.

Thank you for supporting generative mathematical animations! ✨