home-assistant.io/source/javascripts/connect/zwa-2/index.js

179 lines
7.0 KiB
JavaScript

import { ZWA2RenderAnimation } from "./render-animations.js"; // Adjust path if needed
import { ZWA2Animations } from "animations";
import { ConnectHeader } from "header";
import { Simon } from "simon";
// A flag to ensure animations are only initialized once.
let animationsLoaded = false;
// let smoother = ScrollSmoother.create({
// smooth: 2,
// effects: true
// });
/**
* Checks the screen width and initializes the animations if the screen is
* large enough and they haven't been loaded yet.
*/
function maybeLoadAnimations() {
// Only run on larger screens to save resources on mobile.
if (window.innerWidth >= 1024 && !animationsLoaded) {
animationsLoaded = true;
// --- Scene 1 Configuration ---
const scene1Sections = [
// The hero section autoplays from frame 0 to 186, then the user
// can scroll-animate it from frame 186 to 246.
//{ selector: "#hero", start: 186, end: 246, autoplay: { start: 0, end: 186, duration: 1200 } },
{ selector: "#hero", start: 0, end: 246 },
{ selector: "#features", start: 246, end: 314 },
{ selector: "#chipset", start: 314, end: 386 },
// The last section just holds the final frame.
{ selector: "#long-range", start: 386, end: 386 }
];
const scene1TotalFrames = scene1Sections[scene1Sections.length - 1].end;
const scene1 = new ZWA2RenderAnimation("scene1", "canvas.render-scroller#scene-one", scene1Sections, scene1TotalFrames);
scene1.start();
// Deferred Scene 2 loading via IntersectionObserver
const scene2Sections = [
{ selector: "#built-for-home-assistant", start: 63, end: 135 },
{ selector: "#plug-and-play", start: 135, end: 201 },
{ selector: "#buy", start: 201, end: 201 }
];
const scene2TotalFrames = scene2Sections[scene2Sections.length - 1].end;
const scene2 = new ZWA2RenderAnimation("scene2", "canvas.render-scroller#scene-two", scene2Sections, scene2TotalFrames);
const initScene2 = () => {
if (initScene2._done) return; // idempotent
initScene2._done = true;
scene2.start();
};
const scene2Canvas = document.querySelector('#built-for-home-assistant');
if (scene2Canvas) {
// Trigger when the top of scene-two canvas reaches the bottom edge of the viewport
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
initScene2();
obs.disconnect();
}
});
}, {
root: null,
threshold: 0,
rootMargin: '0px 0px -100% 0px' // shrink root bottom by 100% viewport height
});
observer.observe(scene2Canvas);
} else {
// Fallback: if canvas not found yet, retry on next frame
requestAnimationFrame(() => {
const retryCanvas = document.querySelector('canvas.render-scroller#scene-two');
if (retryCanvas) {
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
initScene2();
obs.disconnect();
}
});
}, {
root: null,
threshold: 0,
rootMargin: '0px 0px -100% 0px'
});
observer.observe(retryCanvas);
}
});
}
}
}
// --- Initial Setup ---
// Initialize other page components.
new ConnectHeader();
// A simple utility to hide the canvas for debugging purposes.
if (window.location.search.includes("hide")) {
document.querySelectorAll(".animation-wrapper canvas").forEach(canvas => {
canvas.style.display = "none";
});
}
// --- Additional Page Animations (Not part of the render animation) ---
const featuresEntry = new ZWA2Animations("section#features");
featuresEntry.onEnter(() => {
featuresEntry.el.querySelector(".waves-wrapper svg").style.setProperty("--enter", 1);
});
featuresEntry.onLeave(() => {
featuresEntry.el.querySelector(".waves-wrapper svg").style.setProperty("--enter", 0);
});
const configCards = document.querySelector("section#plug-and-play .config-cards");
const configCardItems = configCards.querySelectorAll('.config-card');
// Use IntersectionObserver to trigger animation when top center enters viewport
if (configCards) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Only fire once
configCardItems.forEach((configCardItem, index) => {
setTimeout(() => {
configCardItem.style.setProperty("--enter", 1);
}, 400 * index);
});
observer.disconnect();
}
});
}, {
root: null,
threshold: 0.5,
rootMargin: "0px 0px -20% 0px" // Top center
});
observer.observe(configCards);
}
const longRange = document.querySelector("section#long-range svg.range-waves");
const devices = document.querySelectorAll("section#long-range .devices .device");
// Use IntersectionObserver to trigger animation when the top of the element is 20% into the viewport
if (longRange) {
const observer1 = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Only fire once
longRange.style.setProperty("--enter", 1);
devices.forEach(d => {
setTimeout(() => {
d.style.opacity = 1;
}, 500 + Math.floor(Math.random() * 1000));
});
obs.disconnect();
}
});
}, {
root: null,
threshold: 0, // trigger as soon as it enters the adjusted root area
// Shrink the root rectangle from the top by 20% of the viewport height so
// intersection occurs when the element's top has moved 20% into view.
rootMargin: "-50% 0px 0px 0px"
});
observer1.observe(longRange);
}
// --- Event Listeners ---
// Attempt to load animations on initial page load.
maybeLoadAnimations();
document.addEventListener('DOMContentLoaded', () => {
let simon = new Simon(document.querySelectorAll("section#long-range .devices .device:not(.device--sprinkler)"));
maybeLoadAnimations();
featuresEntry.checkInViewOnLoad();
console.log("Repeat after me. Let's see how *long* you can last. -Darren");
});
// Also check on resize, in case the user rotates a tablet or resizes a browser window.
window.addEventListener('resize', maybeLoadAnimations);