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)