Created
June 26, 2018 17:10
-
-
Save stla/9a9cf6d986ac60d53167912f3eb0074b to your computer and use it in GitHub Desktop.
Linked cyclides 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>Linked cyclides</title> | |
<style> | |
canvas { | |
width: 100%; | |
height: 100% | |
} | |
</style> | |
</head> | |
<body> | |
<script src="three.min.js"></script> | |
<script src="jquery.min.js"></script> | |
<script> // macro for torus passing by three points | |
function TorusTransfo(p1, p2, p3) { | |
var p12 = new THREE.Vector3(); | |
p12.addVectors(p1, p2).divideScalar(2); | |
var p23 = new THREE.Vector3(); | |
p23.addVectors(p2, p3).divideScalar(2); | |
var xcoef = (p1.y - p2.y) * (p2.z - p3.z) - (p1.z - p2.z) * (p2.y - p3.y); | |
var ycoef = (p1.z - p2.z) * (p2.x - p3.x) - (p1.x - p2.x) * (p2.z - p3.z); | |
var zcoef = (p1.x - p2.x) * (p2.y - p3.y) - (p1.y - p2.y) * (p2.x - p3.x); | |
var offset1 = p1.x * xcoef + p1.y * ycoef + p1.z * zcoef; | |
var v12 = p2.clone(); v12.sub(p1); | |
var v23 = p3.clone(); v23.sub(p2); | |
var offset21 = p12.dot(v12); var offset22 = p23.dot(v23); | |
var M = new THREE.Matrix3(); | |
M.set(xcoef, v12.x, v23.x, ycoef, v12.y, v23.y, zcoef, v12.z, v23.z); | |
invM = new THREE.Matrix3(); | |
invM.getInverse(M); | |
// center = invM * (offset1, offset21, offset22) | |
var A = invM.toArray(); | |
var center = new THREE.Vector3( | |
A[0] * offset1 + A[1] * offset21 + A[2] * offset22, | |
A[3] * offset1 + A[4] * offset21 + A[5] * offset22, | |
A[6] * offset1 + A[7] * offset21 + A[8] * offset22 | |
); | |
var v = p1.clone(); v.sub(center); | |
var radius = v.length(); | |
var T = new THREE.Matrix4(); | |
if (xcoef == 0 && ycoef == 0) { | |
T.identity(); | |
T.setPosition(center); | |
return { matrix: T, radius: radius }; | |
} | |
var n = new THREE.Vector3(xcoef, ycoef, zcoef); | |
n.normalize(); | |
var s = Math.sqrt(n.x * n.x + n.y * n.y); | |
var a = n.x / s; var b = n.y / s; | |
var u = new THREE.Vector3(b, -a, 0); | |
var v = new THREE.Vector3(); v.crossVectors(n, u); | |
T.set( | |
u.x, v.x, n.x, center.x, | |
u.y, v.y, n.y, center.y, | |
u.z, v.z, n.z, center.z, | |
0, 0, 0, 1 | |
); | |
return { matrix: T, radius: radius }; | |
} | |
</script> | |
<script> // add torus to object | |
function addTorus(object, p1, p2, p3, r) { | |
var TR = TorusTransfo(p1, p2, p3); | |
var geom = new THREE.TorusGeometry(TR.radius, r, 32, 100); | |
var mesh = new THREE.Mesh(geom, new THREE.MeshNormalMaterial()); | |
mesh.matrix = TR.matrix; | |
mesh.matrixAutoUpdate = false; | |
object.add(mesh); | |
} | |
</script> | |
<script> | |
// "inverse" Hopf map ------------------------------------------------------ | |
function hopfinverse(q1, q2, q3, t) { | |
var f = 1 / Math.sqrt(2 * (1 + q3)); | |
return [ | |
f * (q1 * Math.cos(t) + q2 * Math.sin(t)), | |
f * Math.sin(t) * (1 + q3), | |
f * Math.cos(t) * (1 + q3), | |
f * (q1 * Math.sin(t) - q2 * Math.cos(t)) | |
]; | |
} | |
// stereographic projection ------------------------------------------------ | |
function stereog(x) { | |
var den = 1 - x[3]; | |
return new THREE.Vector3(x[0] / den, x[1] / den, x[2] / den); | |
} | |
// rotation in 4D space (right-isoclinic) ---------------------------------- | |
function rotate4d(alpha, beta, xi, vec) { | |
var a = Math.cos(xi); | |
var b = Math.sin(alpha) * Math.cos(beta) * Math.sin(xi); | |
var c = Math.sin(alpha) * Math.sin(beta) * Math.sin(xi); | |
var d = Math.cos(alpha) * Math.sin(xi); | |
var p = vec[0]; var q = vec[1]; var r = vec[2]; var s = vec[3]; | |
return [ | |
a * p - b * q - c * r - d * s, | |
a * q + b * p + c * s - d * r, | |
a * r - b * s + c * p + d * q, | |
a * s + b * r - c * q + d * p | |
]; | |
} | |
</script> | |
<script> | |
var nCirclesByCyclide = 50; | |
var nCyclides = 3; | |
var phi = 1.2; | |
var r = 0.15; | |
</script> | |
<script> // three.js scene | |
var scene = new THREE.Scene(); | |
var aspect = window.innerWidth / window.innerHeight; | |
var camera = new THREE.PerspectiveCamera(75, aspect, 1, 10000); | |
camera.position.z = 11; | |
scene.add(camera); | |
var renderer = new THREE.WebGLRenderer(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
var object = new THREE.Object3D(); | |
for (var i = 0; i < nCyclides; i++) { | |
var halfPi = Math.PI / 2; | |
var beta0 = i * 2 * Math.PI / nCyclides; | |
var cosphi = Math.cos(phi); var sinphi = Math.sin(phi); | |
for (var j = 0; j < nCirclesByCyclide; j++) { | |
var theta = j * 2 * Math.PI / nCirclesByCyclide; | |
var costhetacosphi = Math.cos(theta) * cosphi; | |
var sinthetacosphi = Math.sin(theta) * cosphi; | |
var c1 = rotate4d(halfPi, beta0, 1, | |
hopfinverse(costhetacosphi, sinthetacosphi, sinphi, 0)); | |
var c2 = rotate4d(halfPi, beta0, 1, | |
hopfinverse(costhetacosphi, sinthetacosphi, sinphi, 2)); | |
var c3 = rotate4d(halfPi, beta0, 1, | |
hopfinverse(costhetacosphi, sinthetacosphi, sinphi, 4)); | |
var cc1 = stereog(c1); | |
var cc2 = stereog(c2); | |
var cc3 = stereog(c3); | |
addTorus(object, cc1, cc2, cc3, r); | |
} | |
} | |
scene.add(object); | |
</script> | |
<script> // 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' | |
)); | |
object.quaternion.multiplyQuaternions(deltaRotationQuaternion, | |
object.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