From c0c0fd9b109c255c0dcfad6e222fec7fa547443c Mon Sep 17 00:00:00 2001 From: gnacho Date: Wed, 12 Aug 2026 09:00:59 +0200 Subject: [PATCH 1/5] feat(landing): scroll animation, battery sensors, chart spacing, footer polish --- landing/app.js | 52 +++++++++++++++++++++++++++++++++++ landing/i18n.js | 6 ++++- landing/index.html | 37 ++++++++++++------------- landing/styles.css | 67 +++++++++++++++++++++------------------------- 4 files changed, 107 insertions(+), 55 deletions(-) diff --git a/landing/app.js b/landing/app.js index fd59d58..ff8d2f3 100644 --- a/landing/app.js +++ b/landing/app.js @@ -409,6 +409,15 @@ /* ── Batería ── */ const ringFg = $('#ringFg'), batterySoc = $('#batterySoc'), batteryStatus = $('#batteryStatus') + const batTempVal = $('#batTempVal'), batAutonomyVal = $('#batAutonomyVal') + const BATTERY_CAPACITY_KWH = 10 + const fmtAutonomy = (hours) => { + if (hours >= 100) return '∞' + if (hours < 1) return `${Math.max(1, Math.round(hours * 60))} min` + const h = Math.floor(hours), m = Math.round((hours - h) * 60) + if (m >= 60) return `${h + 1} h` + return m ? `${h} h ${m} min` : `${h} h` + } let curSoc = 100 function updateBattery(h) { const st = batteryState(h) @@ -416,6 +425,14 @@ ringFg.style.strokeDashoffset = String(314.16 * (1 - curSoc / 100)) batterySoc.textContent = String(Math.round(curSoc)) batteryStatus.textContent = MICRO[document.documentElement.lang || 'es'][st.mode] + const temp = 23 + (st.mode === 'charge' ? 2.5 : 0) + (st.mode === 'discharge' ? 0.9 : 0) + Math.sin(h * 1.7) * 0.4 + batTempVal.textContent = `${temp.toFixed(1)} °C` + if (st.mode === 'charge') { + batAutonomyVal.textContent = '…' + } else { + const cons = Math.max(0.1, consumptionCurve(h)) + batAutonomyVal.textContent = fmtAutonomy((curSoc / 100) * BATTERY_CAPACITY_KWH / cons) + } } /* ── Flujo de energía (réplica del widget de la app) ── */ @@ -590,6 +607,39 @@ els.forEach((el) => io.observe(el)) } + /* ── Teatro: animación al hacer scroll ── */ + const theaterEl = document.querySelector('.theater') + const theaterStagger = [ + ...document.querySelectorAll('.theater-eyebrow, .theater-title, .theater-sub'), + ...document.querySelectorAll('.theater-widgets .widget'), + ] + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + function theaterScroll() { + if (reduceMotion) return + const vh = window.innerHeight + const top = theaterEl.getBoundingClientRect().top + window.scrollY + const start = top - vh + const total = vh + vh * 1.4 + const p = clamp((window.scrollY - start) / total, 0, 1) + let i = 0 + for (const el of theaterStagger) { + const w0 = (i === 0 ? 0 : 0.06 + (i - 1) * 0.11) + const w1 = w0 + 0.12 + const e = ss(w0, w1, p) + el.style.opacity = e.toFixed(3) + el.style.transform = e >= 1 ? '' : `translateY(${((1 - e) * 18).toFixed(1)}px)` + i++ + } + } + function theaterScrollInit() { + if (reduceMotion) return + for (const el of theaterStagger) { + el.style.opacity = '0' + el.style.transform = 'translateY(18px)' + } + theaterScroll() + } + /* ── Init ── */ function init() { const i18n = window.heliosI18n @@ -606,6 +656,7 @@ setInterval(buildTicker, 10000) setupSlider() setupReveal() + theaterScrollInit() updateFlow(hourNow()) let lastDom = 0 @@ -621,6 +672,7 @@ function frame(ts) { const h = hourNow() drawSky(scrollPhase(), h) + theaterScroll() const heroChart = $('#heroChart'), theaterChart = $('#theaterChart'), theaterHist = $('#theaterHist') if (chartNeedsRedraw(heroChart)) drawDayChart(heroChart, true) if (chartNeedsRedraw(theaterChart)) drawDayChart(theaterChart, false) diff --git a/landing/i18n.js b/landing/i18n.js index 4e722b9..9287bc0 100644 --- a/landing/i18n.js +++ b/landing/i18n.js @@ -55,6 +55,8 @@ const I18N = { 'theater.lCons': 'Consumo', 'theater.bCharged': 'Cargada hoy', 'theater.bDischarged': 'Descargada hoy', + 'theater.bTemp': 'Temperatura', + 'theater.bAutonomy': 'Autonomía', 'theater.bCycles': 'Ciclos', 'theater.bHealth': 'Salud', 'flow.fv': 'Fotovoltaica', @@ -62,7 +64,7 @@ const I18N = { 'flow.battery': 'Batería', 'flow.grid': 'Red', 'screens.eyebrow': 'LA APP', - 'screens.title': 'Hecha para mirarse todos los días.', + 'screens.title': 'Hecha para mirarla todos los días.', 'slide1': 'Vista Hoy · curva en vivo, flujo y totales del día', 'slide2': 'Batería · SOC y curva de carga y descarga', 'slide3': 'Histórico · cientos de días de tu instalación', @@ -160,6 +162,8 @@ const I18N = { 'theater.lCons': 'Consumption', 'theater.bCharged': 'Charged today', 'theater.bDischarged': 'Discharged today', + 'theater.bTemp': 'Temperature', + 'theater.bAutonomy': 'Autonomy', 'theater.bCycles': 'Cycles', 'theater.bHealth': 'Health', 'flow.fv': 'Photovoltaic', diff --git a/landing/index.html b/landing/index.html index 2769451..a972468 100644 --- a/landing/index.html +++ b/landing/index.html @@ -209,6 +209,14 @@

Mírala respirar.

Descargada hoy 0.0 kWh +
+ Temperatura + 24.0 °C +
+
+ Autonomía + … +
Ciclos 312 @@ -325,7 +333,7 @@

Mírala respirar.

LA APP

-

Hecha para mirarse todos los días.

+

Hecha para mirarla todos los días.

@@ -425,6 +433,15 @@

De personas, no de empresas

+ +
+
+

Gracias, Home Assistant. 💛

+

Helios no sería posible sin el proyecto Home Assistant y su comunidad. Se conecta a tu HAOS local por WebSocket, sin tocar su API pública, y lee solo los sensores que tú decides.

+ home-assistant.io → +
+
+

El sol no espera.
Tu instalación tampoco.

@@ -442,25 +459,9 @@

El sol no espera.
Tu instal

- -
-
-

Gracias, Home Assistant. 💛

-

Helios no sería posible sin el proyecto Home Assistant y su comunidad. Se conecta a tu HAOS local por WebSocket, sin tocar su API pública, y lee solo los sensores que tú decides.

- home-assistant.io → -
-
-