Last active
July 11, 2019 06:15
-
-
Save stla/0b7a597c729572e2fb92216d64082409 to your computer and use it in GitHub Desktop.
3D Steiner chain and its cyclide with three.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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