Widget:TwinSuns: Difference between revisions

From WyrdeWiki
No edit summary
No edit summary
Line 6: Line 6:
  | id=ts1
  | id=ts1
  | size=768
  | size=768
  | fps=5
  | fps=8
  | seasonAmp=8        <!-- seasonal equatorial inclination amplitude (deg) -->
  | seasonAmpDeg=2.5
  | wobbleTotal=2.5     <!-- total relative wobble between suns (deg); split 40/60 -->
  | wobbleSepDeg=2.5
  | solara=/wiki/images/f/fe/Solara.png
  | solaraUrl=https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png
  | vaelus=/wiki/images/7/7a/Vaelus.png
  | vaelusUrl=https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png
  | sky=/images/WyrdeHaven_Sky.png
  | skyUrl=http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png
  | cosine=/images/Cosinus.png
  | cosUrl=http://wyrdehavenchronicl.es/images/Cosinus.png
}}
}}
</noinclude><includeonly>
</noinclude><includeonly>
<div class="wh-twinsuns" style="max-width:<!--{$size|default:768|escape:'html'}-->px;margin:1rem 0;">
<div class="wh-twinsuns" style="max-width:<!--{$size|default:768|escape:'html'}-->px;margin:1rem 0;">
   <!-- Top line with centered day readout -->
   <!-- centered day number above the slider -->
   <div style="display:flex;align-items:center;gap:.5rem;">
   <div style="text-align:center;margin-bottom:.25rem;">
    <div style="flex:1;height:0.1rem;"></div>
     <span id="<!--{$id|default:'ts'|escape:'html'}-->-daynum">1</span> / 479
     <div id="<!--{$id|default:'wh'|escape:'html'}-->-daycenter"
        style="font-weight:600;">1 / 479</div>
    <div style="flex:1;height:0.1rem;"></div>
   </div>
   </div>


   <!-- Slider + cosine (cosine exactly the slider width, aligned) -->
   <!-- slider + cosine (cosine sits behind, same width) -->
   <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="<!--{$cosUrl|default:'http://wyrdehavenchronicl.es/images/Cosinus.png'|escape:'html'}-->"
          alt="cosine" decoding="async" draggable="false"
        alt="Cosine"
          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.2rem;height:22px;object-fit:fill;opacity:.6;pointer-events:none;"/>
      <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;min-width:2rem;">▶</button>
             style="font-size:1rem;padding:.2rem .6rem;cursor:pointer;">▶</button>
   </div>
   </div>


   <!-- Date line -->
   <!-- add breathing room below the slider so text never crosses the cosine -->
   <div style="margin:.25rem 0 .5rem;">
  <div style="height:.5rem"></div>
 
   <div style="margin:.25rem 0 .5rem 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'}-->-datestr">Fortnight of Renewal (Dawning), day 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;border:1px solid #333;background:#000;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="<!--{$skyUrl|default:'http://wyrdehavenchronicl.es/images/WyrdeHaven_Sky.png'|escape:'html'}-->"
         decoding="async" draggable="false"
        alt="Sky backdrop"
         decoding="async" fetchpriority="high" draggable="false"
         style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;z-index:1;"/>
         style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center;z-index:1;"/>
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-sol"
    <!-- suns -->
         src="<!--{$solara|default:'/wiki/images/f/fe/Solara.png'|escape:'html'}-->" alt="Solara"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-sol"
        decoding="async" draggable="false"
         src="<!--{$solaraUrl|default:'https://www.wyrdehavenchronicl.es/wiki/images/f/fe/Solara.png'|escape:'html'}-->"
         style="position:absolute;left:50%;top:35%;transform:translate(-50%,-50%);width:12%;height:auto;z-index:3;"/>
        alt="Solara"
     <img id="<!--{$id|default:'wh'|escape:'html'}-->-vae"
         style="position:absolute;transform:translate(-50%,-50%);z-index:3;pointer-events:none;"/>
         src="<!--{$vaelus|default:'/wiki/images/7/7a/Vaelus.png'|escape:'html'}-->" alt="Vaelus"
     <img id="<!--{$id|default:'ts'|escape:'html'}-->-vae"
        decoding="async" draggable="false"
         src="<!--{$vaelusUrl|default:'https://www.wyrdehavenchronicl.es/wiki/images/7/7a/Vaelus.png'|escape:'html'}-->"
         style="position:absolute;left:50%;top:35%;transform:translate(-50%,-50%);width:8%;height:auto;z-index:2;"/>
        alt="Vaelus"
         style="position:absolute;transform:translate(-50%,-50%);z-index:2;pointer-events:none;"/>
   </div>
   </div>


  <!-- Inclination readouts -->
   <div style="margin:.5rem 0 0 0;">
   <div style="margin:.5rem 0 .25rem;">
     <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>
     <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:'wh'|escape:'html'}-->-delta">+2.50° (Closing)</span>
  </div>
  <div>
    <strong>Seasonal Equatorial Inclination (barycenter):</strong>
    <span id="<!--{$id|default:'wh'|escape:'html'}-->-season">-8.00°</span>
   </div>
   </div>
