Skip to content

Instantly share code, notes, and snippets.

@bionboy
Created January 22, 2025 18:51
Show Gist options
  • Select an option

  • Save bionboy/150a1f1c80a1df40737c5f0eec537b62 to your computer and use it in GitHub Desktop.

Select an option

Save bionboy/150a1f1c80a1df40737c5f0eec537b62 to your computer and use it in GitHub Desktop.
Parallax Component (Svelte 5)
<script lang="ts">
import type { Snippet } from 'svelte';
import { mode } from 'mode-watcher';
import { Star } from 'lucide-svelte';
type Props = {
title?: string;
titleContent?: Snippet;
foregroundContent?: Snippet;
midgroundContent?: Snippet;
backgroundContent?: Snippet;
includeBackground?: boolean;
moveWithScroll?: boolean;
moveWithMouse?: boolean;
};
const {
title = 'Parallax',
titleContent,
foregroundContent,
midgroundContent,
backgroundContent,
includeBackground = true,
moveWithScroll = true,
moveWithMouse = true
}: Props = $props();
let scroll = $state({ x: 0, y: 0 });
let mouse = $state({ x: 0, y: 0 });
let hero = $state({ width: 0, height: 0, offset: { x: 0, y: 0 } });
let islandPositions = $derived([
createPositions(hero.width, hero.height, 5),
createPositions(hero.width, hero.height, 10),
createPositions(hero.width, hero.height, 20)
]);
let parallaxShift = $derived(getShift());
let defaultIslandContent = $derived($mode === 'light' ? cloud : star);
function getShift(scale: number = 1) {
let input = { x: 0, y: 0 };
if (moveWithMouse) {
if (mouse.x !== 0) input.x = mouse.x - hero.width / 2;
if (mouse.y !== 0) input.y = mouse.y - hero.height / 2;
}
if (moveWithScroll) {
input.x += scroll.x;
input.y += scroll.y;
}
return {
x: input.x * scale + 'px',
y: input.y * scale + 'px'
};
}
const getRandomRotation = () => {
const rotationRange = 90;
const rotation = (Math.random() - 0.5) * rotationRange;
return rotation + 'deg';
};
function createPositions(width: number, height: number, amount: number) {
const overscanFactor = 1;
const result: number[][] = [];
for (let j = 0; j < amount; j++) {
result[j] = [
width / 2 + (Math.random() - 0.5) * (width * overscanFactor),
height / 2 + (Math.random() - 0.5) * (height * overscanFactor)
];
}
return result;
}
</script>
<svelte:window bind:scrollY={scroll.y} bind:scrollX={scroll.x} />
<div
class="hero"
class:light-background={includeBackground && $mode === 'light'}
class:dark-background={includeBackground && $mode === 'dark'}
role="presentation"
onmousemove={(event: MouseEvent) => (mouse = { x: event.layerX, y: event.layerY })}
bind:clientWidth={hero.width}
bind:clientHeight={hero.height}
style:--x-shift={parallaxShift.x}
style:--y-shift={parallaxShift.y}
>
<div class="parallax">
{#each islandPositions[2] as coords}
{@render island('background', coords[0], coords[1], backgroundContent)}
{/each}
<div class="blur-panel background"></div>
{#each islandPositions[1] as coords}
{@render island('midground', coords[0], coords[1], midgroundContent)}
{/each}
<div class="blur-panel midground"></div>
{#each islandPositions[0] as coords}
{@render island('foreground', coords[0], coords[1], foregroundContent)}
{/each}
<div class="blur-panel foreground"></div>
</div>
<div class="hero-content">
{#if titleContent}
{@render titleContent()}
{:else}
<h1 class="hero-title">{title}</h1>
{/if}
</div>
</div>
{#snippet island(level: string = 'foreground', x: number = 0, y: number = 0, content?: Snippet)}
<div class="island {level}" style="left:{x}px; top:{y}px;">
{@render (content ?? defaultIslandContent)()}
</div>
{/snippet}
{#snippet cloud()}
<div class="cloud">
<div class="cloud-part cloud-part-1"></div>
<div class="cloud-part cloud-part-2"></div>
<div class="cloud-part cloud-part-3"></div>
</div>
<style>
.cloud {
position: relative;
width: 200px;
height: 60px;
background: #fff;
border-radius: 50px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.cloud-part {
position: absolute;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.cloud-part-1 {
width: 80px;
height: 80px;
top: -40px;
left: 20px;
}
.cloud-part-2 {
width: 100px;
height: 100px;
top: -50px;
left: 80px;
}
.cloud-part-3 {
width: 60px;
height: 60px;
top: -30px;
left: 150px;
}
</style>
{/snippet}
{#snippet star()}
<div class="rotate-0" style:--tw-rotate={getRandomRotation()}>
<Star size="3rem" class="stroke-yellow-400 fill-yellow-400"></Star>
</div>
{/snippet}
<style lang="postcss">
.hero {
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: clip;
height: 100%;
&.light-background {
background-image: linear-gradient(
to bottom right in hsl,
hsl(180, 100%, 55%),
hsl(215, 100%, 74%)
);
}
&.dark-background {
@apply bg-black;
}
}
.hero-content {
position: relative;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
@apply pointer-events-none;
/*
TODO: 3d tilting card effect
https://www.youtube.com/watch?v=Z-3tPXf9a7M
*/
/* transform: rotate3d(0.1, 0.1, 0, calc(var(--asdf-shift, 0) * 0.1)); */
}
.hero-title {
font-size: 4rem;
/* color: hsl(180, 20%, 30%); */
}
.parallax {
@apply absolute size-full;
}
.blur-panel {
@apply absolute size-full;
&.background {
@apply backdrop-blur-[1px];
}
&.midground {
@apply backdrop-blur-[1px];
}
&.foreground {
}
}
.island {
--shift-scale: 1;
--size-scale: 1;
--blur-amount: 0px;
--opacity: 1;
position: absolute;
/*
shift inner content to center of coordinates
then translate based on input
*/
transform: translate(-50%, -50%)
translate(
calc(var(--x-shift) * var(--shift-scale)),
calc(var(--y-shift) * var(--shift-scale))
)
scale(var(--size-scale));
opacity: var(--opacity);
&.foreground {
--shift-scale: 0.8;
--size-scale: 1;
--opacity: 0.9;
}
&.midground {
--shift-scale: 0.5;
--size-scale: 0.7;
--opacity: 0.9;
}
&.background {
--shift-scale: 0.2;
--size-scale: 0.5;
--opacity: 0.8;
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment