BUTTERFLY - RETROSCOPIC CRT SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Butterfly Abstraction</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; height: 100vh; }
#stage {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
z-index: 2;
}
#bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
#animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; }
/* Retroscopic Overlay */
#scope {
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
display: block;
}
#scope>div {
position: absolute;
inset: 0;
}
#scope .glow {
background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
animation: nvflick 3.6s steps(24) infinite;
}
#scope .scan {
background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
animation: nvscan 7s linear infinite;
}
#scope .vig {
box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
}
#scope .mask {
background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
}
@keyframes nvscan {
from { background-position: 0 0; }
to { background-position: 0 240px; }
}
@keyframes nvflick {
0%, 100% { opacity: .55; }
40% { opacity: .9; }
62% { opacity: .4; }
80% { opacity: .75; }
}
</style>
</head>
<body>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<div id="stage">
<canvas id="bgCanvas"></canvas>
<canvas id="animationCanvas"></canvas>
</div>
<script>
const C = {
amber: 0xe8700a,
amberLight: 0xf5a623,
border: 0x1a0d00,
vein: 0x150900,
cream: 0xfff5d6,
white: 0xffffff,
apexDark: 0x0d0600,
hindAmber: 0xd4600a,
bodyDark: 0x1a0a00,
bodyMid: 0x3d1a00,
bg: 0x000000,
};
const bgCanvas = document.getElementById('bgCanvas');
const bgCtx = bgCanvas.getContext('2d');
const PIX = 8;
let cols, rows, pixels = [];
function initBG() {
bgCanvas.width = window.innerWidth;
bgCanvas.height = window.innerHeight;
cols = Math.ceil(bgCanvas.width / PIX);
rows = Math.ceil(bgCanvas.height / PIX);
pixels = Array.from({ length: cols * rows }, () => ({
b: Math.random() * 0.3, s: 0.001 + Math.random() * 0.003
}));
}
function drawBG() {
pixels.forEach((p, i) => {
p.b += p.s;
if (p.b > 0.4) p.b = 0;
const x = (i % cols) * PIX, y = Math.floor(i / cols) * PIX;
bgCtx.fillStyle = `rgb(${p.b*3|0},${p.b*6|0},${p.b*10|0})`;
bgCtx.fillRect(x, y, PIX, PIX);
});
requestAnimationFrame(drawBG);
}
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('animationCanvas'), antialias: true, alpha: true
});
let scene, camera, butterfly;
function updateSize() {
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
function updateCam() {
const w = window.innerWidth;
camera.fov = w < 480 ? 62 : w < 768 ? 56 : 50;
camera.position.set(0, 2.2, w < 480 ? 5.5 : w < 768 ? 5.0 : 4.5);
camera.lookAt(0, -0.2, 0);
camera.updateProjectionMatrix();
}
const mSolid = (c, op = 1.0) => new THREE.MeshBasicMaterial({
color: c, side: THREE.DoubleSide, transparent: op < 1, opacity: op
});
const mWire = c => new THREE.MeshBasicMaterial({ color: c, wireframe: true });
const mEdge = c => new THREE.MeshBasicMaterial({ color: c }); // solid, single-side, for raised veins
function wingMesh(shape, color, opacity = 1.0, bow = 0.16) {
const geo = new THREE.ShapeGeometry(shape, 20);
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const t = Math.max(0, Math.min(x / 2.2, 1));
pos.setZ(i, bow * Math.sin(Math.PI * t) * 0.5);
}
pos.needsUpdate = true;
geo.computeVertexNormals();
const mesh = new THREE.Mesh(geo, mSolid(color, opacity));
mesh.rotation.x = -Math.PI / 2;
return mesh;
}
function overlayMesh(shape, color, opacity = 1.0, yOffset = 0.01) {
const mesh = new THREE.Mesh(new THREE.ShapeGeometry(shape, 16), mSolid(color, opacity));
mesh.rotation.x = -Math.PI / 2;
mesh.position.y = yOffset;
return mesh;
}
function vein(parent, pts2d, yHeight = 0.025, radius = 0.010, color = C.vein) {
const v3 = pts2d.map(([x,y]) => new THREE.Vector3(x, yHeight, -y));
const curve = pts2d.length === 3
? new THREE.QuadraticBezierCurve3(v3[0], v3[1], v3[2])
: new THREE.CatmullRomCurve3(v3);
parent.add(new THREE.Mesh(
new THREE.TubeGeometry(curve, 14, radius, 5, false), mEdge(color)
));
}
function spot(parent, sx, sy, rx, rz, color, yOff = 0.028, tubeR = 0.012) {
const mesh = new THREE.Mesh(new THREE.TorusGeometry(1, tubeR, 6, 22), mEdge(color));
mesh.rotation.x = Math.PI / 2;
mesh.scale.set(rx, 1, rz);
mesh.position.set(sx, yOff, -sy);
parent.add(mesh);
}
function solidSpot(parent, sx, sy, rx, rz, color, yOff = 0.026) {
const geo = new THREE.CircleGeometry(1, 16);
const mesh = new THREE.Mesh(geo, mSolid(color));
mesh.rotation.x = -Math.PI / 2;
mesh.scale.set(rx, 1, rz);
mesh.position.set(sx, yOff, -sy);
parent.add(mesh);
}
function buildForewing() {
const W = 2.2, H = 2.6;
const grp = new THREE.Group();
const base = new THREE.Shape();
base.moveTo(0, -H*0.15);
base.bezierCurveTo(W*0.18,-H*0.08, W*0.38,H*0.05, W*0.55,H*0.28);
base.bezierCurveTo(W*0.68,H*0.46, W*0.85,H*0.72, W*0.92,H*0.92);
base.bezierCurveTo(W*0.96,H*1.02, W*1.00,H*1.05, W, H);
base.bezierCurveTo(W*0.98,H*0.88, W*0.95,H*0.72, W*0.88,H*0.55);
base.bezierCurveTo(W*0.78,H*0.32, W*0.62,H*0.08, W*0.45,-H*0.12);
base.bezierCurveTo(W*0.28,-H*0.28, W*0.12,-H*0.30, 0, -H*0.15);
base.closePath();
grp.add(wingMesh(base, C.border, 1.0, 0.16));
const inner = new THREE.Shape();
inner.moveTo(0, -H*0.10);
inner.bezierCurveTo(W*0.14,-H*0.04, W*0.32,H*0.06, W*0.46,H*0.26);
inner.bezierCurveTo(W*0.58,H*0.43, W*0.72,H*0.64, W*0.78,H*0.80);
inner.bezierCurveTo(W*0.80,H*0.88, W*0.82,H*0.94, W*0.80,H*0.96);
inner.bezierCurveTo(W*0.75,H*0.88, W*0.68,H*0.72, W*0.60,H*0.55);
inner.bezierCurveTo(W*0.52,H*0.34, W*0.38,H*0.10, W*0.24,-H*0.08);
inner.bezierCurveTo(W*0.14,-H*0.18, W*0.06,-H*0.20, 0, -H*0.10);
inner.closePath();
grp.add(overlayMesh(inner, C.amber, 1.0, 0.012));
const hilight = new THREE.Shape();
hilight.moveTo(0, -H*0.05);
hilight.bezierCurveTo(W*0.10,H*0.00, W*0.24,H*0.10, W*0.36,H*0.28);
hilight.bezierCurveTo(W*0.46,H*0.44, W*0.58,H*0.60, W*0.62,H*0.72);
hilight.bezierCurveTo(W*0.56,H*0.68, W*0.46,H*0.52, W*0.36,H*0.36);
hilight.bezierCurveTo(W*0.26,H*0.20, W*0.14,H*0.06, W*0.04,-H*0.06);
hilight.closePath();
grp.add(overlayMesh(hilight, C.amberLight, 0.55, 0.015));
const primaryVeins = [
[[0,-H*0.05],[W*0.36,H*0.52],[W*0.70,H*0.94]],
[[0,-H*0.05],[W*0.40,H*0.36],[W*0.78,H*0.60]],
[[0,-H*0.05],[W*0.44,H*0.18],[W*0.86,H*0.26]],
[[0,-H*0.05],[W*0.42,H*0.02],[W*0.84,-H*0.02]],
[[0,-H*0.05],[W*0.36,-H*0.12],[W*0.68,-H*0.16]],
[[0,-H*0.05],[W*0.26,-H*0.18],[W*0.46,-H*0.22]],
];
primaryVeins.forEach(v => vein(grp, v, 0.028, 0.013, C.vein));
const crossVeins = [
[[W*0.18,H*0.14],[W*0.20,-H*0.00],[W*0.20,-H*0.14]],
[[W*0.38,H*0.44],[W*0.40,H*0.24],[W*0.40,H*0.04]],
[[W*0.55,H*0.66],[W*0.57,H*0.44],[W*0.57,H*0.22]],
];
crossVeins.forEach(v => vein(grp, v, 0.026, 0.009, C.vein));
const marginalSpots = [
[W*0.94,H*0.76, 0.042,0.058],
[W*0.87,H*0.52, 0.038,0.052],
[W*0.80,H*0.28, 0.040,0.055],
[W*0.72,H*0.05, 0.040,0.055],
[W*0.61,-H*0.09,0.040,0.048],
[W*0.49,-H*0.18,0.042,0.046],
[W*0.37,-H*0.24,0.040,0.044],
];
marginalSpots.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream));
const submargSpots = [
[W*0.80,H*0.67, 0.058,0.080],
[W*0.72,H*0.42, 0.055,0.075],
[W*0.62,H*0.18, 0.055,0.070],
[W*0.50,-H*0.04,0.052,0.065],
];
submargSpots.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream, 0.028));
const apexSpots = [
[W*0.72,H*0.95, 0.062,0.048],
[W*0.82,H*0.88, 0.035,0.030],
[W*0.60,H*0.98, 0.048,0.036],
[W*0.50,H*0.94, 0.038,0.030],
[W*0.88,H*0.78, 0.028,0.024],
[W*0.78,H*1.00, 0.030,0.025],
];
apexSpots.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.white, 0.030));
marginalSpots.forEach(([sx,sy,rx,rz]) => spot(grp, sx, sy, rx*1.05, rz*1.05, C.vein, 0.032, 0.008));
return grp;
}
function buildHindwing() {
const W = 1.8, H = 1.6;
const grp = new THREE.Group();
const base = new THREE.Shape();
base.moveTo(0, H*0.10);
base.bezierCurveTo(W*0.22,H*0.18, W*0.52,H*0.28, W*0.75,H*0.20);
base.bezierCurveTo(W*0.92,H*0.12, W*1.05,H*0.00, W, -H*0.18);
base.bezierCurveTo(W*1.02,-H*0.42, W*0.98,-H*0.72, W*0.88,-H*0.92);
base.bezierCurveTo(W*0.78,-H*1.05, W*0.62,-H*1.12, W*0.45,-H*1.08);
base.bezierCurveTo(W*0.30,-H*1.04, W*0.18,-H*0.92, W*0.10,-H*0.78);
base.bezierCurveTo(W*0.04,-H*0.62, 0, -H*0.40, 0, H*0.10);
base.closePath();
grp.add(wingMesh(base, C.border, 1.0, 0.13));
const fill = new THREE.Shape();
fill.moveTo(0, H*0.08);
fill.bezierCurveTo(W*0.18,H*0.16, W*0.42,H*0.22, W*0.62,H*0.14);
fill.bezierCurveTo(W*0.78,H*0.08, W*0.88,-H*0.02, W*0.85,-H*0.18);
fill.bezierCurveTo(W*0.88,-H*0.40, W*0.82,-H*0.62, W*0.72,-H*0.80);
fill.bezierCurveTo(W*0.62,-H*0.94, W*0.48,-H*0.98, W*0.36,-H*0.92);
fill.bezierCurveTo(W*0.24,-H*0.86, W*0.14,-H*0.72, W*0.08,-H*0.56);
fill.bezierCurveTo(W*0.03,-H*0.42, 0, -H*0.24, 0, H*0.08);
fill.closePath();
grp.add(overlayMesh(fill, C.hindAmber, 1.0, 0.012));
const fanVeins = [
[[0,H*0.06],[W*0.32,H*0.18],[W*0.72,H*0.08]],
[[0,H*0.06],[W*0.36,H*0.06],[W*0.84,-H*0.12]],
[[0,H*0.06],[W*0.38,-H*0.08],[W*0.82,-H*0.28]],
[[0,H*0.06],[W*0.36,-H*0.22],[W*0.78,-H*0.48]],
[[0,H*0.06],[W*0.32,-H*0.36],[W*0.70,-H*0.68]],
[[0,H*0.06],[W*0.26,-H*0.50],[W*0.58,-H*0.88]],
[[0,H*0.06],[W*0.18,-H*0.58],[W*0.38,-H*0.96]],
[[0,H*0.06],[W*0.10,-H*0.52],[W*0.16,-H*0.80]],
];
fanVeins.forEach(v => vein(grp, v, 0.026, 0.011, C.vein));
[[W*0.26,H*0.12],[W*0.28,-H*0.06],[W*0.26,-H*0.22]],
[[W*0.46,H*0.10],[W*0.48,-H*0.14],[W*0.46,-H*0.40]],
[[W*0.64,H*0.02],[W*0.66,-H*0.22],[W*0.64,-H*0.55]],
[
[[W*0.26,H*0.12],[W*0.28,-H*0.06],[W*0.26,-H*0.22]],
[[W*0.46,H*0.10],[W*0.48,-H*0.14],[W*0.46,-H*0.40]],
[[W*0.64,H*0.02],[W*0.66,-H*0.22],[W*0.64,-H*0.55]],
].forEach(v => vein(grp, v, 0.024, 0.008, C.vein));
const innerBorder = [
[W*0.78,-H*0.08, 0.052,0.068],
[W*0.82,-H*0.28, 0.050,0.065],
[W*0.80,-H*0.50, 0.050,0.064],
[W*0.74,-H*0.68, 0.048,0.062],
[W*0.62,-H*0.85, 0.046,0.060],
[W*0.48,-H*0.94, 0.046,0.058],
[W*0.34,-H*0.90, 0.044,0.056],
[W*0.22,-H*0.82, 0.040,0.052],
];
innerBorder.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream));
const outerBorder = [
[W*0.88,-H*0.08, 0.032,0.042],
[W*0.92,-H*0.28, 0.030,0.040],
[W*0.90,-H*0.50, 0.030,0.040],
[W*0.84,-H*0.68, 0.030,0.038],
[W*0.74,-H*0.84, 0.028,0.036],
[W*0.60,-H*0.96, 0.028,0.034],
[W*0.44,-H*1.01, 0.028,0.034],
[W*0.30,-H*0.97, 0.026,0.032],
[W*0.17,-H*0.88, 0.026,0.030],
];
outerBorder.forEach(([sx,sy,rx,rz]) => solidSpot(grp, sx, sy, rx, rz, C.cream, 0.029));
[[0.090,C.border],[0.062,C.amber],[0.034,C.vein]].forEach(([r,c]) => {
solidSpot(grp, W*0.20, -H*0.38, r, r, c, 0.030 + (0.090-r)*0.1);
});
return grp;
}
function createButterfly() {
const group = new THREE.Group();
const head = new THREE.Mesh(new THREE.SphereGeometry(0.13, 16, 16), mEdge(C.bodyDark));
head.position.set(0, 0, 0.52);
group.add(head);
const thorax = new THREE.Mesh(new THREE.SphereGeometry(0.16, 20, 20), mEdge(C.bodyDark));
thorax.position.set(0, 0, 0.28);
thorax.scale.set(0.80, 1.10, 1.2);
group.add(thorax);
const tHiCurve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(-0.08, 0.16, 0.38),
new THREE.Vector3(0, 0.18, 0.28),
new THREE.Vector3(0.08, 0.16, 0.18)
);
group.add(new THREE.Mesh(new THREE.TubeGeometry(tHiCurve, 8, 0.012, 5, false), mEdge(C.amber)));
const abdSegs = 8;
for (let i = 0; i < abdSegs; i++) {
const t = i / (abdSegs - 1);
const r = 0.12 * (1 - t * 0.82);
const seg = new THREE.Mesh(
new THREE.SphereGeometry(r, 14, 10),
mEdge(i % 2 === 0 ? C.bodyDark : C.bodyMid)
);
seg.position.set(0, 0, 0.08 - i * 0.24);
seg.scale.set(0.72, 1.05, 1.0);
group.add(seg);
if (i > 0 && i < abdSegs - 1) {
const stripe = new THREE.Mesh(
new THREE.SphereGeometry(r * 0.45, 8, 6),
mEdge(C.amber)
);
stripe.position.set(0, r * 0.9, 0.08 - i * 0.24);
stripe.scale.set(0.4, 0.5, 1.4);
group.add(stripe);
}
}
[-1, 1].forEach(s => {
const pts = [];
for (let i = 0; i < 18; i++) {
const t = i / 17;
pts.push(new THREE.Vector3(
s * (0.05 + t * 0.38),
t * 0.05,
0.52 - t * 0.55 + t * t * 0.15
));
}
const crv = new THREE.CatmullRomCurve3(pts);
group.add(new THREE.Mesh(
new THREE.TubeGeometry(crv, 16, 0.006, 5, false), mEdge(C.bodyDark)
));
const club = new THREE.Mesh(new THREE.SphereGeometry(0.024, 8, 8), mEdge(C.amber));
club.position.copy(pts[pts.length - 1]);
group.add(club);
});
[1, -1].forEach(side => {
const fwGrp = buildForewing();
if (side === -1) fwGrp.scale.x = -1;
const fwPivot = new THREE.Group();
fwPivot.position.set(0, 0.02, 0.22);
fwPivot.rotation.y = side * (-Math.PI * 0.08);
fwPivot.rotation.x = -Math.PI * 0.12;
fwPivot.add(fwGrp);
group.add(fwPivot);
const hwGrp = buildHindwing();
if (side === -1) hwGrp.scale.x = -1;
const hwPivot = new THREE.Group();
hwPivot.position.set(0, -0.01, 0.05);
hwPivot.rotation.y = side * (Math.PI * 0.10);
hwPivot.rotation.x = -Math.PI * 0.10;
hwPivot.add(hwGrp);
group.add(hwPivot);
});
return group;
}
function init() {
initBG();
drawBG();
updateSize();
scene = new THREE.Scene();
scene.background = new THREE.Color(C.bg);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
updateCam();
butterfly = createButterfly();
scene.add(butterfly);
scene.add(new THREE.AmbientLight(0xffe0a0, 0.7));
const key = new THREE.PointLight(0xffd080, 2.8, 100);
key.position.set(3, 4, 3); scene.add(key);
const fill = new THREE.PointLight(0xa0c8ff, 1.2, 100);
fill.position.set(-4, 2, 2); scene.add(fill);
const rim = new THREE.PointLight(0xff9040, 1.0, 100);
rim.position.set(0, -1, -5); scene.add(rim);
animate();
}
function animate() {
requestAnimationFrame(animate);
const T = Date.now();
if (butterfly) {
butterfly.rotation.y += 0.004;
butterfly.position.y = Math.sin(T * 0.0015) * 0.06;
butterfly.rotation.z = Math.sin(T * 0.0011) * 0.012;
}
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
updateSize(); initBG();
if (camera) { camera.aspect = window.innerWidth / window.innerHeight; updateCam(); }
});
window.addEventListener('load', init);
</script>
</body>
</html>