Skip to content

Instantly share code, notes, and snippets.

@stla
Last active July 11, 2019 06:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stla/0b7a597c729572e2fb92216d64082409 to your computer and use it in GitHub Desktop.
Save stla/0b7a597c729572e2fb92216d64082409 to your computer and use it in GitHub Desktop.
3D Steiner chain and its cyclide with three.js
<html>
<head>
<title>3D Steiner Chain</title>
<style>
canvas {
width: 100%;
height: 100%
}
</style>
</head>
<body>
<script src="three.min.js"></script>
<script src="jquery.min.js"></script>
<script>
function fcyclide(a, c, mu) {
var b = Math.sqrt(a * a - c * c);
return function (u, v, vector) {
var cosu = Math.cos(2 * u * Math.PI);
var cosv = Math.cos(2 * v * Math.PI);
var sinu = Math.sin(2 * u * Math.PI);
var sinv = Math.sin(2 * v * Math.PI);
var h = a - c * cosu * cosv;
var x = (mu * (c - a * cosu * cosv) + b * b * cosu) / h;
var y = (b * sinu * (a - mu * cosv)) / h;
var z = b * sinv * (c * cosu - mu) / h;
vector.x = x; vector.y = y; vector.z = z;
}
}
</script>
<script>
function det3x3(A) {
return A[0][0] * A[1][1] * A[2][2]
+ A[0][1] * A[1][2] * A[2][0]
+ A[0][2] * A[1][0] * A[2][1]
- A[2][0] * A[1][1] * A[0][2]
- A[2][1] * A[1][2] * A[0][0]
- A[2][2] * A[1][0] * A[0][1];
}
function orient(p1, p2, p3, p4) {
var A1 = [p2, p3, p4];
var A2 = [p1, p3, p4];
var A3 = [p1, p2, p4];
var A4 = [p1, p2, p3];
return -det3x3(A1) + det3x3(A2) - det3x3(A3) + det3x3(A4);
}
function circumsphere(p1, p2, p3, p4) {
var a = orient(p1, p2, p3, p4);
var q1 = p1[0] * p1[0] + p1[1] * p1[1] + p1[2] * p1[2];
var q2 = p2[0] * p2[0] + p2[1] * p2[1] + p2[2] * p2[2];
var q3 = p3[0] * p3[0] + p3[1] * p3[1] + p3[2] * p3[2];
var q4 = p4[0] * p4[0] + p4[1] * p4[1] + p4[2] * p4[2];
var Dx = orient([q1, p1[1], p1[2]], [q2, p2[1], p2[2]],
[q3, p3[1], p3[2]], [q4, p4[1], p4[2]]);
var Dy = orient([q1, p1[0], p1[2]], [q2, p2[0], p2[2]],
[q3, p3[0], p3[2]], [q4, p4[0], p4[2]]);
var Dz = orient([q1, p1[0], p1[1]], [q2, p2[0], p2[1]],
[q3, p3[0], p3[1]], [q4, p4[0], p4[1]]);
var center = [0.5 / a * Dx, -0.5 / a * Dy, 0.5 / a * Dz];
var radius = Math.sqrt(
(p1[0] - center[0]) ** 2 +
(p1[1] - center[1]) ** 2 +
(p1[2] - center[2]) ** 2);
return { center: center, radius: radius };
}
function inversion(phi, pnt, r, center) {
var invphi = 1 / phi;
var k = r * r * (invphi * invphi - 1);
var Ix = invphi * r;
var vx = pnt[0] - Ix - center[0];
var vy = pnt[1] - center[1];
var vz = pnt[2] - center[2];
var c = k / (vx * vx + vy * vy + vz * vz);
var outx = Ix + center[0] - c * vx;
var outy = center[1] - c * vy;
var outz = center[2] - c * vz;
return [outx, outy, outz];
}
function oneSphere(n, phi, center, r, beta) {
var sine = Math.sin(Math.PI / n);
var coef = 1 / (1 + sine);
var cHalfside = coef * r * sine;
var cRadius = coef * r;
var O1x = 2 / phi * r;
var cosbeta = Math.cos(beta); var sinbeta = Math.sin(beta);
var pntx = center[0] + cRadius * cosbeta;
var pnty = center[1] + cRadius * sinbeta;
var pntz = center[2];
var p1 = [pntx + cHalfside, pnty, pntz];
var p2 = [pntx, pnty + cHalfside, pntz];
var p3 = [pntx - cHalfside, pnty, pntz];
var p4 = [pntx, pnty, pntz + cHalfside];
var q1 = inversion(phi, p1, r, center);
var q2 = inversion(phi, p2, r, center);
var q3 = inversion(phi, p3, r, center);
var q4 = inversion(phi, p4, r, center);
var cs = circumsphere(q1, q2, q3, q4);
var center2 = [cs.center[0] - O1x, cs.center[1], cs.center[2]];
return { center: center2, radius: cs.radius };
}
function cyclideParameters(n, phi) {
var cs;
if (n > 2) {
var sr = (1 - Math.sin(Math.PI / n)) / (1 + Math.sin(Math.PI / n));
var p1 = inversion(phi, [sr, 0, 0], 1, [0, 0, 0]);
var p2 = inversion(phi, [0, sr, 0], 1, [0, 0, 0]);
var p3 = inversion(phi, [-sr, 0, 0], 1, [0, 0, 0]);
var p4 = inversion(phi, [0, 0, sr], 1, [0, 0, 0]);
cs = circumsphere(p1, p2, p3, p4);
} else {
cs = {
center: inversion(phi, [0, 0, 0], 1, [0, 0, 0]),
radius: 0
};
}
var a = (cs.radius + 1) / 2;
var mu = 1 - a;
var c = -(2 / phi - cs.center[0]) / 2;
var shift = {
x: (cs.center[0] - 2 / phi) / 2,
y: cs.center[1] / 2,
z: cs.center[2] / 2
};
return {
a: a,
c: c,
mu: mu,
shift: shift
};
}
</script>
<script>
var n = 5; var phi = 0.4; // n>1; -1 < phi < 1, phi /= 0
</script>
<script>
var scene = new THREE.Scene();
var aspect = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(70, aspect, 1, 10000);
camera.position.z = 2;
scene.add(camera);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var chain = new THREE.Object3D();
for (i = 0; i < n; i++) {
var sphere = oneSphere(n, phi, [0, 0, 0], 1, 2 * i * Math.PI / n);
var geom = new THREE.SphereGeometry(sphere.radius, 32, 32);
geom.translate(sphere.center[0], sphere.center[1], 0);
var material = new THREE.MeshNormalMaterial(
{
wireframe: false,
fog: false
})
var mesh = new THREE.Mesh(geom, material);
chain.add(mesh);
}
var params = cyclideParameters(n, phi);
var geom2 = new THREE.ParametricGeometry(
fcyclide(params.a, params.c, params.mu),
60, 60);
geom2.translate(params.shift.x, params.shift.y, 0);
var material2 = new THREE.MeshNormalMaterial({ wireframe: true });
var mesh2 = new THREE.Mesh(geom2, material2);
chain.add(mesh2);
scene.add(chain);
// mouse dragging ----------------------------------------------------------
var isDragging = false;
var previousMousePosition = {
x: 0,
y: 0
};
$(renderer.domElement).on('mousedown', function (e) {
isDragging = true;
}).on('mousemove', function (e) {
var deltaMove = {
x: e.offsetX - previousMousePosition.x,
y: e.offsetY - previousMousePosition.y
};
if (isDragging) {
var deltaRotationQuaternion = new THREE.Quaternion()
.setFromEuler(new THREE.Euler(
Math.PI / 180 * (deltaMove.y * 1),
Math.PI / 180 * (deltaMove.x * 1),
0,
'XYZ'
));
chain.quaternion.multiplyQuaternions(deltaRotationQuaternion,
chain.quaternion);
}
previousMousePosition = {
x: e.offsetX,
y: e.offsetY
};
});
$(document).on('mouseup', function (e) {
isDragging = false;
});
// shim layer with setTimeout fallback ------------------------------------
window.requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var lastFrameTime = new Date().getTime() / 1000;
var totalGameTime = 0;
function update(dt, t) {
setTimeout(function () {
var currTime = new Date().getTime() / 1000;
var dt = currTime - (lastFrameTime || currTime);
totalGameTime += dt;
update(dt, totalGameTime);
lastFrameTime = currTime;
}, 0);
}
function render() {
renderer.render(scene, camera);
requestAnimFrame(render);
}
render();
update(0, totalGameTime);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment