/* HEADLINE / JOIN */
001const target = reduced ? (scroll === 1 || proximity === 1 ? 1 : 0) : Math.max(scroll, proximity)
002const offscreen = rect.bottom < 0 || rect.top > viewportHeight || rect.width === 0
003const elapsed = lastTime ? Math.min(64, time - lastTime) : 16
004lastTime = time
005const next = reduced || offscreen ? target : progress.current + (target - progress.current) * (1 - Math.exp(-elapsed / 60))
006progress.current = Math.abs(next - target) < 0.0005 ? target : next
007node.style.setProperty("--synth-join", String(progress.current))
008node.setAttribute("data-join-progress", progress.current.toFixed(5))
009node.setAttribute("data-scroll-progress", scroll.toFixed(5))
010node.setAttribute("data-proximity-progress", proximity.toFixed(5))
011node.setAttribute("data-connection-state", progress.current === 1 ? "disconnected" : "dis/connected")
012
013opacity: "calc(1 - var(--synth-join, 0))",
014scale: "calc(1 - var(--synth-join, 0)) 1",
015transformOrigin: "left center",
016translate: "calc(-1 * var(--synth-join, 0) * var(--synth-slash-width, 0px)) 0px",
/* EXPLORE SYSTEMS / CONTROL */
017export const CURSOR_FRAMES: Keyframe[] = [
018 { offset: 0, opacity: 1, easing: "steps(1, end)" },
019 { offset: 0.5, opacity: 0, easing: "steps(1, end)" },
020 { offset: 1, opacity: 1 },
021]
022export const CURSOR_TIMING: KeyframeAnimationOptions = {
023 duration: 900,
024 iterations: Infinity,
025 easing: "linear",
026 fill: "none",
027}
028animation = node.animate(CURSOR_FRAMES, CURSOR_TIMING)
029
030const dx = Math.max(rect.left - point.x, 0, point.x - rect.right)
031 const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom)
032 const distance = Math.hypot(dx, dy)
033 // Near is deliberately incomplete; actual hover/focus finishes the join.
034 publishProduct(id, hovering.current && distance === 0 ? 1 : 0.96 * clamp(1 - distance / 320))
035}
/* ABOUT US / CONTROL */
036export const CURSOR_FRAMES: Keyframe[] = [
037 { offset: 0, opacity: 1, easing: "steps(1, end)" },
038 { offset: 0.5, opacity: 0, easing: "steps(1, end)" },
039 { offset: 1, opacity: 1 },
040]
041export const CURSOR_TIMING: KeyframeAnimationOptions = {
042 duration: 900,
043 iterations: Infinity,
044 easing: "linear",
045 fill: "none",
046}
047
048const animation = node.animate([
049 { offset: 0, textShadow: `1px 0 0 rgba(196,58,58,.55), -1px 0 0 rgba(236,234,228,.32)${base}`, easing: "steps(1, end)" },
050 { offset: 0.38, textShadow: `-1px 0 0 rgba(196,58,58,.36), 1px 0 0 rgba(236,234,228,.22)${base}`, easing: "steps(1, end)" },
051 { offset: 0.72, textShadow: original, easing: "steps(1, end)" },
052 { offset: 1, textShadow: original },
053], { duration: 120, easing: "linear", fill: "none", iterations: 1 })
/* SURFACE / RENDER */
054.synthtech-analogue-atmosphere > .synthtech-analogue-signal {
055 animation: synthtech-analogue-signal-shift 9s steps(1, end) infinite;
056}
057.synthtech-analogue-atmosphere[data-running="false"] > .synthtech-analogue-signal {
058 animation-play-state: paused;
059 visibility: hidden;
060}
061@keyframes synthtech-analogue-signal-shift {
062 0%, 94%, 100% { opacity: 0; transform: translateX(0); }
063 95% { opacity: 0.34; transform: translateX(-3px); }
064 96% { opacity: 0.34; transform: translateX(1px); }
065 97% { opacity: 0; transform: translateX(0); }
066}
067@media (prefers-reduced-motion: reduce) {
068 .synthtech-analogue-atmosphere > .synthtech-analogue-signal {
069 animation: none;
070 display: none;
071 }
072}