import { Controller } from "@hotwired/stimulus" // The animation this drives only exists on small screens, and never for // someone who asked for less motion. Anywhere else the link just navigates. const MOTION = "(max-width: 1023.98px) and (prefers-reduced-motion: no-preference)" export default class extends Controller { static targets = ["video"] // Tapping through to the next stage hands over to the stage's own video: the // copy leaves, the hero grows into the space it frees, and the video fades up // behind the result. The next screen then arrives over the video's tail. exit(event) { if (this.leaving) return if (!this.hasVideoTarget) return if (!window.matchMedia(MOTION).matches) return event.preventDefault() this.link = event.currentTarget const answers = this.element.querySelector(".answers-container") if (answers) this.element.style.setProperty("--exit-shift", `${answers.offsetHeight}px`) // The last video hands over to the results rather than to another stage, so // the header leaves with the copy and the video takes the whole screen. if (event.params.full) { const header = this.element.querySelector(".stage-header-container") if (header) this.element.style.setProperty("--header-shift", `${header.offsetHeight}px`) this.element.classList.add("is-finishing") } this.element.classList.add("is-exiting") this.videoTarget.addEventListener("timeupdate", this.checkTail) this.videoTarget.addEventListener("ended", this.follow, { once: true }) this.videoTarget.addEventListener("error", this.follow, { once: true }) this.videoTarget.play().catch(this.follow) } // The next stage assembles itself over the tail of the video rather than // after it, so the last frame is still moving while the new screen arrives. checkTail = () => { const { duration, currentTime } = this.videoTarget if (!duration || duration === Infinity) return if ((duration - currentTime) * 1000 <= this.#lead()) this.follow() } follow = () => { if (this.leaving) return this.leaving = true this.videoTarget.removeEventListener("timeupdate", this.checkTail) this.link?.click() } // How much of the video the incoming screen overlaps -- the same length the // transition itself runs for. #lead() { const declared = getComputedStyle(document.documentElement).getPropertyValue("--dur-base") return parseFloat(declared) * (declared.includes("ms") ? 1 : 1000) || 600 } }