// thanks to https://stackoverflow.com/a/74129799 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') cvs.style.cssText = ` background: #00000000; ` const primaryCol = getComputedStyle(document.body).getPropertyValue('--secondary'); const secondCol = getComputedStyle(document.body).getPropertyValue('--accent'); 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.getElementById("greeter-sim").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 // project v to the camera, output to o function project(o, { x, y, z }) { let ct = cos(theta), st = sin(theta) let cp = cos(phi), sp = sin(phi) let a = x * ct + y * st return vec.set(o, y * ct - x * st, cp * z - sp * a, cp * a + sp * z) } // 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() { 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)) //draw_section(X, cos(a)) //draw_section(Y, cos(a)) } } ctx.lineCap = 'round' ctx.scale(SCALE, SCALE) function render() { requestAnimationFrame(render) theta = performance.now() / 24000 * PI phi = cos(performance.now() / 12000 * PI) // 1. change the basis of the canvas ctx.save() ctx.clearRect(0, 0, width, height) ctx.translate(width >> 1, height >> 1) ctx.scale(1, -1) // 2. draw back arcs ctx.lineWidth = LINE_WIDTH / 2 ctx.strokeStyle = secondCol ctx.scale(-1, -1) // the trick is to flip the canvas draw_arcs() ctx.scale(-1, -1) // 3. draw sphere border ctx.strokeStyle = primaryCol 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 = primaryCol draw_arcs() ctx.restore() } requestAnimationFrame(render)