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) => (

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;