Widget:TwinSuns: Difference between revisions

From WyrdeWiki
No edit summary
No edit summary
 
(15 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=
Line 7: Line 7:
  | size=768
  | size=768
  | fps=5
  | fps=5
  | seasonAmp=8         <!-- seasonal equatorial inclination amplitude (deg) -->
  | wobble=5
  | wobbleTotal=2.5     <!-- total relative wobble between suns (deg); split 40/60 -->
| equator=8
  | solara=/wiki/images/f/fe/Solara.png
  | sep=2.5
  | vaelus=/wiki/images/7/7a/Vaelus.png
| degpx=10
  | sky=/images/WyrdeHaven_Sky.png
  | solara=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png
  | cosine=/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;">
   <!-- Top line with centered day readout -->
   <!-- centered day label -->
   <div style="display:flex;align-items:center;gap:.5rem;">
   <div id="<!--{$id|default:'ts'|escape:'html'}-->-daylabel"
    <div style="flex:1;height:0.1rem;"></div>
      style="text-align:center;font-weight:600;margin-bottom:.25rem;">1 / 479</div>
    <div id="<!--{$id|default:'wh'|escape:'html'}-->-daycenter"
        style="font-weight:600;">1 / 479</div>
    <div style="flex:1;height:0.1rem;"></div>
  </div>


   <!-- Slider + cosine (cosine exactly the slider width, aligned) -->
   <!-- slider + cosine -->
   <div style="position:relative;display:flex;align-items:center;gap:.5rem;margin:.25rem 0 .5rem;">
   <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 src="<!--{$cosine|default:'/images/Cosinus.png'|escape:'html'}-->"
        src="<!--{$cos|escape:'html'}-->"
          alt="cosine" decoding="async" draggable="false"
        alt="" decoding="async"
          style="position:absolute;left:0;top:50%;transform:translateY(-50%);width:100%;height:auto;pointer-events:none;opacity:.65;"/>
        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:'wh'|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="position:relative;display:block;width:100%;background:transparent;">
          style="flex:1;position:relative;z-index:1;">
    </div>
     <button id="<!--{$id|default:'ts'|escape:'html'}-->-play"
     <button id="<!--{$id|default:'wh'|escape:'html'}-->-play"
             style="font-size:1rem;padding:.2rem .5rem;cursor:pointer;">▶</button>
             style="font-size:1rem;padding:.2rem .6rem;cursor:pointer;">▶</button>
   </div>
   </div>


   <!-- Date line -->
   <!-- date line moved slightly lower to avoid cosine overlap -->
   <div style="margin:.25rem 0 .5rem;">
   <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 1</span>
     <span id="<!--{$id|default:'ts'|escape:'html'}-->-date">Fortnight of Renewal (Dawning), day 8 (FR 8)</span>
   </div>
   </div>


   <!-- Stage (centered sky, suns layered) -->
   <!-- stage -->
   <div id="<!--{$id|default:'wh'|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:'wh'|escape:'html'}-->-sky"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-sky"
         src="<!--{$sky|default:'/images/WyrdeHaven_Sky.png'|escape:'html'}-->" alt="Sky"
         src="<!--{$sky|escape:'html'}-->" alt="Sky" decoding="async"
        decoding="async" draggable="false"
         style="position:absolute;inset:0;object-fit:contain;object-position:center;z-index:1;width:100%;height:100%;"/>
         style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;z-index:1;"/>
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-solara"
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-sol"
         src="<!--{$solara|escape:'html'}-->" alt="Solara"
         src="<!--{$solara|default:'/wiki/images/f/fe/Solara.png'|escape:'html'}-->" alt="Solara"
         style="position:absolute;z-index:3;transform:translate(-50%,-50%);"/>
        decoding="async" draggable="false"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-vaelus"
         style="position:absolute;left:50%;top:35%;transform:translate(-50%,-50%);width:12%;height:auto;z-index:3;"/>
         src="<!--{$vaelus|escape:'html'}-->" alt="Vaelus"
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-vae"
         style="position:absolute;z-index:2;transform:translate(-50%,-50%);"/>
         src="<!--{$vaelus|default:'/wiki/images/7/7a/Vaelus.png'|escape:'html'}-->" alt="Vaelus"
        decoding="async" draggable="false"
         style="position:absolute;left:50%;top:35%;transform:translate(-50%,-50%);width:8%;height:auto;z-index:2;"/>
   </div>
   </div>


  <!-- Inclination readouts -->
   <div style="margin-top:.5rem;">
   <div style="margin:.5rem 0 .25rem;">
     <div><strong>Relative Solar Inclination (Δ):</strong>
     <strong>Relative Solar Inclination (Δ):</strong>
      <span id="<!--{$id|default:'ts'|escape:'html'}-->-relinc">+2.50° (Closing)</span>
    <span id="<!--{$id|default:'wh'|escape:'html'}-->-delta">+2.50° (Closing)</span>
    </div>
  </div>
    <div><strong>Seasonal Equatorial Inclination (barycenter):</strong>
  <div>
      <span id="<!--{$id|default:'ts'|escape:'html'}-->-eqinc">-8.00°</span>
    <strong>Seasonal Equatorial Inclination (barycenter):</strong>
    </div>
    <span id="<!--{$id|default:'wh'|escape:'html'}-->-season">-8.00°</span>
   </div>
   </div>
</div>
</div>
Line 73: 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'}-->";
   var FPS = parseFloat("<!--{$fps|default:5|escape:'js'}-->") || 5;
   var FPS = parseFloat("<!--{$fps|default:5|escape:'js'}-->") || 5;
   var N    = 479;
   var WOBBLE  = parseFloat("<!--{$wobble|default:2.5|escape:'js'}-->") || 2.5; // observable Δ (deg)
   var SEASON_AMP = parseFloat("<!--{$seasonAmp|default:8|escape:'js'}-->");     // degrees
   var EQUATOR = parseFloat("<!--{$equator|default:8|escape:'js'}-->") || 8;   // seasonal max (deg)
   var WOBBLE_TOT = parseFloat("<!--{$wobbleTotal|default:2.5|escape:'js'}-->"); // degrees total
   var SEP    = parseFloat("<!--{$sep|default:2.5|escape:'js'}-->")   || 2.5; // horizontal sep (deg)
  // Split wobble: Solara 40%, Vaelus 60% (1.0° / 1.5° if total=2.5°)
   var DEGPX  = parseFloat("<!--{$degpx|default:10|escape:'js'}-->")   || 10; // px per degree
   var SOL_WOB = WOBBLE_TOT * 0.40;
   var VAE_WOB = WOBBLE_TOT * 0.60;


   function ge(s){ return document.getElementById(ID + s); }
  // Find and init exactly one instance in a given DOM scope
  var el = {
   function initIn(root){
    x:    ge("-x"),
    var wrap = root.querySelector('.wh-twins');
     play: ge("-play"),
     if(!wrap) return;
    dayc: ge("-daycenter"),
    date: ge("-date"),
    sol:  ge("-sol"),
    vae:  ge("-vae"),
    delta: ge("-delta"),
    season: ge("-season"),
    stage: ge("-stage")
  };


  // Month / calendar helpers
    function ge(s){ return document.getElementById(ID + s); }
  var months = [
    var el = {
    "Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren",
      day: ge("-daylabel"),
    "Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"
      rng: ge("-x"),
  ];
      play: ge("-play"),
      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


  function dayToCalendar(d){
    var TAU = Math.PI*2;
    // d: 1..479 ; first 7 = FR 8..14 (Dawning), last 7 = FR 1..7 (Fading)
    function cosPhase(d){ return Math.cos(TAU*((d-1)/479)); }
     if (d <= 7) {
     function fmtDeg(x){ var s=(x>=0?"+":""); return s + x.toFixed(2) + "°"; }
      return "Fortnight of Renewal (Dawning), day " + d;
     function openingClosing(_, phase){
    } else if (d > 472) {
       var s = Math.sin(TAU*phase);
      return "Fortnight of Renewal (Fading), day " + (d - 472);
       return (-s>0) ? "Closing" : "Opening";
     } else {
       var dd = d - 7; // shift into 1..465
      var m  = Math.floor((dd-1)/31);
       var md = ((dd-1)%31) + 1;
      return months[m] + ", day " + md;
     }
     }
  }


  function fmtDeg(x){
    var months = ["Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren","Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"];
    var s = (x>=0?"+":"−"); // true minus when negative
    function dateText(d){
     var a = Math.abs(x).toFixed(2);
      if(d<=7)        return "Fortnight of Renewal (Dawning), day " + (d+7) + " (FR " + (d+7) + ")";
     return s + a + "°";
      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];
    }
 
    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);
    }
 
    function update(){
      try{
        var d = parseInt(el.rng.value,10) || 1;
        var phase = (d-1)/479;
 
        // Your “observable” Δ already in sky-arc degrees
        var delta = (WOBBLE * cosPhase(d)) / 8;     // ±WOBBLE
        var eq    = -EQUATOR * cosPhase(d);  // lowest at midwinter
 
        el.day.textContent  = d + " / 479";
        el.date.textContent = dateText(d);
        el.rel.textContent  = fmtDeg(delta) + " (" + openingClosing(delta, phase) + ")";
        el.eq.textContent   = fmtDeg(eq);
 
        var w = el.stage.clientWidth, h = el.stage.clientHeight;
        if(!w||!h) return;
 
        var cx = w*0.5, cy = h*0.33;
        var dx = (SEP/2)*DEGPX;


  function update(){
        var solOff =  (delta*(SOL_SHARE/SUM))*DEGPX;
    var X = parseInt(el.x.value,10); // 1..479
        var vaeOff = -(delta*(VAE_SHARE/SUM))*DEGPX;
    var t = (X-1)/N * 2*Math.PI;      // phase, cos starts at +1 on day 1
        var yBase  = cy - (eq*DEGPX);
    // Seasonal inclination: lowest at midwinter (day 1), highest at midsummer
    var season = -SEASON_AMP * Math.cos(t);
    // Relative Δ between suns follows cos(t) with total amplitude WOBBLE_TOT
    var delta  =  WOBBLE_TOT * Math.cos(t);
    // Opening / Closing based on derivative sign of |Δ| toward 0 or max
    var ddt = -WOBBLE_TOT * Math.sin(t); // derivative of Δ
    var opening = (ddt > 0);            // increasing toward +max => moving away from 0
    var phaseWord;
    // We define "Closing" = moving from max toward 0; "Opening" = from 0 toward max
    // Around Δ ~ cos(t): when |Δ| decreasing => Closing; when increasing => Opening
    var dAbs = (delta>=0?1:-1)*ddt; // derivative of |Δ| (sign(Δ)*Δ')
    phaseWord = (dAbs < 0) ? "Closing" : "Opening";


    // Vertical placements:
        el.s.style.left = (cx - dx) + "px";
    // Base center (percent from top). Keep near top third for visibility.
        el.v.style.left = (cx + dx) + "px";
    var centerY = 35; // %
        el.s.style.top  = (yBase - solOff) + "px";
    // Convert degrees to pixel offset relative to stage height (small scaling for realism)
        el.v.style.top  = (yBase - vaeOff) + "px";
    var H = el.stage.clientHeight || 1;
      }catch(e){}
    // 1 visual degree = 0.9% of stage height (tweakable, purely visual)
     }
     var pxPerDeg = 0.009 * H;


     var solY = centerY - (season + SOL_WOB * Math.cos(t)) * (pxPerDeg*100/H);
     function ensureSizedThen(fn){
    var vaeY = centerY - (season - VAE_WOB * Math.cos(t)) * (pxPerDeg*100/H);
      setStarSizes();
      if(el.stage.clientWidth) fn();
      else setTimeout(function(){ ensureSizedThen(fn); }, 50);
    }


     el.sol.style.top = solY + "%";
     var timer=null, playing=false, interval=1000/Math.max(1,FPS);
    el.vae.style.top = vaeY + "%";
     function step(){ var v=parseInt(el.rng.value,10)||1; el.rng.value=(v>=479)?1:(v+1); update(); }
     // Keep both horizontally centered over the stage (no right offset)
    el.sol.style.left = "50%";
    el.vae.style.left = "50%";


     // Readouts
     el.rng.addEventListener("input", update);
     el.dayc.textContent = X + " / " + N;
     el.play.addEventListener("click", function(){
    el.date.textContent = dayToCalendar(X);
      if(!playing){ playing=true; el.play.textContent="■"; timer=setInterval(step, interval); }
    el.delta.textContent = fmtDeg(delta) + " (" + phaseWord + ")";
      else { playing=false; el.play.textContent="▶"; clearInterval(timer); timer=null; }
    el.season.textContent = fmtDeg(season);
    });
    window.addEventListener("resize", function(){ setStarSizes(); update(); });


     // Cosine image is already width:100% under the slider and vertically centered.
     ensureSizedThen(update);
   }
   }


   // Scrub with rAF
   // Run on normal load
   var raf = null;
   if (document.readyState === "complete" || document.readyState === "interactive"){
  el.x.addEventListener("input", function(){
     initIn(document);
     if(raf) return;
  } else {
     raf = requestAnimationFrame(function(){ raf=null; update(); });
     document.addEventListener("DOMContentLoaded", function(){ initIn(document); }, {once:true});
   });
   }


   // Play/stop
   // Also run when MediaWiki injects content (tabs, previews, etc.)
  var playing=false, timer=null, interval=1000/Math.max(1,FPS);
   if (window.mw && mw.hook) {
   function step(){
     mw.hook('wikipage.content').add(function($c){
     var v = parseInt(el.x.value,10);
      initIn($c && $c[0] ? $c[0] : document);
    v = (v>=N)?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
  update();
})();
})();
</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