A dependency-free 3D planetarium engine in native canvas 2D. Zero dependencies. ~27 KB of JavaScript + 150 KB of real star data.
Renders a celestial sphere with perspective 3D projection: 5,044 real stars (Yale Bright Star Catalogue + Hipparcos/Gaia), 89 constellations with FR/EN names, 17 Messier/NGC deep-sky objects, the real-time positions of the Sun, Moon and naked-eye planets, and a procedural Milky Way. Mouse/touch drag with inertia, wheel/pinch zoom, animated fly-to, custom markers and render layers.
🔭 Live demo: it powers the planetarium at vigi-sky.fr/planetarium-3d.html
<canvasid="sky" style="position:fixed;inset:0;width:100%;height:100%"></canvas><scriptsrc="sky-data.js"></script><scriptsrc="sky-engine.js"></script><script>consteng=newSkyEngine(document.getElementById('sky'),SKY);eng.start();</script>That's it — open demo.html in any browser to see it run.
Most web planetariums pull in three.js, WebGL shaders, or a tiling server. SkyEngine is a single class over the 2D canvas: it precomputes unit vectors for every star (Float32Array), projects them through a camera basis each frame, and draws pre-rendered star sprites. It hits 60 fps on mobile, ships in two <script> tags, and has no build step. Drop it into any page.
| File | Role | Size |
|---|---|---|
sky-engine.js | The engine (class SkyEngine) | 27 KB |
sky-data.js | Data (const SKY): 5,044 stars (RA/Dec/mag/B-V), 493 proper names, 89 constellations with FR+EN names, 17 Messier/NGC DSOs | 150 KB |
Star data derived from the open-source d3-celestial project (BSD licence), itself built on the Yale Bright Star and Hipparcos/Gaia (ESA) catalogues.
eng.cam// {yaw: RA°, pitch: Dec°, fov: °}awaiteng.flyTo(ra,dec,fov,durMs)// animated easeInOutCubic flight, shortest patheng.set('constellations',bool)// lines + names (FR or EN)eng.set('labels',bool)// star names (auto density by FOV)eng.set('dsos',bool)// deep-sky objectseng.set('milkyway',bool)// procedural Milky Way bandeng.set('meteors',bool)// random shooting starseng.set('planets',bool)// Sun/Moon/planets at today's positionseng.onSelect=obj=>{ ... }// click/tap. obj = {type:'star'|'dso'|'marker', name, ra, dec, mag, bv, ...}eng.pick(x,y)// manual hit-test (CSS px)// 1. Markers: custom points (sightings, meteor radiants, a birth star…)constid=eng.addMarker({ra: 88.8,dec: 7.4,label: 'Marker #42',color: '#ef4444',size: 12,pulse: true});eng.clearMarkers();// 2. Layers: custom per-frame rendering (satellite tracks, heatmaps, lines…)eng.addLayer('iss-track',(ctx,eng)=>{constp=eng.project(issRa,issDec);// project RA/Dec → screenif(p.visible){/* draw on ctx — note: device coords = ×eng.dpr */}});eng.removeLayer('iss-track');eng.tour([{ra: 83.8,dec: -5.4,fov: 16,title: 'M42',text: '…',holdMs: 5000,flyMs: 2600},
...
],{onStep: (stop,i)=>{/* render your narrative card */},onEnd: ()=>{ ... }});eng.stopTour();// also called automatically when the user dragseng.searchIndex()// → [{name, ra, dec, type, fov}] (~600 entries)// filter in your UI, then eng.flyTo(it.ra, it.dec, it.fov)eng.unproject(px,py)// → {ra, dec} (0.00° roundtrip)SkyEngine.angularDist(ra1,dec1,ra2,dec2)// static → angular distance in degreesSwitch from the equatorial star-map to the sky as you actually see it from a given place and time: real horizon line, N/E/S/W cardinal points, and stars below the ground are hidden. Drag becomes azimuth/altitude.
eng.setHorizon(lat,lon,date)// e.g. (48.8566, 2.3522, new Date())eng.lookAtAltAz(azDeg,altDeg)// azimuth from North through East, altitude above horizoneng.setDate(date)// change the moment — drives a time slidereng.setEquatorial()// back to the classic RA/Dec mapIt only rotates the 3-vector camera basis into the observer's frame, so the 5,044-star render path is untouched — zero per-frame cost. unproject() still returns RA/Dec, so click-to-report keeps working in horizon mode.
On a phone, drive the camera with the device's orientation: lift the phone toward the sky and the real stars are labelled over it. Runs in the browser, no app.
eng.enableAR(state=>{// state: 'on' | 'denied' | 'unsupported' (handles the iOS 13+ permission prompt)});eng.disableAR();Call enableAR() from a user gesture (a tap) so iOS grants motion access. It implies setHorizon() if you aren't already in horizon mode.
newSkyEngine(canvas,SKY,{conNameKey: 'e',// 'n' = native/FR names (default), 'e' = English constellation namesplanets: true,// show Sun/Moon/planets});- Sighting overlay —
addMarker()for each report in your database at its celestial position - Meteor-shower radiants — markers for Perseids/Geminids/Quadrantids with peak dates
- "Your birth star" —
flyTo()toward a star with a pulsing gold marker - Live ISS / satellite track — a layer fed by TLE data (convert lat/lon → RA/Dec)
- Cardboard VR mode — two side-by-side renders (the engine takes any canvas)
- Constellation quiz — hide names,
pick()to check answers - Precession time-lapse — animate the celestial pole position
- Projection: RA/Dec → precomputed unit vectors (
Float32Array) → camera basis (forward/right/up) → perspective projection. Culling at z<0.02. - Performance: pre-rendered star sprites (6 colours × 7 sizes, keyed by B-V) drawn via
drawImage; stars fainter than mag 4.6 batched asfillRect. 60 fps on mobile. - Stellar colours: B-V < 0 blue → > 1.5 red-orange (6 classes).
- Planets: Sun, Moon, Mercury–Saturn at today's positions via Paul Schlyter's algorithm (~1° accuracy), recomputed every 2 min.
- Milky Way: galactic equator converted to equatorial (J2000 rotation), soft radial blobs.
- Inertia: velocity preserved on release, friction 0.94/frame.
- Custom-layer coordinates are in device pixels — multiply by
eng.dpr. project()returnsvisible:falsebehind the camera — always test before drawing.
MIT © 2026 — see LICENSE. Star data under BSD (d3-celestial).