1import { Controller } from "@hotwired/stimulus" 2 3const POWER_ON = [ 4 { transform: "scale(0.02, 0.004)", filter: "brightness(8)" }, 5 { transform: "scale(1, 0.004)", filter: "brightness(8)", offset: 0.3 }, 6 { transform: "scale(1, 1.02)", filter: "brightness(1.6)", offset: 0.7 }, 7 { transform: "scale(1, 1)", filter: "brightness(1)" } 8] 9 10const POWER_OFF = [ 11 { transform: "scale(1, 1)", filter: "brightness(1)", opacity: 1 }, 12 { transform: "scale(1, 0.004)", filter: "brightness(6)", opacity: 1, offset: 0.45 }, 13 { transform: "scale(0.02, 0.004)", filter: "brightness(10)", opacity: 1, offset: 0.8 }, 14 { transform: "scale(0, 0)", filter: "brightness(10)", opacity: 0 } 15] 16 17export default class extends Controller { 18 static targets = [ "power", "phosphor" ] 19 20 connect() { 21 this.#screen = sessionStorage.getItem("crt") 22 } 23 24 disconnect() { 25 delete document.documentElement.dataset.crt 26 } 27 28 toggle() { 29 if (this.powering) return 30 31 if (this.#screen) { 32 this.#powerOff() 33 } else { 34 this.#powerOn() 35 } 36 } 37 38 tune({ params: { phosphor } }) { 39 this.#screen = this.#screen === phosphor ? "color" : phosphor 40 } 41 42 #powerOn() { 43 this.#screen = "color" 44 this.#animate(POWER_ON, 550) 45 } 46 47 async #powerOff() { 48 await this.#animate(POWER_OFF, 450) 49 this.#screen = null 50 } 51 52 async #animate(keyframes, duration) { 53 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return 54 55 const root = document.documentElement 56 const tint = getComputedStyle(root).filter.replace("none", "") 57 this.powering = true 58 root.classList.add("crt-powering") 59 root.style.transformOrigin = `50% ${window.scrollY + window.innerHeight / 2}px` 60 61 try { 62 await root.animate(keyframes.map((frame) => ({ ...frame, filter: `${tint} ${frame.filter}` })), { duration, easing: "ease-out" }).finished 63 } finally { 64 this.powering = false 65 root.classList.remove("crt-powering") 66 root.style.removeProperty("transform-origin") 67 } 68 } 69 70 #render() { 71 const screen = this.#screen 72 73 this.powerTargets.forEach((button) => button.setAttribute("aria-pressed", Boolean(screen))) 74 this.phosphorTargets.forEach((button) => { 75 button.classList.toggle("hidden", !screen) 76 button.setAttribute("aria-pressed", button.dataset.crtPhosphorParam === screen) 77 }) 78 } 79 80 get #screen() { 81 return document.documentElement.dataset.crt 82 } 83 84 set #screen(value) { 85 if (value) { 86 document.documentElement.dataset.crt = value 87 sessionStorage.setItem("crt", value) 88 } else { 89 delete document.documentElement.dataset.crt 90 sessionStorage.removeItem("crt") 91 } 92 93 this.#render() 94 } 95}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.