Widget:TwinSuns: Difference between revisions

From WyrdeWiki
(Created page with "<noinclude>__NOTOC__ This widget displays WyrdeHaven’s Twin Suns with seasonal wobble and calendar. =Usage= {{#widget:TwinSuns | id=wh | size=512 | fps=5 | solara=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png | vaelus=https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png | backdrop=http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png | cosine=http://wyrdehavenchronicl.es/images/Cosinus.png }} {| class="wikitable" |- ! Param !! Meaning...")
 
No edit summary
 
(17 intermediate revisions by the same user not shown)
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=
{{#widget:TwinSuns
{{#widget:TwinSuns
  | id=wh
  | id=ts1
  | size=512
  | size=768
  | fps=5
  | fps=5
| wobble=5
| equator=8
| sep=2.5
| degpx=10
  | solara=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png
  | solara=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png
  | vaelus=https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png
  | vaelus=https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png
  | backdrop=http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png
  | sky=http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png
  | cosine=http://wyrdehavenchronicl.es/images/Cosinus.png
  | cos=http://wyrdehavenchronicl.es/images/Cosinus.png
}}
}}
{| class="wikitable"
{| class="wikitable"
|-
|-
! Param !! Meaning !! Default
! param !! meaning !! default
|-
|-
| id || Unique id prefix || wh
| size || max stage width (px) || 768
|-
|-
| size || Stage width/height in px || 512
| fps || play speed (frames/s) || 5
|-
|-
| fps || Animation fps || 5
| wobble || solar relative wobble amplitude (Δ, degrees) || 2.5
|-
|-
| solara || Solara PNG (transparent) || (URL above)
| equator || seasonal/barycenter max tilt (degrees) || 8
|-
|-
| vaelus || Vaelus PNG (transparent) || (URL above)
| sep || visible horizontal separation between suns (degrees) || 2.5
|-
|-
| backdrop || Sky backdrop image || (URL above)
| degpx || vertical degree→pixel scale (px per degree) || 10
|-
| cosine || Cosinus trace PNG (transparent) || (URL above)
|}
|}
</noinclude><includeonly>
</noinclude><includeonly>
<div class="wh-twinsuns" style="max-width:<!--{$size|default:512|escape:'html'}-->px;margin:1rem 0;">
<div class="wh-twins" style="max-width:<!--{$size|default:768|escape:'html'}-->px;margin:1rem 0;">
   <!-- Day number centered over slider -->
   <!-- centered day label -->
   <div style="display:flex;justify-content:center;font-weight:600;margin-bottom:.25rem;">
   <div id="<!--{$id|default:'ts'|escape:'html'}-->-daylabel"
    <span id="<!--{$id|default:'wh'|escape:'html'}-->-daynum">1</span> / 479
      style="text-align:center;font-weight:600;margin-bottom:.25rem;">1 / 479</div>
  </div>


   <!-- Slider with cosine behind -->
   <!-- slider + cosine -->
   <div style="position:relative;display:flex;align-items:center;gap:.5rem;margin-bottom:.25rem;">
   <div style="position:relative;display:flex;align-items:center;gap:.5rem;">
     <div style="position:relative;flex:1;">
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-cos"
      <img id="<!--{$id|default:'wh'|escape:'html'}-->-cos"
        src="<!--{$cos|escape:'html'}-->"
          src="<!--{$cosine|default:'http://wyrdehavenchronicl.es/images/Cosinus.png'|escape:'html'}-->"
        alt="" decoding="async"
          alt="Cosinus"
        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;"/>
          decoding="async" fetchpriority="low"
    <input id="<!--{$id|default:'ts'|escape:'html'}-->-x" type="range" min="1" max="479" value="1"
          style="position:absolute;inset:0;object-fit:contain;pointer-events:none;"/>
          style="flex:1;position:relative;z-index:1;">
      <input id="<!--{$id|default:'wh'|escape:'html'}-->-x" type="range" min="1" max="479" value="1"
     <button id="<!--{$id|default:'ts'|escape:'html'}-->-play"
            style="position:relative;width:100%;background:transparent;">
             style="font-size:1rem;padding:.2rem .5rem;cursor:pointer;">▶</button>
    </div>
     <button id="<!--{$id|default:'wh'|escape:'html'}-->-play"
             style="font-size:1rem;padding:.2rem .5rem;cursor:pointer;line-height:1;">▶</button>
   </div>
   </div>


   <!-- Date text -->
   <!-- date line moved slightly lower to avoid cosine overlap -->
   <div style="margin:.25rem 0;">
   <div style="margin:2rem 0 .4rem 0;">
     <strong>Date:</strong>
     <strong>Date:</strong>
     <span id="<!--{$id|default:'wh'|escape:'html'}-->-date">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:'wh'|escape:'html'}-->-stage"
   <div id="<!--{$id|default:'ts'|escape:'html'}-->-stage"
       style="position:relative;width:<!--{$size|default:512|escape:'html'}-->px;height:<!--{$size|default:512|escape:'html'}-->px;isolation:isolate;border:1px solid #333;overflow:hidden;background:#9cccf6;">
       style="position:relative;width:100%;aspect-ratio:3/2;isolation:isolate;background:#000;border:1px solid #333;overflow:hidden;">
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-bg"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-sky"
         src="<!--{$backdrop|default:'http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png'|escape:'html'}-->"
         src="<!--{$sky|escape:'html'}-->" alt="Sky" decoding="async"
        alt="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;z-index:1;"/>
         src="<!--{$solara|escape:'html'}-->" alt="Solara"
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-solara"
         style="position:absolute;z-index:3;transform:translate(-50%,-50%);"/>
         src="<!--{$solara|default:'https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png'|escape:'html'}-->"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-vaelus"
        alt="Solara"
         src="<!--{$vaelus|escape:'html'}-->" alt="Vaelus"
         style="position:absolute;transform:translate(-50%,-50%);z-index:3;width:24%;max-width:180px;image-rendering:auto;"/>
         style="position:absolute;z-index:2;transform:translate(-50%,-50%);"/>
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-vaelus"
         src="<!--{$vaelus|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;width:18%;max-width:140px;image-rendering:auto;"/>
   </div>
   </div>


  <!-- Inclination info -->
   <div style="margin-top:.5rem;">
   <div style="margin-top:.5rem;line-height:1.4;">
     <div><strong>Relative Solar Inclination (Δ):</strong>
     <div><strong>Relative Solar Inclination (Δ):</strong>
       <span id="<!--{$id|default:'wh'|escape:'html'}-->-rel">+2.50°</span>
       <span id="<!--{$id|default:'ts'|escape:'html'}-->-relinc">+2.50° (Closing)</span>
      (<span id="<!--{$id|default:'wh'|escape:'html'}-->-state">Closing</span>)
     </div>
     </div>
     <div><strong>Seasonal Equatorial Inclination (barycenter):</strong>
     <div><strong>Seasonal Equatorial Inclination (barycenter):</strong>
       <span id="<!--{$id|default:'wh'|escape:'html'}-->-bar">-2.50°</span>
       <span id="<!--{$id|default:'ts'|escape:'html'}-->-eqinc">-8.00°</span>
     </div>
     </div>
   </div>
   </div>