</div>
</div>
Line 73: Line 68:
<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 SEASON_AMP = parseFloat("<!--{$seasonAmp|default:8|escape:'js'}-->");     // degrees
   // Configurable amplitudes (degrees)
   var WOBBLE_TOT = parseFloat("<!--{$wobbleTotal|default:2.5|escape:'js'}-->"); // degrees total
   var SEASON_AMP = parseFloat("<!--{$seasonAmpDeg|default:2.5|escape:'js'}-->") || 2.5;   // barycenter (seasonal) amplitude
   // Split wobble: Solara 40%, Vaelus 60% (1.0° / 1.5° if total=2.5°)
   var WOBBLE_SEP = parseFloat("<!--{$wobbleSepDeg|default:2.5|escape:'js'}-->") || 2.5;  // total Solara–Vaelus separation cue
  var SOL_WOB = WOBBLE_TOT * 0.40;
  var VAE_WOB = WOBBLE_TOT * 0.60;


   function ge(s){ return document.getElementById(ID + s); }
   function ge(s){ return document.getElementById(ID + s); }
   var el = {
   var el = {
     x:   ge("-x"),
    daynum: ge("-daynum"),
     play: ge("-play"),
     x:     ge("-x"),
     dayc: ge("-daycenter"),
     play:   ge("-play"),
     date: ge("-date"),
     cos:   ge("-cos"),
     sol: ge("-sol"),
     stage: ge("-stage"),
     vae:  ge("-vae"),
    sky:    ge("-sky"),
     delta: ge("-delta"),
     sol:   ge("-sol"),
     season: ge("-season"),
     vae:    ge("-vae"),
     stage: ge("-stage")
    dates:  ge("-datestr"),
     delta: ge("-delta"),
     phase: ge("-phase"),
     season: ge("-season")
   };
   };


   // Month / calendar helpers
   // keep cosine exactly behind the slider track
   var months = [
  function syncCosWidth(){
    "Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren",
    var r = el.x.getBoundingClientRect();
    "Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"
    var C = el.cos;
  ];
    C.style.left = "0";
    C.style.right = (r.height + 6) + "px"; // reserve space for the play button
    C.style.top = ((r.height - 22)/2) + "px";
  }
  window.addEventListener("resize", syncCosWidth);
 
  // calendar helpers
   var months = ["Primaris","Vernalis","Tharion","Lunarae","Cindrel","Solvane","Auren","Veyrath","Rhendol","Tirien","Gairos","Fenmir","Wyrdrun","Cernis","Eirathen"];


   function dayToCalendar(d){
   function dateLabel(d){
     // d: 1..479 ; first 7 = FR 8..14 (Dawning), last 7 = FR 1..7 (Fading)
     // d: 1..479 (year starts at midwinter)
     if (d <= 7) {
     if(d <= 7){
       return "Fortnight of Renewal (Dawning), day " + d;
       return "Fortnight of Renewal (Dawning), day " + (d+7) + " (FR " + (d+7) + ")";
    } else if (d > 472) {
     }
      return "Fortnight of Renewal (Fading), day " + (d - 472);
    if(d >= 473){
     } else {
       var k = d - 472; // 1..7
       var dd = d - 7; // shift into 1..465
       return "Fortnight of Renewal (Fading), day " + k + " (FR " + k + ")";
       var m  = Math.floor((dd-1)/31);
      var md = ((dd-1)%31) + 1;
      return months[m] + ", day " + md;
     }
     }
    // 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];
   }
   }


   function fmtDeg(x){
  // trig helpers
    var s = (x>=0?"+":"−"); // true minus when negative
  var TWO_PI = Math.PI*2;
     var a = Math.abs(x).toFixed(2);
   function cosCycle(day){ return Math.cos(TWO_PI * (day-1) / 479); } // day 1 -> +1 (midwinter max)
     return s + a + "°";
 
  // 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
   }
   }


   function update(){
  // place suns
     var X = parseInt(el.x.value,10);  // 1..479
   function layout(day){
     var t = (X-1)/N * 2*Math.PI;     // phase, cos starts at +1 on day 1
     // seasonal declination (barycenter)
     // Seasonal inclination: lowest at midwinter (day 1), highest at midsummer
     var seasonDeg = SEASON_AMP * cosCycle(day);
     var season = -SEASON_AMP * Math.cos(t);
 
     // Relative Δ between suns follows cos(t) with total amplitude WOBBLE_TOT
     // relative split stays constant at WOBBLE_SEP; Solara above when positive season phase starts (day 1)
     var delta  = WOBBLE_TOT * Math.cos(t);
     var halfSep = WOBBLE_SEP / 2;
    // Opening / Closing based on derivative sign of |Δ| toward 0 or max
 
     var ddt = -WOBBLE_TOT * Math.sin(t); // derivative of Δ
     // pixel mapping
     var opening = (ddt > 0);             // increasing toward +max => moving away from 0
     var k = pxPerDeg();
     var phaseWord;
 
     // We define "Closing" = moving from max toward 0; "Opening" = from 0 toward max
     var stageRect = el.stage.getBoundingClientRect();
     // Around Δ ~ cos(t): when |Δ| decreasing => Closing; when increasing => Opening
     var W = el.stage.clientWidth, H = el.stage.clientHeight;
     var dAbs = (delta>=0?1:-1)*ddt; // derivative of |Δ| (sign(Δ)*Δ')
 
     phaseWord = (dAbs < 0) ? "Closing" : "Opening";
    // 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);


     // Vertical placements:
     // set sizes (Solara bigger than Vaelus, keep proportion within 256 canvas scale)
    // Base center (percent from top). Keep near top third for visibility.
     var solSize = Math.round(Math.min(W, H) * 0.14);
     var centerY = 35; // %
     var vaeSize = Math.round(solSize * 0.65);
    // Convert degrees to pixel offset relative to stage height (small scaling for realism)
    var H = el.stage.clientHeight || 1;
     // 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);
     Object.assign(el.sol.style, { left: cx+"px", top: ySol+"px", width: solSize+"px", height: "auto" });
     var vaeY = centerY - (season - VAE_WOB * Math.cos(t)) * (pxPerDeg*100/H);
     Object.assign(el.vae.style, { left: cx+"px", top: yVae+"px", width: vaeSize+"px", height: "auto" });


     el.sol.style.top = solY + "%";
    // texts
    el.vae.style.top = vaeY + "%";
     el.delta.textContent = (WOBBLE_SEP >= 0 ? "+" : "") + WOBBLE_SEP.toFixed(2) + "°";
    // Keep both horizontally centered over the stage (no right offset)
     el.season.textContent = (seasonDeg>=0?"+":"") + seasonDeg.toFixed(2) + "°";
     el.sol.style.left = "50%";
    el.vae.style.left = "50%";


     // Readouts
     // Opening/Closing: |season| increasing -> Opening; decreasing -> Closing
     el.dayc.textContent = X + " / " + N;
     // derivative sign of |A cos| at day ~ compare neighboring days
     el.date.textContent = dayToCalendar(X);
    var eps = 1;
     el.delta.textContent = fmtDeg(delta) + " (" + phaseWord + ")";
    var prev = Math.abs(SEASON_AMP * cosCycle(Math.max(1, day-eps)));
    el.season.textContent = fmtDeg(season);
     var next = Math.abs(SEASON_AMP * cosCycle(Math.min(479, day+eps)));
     el.phase.textContent = (next > prev) ? "(Opening)" : "(Closing)";
  }


     // Cosine image is already width:100% under the slider and vertically centered.
  function update(){
     var d = parseInt(el.x.value,10);
    el.daynum.textContent = d;
    el.dates.textContent  = dateLabel(d);
    layout(d);
   }
   }


   // Scrub with rAF
   // scrub throttle
   var raf = null;
   var raf = null;
   el.x.addEventListener("input", function(){
   el.x.addEventListener("input", function(){
Line 170: Line 187:
   });
   });


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


   // Initial
   // initial sync
  syncCosWidth();
   update();
   update();
})();
})();
</script>
</script>
</includeonly>
</includeonly>

Revision as of 20:18, 11 November 2025

This widget displays WyrdeHaven’s Twin Suns with seasonal wobble and 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°