1+ import { levels , type Level , type Planet } from "./planets.js" ;
2+ import { drawPlanetCanvas } from "./drawPlanets.js" ;
3+
4+ function hasOzoneLayer ( p : Planet ) : boolean {
5+ const o = p as Planet & { ozoneLayer ?: boolean ; ozonLayer ?: boolean } ;
6+ if ( typeof o . ozoneLayer === "boolean" ) return o . ozoneLayer ;
7+ if ( typeof o . ozonLayer === "boolean" ) return o . ozonLayer ;
8+ return false ;
9+ }
10+
11+ function planetTooltip ( p : Planet ) : string {
12+ const lines : string [ ] = [ `${ p . name } (${ p . id } )` , p . type ] ;
13+ if ( p . temperature != null ) lines . push ( `Temperature: ${ p . temperature } °C` ) ;
14+ if ( p . waterCoverage != null ) lines . push ( `Water coverage: ${ p . waterCoverage } %` ) ;
15+ if ( p . atmosphere ) {
16+ lines . push (
17+ `Atmosphere: ${ p . atmosphere } ${ p . atmosphereThickness ? ` (${ p . atmosphereThickness } )` : "" } ` ,
18+ ) ;
19+ }
20+ if ( p . mass != null ) lines . push ( `Mass: ${ p . mass } × Earth` ) ;
21+ if ( p . weather ) lines . push ( `Weather: ${ p . weather } ` ) ;
22+ lines . push ( `Ozone layer: ${ hasOzoneLayer ( p ) ? "Yes" : "No" } ` ) ;
23+ if ( p . description ) lines . push ( p . description ) ;
24+ return lines . join ( "\n" ) ;
25+ }
26+
27+ function appendStatRow ( container : HTMLElement , label : string , value : string ) {
28+ const row = document . createElement ( "div" ) ;
29+ row . className = "planet-panel__stat" ;
30+ const lab = document . createElement ( "span" ) ;
31+ lab . className = "planet-panel__label" ;
32+ lab . textContent = label ;
33+ const val = document . createElement ( "span" ) ;
34+ val . className = "planet-panel__value" ;
35+ val . textContent = value ;
36+ row . append ( lab , val ) ;
37+ container . append ( row ) ;
38+ }
39+
40+ function renderPlanetPanel ( p : Planet ) : void {
41+ const title = document . getElementById ( "planetPanelTitle" ) ;
42+ const meta = document . getElementById ( "planetPanelMeta" ) ;
43+ const desc = document . getElementById ( "planetPanelDesc" ) ;
44+ const stats = document . getElementById ( "planetPanelStats" ) ;
45+ if ( ! title || ! meta || ! desc || ! stats ) return ;
46+
47+ title . textContent = p . name ;
48+ meta . textContent = `${ p . id } · ${ p . type } ` ;
49+
50+ desc . textContent = p . description ?? "" ;
51+
52+ stats . replaceChildren ( ) ;
53+ if ( p . temperature != null ) appendStatRow ( stats , "Temperature" , `${ p . temperature } °C` ) ;
54+ if ( p . waterCoverage != null ) appendStatRow ( stats , "Water coverage" , `${ p . waterCoverage } %` ) ;
55+ if ( p . atmosphere ) {
56+ appendStatRow (
57+ stats ,
58+ "Atmosphere" ,
59+ p . atmosphereThickness ? `${ p . atmosphere } (${ p . atmosphereThickness } )` : p . atmosphere ,
60+ ) ;
61+ }
62+ if ( p . mass != null ) appendStatRow ( stats , "Mass" , `${ p . mass } × Earth` ) ;
63+ if ( p . weather ) appendStatRow ( stats , "Weather" , p . weather ) ;
64+ if ( p . type === "Meteor" && p . size ) appendStatRow ( stats , "Size" , p . size ) ;
65+ appendStatRow ( stats , "Ozone layer" , hasOzoneLayer ( p ) ? "Yes" : "No" ) ;
66+ }
67+
68+ const level1 = levels . find ( ( l : Level ) => l . id === 1 ) ;
69+ if ( ! level1 ) {
70+ throw new Error ( "planets.js: level 1 (System Solara) not found" ) ;
71+ }
72+
73+ const viewportEl = document . getElementById ( "viewport" ) ;
74+ const sunEl = document . getElementById ( "sun" ) ;
75+ const zoomHint = document . getElementById ( "zoomHint" ) ;
76+ const planetPanelEl = document . getElementById ( "planetPanel" ) ;
77+ const planetPanelClose = document . getElementById ( "planetPanelClose" ) ;
78+
79+ if ( ! viewportEl || ! ( sunEl instanceof HTMLButtonElement ) || ! planetPanelEl ) {
80+ throw new Error ( "Landing DOM: #viewport, #sun, or #planetPanel missing" ) ;
81+ }
82+
83+ const viewport = viewportEl ;
84+ const sun = sunEl ;
85+ const planetPanel = planetPanelEl ;
86+
87+ sun . title = `${ level1 . star . name } — ${ level1 . star . type } ` ;
88+ if ( zoomHint ) {
89+ zoomHint . textContent = `${ level1 . name } — click the sun to zoom back in` ;
90+ }
91+
92+ const planetById = new Map ( level1 . planets . map ( ( p : Planet ) => [ p . id , p ] ) ) ;
93+ let selectedPlanetId : string | null = null ;
94+
95+ function setPanelOpen ( open : boolean ) {
96+ planetPanel . classList . toggle ( "planet-panel--open" , open ) ;
97+ planetPanel . setAttribute ( "aria-hidden" , open ? "false" : "true" ) ;
98+ }
99+
100+ function clearPlanetSelection ( ) {
101+ selectedPlanetId = null ;
102+ document . querySelectorAll ( ".planet.planet--selected" ) . forEach ( ( el ) => {
103+ el . classList . remove ( "planet--selected" ) ;
104+ } ) ;
105+ setPanelOpen ( false ) ;
106+ }
107+
108+ function selectPlanet ( id : string ) {
109+ const planet = planetById . get ( id ) ;
110+ if ( ! planet ) return ;
111+
112+ selectedPlanetId = id ;
113+ document . querySelectorAll ( ".planet.planet--selected" ) . forEach ( ( el ) => {
114+ el . classList . remove ( "planet--selected" ) ;
115+ } ) ;
116+ const dot = document . querySelector ( `.planet[data-planet-id="${ id } "]` ) ;
117+ if ( dot instanceof HTMLElement ) dot . classList . add ( "planet--selected" ) ;
118+
119+ renderPlanetPanel ( planet ) ;
120+ setPanelOpen ( true ) ;
121+ planetPanelClose ?. focus ( ) ;
122+ }
123+
124+ const orbitEls = document . querySelectorAll ( ".system .orbit" ) ;
125+ level1 . planets . forEach ( ( planet : Planet , index : number ) => {
126+ const orbit = orbitEls [ index ] ;
127+ if ( ! ( orbit instanceof HTMLElement ) ) return ;
128+ const dot = orbit . querySelector ( ".planet" ) ;
129+ if ( ! ( dot instanceof HTMLElement ) ) return ;
130+
131+ dot . className = "planet" ;
132+ dot . dataset . planetId = planet . id ;
133+ dot . title = planetTooltip ( planet ) ;
134+ dot . setAttribute (
135+ "aria-label" ,
136+ `${ planet . name } , ${ planet . type } . ${ planet . id } : orbit ${ index + 1 } from the star.` ,
137+ ) ;
138+ dot . setAttribute ( "role" , "button" ) ;
139+ dot . tabIndex = - 1 ;
140+
141+ // Determine size based on planet type
142+ const isMeteor = planet . type === "Meteor" ;
143+ const sz = isMeteor
144+ ? planet . size === "Medium" ? 12 : 10
145+ : planet . type === "Ice Planet" ? 15 : 17 ;
146+
147+ // Create a canvas and draw the planet art onto it
148+ const canvas = document . createElement ( "canvas" ) ;
149+ canvas . width = sz * 2 ;
150+ canvas . height = sz * 2 ;
151+ canvas . style . width = `${ sz * 2 } px` ;
152+ canvas . style . height = `${ sz * 2 } px` ;
153+ canvas . style . borderRadius = "50%" ;
154+ canvas . style . display = "block" ;
155+
156+ dot . style . width = `${ sz * 2 } px` ;
157+ dot . style . height = `${ sz * 2 } px` ;
158+ dot . style . background = "none" ;
159+ dot . appendChild ( canvas ) ;
160+
161+ const c2d = canvas . getContext ( "2d" ) ;
162+ if ( c2d ) drawPlanetCanvas ( c2d , ( planet as any ) . artKey ?? "meteor" , sz ) ;
163+
164+ dot . addEventListener ( "click" , ( e ) => {
165+ e . stopPropagation ( ) ;
166+ if ( ! viewport . classList . contains ( "is-zoomed-out" ) ) return ;
167+ selectPlanet ( planet . id ) ;
168+ } ) ;
169+
170+ dot . addEventListener ( "keydown" , ( e ) => {
171+ if ( ! viewport . classList . contains ( "is-zoomed-out" ) ) return ;
172+ if ( e . key === "Enter" || e . key === " " ) {
173+ e . preventDefault ( ) ;
174+ selectPlanet ( planet . id ) ;
175+ }
176+ } ) ;
177+ } ) ;
178+
179+ function setZoomedOut ( on : boolean ) {
180+ viewport . classList . toggle ( "is-zoomed-out" , on ) ;
181+ sun . setAttribute (
182+ "aria-label" ,
183+ on ? "Zoom in: return to landing view" : "Zoom out: show full star system" ,
184+ ) ;
185+ document . querySelectorAll ( ".planet" ) . forEach ( ( el ) => {
186+ if ( el instanceof HTMLElement ) el . tabIndex = on ? 0 : - 1 ;
187+ } ) ;
188+ if ( ! on ) {
189+ clearPlanetSelection ( ) ;
190+ }
191+ }
192+
193+ sun . addEventListener ( "click" , ( ) => {
194+ setZoomedOut ( ! viewport . classList . contains ( "is-zoomed-out" ) ) ;
195+ } ) ;
196+
197+ planetPanelClose ?. addEventListener ( "click" , ( ) => {
198+ clearPlanetSelection ( ) ;
199+ } ) ;
200+
201+ viewport . addEventListener ( "click" , ( e ) => {
202+ const t = e . target ;
203+ if ( ! ( t instanceof Node ) ) return ;
204+ if ( planetPanel . contains ( t ) || sun . contains ( t ) ) return ;
205+ if ( t instanceof Element && t . closest ( ".planet" ) ) return ;
206+ if ( viewport . classList . contains ( "is-zoomed-out" ) && selectedPlanetId ) {
207+ clearPlanetSelection ( ) ;
208+ }
209+ } ) ;
0 commit comments