{
  "title": "How Stars works",
  "eyebrow": "BOOMER RAWLINGS / STRUCTURE & CODE",
  "description": "Follow the data, animation and interface through ten connected explanations, each beside the code that makes it work.",
  "footer": "Figure data, software credits and licenses: /stars-assets/credits.txt.",
  "plans": [
    {
      "id": "development",
      "title": "Build the constellation behavior",
      "summary": "Source the figures, preserve the wandering, then refine the lock and release.",
      "steps": [
        {
          "id": "sources",
          "title": "Separate the figure from the surrounding sky",
          "description": "Transcribe explicit figure membership and catalog coordinates. Chart regions contain many background stars; they must not become part of the recognizable figure by accident.",
          "artifact": {
            "title": "Audited catalog record: Hercules",
            "language": "JSON",
            "code": "{\n  \"id\": \"Her\",\n  \"name\": \"Hercules\",\n  \"source\": \"IAU / Sky & Telescope (MacRobert), Ford transcription\",\n  \"chart\": \"https://iauarchive.eso.org/static/public/constellations/gif/HER.gif\",\n  \"sourcePaths\": [\n    {\n      \"sourceFile\": \"iau-lines.dat\",\n      \"line\": 238,\n      \"sourcePattern\": \"Hercules\",\n      \"hip\": [\n        \"86414\",\n        \"87808\",\n        \"85112\",\n        \"84380\",\n        \"81833\",\n        \"81126\",\n        \"79992\",\n        \"79101\",\n        \"77760\"\n      ]\n    },\n    {\n      \"sourceFile\": \"iau-lines.dat\",\n      \"line\": 239,\n      \"sourcePattern\": \"Hercules\",\n      \"hip\": [\n        \"81833\",\n        \"81693\",\n        \"80816\",\n        \"80170\",\n        \"80463\",\n        \"81008\"\n      ]\n    },\n    {\n      \"sourceFile\": \"iau-lines.dat\",\n      \"line\": 240,\n      \"sourcePattern\": \"Hercules\",\n      \"hip\": [\n        \"81693\",\n        \"83207\",\n        \"84380\"\n      ]\n    },\n    {\n      \"sourceFile\": \"iau-lines.dat\",\n      \"line\": 241,\n      \"sourcePattern\": \"Hercules\",\n      \"hip\": [\n        \"88794\",\n        \"87933\",\n        \"86974\",\n        \"85693\",\n        \"84379\"\n      ]\n    },\n    {\n      \"sourceFile\": \"iau-lines.dat\",\n      \"line\": 242,\n      \"sourcePattern\": \"Hercules\",\n      \"hip\": [\n        \"83207\",\n        \"84379\",\n        \"84345\",\n        \"80816\"\n      ]\n    }\n  ],\n  \"center\": {\n    \"ra\": 255.32407314989229,\n    \"dec\": 31.417352865271248\n  },\n  \"stars\": [\n    {\n      \"hip\": 86414,\n      \"ra\": 264.86619337986116,\n      \"dec\": 46.00633180930556,\n      \"mag\": 3.82,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Iot Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.165176114,\n      \"y\": -0.489325825\n    },\n    {\n      \"hip\": 87808,\n      \"ra\": 269.0632522565221,\n      \"dec\": 37.25053917722222,\n      \"mag\": 3.86,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"The Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.284887505,\n      \"y\": -0.250714501\n    },\n    {\n      \"hip\": 85112,\n      \"ra\": 260.92060269004094,\n      \"dec\": 37.145946296805555,\n      \"mag\": 4.15,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Rho Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.101241209,\n      \"y\": -0.230136131\n    },\n    {\n      \"hip\": 84380,\n      \"ra\": 258.7618097913527,\n      \"dec\": 36.809161832499996,\n      \"mag\": 3.16,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Pi Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.052779586,\n      \"y\": -0.218418497\n    },\n    {\n      \"hip\": 81833,\n      \"ra\": 250.72402183618192,\n      \"dec\": 38.92225448138889,\n      \"mag\": 3.48,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Eta Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.126859053,\n      \"y\": -0.279404485\n    },\n    {\n      \"hip\": 81126,\n      \"ra\": 248.52575889112055,\n      \"dec\": 42.43704099722222,\n      \"mag\": 4.2,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Sig Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.168403588,\n      \"y\": -0.382230051\n    },\n    {\n      \"hip\": 79992,\n      \"ra\": 244.93515330648867,\n      \"dec\": 46.31336638513889,\n      \"mag\": 3.91,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Tau Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.231487685,\n      \"y\": -0.5\n    },\n    {\n      \"hip\": 79101,\n      \"ra\": 242.19240794972492,\n      \"dec\": 44.93490598972222,\n      \"mag\": 4.23,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Phi Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.291588614,\n      \"y\": -0.467630634\n    },\n    {\n      \"hip\": 77760,\n      \"ra\": 238.16892282053183,\n      \"dec\": 42.45151804333334,\n      \"mag\": 4.6,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Chi Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.386224669,\n      \"y\": -0.410382477\n    },\n    {\n      \"hip\": 81693,\n      \"ra\": 250.32150123826412,\n      \"dec\": 31.602725613194444,\n      \"mag\": 2.81,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Zet Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.145719119,\n      \"y\": -0.072518751\n    },\n    {\n      \"hip\": 80816,\n      \"ra\": 247.55499985688138,\n      \"dec\": 21.48961328125,\n      \"mag\": 2.78,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Bet Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.231308624,\n      \"y\": 0.210651014\n    },\n    {\n      \"hip\": 80170,\n      \"ra\": 245.48005996777522,\n      \"dec\": 19.15313027708333,\n      \"mag\": 3.74,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Gam Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.29150423,\n      \"y\": 0.273752478\n    },\n    {\n      \"hip\": 80463,\n      \"ra\": 246.35397192488594,\n      \"dec\": 14.033269574027779,\n      \"mag\": 4.57,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Ome Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.277334093,\n      \"y\": 0.423126849\n    },\n    {\n      \"hip\": 81008,\n      \"ra\": 248.15121699376718,\n      \"dec\": 11.488041507222222,\n      \"mag\": 4.84,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.230281793,\n      \"y\": 0.5\n    },\n    {\n      \"hip\": 83207,\n      \"ra\": 255.07239072439882,\n      \"dec\": 30.926404617638887,\n      \"mag\": 3.92,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Eps Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": 0.031329127,\n      \"y\": -0.050650039\n    },\n    {\n      \"hip\": 88794,\n      \"ra\": 271.88562776865155,\n      \"dec\": 28.762488503472223,\n      \"mag\": 3.84,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Omi Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.386224669,\n      \"y\": -0.019470914\n    },\n    {\n      \"hip\": 87933,\n      \"ra\": 269.4411904061282,\n      \"dec\": 29.247879745694444,\n      \"mag\": 3.7,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Xi Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.323612119,\n      \"y\": -0.024989227\n    },\n    {\n      \"hip\": 86974,\n      \"ra\": 266.61469536768493,\n      \"dec\": 27.720676253333334,\n      \"mag\": 3.42,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Mu Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.257939503,\n      \"y\": 0.026222697\n    },\n    {\n      \"hip\": 85693,\n      \"ra\": 262.6846248279487,\n      \"dec\": 26.110645284722224,\n      \"mag\": 4.41,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Lam Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.162150853,\n      \"y\": 0.079835035\n    },\n    {\n      \"hip\": 84379,\n      \"ra\": 258.7579607299944,\n      \"dec\": 24.83920414,\n      \"mag\": 3.12,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Del Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.063194093,\n      \"y\": 0.1204394\n    },\n    {\n      \"hip\": 84345,\n      \"ra\": 258.6619100327099,\n      \"dec\": 14.390332813611112,\n      \"mag\": 2.78,\n      \"catalogConstellation\": \"Her\",\n      \"catalogDesignation\": \"Alp-1 Her\",\n      \"outsideCatalogRegion\": false,\n      \"sourceMarkedNeighbor\": false,\n      \"x\": -0.068262486,\n      \"y\": 0.419455306\n    }\n  ],\n  \"edges\": [\n    [\n      0,\n      1\n    ],\n    [\n      1,\n      2\n    ],\n    [\n      2,\n      3\n    ],\n    [\n      3,\n      4\n    ],\n    [\n      3,\n      14\n    ],\n    [\n      4,\n      5\n    ],\n    [\n      4,\n      9\n    ],\n    [\n      5,\n      6\n    ],\n    [\n      6,\n      7\n    ],\n    [\n      7,\n      8\n    ],\n    [\n      9,\n      10\n    ],\n    [\n      9,\n      14\n    ],\n    [\n      10,\n      11\n    ],\n    [\n      10,\n      20\n    ],\n    [\n      11,\n      12\n    ],\n    [\n      12,\n      13\n    ],\n    [\n      14,\n      19\n    ],\n    [\n      15,\n      16\n    ],\n    [\n      16,\n      17\n    ],\n    [\n      17,\n      18\n    ],\n    [\n      18,\n      19\n    ],\n    [\n      19,\n      20\n    ]\n  ]\n}",
            "note": "Each star has a catalog ID and normalized x/y coordinates. An edge stores two indices into that star array; it describes a connection without duplicating positions."
          },
          "resources": [
            {
              "label": "Full figure database",
              "url": "https://boomerrawlings.com/stars-assets/constellations.json"
            }
          ]
        },
        {
          "id": "fit",
          "title": "Match drifting points to a figure",
          "description": "The assignment algorithm pairs nearby dots with catalog points, then refines the rotation, scale and position. Its result is a list of particle indices and target coordinates that the animation can interpolate.",
          "artifact": {
            "title": "Particle assignment and fitting",
            "language": "JavaScript",
            "code": "function candidate(pattern,particles,w,h,ctx,proposal){\n   if(!pattern||pattern.stars.length<2)return null;\n   var n=pattern.stars.length,size=proposal?proposal.size:Math.min(Math.min(w,h)*0.80,150+n*27)*between(.88,1.12);\n   if(size<100)return null;\n   var pool=poolFor(particles,9,w,h);if(pool.length<n+6)return null;\n   var point=pool[Math.floor(random()*pool.length)],cx=proposal?proposal.x:point.x,cy=proposal?proposal.y:point.y,angle=proposal?proposal.angle:between(-Math.PI,Math.PI);\n   var label=labelInfo(pattern,ctx),ui=obstacles(),targets,indices;\n   if(!proposal){\n    var local=bounds(transform(pattern,size,angle,0,0),label,6).box,clear=false;\n    for(var trial=0;trial<8;trial++){\n     point=pool[Math.floor(random()*pool.length)];cx=clamp(point.x,padding-local.x,w-padding-local.x-local.w);cy=clamp(point.y,padding-local.y,h-padding-local.y-local.h);\n     if(safe({x:cx+local.x,y:cy+local.y,w:local.w,h:local.h},w,h,ui,others())){clear=true;break;}\n    }\n    if(!clear)return null;\n   }\n   targets=transform(pattern,size,angle,cx,cy);\n   for(var pass=0;pass<3;pass++){indices=assign(targets,pool);if(!indices)return null;if(pass<2)targets=refine(pattern,pool,indices,size*.9,size*1.1);}\n   var current=indices.map(function(i){var p=particles[pool[i].index];return {x:p.x,y:p.y};});\n   var radius=Math.max.apply(null,indices.map(function(i){return particles[pool[i].index].radius||3;}));\n   var end=bounds(targets,label,radius),start=bounds(current,label,radius),reservation=union(start.box,end.box);\n   if(!safe(reservation,w,h,ui,others()))return null;\n   var sum=0,max=0,startMax=0;indices.forEach(function(i,k){var d=Math.hypot(targets[k].x-pool[i].x,targets[k].y-pool[i].y);sum+=d*d;max=Math.max(max,d);startMax=Math.max(startMax,Math.hypot(targets[k].x-current[k].x,targets[k].y-current[k].y));});\n   var rms=Math.sqrt(sum/n);\n   if(max>Math.min(210,45+n*6)||rms>Math.min(115,22+n*3.2)||startMax>260)return null;\n   return {pattern:pattern,indices:indices.map(function(i){return pool[i].index;}),targets:targets,label:label,reservation:reservation,fitRms:rms,maxDisplacement:startMax};\n  }",
            "note": "assign() minimizes combined travel between particles and target stars. refine() adjusts one uniform transform. indices identifies the particles; targets gives their display destinations."
          },
          "related": [
            "sources"
          ]
        },
        {
          "id": "lock",
          "title": "Animate the approach, hold and release",
          "description": "One motion clock drives the transition. The figure, name and underline resolve together with a brief blue-and-gold scan accent. The finished figure remains readable for15–20 seconds. The frequency slider sets the pace of new formations.",
          "artifact": {
            "title": "Animation time and opacity",
            "language": "JavaScript",
            "code": "var until=g.snapMotion-frame.motion,after=frame.motion-g.snapMotion,exit=after-g.hold;\n    if(exit>=exitTime){tail(g,particles,points);stats.completed++;return;}\n    var t=smooth((frame.motion-g.start)/Math.max(.1,g.snapMotion-g.start));\n    g.phase=until>0?'gather':exit>0?'exit':'hold';g.visible=until>0?0:1-smooth((exit-.82)/.18);\n    g.labelAlpha=after<0?0:smooth(after/.18)*(1-smooth((exit-.45)/.2));\n\nvar approach=cadence?cadence*1000:Math.max(14000,Math.min(32000,c.maxDisplacement/8*1000));\n   // Browser cadence spaces actual locks, rather than batching them on30-second scans.\n   var snapEpoch=cadence?Math.max(frame.now+approach,lastLock===null?0:lastLock+cadence*1000)-scan:Math.ceil((frame.now+between(approach,approach+8000))/30000)*30000;\n   lastLock=snapEpoch+scan;\n   c.start=frame.motion;c.snapMotion=frame.motion+(snapEpoch+scan-frame.now)/1000;c.snapEpoch=snapEpoch;c.hold=between(15,20)+lockTime;c.id=++serial;c.radius=radius;c.phase='gather';c.visible=0;",
            "note": "snapMotion is the end of the approach. Subtracting it from motion gives the age of the hold. visible and labelAlpha feed drawing opacity; cadence determines the next formation time."
          },
          "related": [
            "fit"
          ]
        },
        {
          "id": "release",
          "title": "Retract connections before returning to drift",
          "description": "Each edge splits at its center and shortens into both endpoint stars. A cardinal sweep staggers the retraction; stars remain fixed until their lines clear.",
          "artifact": {
            "title": "Actual edge retraction",
            "language": "JavaScript",
            "code": "function pieces(g,a,b){\n  if(g.phase!=='exit')return [[a,b]];\n  var began=arrival(g,(a.x+b.x)/2,(a.y+b.y)/2,g.exitDirection),t=smooth((g.exitAge-began)/400);if(t>=1)return [];\n  var f=(1-t)*.5;return [[a,{x:a.x+(b.x-a.x)*f,y:a.y+(b.y-a.y)*f}],[b,{x:b.x+(a.x-b.x)*f,y:b.y+(a.y-b.y)*f}]];\n }",
            "note": "pieces() returns two shortened segments, one anchored at each endpoint. The factor f shrinks from one-half to zero, so each connection retracts toward its stars."
          },
          "related": [
            "lock"
          ]
        },
        {
          "id": "clearance",
          "title": "Compose the figure and its label",
          "description": "A figure is a set of points plus its name. The layout function measures the point extents, centers the text beneath them, and returns a shared bounding rectangle. This gives the whole composition a coherent position.",
          "artifact": {
            "title": "Figure and label geometry",
            "language": "JavaScript",
            "code": "function bounds(points,label,radius){\n  var xs=points.map(function(p){return p.x;}),ys=points.map(function(p){return p.y;});\n  var x0=Math.min.apply(null,xs),x1=Math.max.apply(null,xs),y0=Math.min.apply(null,ys),y1=Math.max.apply(null,ys),cx=(x0+x1)/2;\n  var text={x:cx-label.width/2-4,y:y1+radius+14,w:label.width+8,h:27};\n  return {box:union(expand({x:x0,y:y0,w:x1-x0,h:y1-y0},radius+4),text),label:text,baseline:y1+radius+29};\n }",
            "note": "The minimum and maximum point coordinates define the figure rectangle. label.width determines text centering. union() combines both into one layout footprint."
          },
          "related": [
            "fit"
          ]
        }
      ]
    },
    {
      "id": "browser",
      "title": "Build the browser interface",
      "summary": "Connect settings, local time, reminders and wave data to one quiet display.",
      "steps": [
        {
          "id": "settings",
          "title": "Define one settings model",
          "description": "One object describes the clock, timezone, optional panels and both frequency sliders. defaults() returns10-second constellations and5-minute reminders. normalize() merges a saved choice into that object; the same model drives both the form and the renderer.",
          "artifact": {
            "title": "Defaults and configuration model",
            "language": "JavaScript",
            "code": "export const intervals=[0,5,10,15,20,30,60];\nexport function tipsLabel(value){return value===0?'Disabled':value===60?'Once an hour':'Every '+value+' minutes';}\nexport function constellationOptions(settings){const n=normalize(settings).constellationInterval;return {interval:[n,n],initialDelay:[0,0],cadence:n};}\nconst validZones=new Set(),formatters=new Map();let systemName,systemChecked=0;\nexport function zone(value='system'){\n if(value==='system'&&(!systemName||Date.now()-systemChecked>60000)){systemName=Intl.DateTimeFormat().resolvedOptions().timeZone;systemChecked=Date.now();}\n const name=value==='system'?systemName:value;\n if(!validZones.has(name)){new Intl.DateTimeFormat('en',{timeZone:name}).format();validZones.add(name);}return name;\n}\nexport function defaults(){return {clock:Intl.DateTimeFormat(undefined,{hour:'numeric'}).resolvedOptions().hour12?'12':'24',seconds:true,zone:'system',surf:true,sky:true,tips:5,constellationInterval:10};}\nexport function jumpinSettings(){return {clock:'24',seconds:false,zone:'system',surf:false,sky:true,tips:5,constellationInterval:10};}\nexport function normalize(raw={}){\n const d=defaults(),v={...d,...raw};v.clock=['12','24','off'].includes(v.clock)?v.clock:d.clock;\n v.seconds=typeof v.seconds==='boolean'?v.seconds:d.seconds;v.surf=typeof v.surf==='boolean'?v.surf:d.surf;v.sky=typeof v.sky==='boolean'?v.sky:d.sky;\n v.tips=intervals.includes(Number(v.tips))?Number(v.tips):d.tips;\n const cadence=Number(v.constellationInterval);v.constellationInterval=Number.isFinite(cadence)&&cadence>=10&&cadence<=60?Math.round(cadence/5)*5:d.constellationInterval;\n try{zone(v.zone);}catch{v.zone='system';}return v;\n}",
            "note": "The reminder slider uses seven positions mapped through intervals[]. The constellation slider stores seconds directly. constellationOptions() passes that interval to the animation scheduler."
          }
        },
        {
          "id": "tips",
          "title": "Turn wall-clock time into reminders",
          "description": "Convert the chosen timezone into hour, minute and second values. A modulo calculation finds progress through the selected interval; the first30 seconds display the corresponding line from a288-message daily sequence.",
          "artifact": {
            "title": "Local-time reminder schedule",
            "language": "JavaScript",
            "code": "export function reminder(now,settings,messages){\n if(!settings.tips)return null;const p=parts(now,settings.zone),minute=Number(p.hour)*60+Number(p.minute),age=(minute%settings.tips)*60000+Number(p.second)*1000+((now%1000)+1000)%1000;\n return age<30150?{text:messages[Math.floor(minute/5)],age,slot:Math.floor(minute/5)}:null;\n}\nexport function tipOpacity(age,x=0){const smooth=v=>{v=Math.max(0,Math.min(1,v));return v*v*(3-2*v);},delay=Math.max(0,Math.min(1,x))*55;return age<30000?smooth((age-delay)/95):1-smooth((age-30000-delay)/95);}",
            "note": "minute % settings.tips is the number of minutes into the current reminder interval. slot selects one of288 five-minute messages; age controls the30-second visibility and fade."
          },
          "related": [
            "settings"
          ]
        },
        {
          "id": "water",
          "title": "Connect wave data to the scene",
          "description": "The Marine API supplies wave height and period near Scripps Pier. The model converts metres to feet for display, and the renderer uses the height to shape its small animated wave. The panel labels this as an offshore model estimate.",
          "artifact": {
            "title": "Wave data model",
            "language": "JavaScript",
            "code": "export function marine(data,now=Date.now()){\n const c=data?.current,t=Number(c?.time)*1000;\n if(!Number.isFinite(c?.wave_height)||c.wave_height<0||c.wave_height>30||!Number.isFinite(t)||Math.abs(now-t)>3*3600000)return null;\n return {feet:c.wave_height*3.28084,period:Number.isFinite(c.wave_period)?c.wave_period:null,time:t};\n}",
            "note": "wave_height arrives in metres. Multiplying by3.28084 yields feet. period is kept in seconds and time is stored as a JavaScript timestamp."
          },
          "related": [
            "settings"
          ]
        },
        {
          "id": "quiet",
          "title": "Start the scene with one action",
          "description": "Jump in passes defaults() directly to enterStars(). Applying custom settings calls the same function with the form values. It creates the configured constellation system, closes settings, and gives the canvas the full screen.",
          "artifact": {
            "title": "Scene entry and Jump in",
            "language": "JavaScript",
            "code": "function enterStars(value,wantFullscreen=false,persist=true){\n settings=normalize(value);if(persist)try{localStorage.setItem(storageKey,JSON.stringify(settings));}catch{}\n running=true;skyDisplay=skyPending=null;window.ScrippsConstellations=window.ScrippsConstellationSystem.create(constellationOptions(settings));dialog.close();controls.hidden=false;document.body.classList.add('running');\n if(wantFullscreen&&!document.fullscreenElement)fullscreen();keepAwake();reveal();if(settings.surf)loadWaves();canvas.focus({preventScroll:true});\n}\ndocument.querySelector('#jump-in').addEventListener('click',()=>enterStars(defaults()));",
            "note": "The two entry paths share initialization, so default and customized screens use the same rendering code. The configuration object is saved locally before the canvas takes over."
          },
          "related": [
            "settings"
          ]
        },
        {
          "id": "checks",
          "title": "Check the behavior, then publish",
          "description": "The desktop constellation work passed 150 database, geometry, timing and overlay checks. The browser adaptation adds focused model and page tests plus the existing website build. Browser layout and interactions are checked separately.",
          "artifact": {
            "title": "Reproducible checks",
            "language": "shell",
            "code": "node scripts/build-stars-workflow.mjs\nnode --test scripts/test-stars.mjs\nnpm test",
            "note": "The focused tests cover time formatting, reminder intervals, slider values and actual constellation lock spacing. The site build checks the published pages and their links."
          },
          "related": [
            "tips",
            "water",
            "quiet"
          ]
        }
      ]
    }
  ]
}
