diff --git a/index.html b/index.html
index 927497a..756dd34 100644
--- a/index.html
+++ b/index.html
@@ -80,6 +80,7 @@
+
diff --git a/script.js b/script.js
deleted file mode 100644
index e69de29..0000000
diff --git a/sphere.js b/sphere.js
new file mode 100644
index 0000000..3b95e36
--- /dev/null
+++ b/sphere.js
@@ -0,0 +1,138 @@
+const {cos, sin, sqrt, acos, atan, atan2, abs, PI} = Math
+const clamp = (a, b, x) => x < a ? a : x > b ? b : x
+const cvs = document.createElement('canvas')
+const ctx = cvs.getContext('2d')
+
+const RADIUS = 150
+const NB_SECTIONS = 6
+const LINE_WIDTH = 3
+
+const SCALE = devicePixelRatio
+const width = RADIUS * 2 + 20
+const height = RADIUS * 2 + 20
+cvs.width = width * SCALE
+cvs.height = height * SCALE
+cvs.style.width = `${width }px`
+cvs.style.height = `${height}px`
+
+document.body.appendChild(cvs)
+
+const vec = (x = 0, y = 0, z = 0) => ({x, y, z})
+
+vec.set = (o, x = 0, y = 0, z = 0) => {
+ o.x = x
+ o.y = y
+ o.z = z
+ return o
+}
+
+const X = vec(1, 0, 0)
+const Y = vec(0, 1, 0)
+const Z = vec(0, 0, 1)
+
+// orientation of camera
+let theta, phi
+
+function project(o, {x, y, z}) {
+ let ct = cos(theta), st = sin(theta)
+ let cp = cos(phi), sp = sin(phi)
+
+ // original projection
+ let a = x * ct + y * st
+ let px = y * ct - x * st
+ let py = cp * z - sp * a
+ let pz = cp * a + sp * z
+
+ // --- add subtle right tilt (KEY PART) ---
+ let tilt = -0.2
+ let cr = cos(tilt), sr = sin(tilt)
+
+ let tx = cr * px - sr * pz
+ let tz = sr * px + cr * pz
+
+ return vec.set(o, tx, py, tz)
+}
+
+// draw camera-facing section of sphere with normal v and offset o (-1 < o < 1)
+const _p = vec()
+function draw_section(n, o = 0) {
+ let {x, y, z} = project(_p, n) // project normal on camera
+ let a = atan2(y, x) // angle of projected normal -> angle of ellipse
+ let ry = sqrt(1 - o * o) // radius of section -> y-radius of ellipse
+ let rx = ry * abs(z) // x-radius of ellipse
+ let W = sqrt(x * x + y * y)
+ let sa = acos(clamp(-1, 1, o * (1 / W - W) / rx)) // ellipse start angle
+ let sb = z > 0 ? 2 * PI - sa : - sa // ellipse end angle
+
+ ctx.beginPath()
+ ctx.ellipse(x * o * RADIUS, y * o * RADIUS, rx * RADIUS, ry * RADIUS, a, sa, sb, z <= 0)
+ ctx.stroke()
+}
+
+const _n = vec()
+function draw_arcs() {
+ if (with_great_circles.checked)
+ for (let i = NB_SECTIONS; i--;) {
+ let a = i / NB_SECTIONS * Math.PI
+ draw_section(vec.set(_n, cos(a), sin(a)))
+ }
+
+ for (let i = NB_SECTIONS - 1; i--;) {
+ let a = (i + 1) / NB_SECTIONS * Math.PI
+ draw_section(Z, cos(a))
+ if (with_sections.checked) {
+ draw_section(X, cos(a))
+ draw_section(Y, cos(a))
+ }
+ }
+}
+
+const front_grad = ctx.createRadialGradient(0, 0, RADIUS * 2 / 3, 0, 0, RADIUS)
+const back_grad = ctx.createRadialGradient(0, 0, RADIUS * 2 / 3, 0, 0, RADIUS)
+
+front_grad.addColorStop(0, '#8bc8feff')
+front_grad.addColorStop(1, '#8bc8feff')
+back_grad.addColorStop(1, '#8bc8feff')
+back_grad.addColorStop(0, '#8bc8feff')
+
+ctx.fillStyle = '#071c2dff'
+ctx.lineCap = 'round'
+ctx.scale(SCALE, SCALE)
+
+function render() {
+ requestAnimationFrame(render)
+
+ theta = performance.now() / 6000 * PI
+ phi = 1
+
+ // 1. change the basis of the canvas
+ ctx.save()
+ ctx.fillRect(0, 0, width, height)
+ ctx.translate(width >> 1, height >> 1)
+ ctx.scale(1, -1)
+
+ // 2. draw back arcs
+ if (with_back.checked) {
+ ctx.lineWidth = LINE_WIDTH / 2
+ ctx.strokeStyle = with_gradient.checked ? back_grad : '#8bc8feff'
+ ctx.scale(-1, -1) // the trick is to flip the canvas
+ draw_arcs()
+ ctx.scale(-1, -1)
+ }
+
+ // 3. draw sphere border
+ ctx.strokeStyle = with_gradient.checked ? '#8bc8feff' : '#8bc8feff'
+ ctx.lineWidth = LINE_WIDTH + 2
+ ctx.beginPath()
+ ctx.arc(0, 0, RADIUS, 0, 2 * Math.PI)
+ ctx.stroke()
+
+ // 4. draw front arcs
+ ctx.lineWidth = LINE_WIDTH
+ ctx.strokeStyle = with_gradient.checked ? front_grad : '#8bc8feff'
+ draw_arcs()
+
+ ctx.restore()
+}
+
+requestAnimationFrame(render)