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.
60 FPS • Canvas 2D
Click + Drag to interact with field
</>
Full Executable Algorithm Code
255 lines
10182 chars
// 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.5⋅Asin(ks−ωt),pray(u)=ptail+L[cosθrsinθr]+wveil(t)
Click to expand
∑
Royal Siamese Betta Splendens
Full Mathematical System • creatures
100%
Complete System of Equations
[Governing Law][Discrete Progression][Domain & Space][Parameter State]yspine(s,t)=s1.5⋅Asin(ks−ωt),pray(u)=ptail+L[cosθrsinθr]+wveil(t)spine=(s/N)1.5⋅amp⋅sin(ks−ωt),finveil=cubicBezier(p0,cp1,cp2,p3)x∈R2,t∈R+,ω∈[0,2π]λswimCadence=0.85(Swimming Cadence),λveilSpread=1.1(Veil Fin Volume),λiridescenceSheen=1(Scales Iridescence)
yspine(s,t)=s1.5⋅Asin(ks−ωt),pray(u)=ptail+L[cosθrsinθr]+wveil(t)
Computational Implementation (JavaScript Engine Equivalent)
spine = (s/N)^1.5 * amp * sin(ks - ωt), fin_veil = cubicBezier(p0, cp1, cp2, p3) 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
Press ESC or F to exit