Widget:TwinSuns: Difference between revisions

From WyrdeWiki
No edit summary
No edit summary
Line 1: Line 1:
<noinclude>__NOTOC__
<noinclude>__NOTOC__
This widget displays WyrdeHaven’s Twin Suns with seasonal wobble and calendar.
This widget shows WyrdeHaven’s twin suns with seasonal wobble & calendar.


=Usage=
=Usage=
Line 6: Line 6:
  | id=ts1
  | id=ts1
  | size=768
  | size=768
  | fps=8
  | fps=5
  | seasonAmpDeg=2.5
  | wobble=2.5
  | wobbleSepDeg=2.5
  | equator=8
  | solaraUrl=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png
| sep=2.5
  | vaelusUrl=https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png
  | degpx=10
  | skyUrl=http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png
| solara=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png
  | cosUrl=http://wyrdehavenchronicl.es/images/Cosinus.png
  | vaelus=https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png
  | sky=http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png
  | cos=http://wyrdehavenchronicl.es/images/Cosinus.png
}}
}}
{| class="wikitable"
|-
! param !! meaning !! default
|-
| size || max stage width (px) || 768
|-
| fps || play speed (frames/s) || 5
|-
| wobble || solar relative wobble amplitude (Δ, degrees) || 2.5
|-
| equator || seasonal/barycenter max tilt (degrees) || 8
|-
| sep || visible horizontal separation between suns (degrees) || 2.5
|-
| degpx || vertical degree→pixel scale (px per degree) || 10
|}
</noinclude><includeonly>
</noinclude><includeonly>
<div class="wh-twinsuns" style="max-width:<!--{$size|default:768|escape:'html'}-->px;margin:1rem 0;">
<div class="wh-twins" style="max-width:<!--{$size|default:768|escape:'html'}-->px;margin:1rem 0;">
   <!-- centered day number above the slider -->
   <!-- centered day label -->
   <div style="text-align:center;margin-bottom:.25rem;">
   <div id="<!--{$id|default:'ts'|escape:'html'}-->-daylabel"
    <span id="<!--{$id|default:'ts'|escape:'html'}-->-daynum">1</span> / 479
      style="text-align:center;font-weight:600;margin-bottom:.25rem;">1 / 479</div>
  </div>


   <!-- slider + cosine (cosine sits behind, same width) -->
   <!-- slider + cosine -->
   <div style="position:relative;display:flex;align-items:center;gap:.5rem;">
   <div style="position:relative;display:flex;align-items:center;gap:.5rem;">
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-cos"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-cos"
         src="<!--{$cosUrl|default:'http://wyrdehavenchronicl.es/images/Cosinus.png'|escape:'html'}-->"
         src="<!--{$cos|escape:'html'}-->"
         alt="Cosine"
         alt="" decoding="async"
         style="position:absolute;left:0;right:2.2rem;height:22px;object-fit:fill;opacity:.6;pointer-events:none;"/>
         style="position:absolute;left:0;right:2.25rem;top:50%;transform:translateY(-50%);opacity:.6;pointer-events:none;width:calc(100% - 2.25rem);height:auto;"/>
     <input id="<!--{$id|default:'ts'|escape:'html'}-->-x" type="range" min="1" max="479" value="1"
     <input id="<!--{$id|default:'ts'|escape:'html'}-->-x" type="range" min="1" max="479" value="1"
           style="flex:1;position:relative;z-index:1;">
           style="flex:1;position:relative;z-index:1;">
     <button id="<!--{$id|default:'ts'|escape:'html'}-->-play"
     <button id="<!--{$id|default:'ts'|escape:'html'}-->-play"
             style="font-size:1rem;padding:.2rem .5rem;cursor:pointer;min-width:2rem;">▶</button>
             style="font-size:1rem;padding:.2rem .5rem;cursor:pointer;">▶</button>
   </div>
   </div>


   <!-- add breathing room below the slider so text never crosses the cosine -->
   <!-- date line with extra top margin to avoid cosine overlap -->
  <div style="height:.5rem"></div>
   <div style="margin:.65rem 0 .35rem 0;">
 
   <div style="margin:.25rem 0 .5rem 0;">
     <strong>Date:</strong>
     <strong>Date:</strong>
     <span id="<!--{$id|default:'ts'|escape:'html'}-->-datestr">Fortnight of Renewal (Dawning), day 8</span>
     <span id="<!--{$id|default:'ts'|escape:'html'}-->-date">Fortnight of Renewal (Dawning), day 8 (FR 8)</span>
   </div>
   </div>


   <!-- stage -->
   <!-- stage -->
   <div id="<!--{$id|default:'ts'|escape:'html'}-->-stage"
   <div id="<!--{$id|default:'ts'|escape:'html'}-->-stage"
       style="position:relative;width:100%;aspect-ratio:3/2;isolation:isolate;border:1px solid #333;background:#000;overflow:hidden;">
       style="position:relative;width:100%;aspect-ratio:3/2;isolation:isolate;background:#000;border:1px solid #333;overflow:hidden;">
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-sky"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-sky"
         src="<!--{$skyUrl|default:'http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png'|escape:'html'}-->"
         src="<!--{$sky|escape:'html'}-->" alt="Sky" decoding="async"
        alt="Sky backdrop"
         style="position:absolute;inset:0;object-fit:contain;object-position:center;z-index:1;width:100%;height:100%;"/>
        decoding="async" fetchpriority="high" draggable="false"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-solara"
         style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;z-index:1;"/>
         src="<!--{$solara|escape:'html'}-->" alt="Solara"
    <!-- suns -->
         style="position:absolute;z-index:3;transform:translate(-50%,-50%);"/>
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-sol"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-vaelus"
         src="<!--{$solaraUrl|default:'https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png'|escape:'html'}-->"
         src="<!--{$vaelus|escape:'html'}-->" alt="Vaelus"
        alt="Solara"
         style="position:absolute;z-index:2;transform:translate(-50%,-50%);"/>
         style="position:absolute;transform:translate(-50%,-50%);z-index:3;pointer-events:none;"/>
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-vae"
         src="<!--{$vaelusUrl|default:'https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png'|escape:'html'}-->"
        alt="Vaelus"
         style="position:absolute;transform:translate(-50%,-50%);z-index:2;pointer-events:none;"/>
   </div>
   </div>


   <div style="margin:.5rem 0 0 0;">
   <div style="margin-top:.5rem;">
     <div><strong>Relative Solar Inclination (Δ):</strong> <span id="<!--{$id|default:'ts'|escape:'html'}-->-delta">+2.50°</span> <span id="<!--{$id|default:'ts'|escape:'html'}-->-phase">(Closing)</span></div>
     <div><strong>Relative Solar Inclination (Δ):</strong>
     <div><strong>Seasonal Equatorial Inclination (barycenter):</strong> <span id="<!--{$id|default:'ts'|escape:'html'}-->-season">+0.00°</span></div>
      <span id="<!--{$id|default:'ts'|escape:'html'}-->-relinc">+2.50° (Closing)</span>
    </div>
     <div><strong>Seasonal Equatorial Inclination (barycenter):</strong>
      <span id="<!--{$id|default:'ts'|escape:'html'}-->-eqinc">-8.00°</span>
    </div>
   </div>
   </div>
