Skip to content

Instantly share code, notes, and snippets.

@stla
Created June 26, 2018 17:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stla/9a9cf6d986ac60d53167912f3eb0074b to your computer and use it in GitHub Desktop.
Save stla/9a9cf6d986ac60d53167912f3eb0074b to your computer and use it in GitHub Desktop.
Linked cyclides with three.js
<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