import { createSignal, createEffect, onMount, onCleanup, children as resolveChildren, type ParentComponent, createMemo, For } from "solid-js"; import { animate } from "motion"; import { createWindowWidth } from "~/lib/resize-utils"; type ParallaxBackground = { imageSet: { [key: number]: string }; size: { width: number; height: number }; verticalOffset: number; }; type ParallaxLayerProps = { layer: number; caveParallax: ParallaxBackground; dimensions: { width: number; height: number }; scale: number; scaledWidth: number; scaledHeight: number; verticalOffsetPixels: number; imagesNeeded: number; direction: number; }; function ParallaxLayer(props: ParallaxLayerProps) { let containerRef: HTMLDivElement | undefined; const layerDepthFactor = createMemo( () => props.layer / (Object.keys(props.caveParallax.imageSet).length - 1) ); const layerVerticalOffset = createMemo( () => props.verticalOffsetPixels * layerDepthFactor() ); const speed = createMemo(() => (120 - props.layer * 10) * 1000); const targetX = createMemo( () => props.direction * -props.caveParallax.size.width * props.imagesNeeded ); const containerStyle = createMemo(() => ({ width: `${props.caveParallax.size.width * props.imagesNeeded * 3}px`, height: `${props.caveParallax.size.height}px`, left: `${(props.dimensions.width - props.scaledWidth) / 2}px`, top: `${(props.dimensions.height - props.scaledHeight) / 2 + layerVerticalOffset()}px`, "transform-origin": "center center", "will-change": "transform" })); // Set up animation when component mounts or when direction/speed changes createEffect(() => { if (!containerRef) return; const target = targetX(); const duration = speed() / 1000; const controls = animate( containerRef, { transform: [ `translateX(0px) scale(${props.scale})`, `translateX(${target}px) scale(${props.scale})` ] }, { duration, easing: "linear", repeat: Infinity } ); onCleanup(() => controls.stop()); }); const imageGroups = createMemo(() => { return [-1, 0, 1].map((groupOffset) => (
{Array.from({ length: props.imagesNeeded }).map((_, index) => (
{`Parallax Object.keys(props.caveParallax.imageSet).length - 3 ? "eager" : "lazy" } />
))}
)); }); return (
{imageGroups()}
); } const SimpleParallax: ParentComponent = (props) => { let containerRef: HTMLDivElement | undefined; const windowWidth = createWindowWidth(100); const [windowHeight, setWindowHeight] = createSignal( typeof window !== "undefined" ? window.innerHeight : 800 ); const [direction, setDirection] = createSignal(1); const dimensions = createMemo(() => ({ width: windowWidth(), height: windowHeight() })); const caveParallax = createMemo(() => ({ imageSet: { 0: "/Cave/0.png", 1: "/Cave/1.png", 2: "/Cave/2.png", 3: "/Cave/3.png", 4: "/Cave/4.png", 5: "/Cave/5.png", 6: "/Cave/6.png", 7: "/Cave/7.png" }, size: { width: 384, height: 216 }, verticalOffset: 0.4 })); const layerCount = createMemo( () => Object.keys(caveParallax().imageSet).length - 1 ); const imagesNeeded = 3; onMount(() => { let timeoutId: ReturnType; const handleResize = () => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { setWindowHeight(window.innerHeight); }, 100); }; window.addEventListener("resize", handleResize); const intervalId = setInterval(() => { setDirection((prev) => prev * -1); }, 30000); onCleanup(() => { clearTimeout(timeoutId); clearInterval(intervalId); window.removeEventListener("resize", handleResize); }); }); const calculations = createMemo(() => { const dims = dimensions(); if (dims.width === 0) { return { scale: 0, scaledWidth: 0, scaledHeight: 0, verticalOffsetPixels: 0 }; } const cave = caveParallax(); const scaleHeight = dims.height / cave.size.height; const scaleWidth = dims.width / cave.size.width; const scale = Math.max(scaleHeight, scaleWidth) * 1.21; return { scale, scaledWidth: cave.size.width * scale, scaledHeight: cave.size.height * scale, verticalOffsetPixels: cave.verticalOffset * dims.height }; }); const parallaxLayers = createMemo(() => { const dims = dimensions(); if (dims.width === 0) return null; const calc = calculations(); const cave = caveParallax(); const dir = direction(); return Array.from({ length: layerCount() }).map((_, i) => { const layerIndex = layerCount() - i; return ( ); }); }); const resolved = resolveChildren(() => props.children); return (
{parallaxLayers()}
{resolved()}
); }; export default SimpleParallax;