Sonnenkarte animiert

Hallo, ich bin an einer animierten Sonnstandskarte am basteln.
Irgendwie klappt das noch nicht so richtig.
Die Idee dahinter ist die Elevation zu nutzen um die Sonne je nach Stand über den Horizont zu schieben. Bei Noon sollte die Sonne dann voll zu sehen sein. Da liegt aber der Haken: die max-Elevation hier bei uns ist so um die 67°, das wird im Winter ja nicht erreicht. Deshalb ist das Bild am Mittag nicht voll zu sehen :roll_eyes: Abgeschnitten wird das Bild unten durch clip-path.
Könnte da mal jemand mit testen?

Hier der Code:

type: custom:button-card
entity: sun.sun
show_icon: false
show_state: false
show_name: false
tap_action:
  action: none
triggers_update:
  - sun.sun
  - sensor.sun_next_rising
  - sensor.sun_next_dawn
  - sensor.sun_next_noon
  - sensor.sun_next_dusk
  - sensor.sun_next_setting
  - sensor.time
custom_fields:
  header: |
    [[[
      const sun = states['sun.sun'];
      const isDay = sun && sun.state === 'above_horizon';

      const dawnEnt = states['sensor.sun_next_rising'];
      const duskEnt = states['sensor.sun_next_setting'];

      let dayLenStr = '-';
      let nightLenStr = '-';

      if (dawnEnt && duskEnt) {
        const dawn = new Date(dawnEnt.state);
        const dusk = new Date(duskEnt.state);

        if (!isNaN(dawn) && !isNaN(dusk)) {
          const fullDayMs = 24 * 60 * 60 * 1000;
          let diffMs = dusk - dawn;

          // falls Dämmerung schon vorbei ist: auf nächsten Tag korrigieren
          if (diffMs <= 0) {
            diffMs += fullDayMs;
          }

          if (diffMs > 0 && diffMs <= fullDayMs) {
            const minsTotalDay = Math.floor(diffMs / 60000);
            const dayHours = Math.floor(minsTotalDay / 60);
            const dayMins = minsTotalDay % 60;
            dayLenStr = `${dayHours} h ${String(dayMins).padStart(2,'0')} min`;

            const nightMs = fullDayMs - diffMs;
            const minsTotalNight = Math.floor(nightMs / 60000);
            const nightHours = Math.floor(minsTotalNight / 60);
            const nightMins = minsTotalNight % 60;
            nightLenStr = `${nightHours} h ${String(nightMins).padStart(2,'0')} min`;
          }
        }
      }

      const label = isDay ? 'Tageslicht' : 'Nachtlänge';
      const value = isDay ? dayLenStr : nightLenStr;

      return `
        <div style="display:flex;justify-content:space-between;align-items:baseline;width:55%;">
          <span>${label}</span>
          <span>${value}</span>
        </div>
      `;
    ]]]
  sun: " "
  horizon: " "
  times: |
    [[[
      function fmt(id) {
        const s = states[id];
        if (!s) return '-';
        const d = new Date(s.state);
        if (isNaN(d)) return '-';
        return d.toLocaleTimeString('de-DE', {
          hour: '2-digit',
          minute: '2-digit'
        });
      }

      return `
        <div style="display:flex;flex-direction:column;gap:8px;">
          <div style="display:flex;justify-content:space-between;">
            <span>Morgendämmerung</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_dawn')}</span>
          </div>        
          <div style="display:flex;justify-content:space-between;">
            <span>Sonnenaufgang</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_rising')}</span>
          </div>
          <div style="display:flex;justify-content:space-between;">
            <span>Mittag</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_noon')}</span>
          </div>
          <div style="display:flex;justify-content:space-between;">
            <span>Sonnenuntergang</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_setting')}</span>
          </div>
          <div style="display:flex;justify-content:space-between;">
            <span>Abenddämmerung</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_dusk')}</span>
          </div>
        </div>
      `;
    ]]]