Line 91: Line 82:
<script type="text/javascript">
<script type="text/javascript">
(function(){
(function(){
   var ID = "<!--{$id|default:'wh'|escape:'js'}-->";
   var ID = "<!--{$id|default:'ts'|escape:'js'}-->";
   function ge(s){ return document.getElementById(ID + s); }
   var FPS = parseFloat("<!--{$fps|default:5|escape:'js'}-->") || 5;
   var el = {
   var WOBBLE  = parseFloat("<!--{$wobble|default:2.5|escape:'js'}-->") || 2.5; // observable Δ (deg)
    x:   ge("-x"),
  var EQUATOR = parseFloat("<!--{$equator|default:8|escape:'js'}-->") || 8;  // seasonal max (deg)
    day: ge("-daynum"),
  var SEP     = parseFloat("<!--{$sep|default:2.5|escape:'js'}-->")   || 2.5; // horizontal sep (deg)
    date:ge("-date"),
   var DEGPX  = parseFloat("<!--{$degpx|default:10|escape:'js'}-->")   || 10;  // px per degree
     cos: ge("-cos"),
 
    play:ge("-play"),
   // Find and init exactly one instance in a given DOM scope
    stage:ge("-stage"),
  function initIn(root){
    s:   ge("-solara"),
     var wrap = root.querySelector('.wh-twins');
    v:   ge("-vaelus"),
     if(!wrap) return;
    rel: ge("-rel"),
     bar: ge("-bar"),
     st:  ge("-state")
  };


  // sizing
    function ge(s){ return document.getElementById(ID + s); }
  var SIZE = <!--{$size|default:512|escape:'js'}-->;
    var el = {
  // animation
      day: ge("-daylabel"),
  var FPS = <!--{$fps|default:5|escape:'js'}-->;
      rng: ge("-x"),
  var interval = 1000 / Math.max(1, FPS);
      play: ge("-play"),
  var timer = null, playing = false;
      date: ge("-date"),
      stage: ge("-stage"),
      sky: ge("-sky"),
      s: ge("-solara"),
      v: ge("-vaelus"),
      rel: ge("-relinc"),
      eq: ge("-eqinc")
    };
    for (var k in el){ if(!el[k]) return; } // quietly bail if something’s missing


  // months (15×31), Fortnight mapping per spec
    var TAU = Math.PI*2;
  var months = [
    function cosPhase(d){ return Math.cos(TAU*((d-1)/479)); }
    "Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren","Veyrath",
    function fmtDeg(x){ var s=(x>=0?"+":""); return s + x.toFixed(2) + "°"; }
     "Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"
     function openingClosing(_, phase){
  ];
      var s = Math.sin(TAU*phase);
      return (-s>0) ? "Closing" : "Opening";
    }


  function dateText(D){
    var months = ["Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren","Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"];
    // D: 1..479
    function dateText(d){
    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);
       if(d>=473){var k=d-472; return "Fortnight of Renewal (Fading), day " + k + " (FR " + k + ")";}
      var n=d-7, m=Math.ceil(n/31), day=((n-1)%31)+1; return day + " " + months[m-1];
     }
     }
     if(D >= 473){
 
       return "Fortnight of Renewal (Fading), day " + (D - 472);
     var SOL_SHARE=1.0, VAE_SHARE=1.5, SUM=SOL_SHARE+VAE_SHARE;
 
    function setStarSizes(){
      var w = el.stage.clientWidth || el.stage.getBoundingClientRect().width || 0;
       if(!w) return;
      var base = Math.round(w*0.08);
      el.s.width = el.s.height = base;
      el.v.width = el.v.height = Math.round(base*0.8);
     }
     }
    var idx = D - 7; // 1-based inside the 465-day span
    var mIx = Math.ceil(idx / 31) - 1; // 0..14
    var dIn = ((idx - 1) % 31) + 1;    // 1..31
    return months[mIx] + " " + dIn;
  }


  // inclination model
     function update(){
  // Day 1 => +2.5° (max), midsummer => -2.5°.
      try{
  var TWO_PI = Math.PI * 2;
        var d = parseInt(el.rng.value,10) || 1;
  function relIncl(D){ // degrees
        var phase = (d-1)/479;
     return 2.5 * Math.cos(TWO_PI * ( (D-1) / 479 ));
  }
  function barIncl(D){ // inverted sign (lowest at midwinter when rel=+2.5)
    return -relIncl(D);
  }
  function openingOrClosing(D){
    var cur = Math.abs(relIncl(D));
    var nxt = Math.abs(relIncl(D === 479 ? 1 : D+1));
    return (nxt < cur) ? "Closing" : "Opening";
  }


  // stage placement
        // Your “observable” Δ already in sky-arc degrees
  // Base anchor for the pair (roughly upper right sky)
        var delta = (WOBBLE * cosPhase(d)) / 8;     // ±WOBBLE
  var anchor = {
        var eq    = -EQUATOR * cosPhase(d);   // lowest at midwinter
    x: Math.round(SIZE * 0.68),
    y: Math.round(SIZE * 0.22)
  };
  // Horizontal separation cue (~constant small separation to suggest 2.5°)
  var sepX = Math.round(SIZE * 0.10); // tweakable; ~10% width
  // Vertical wobble scale (px per degree), makes Δ±2.5° visible but subtle
  var pxPerDeg = Math.max(6, Math.round(SIZE * 0.03 / 2.5)); // scales with size


  function placeSuns(D){
        el.day.textContent  = d + " / 479";
    var rel = relIncl(D); // ±2.5°
        el.date.textContent = dateText(d);
    // split the relative Δ: Solara +Δ/2, Vaelus -Δ/2
        el.rel.textContent  = fmtDeg(delta) + " (" + openingClosing(delta, phase) + ")";
    var dY = (rel * pxPerDeg) / 2;
        el.eq.textContent  = fmtDeg(eq);


    var sX = anchor.x - Math.round(sepX/2);
        var w = el.stage.clientWidth, h = el.stage.clientHeight;
    var vX = anchor.x + Math.round(sepX/2);
        if(!w||!h) return;
    var sY = anchor.y - Math.round(dY);
    var vY = anchor.y + Math.round(dY);


    el.s.style.left = sX + "px";
        var cx = w*0.5, cy = h*0.33;
    el.s.style.top  = sY + "px";
        var dx = (SEP/2)*DEGPX;
    el.v.style.left = vX + "px";
    el.v.style.top  = vY + "px";
  }


  function fmtDeg(x){
        var solOff =  (delta*(SOL_SHARE/SUM))*DEGPX;
    var sign = (x >= 0) ? "+" : "−";
        var vaeOff = -(delta*(VAE_SHARE/SUM))*DEGPX;
    return sign + Math.abs(x).toFixed(2) + "°";
        var yBase  = cy - (eq*DEGPX);
  }


  function update(){
        el.s.style.left = (cx - dx) + "px";
    var D = parseInt(el.x.value,10) || 1;
        el.v.style.left = (cx + dx) + "px";
    el.day.textContent = D;
        el.s.style.top  = (yBase - solOff) + "px";
    el.date.textContent = dateText(D);
        el.v.style.top  = (yBase - vaeOff) + "px";
      }catch(e){}
    }


     var r = relIncl(D);
     function ensureSizedThen(fn){
    var b = barIncl(D);
      setStarSizes();
    el.rel.textContent = fmtDeg(r);
      if(el.stage.clientWidth) fn();
    el.bar.textContent = fmtDeg(b);
      else setTimeout(function(){ ensureSizedThen(fn); }, 50);
     el.st.textContent  = openingOrClosing(D);
     }


     placeSuns(D);
     var timer=null, playing=false, interval=1000/Math.max(1,FPS);
  }
    function step(){ var v=parseInt(el.rng.value,10)||1; el.rng.value=(v>=479)?1:(v+1); update(); }


  // Slider input throttled by rAF
    el.rng.addEventListener("input", update);
  var raf = null;
    el.play.addEventListener("click", function(){
  el.x.addEventListener("input", function(){
      if(!playing){ playing=true; el.play.textContent="■"; timer=setInterval(step, interval); }
    if(raf) return;
      else { playing=false; el.play.textContent="▶"; clearInterval(timer); timer=null; }
    raf = requestAnimationFrame(function(){ raf=null; update(); });
    });
  });
    window.addEventListener("resize", function(){ setStarSizes(); update(); });


  function step(){
     ensureSizedThen(update);
    var v = parseInt(el.x.value,10) || 1;
     v = (v >= 479) ? 1 : (v+1);
    el.x.value = v;
    update();
   }
   }


   el.play.addEventListener("click", function(){
   // Run on normal load
    if(!playing){
  if (document.readyState === "complete" || document.readyState === "interactive"){
      playing = true;
    initIn(document);
      el.play.textContent = "■";
  } else {
      timer = setInterval(step, interval);
    document.addEventListener("DOMContentLoaded", function(){ initIn(document); }, {once:true});
    } else {
   }
      playing = false;
      el.play.textContent = "▶";
      clearInterval(timer); timer = null;
    }
   });


   // Initial paint
   // Also run when MediaWiki injects content (tabs, previews, etc.)
   update();
   if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(function($c){
      initIn($c && $c[0] ? $c[0] : document);
    });
  }
})();
})();
</script>
</script>
</includeonly>
</includeonly>

Latest revision as of 21:50, 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