</div>
</div>
Line 70: Line 84:
   var ID  = "<!--{$id|default:'ts'|escape:'js'}-->";
   var ID  = "<!--{$id|default:'ts'|escape:'js'}-->";
   var FPS = parseFloat("<!--{$fps|default:5|escape:'js'}-->") || 5;
   var FPS = parseFloat("<!--{$fps|default:5|escape:'js'}-->") || 5;
 
   var WOBBLE = parseFloat("<!--{$wobble|default:2.5|escape:'js'}-->") || 2.5;     // Δ amplitude (deg)
  // Configurable amplitudes (degrees)
   var EQUATOR = parseFloat("<!--{$equator|default:8|escape:'js'}-->") || 8;      // seasonal max (deg)
   var SEASON_AMP = parseFloat("<!--{$seasonAmpDeg|default:2.5|escape:'js'}-->") || 2.5;  // barycenter (seasonal) amplitude
   var SEP = parseFloat("<!--{$sep|default:2.5|escape:'js'}-->") || 2.5;           // horizontal sep (deg)
   var WOBBLE_SEP = parseFloat("<!--{$wobbleSepDeg|default:2.5|escape:'js'}-->") || 2.5;  // total Solara–Vaelus separation cue
   var DEGPX = parseFloat("<!--{$degpx|default:10|escape:'js'}-->") || 10;        // px per degree


   function ge(s){ return document.getElementById(ID + s); }
   function ge(s){ return document.getElementById(ID + s); }
   var el = {
   var el = {
     daynum: ge("-daynum"),
     day: ge("-daylabel"),
     x:     ge("-x"),
     rng: ge("-x"),
     play:   ge("-play"),
     play: ge("-play"),
     cos:   ge("-cos"),
     date: ge("-date"),
     stage: ge("-stage"),
     stage: ge("-stage"),
     sky:   ge("-sky"),
     sky: ge("-sky"),
     sol:    ge("-sol"),
     s: ge("-solara"),
    vae:    ge("-vae"),
     v: ge("-vaelus"),
    dates: ge("-datestr"),
     rel: ge("-relinc"),
     delta: ge("-delta"),
     eq: ge("-eqinc")
     phase: ge("-phase"),
     season: ge("-season")
   };
   };


   // keep cosine exactly behind the slider track
   // sizes: keep suns proportionally similar to your PNGs
   function syncCosWidth(){
   function setStarSizes(){
     var r = el.x.getBoundingClientRect();
     var w = el.stage.clientWidth;
     var C = el.cos;
    var base = Math.round(w * 0.05);     // tweakable
     C.style.left = "0";
     el.s.width = base; el.s.height = base;
     C.style.right = (r.height + 6) + "px"; // reserve space for the play button
     el.v.width = Math.round(base * 0.72);
    C.style.top = ((r.height - 22)/2) + "px";
     el.v.height = Math.round(base * 0.72);
  }
  setStarSizes();
  window.addEventListener("resize", setStarSizes);
 
  // helpers
  var TAU = Math.PI * 2;
  function cosPhase(d){ return Math.cos(TAU * ( (d-1) / 479 )); }  // day1 => +1.0
  function fmtDeg(x){ var s = (x>=0?"+":""); return s + x.toFixed(2) + "°"; }
  function openingClosing(val, phase){ // val is Δ(d); dφ/dt sign decides
    // derivative of cos is -sin; positive means decreasing (Closing), negative means Opening
    var s = Math.sin(TAU * ((phase))); // phase=(d-1)/479
    return ( -s > 0 ? "Closing" : "Opening" );
   }
   }
  window.addEventListener("resize", syncCosWidth);


   // calendar helpers
   // calendar text
   var months = ["Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren","Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"];
   var months = ["Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren","Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"];
 
   function dateText(d){
   function dateLabel(d){
    // d: 1..479  (year starts at midwinter)
     if(d <= 7){
     if(d <= 7){
       return "Fortnight of Renewal (Dawning), day " + (d+7) + " (FR " + (d+7) + ")";
       return "Fortnight of Renewal (Dawning), day " + (d+7) + " (FR " + (d+7) + ")";
     }
     } else if (d >= 473){
    if(d >= 473){
       var k = d - 472; // 1..7
       var k = d - 472; // 1..7
       return "Fortnight of Renewal (Fading), day " + k + " (FR " + k + ")";
       return "Fortnight of Renewal (Fading), day " + k + " (FR " + k + ")";
    } else {
      var n = d - 7;                // 1..465
      var m = Math.ceil(n / 31);    // 1..15
      var day = ((n-1) % 31) + 1;    // 1..31
      return day + " " + months[m-1];
     }
     }
    // months: 15 × 31 days = 465
    var n = d - 7; // 1..465
    var mIndex = Math.floor((n-1)/31); // 0..14
    var dayInMonth = ((n-1)%31) + 1;
    return dayInMonth + " " + months[mIndex];
  }
  // trig helpers
  var TWO_PI = Math.PI*2;
  function cosCycle(day){ return Math.cos(TWO_PI * (day-1) / 479); } // day 1 -> +1 (midwinter max)
  // mapping degrees->pixels; assume ~90° sky spans full stage height -> tweakable if needed
  function pxPerDeg(){
    var h = el.stage.clientHeight;
    return h / 90; // 1° ≈ stageHeight/90
   }
   }


   // place suns
   // layout constants
   function layout(day){
   var SOL_SHARE = 1.0, VAE_SHARE = 1.5; // sum = Δ split
    // seasonal declination (barycenter)
  var SUM = SOL_SHARE + VAE_SHARE;
    var seasonDeg = SEASON_AMP * cosCycle(day);
 
    // relative split stays constant at WOBBLE_SEP; Solara above when positive season phase starts (day 1)
    var halfSep = WOBBLE_SEP / 2;
 
    // pixel mapping
    var k = pxPerDeg();
 
    var stageRect = el.stage.getBoundingClientRect();
    var W = el.stage.clientWidth, H = el.stage.clientHeight;
 
    // center X for both suns
    var cx = W * 0.50;
 
    // base vertical position near upper third of sky
    var baseY = H * 0.28;
 
    var ySeason = baseY - (seasonDeg * k);
    var ySol = ySeason - (halfSep * k);
    var yVae = ySeason + (halfSep * k);
 
    // set sizes (Solara bigger than Vaelus, keep proportion within 256 canvas scale)
    var solSize = Math.round(Math.min(W, H) * 0.14);
    var vaeSize = Math.round(solSize * 0.65);
 
    Object.assign(el.sol.style, { left: cx+"px", top: ySol+"px", width: solSize+"px", height: "auto" });
    Object.assign(el.vae.style, { left: cx+"px", top: yVae+"px", width: vaeSize+"px", height: "auto" });
 
    // texts
    el.delta.textContent = (WOBBLE_SEP >= 0 ? "+" : "") + WOBBLE_SEP.toFixed(2) + "°";
    el.season.textContent = (seasonDeg>=0?"+":"") + seasonDeg.toFixed(2) + "°";
 
    // Opening/Closing: |season| increasing -> Opening; decreasing -> Closing
    // derivative sign of |A cos| at day ~ compare neighboring days
    var eps = 1;
    var prev = Math.abs(SEASON_AMP * cosCycle(Math.max(1, day-eps)));
    var next = Math.abs(SEASON_AMP * cosCycle(Math.min(479, day+eps)));
    el.phase.textContent = (next > prev) ? "(Opening)" : "(Closing)";
  }


   function update(){
   function update(){
     var d = parseInt(el.x.value,10);
     var d = parseInt(el.rng.value,10);
     el.daynum.textContent = d;
     var phase = (d-1)/479;
    el.dates.textContent  = dateLabel(d);
    layout(d);
  }


  // scrub throttle
    // angles (deg)
  var raf = null;
     var delta = WOBBLE * cosPhase(d);       // +Δ at day 1
  el.x.addEventListener("input", function(){
     var eq = -EQUATOR * cosPhase(d);         // lowest (negative) at day 1, highest midsummer
     if(raf) return;
     raf = requestAnimationFrame(function(){ raf=null; update(); });
  });


  // play/pause
     el.day.textContent = d + " / 479";
  var timer=null, playing=false, interval=1000/Math.max(1,FPS);
     el.date.textContent = dateText(d);
  function step(){
     var v = parseInt(el.x.value,10);
    v = (v>=479)?1:(v+1);
    el.x.value = v;
    update();
  }
  el.play.addEventListener("click", function(){
    if(!playing){
      playing=true; el.play.textContent="■";
      timer = setInterval(step, interval);
     }else{
      playing=false; el.play.textContent="▶";
      clearInterval(timer); timer=null;
    }
  });


  // initial sync
    var oc = openingClosing(delta, phase);
  syncCosWidth();
    el.rel.textContent = fmtDeg(delta) + " (" + oc + ")";
  update();
    el
})();
</script>
</includeonly>

Revision as of 20:24, 11 November 2025

This widget shows WyrdeHaven’s twin suns with seasonal wobble & calendar.

Usage

1 / 479
Date: Fortnight of Renewal (Dawning), day 8 (FR 8)
Sky Solara Vaelus
Relative Solar Inclination (Δ): +2.50° (Closing)
Seasonal Equatorial Inclination (barycenter): -8.00°

param meaning default
size max stage width (px) 768
fps play speed (frames/s) 5
wobble solar relative wobble amplitude (Δ, degrees) 2.5
equator seasonal/barycenter max tilt (degrees) 8
sep visible horizontal separation between suns (degrees) 2.5
degpx vertical degree→pixel scale (px per degree) 10