styles:
  card:
    - height: 272px
    - padding: 20px
    - border-radius: 15px
    - background: |
        linear-gradient(130deg, #4398C3 0%, #3380A5 100%)
    - box-shadow: >
        inset 4px 4px 8px #4DB0DD, inset -4px -4px 8px #2E6A89, 4px 8px 12px
        #91A09C
    - color: white
    - position: relative
    - overflow: hidden
    - font-family: poppins_regular
    - border: none
  grid:
    - grid-template-areas: "\"header\" \"sun\" \"times\""
  custom_fields:
    header:
      - position: absolute
      - top: 18px
      - left: 24px
      - right: 24px
      - font-size: 20px
      - font-family: poppins_regular
      - z-index: 3
    sun:
      - position: absolute
      - left: 20px
      - width: 150px
      - height: 150px
      - border-radius: 50%
      - background-image: url('/local/pictures/sonne_sun.png')
      - background-size: 150px
      - background-repeat: no-repeat
      - background-position: center
      - z-index: 1
      - top: |
          [[[
            const sun = states['sun.sun'];
            if (!sun) return '225px';

            const elev = sun.attributes.elevation ?? 0;
            const maxElev = 70;

            // Elevation auf sinnvollen Bereich begrenzen
            const clamped = Math.max(0, Math.min(maxElev, elev));

            const radius = 75;          // 150px / 2
            const centerAtMin = 260;    // Mittelpunkt bei Elevation 0° (unter dem Horizont)
            const centerAtMax = 140;    // Mittelpunkt bei hoher Elevation (Sonne voll sichtbar)

            const centerY =
              centerAtMin - (clamped / maxElev) * (centerAtMin - centerAtMax);

            const top = centerY - radius;
            return `${top}px`;
          ]]]
      - opacity: |
          [[[
            const sun = states['sun.sun'];
            if (!sun) return 0;
            // nur ausblenden, wenn Sonne wirklich unter dem Horizont ist
            return sun.state === 'below_horizon' ? 0 : 1;
          ]]]
      - clip-path: |
          [[[
            const sun = states['sun.sun'];
            if (!sun) return 'inset(0 0 150px 0)';

            // unter dem Horizont: komplett weg
            if (sun.state === 'below_horizon') {
              return 'inset(0 0 150px 0)';
            }

            const elev = sun.attributes.elevation ?? 0;
            const maxElev = 70;
            const clamped = Math.max(0, Math.min(maxElev, elev));

            const h = 150;
            const radius = h / 2;

            const centerAtMin = 260;
            const centerAtMax = 140;
            const centerY =
              centerAtMin - (clamped / maxElev) * (centerAtMin - centerAtMax);
            const topPos = centerY - radius;

            const horizonY = 225;  // Position der Horizontlinie

            // wieviel von unten „unterhalb“ des Horizonts liegt?
            let bottomClip = Math.max(0, (topPos + h) - horizonY);
            if (bottomClip > h) bottomClip = h;

            return `inset(0 0 ${bottomClip}px 0)`;
          ]]]
    horizon:
      - position: absolute
      - left: 20px
      - right: 270px
      - top: 225px
      - height: 2px
      - background: rgba(255,255,255,0.35)
      - box-shadow: 0px 1px 4px rgba(0,0,0,0.35)
      - border-radius: 999px
      - z-index: 2
    times:
      - position: absolute
      - top: 100px
      - right: 24px
      - width: calc(100% - 215px)
      - font-size: 14px
      - font-family: poppins_regular
      - line-height: 1.6
      - text-align: left
      - z-index: 3

PS: die Karte ist auf ein iPad 10 ausgerichtet.

Gruß
Frank

1 „Gefällt mir“

kann Dir leider da nicht helfen :frowning: aber sieht sehr gut aus.
Die Mondkarte oben funktioniert?

Bitte wenn alles läuft Code + Bilder bereitstellen :slight_smile:
DANKE

Ich arbeite daran.
Mondkarte ist aus HACS und mit card-mod geändert.
Läuft ganz gut. Nur mit der Beleuchtung des Mondes bin ich noch nicht zufrieden. Gibt bestimmt noch einen anderen Weg, eventuell über die Website der NASA.

Wenn alles klappt, folgt für interessierte der Code.

Gerne :grinning_face:

Hab das jetzt über die Stunden realisiert.
Mit dem Wert der Elevation klappt das so wohl nicht.
Die Sonne richtet sich nun nach Aufgang - Mittag und Untergang.
So habe ich dann Mittags die Sonne im Zenit, also voll sichtbar.
Ich wollte halt nicht 10 verschiedene Fotos ein oder ausblenden.
Deswegen hab ich mal einen anderen Weg probiert :hugs:

Wie versprochen hier auch der Code:

type: custom:button-card
card_mod:
  style: |
    ha-card {
      margin-top: -15px;
    }
entity: sun.sun
show_icon: false
show_state: false
show_name: false
tap_action:
  action: none
triggers_update:
  - sun.sun
  - sensor.sun_next_rising
  - sensor.sun_next_dawn
  - sensor.sun_next_noon
  - sensor.sun_next_dusk
  - sensor.sun_next_setting
  - sensor.time
custom_fields:
  header: |
    [[[
      const sun = states['sun.sun'];
      const isDay = sun && sun.state === 'above_horizon';

      const dawnEnt = states['sensor.sun_next_rising'];
      const duskEnt = states['sensor.sun_next_setting'];

      let dayLenStr = '-';
      let nightLenStr = '-';

      if (dawnEnt && duskEnt) {
        const dawn = new Date(dawnEnt.state);
        const dusk = new Date(duskEnt.state);

        if (!isNaN(dawn) && !isNaN(dusk)) {
          const fullDayMs = 24 * 60 * 60 * 1000;
          let diffMs = dusk - dawn;

          // falls Dämmerung schon vorbei ist: auf nächsten Tag korrigieren
          if (diffMs <= 0) {
            diffMs += fullDayMs;
          }

          if (diffMs > 0 && diffMs <= fullDayMs) {
            const minsTotalDay = Math.floor(diffMs / 60000);
            const dayHours = Math.floor(minsTotalDay / 60);
            const dayMins = minsTotalDay % 60;
            dayLenStr = `${dayHours} h ${String(dayMins).padStart(2,'0')} min`;

            const nightMs = fullDayMs - diffMs;
            const minsTotalNight = Math.floor(nightMs / 60000);
            const nightHours = Math.floor(minsTotalNight / 60);
            const nightMins = minsTotalNight % 60;
            nightLenStr = `${nightHours} h ${String(nightMins).padStart(2,'0')} min`;
          }
        }
      }

      const label = isDay ? 'Tageslicht' : 'Nachtlänge';
      const value = isDay ? dayLenStr : nightLenStr;

      // aktuelle Uhrzeit
      const now = new Date();
      const timeStr = now.toLocaleTimeString('de-DE', {
        hour: '2-digit',
        minute: '2-digit'
      });

      return `
        <div style="display:flex;justify-content:space-between;align-items:baseline;width:100%;">
          <div style="display:flex;gap:6px;align-items:baseline;">
            <span>${label}</span>
            <span>${value}</span>
          </div>
          <span style="margin-left:auto;font-family:poppins_regular;font-size:20px;">
            ${timeStr} Uhr
          </span>
        </div>
      `;
    ]]]
  sun: " "
  horizon: " "
  times: |
    [[[
      function fmt(id) {
        const s = states[id];
        if (!s) return '-';
        const d = new Date(s.state);
        if (isNaN(d)) return '-';
        return d.toLocaleTimeString('de-DE', {
          hour: '2-digit',
          minute: '2-digit'
        });
      }

      return `
        <div style="display:flex;flex-direction:column;gap:8px;">
          <div style="display:flex;justify-content:space-between;">
            <span>Morgendämmerung</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_dawn')}</span>
          </div>        
          <div style="display:flex;justify-content:space-between;">
            <span>Sonnenaufgang</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_rising')}</span>
          </div>
          <div style="display:flex;justify-content:space-between;">
            <span>Mittag</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_noon')}</span>
          </div>
          <div style="display:flex;justify-content:space-between;">
            <span>Sonnenuntergang</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_setting')}</span>
          </div>
          <div style="display:flex;justify-content:space-between;">
            <span>Abenddämmerung</span>
            <span style="font-family:poppins_medium;">${fmt('sensor.sun_next_dusk')}</span>
          </div>
        </div>
      `;
    ]]]
styles:
  card:
    - height: 272px
    - padding: 20px
    - border-radius: 15px
    - background: |
        linear-gradient(130deg, #4398C3 0%, #3380A5 100%)
    - box-shadow: >
        inset 4px 4px 8px #4DB0DD, inset -4px -4px 8px #2E6A89, 4px 8px 12px
        #91A09C
    - color: white
    - position: relative
    - overflow: hidden
    - font-family: poppins_regular
    - border: none
  grid:
    - grid-template-areas: "\"header\" \"sun\" \"times\""
  custom_fields:
    header:
      - position: absolute
      - top: 18px
      - left: 24px
      - right: 24px
      - font-size: 20px
      - font-family: poppins_regular
      - z-index: 3
    sun:
      - position: absolute
      - left: 7px
      - width: 170px
      - height: 170px
      - border-radius: 50%
      - background-image: url('/local/pictures/sonne_sun.png')
      - background-size: 170px
      - background-repeat: no-repeat
      - background-position: center
      - z-index: 1
      - top: |
          [[[
            const sun = states['sun.sun'];
            if (!sun) return '270px';

            const now = new Date();

            // Nachts: einfach unter den Horizont "parken"
            if (sun.state !== 'above_horizon') {
              return '270px';
            }

            const riseEnt = states['sensor.sun_next_rising'];
            const noonEnt = states['sensor.sun_next_noon'];
            const setEnt  = states['sensor.sun_next_setting'];
            if (!riseEnt || !noonEnt || !setEnt) return '270px';

            const riseNext = new Date(riseEnt.state);
            const noonNext = new Date(noonEnt.state);
            const setNext  = new Date(setEnt.state);
            if ([riseNext, noonNext, setNext].some(d => isNaN(d))) return '270px';

            const dayMs = 24 * 60 * 60 * 1000;

            const set = setNext;
            const rise = new Date(riseNext.getTime() - dayMs);
            let noon = noonNext <= set ? noonNext : new Date(noonNext.getTime() - dayMs);

            const radius = 85;          
            const horizonY = 235;       // 10px tiefer

            const centerAtHorizon = horizonY + radius - 15;  // ~15px sichtbar
            const centerAtMax = horizonY - radius - 10;      // ~10px Abstand

            let phase = 0; // 0 = Horizont, 1 = höchster Punkt

            if (now <= noon) {
              const total = noon - rise;
              const elapsed = now - rise;
              phase = total > 0 ? elapsed / total : 0;
            } else {
              const total = set - noon;
              const remaining = set - now;
              phase = total > 0 ? remaining / total : 0;
            }

            phase = Math.max(0, Math.min(1, phase));

            const centerY =
              centerAtHorizon + (centerAtMax - centerAtHorizon) * phase;

            const top = centerY - radius;
            return `${top}px`;
          ]]]
      - opacity: |
          [[[
            const sun = states['sun.sun'];
            if (!sun) return 0;
            return sun.state === 'below_horizon' ? 0 : 1;
          ]]]
      - clip-path: |
          [[[
            const sun = states['sun.sun'];
            if (!sun) return 'inset(0 0 170px 0)';

            // unter Horizont: komplett weg
            if (sun.state === 'below_horizon') {
              return 'inset(0 0 170px 0)';
            }

            const now = new Date();

            const riseEnt = states['sensor.sun_next_rising'];
            const noonEnt = states['sensor.sun_next_noon'];
            const setEnt  = states['sensor.sun_next_setting'];
            if (!riseEnt || !noonEnt || !setEnt) return 'inset(0 0 170px 0)';

            const riseNext = new Date(riseEnt.state);
            const noonNext = new Date(noonEnt.state);
            const setNext  = new Date(setEnt.state);
            if ([riseNext, noonNext, setNext].some(d => isNaN(d))) return 'inset(0 0 170px 0)';

            const dayMs = 24 * 60 * 60 * 1000;

            const set = setNext;
            const rise = new Date(riseNext.getTime() - dayMs);
            let noon = noonNext <= set ? noonNext : new Date(noonNext.getTime() - dayMs);

            const h = 170;
            const radius = 85;
            const horizonY = 235;       // 10px tiefer

            const centerAtHorizon = horizonY + radius - 15;
            const centerAtMax = horizonY - radius - 10;

            let phase = 0;

            if (now <= noon) {
              const total = noon - rise;
              const elapsed = now - rise;
              phase = total > 0 ? elapsed / total : 0;
            } else {
              const total = set - noon;
              const remaining = set - now;
              phase = total > 0 ? remaining / total : 0;
            }
            phase = Math.max(0, Math.min(1, phase));

            const centerY =
              centerAtHorizon + (centerAtMax - centerAtHorizon) * phase;

            const topPos = centerY - radius;

            // wieviel von unten unterhalb der Horizontlinie liegt?
            let bottomClip = Math.max(0, (topPos + h) - horizonY);
            if (bottomClip > h) bottomClip = h;

            return `inset(0 0 ${bottomClip}px 0)`;
          ]]]
    times:
      - position: absolute
      - top: 100px
      - right: 24px
      - width: calc(100% - 215px)
      - font-size: 14px
      - font-family: poppins_regular
      - line-height: 1.6
      - text-align: left
      - z-index: 3

Karte ist für ein iPad10 ausgelegt. Das Sonnenfoto hab ich aus dem Internet.

3 „Gefällt mir“

Danke für deine Berechnung zum Sonnenstand, gefällt mir sehr gut. Die Berechnung über Stunden wird mit Sicherheit auch stabiler laufen. Finde ich also besser, wie der Weg über Azimuth. Ich weiß nicht ob du es schon gesehen hast, ich habe eine custom-card für den Sonnenstand erstellt und werde nun mal schauen, dass ich deine Berechnung mit als Option aufnehme und man dann zwischen klassischer und berechneter Ansicht wechseln kann. :slight_smile:

3 „Gefällt mir“

Teamwork :+1:

3 „Gefällt mir“