feat: page transitions

This commit is contained in:
not-nullptr 2024-11-11 20:09:45 +00:00
parent 08353cf8ff
commit 38709395d4
8 changed files with 285 additions and 23 deletions

View File

@ -18,5 +18,5 @@
}
body {
@apply text-foreground bg-background font-body;
@apply text-foreground bg-background font-body overflow-x-hidden;
}

118
src/lib/animation/index.ts Normal file
View File

@ -0,0 +1,118 @@
import type { EasingFunction, TransitionConfig } from "svelte/transition";
export const transition =
"linear(0,0.006,0.025 2.8%,0.101 6.1%,0.539 18.9%,0.721 25.3%,0.849 31.5%,0.937 38.1%,0.968 41.8%,0.991 45.7%,1.006 50.1%,1.015 55%,1.017 63.9%,1.001)";
export const duration = 500;
const remap = (
value: number,
low1: number,
high1: number,
low2: number,
high2: number,
) => low2 + ((high2 - low2) * (value - low1)) / (high1 - low1);
const choose = (
direction: "in" | "out" | "both",
defaultValue: number,
inValue?: number,
outValue?: number,
) =>
direction !== "out"
? typeof inValue === "number"
? inValue
: defaultValue
: typeof outValue === "number"
? outValue
: defaultValue;
export const blur = (
_: HTMLElement,
config:
| Partial<{
blurMultiplier: number;
duration: number;
easing: EasingFunction;
scale: {
start: number;
end: number;
};
x: {
start: number;
end: number;
};
y: {
start: number;
end: number;
};
delay: number;
opacity: boolean;
}>
| undefined,
dir: {
direction: "in" | "out" | "both";
},
): TransitionConfig => {
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (typeof config?.opacity === "undefined" && config) config.opacity = true;
return {
delay: config?.delay || 0,
duration: prefersReducedMotion ? 0 : config?.duration || 300,
css: (t) =>
prefersReducedMotion
? ""
: `filter: blur(${(1 - t) * (config?.blurMultiplier || 1)}px); opacity: ${config?.opacity ? t : 1}; transform: translate(${remap(
t,
0,
1,
choose(
dir.direction,
0,
config?.x?.start,
config?.x?.end,
),
choose(
dir.direction,
0,
config?.x?.end,
config?.x?.start,
),
)}px, ${remap(
t,
0,
1,
choose(
dir.direction,
0,
config?.y?.start,
config?.y?.end,
),
choose(
dir.direction,
0,
config?.y?.end,
config?.y?.start,
),
)}px) scale(${remap(
t,
0,
1,
choose(
dir.direction,
0.9,
config?.scale?.start,
config?.scale?.end,
),
choose(
dir.direction,
1,
config?.scale?.end,
config?.scale?.start,
),
)});`,
easing: config?.easing,
};
};

View File

@ -0,0 +1,11 @@
class Files {
public files = $state<File[]>([]);
public conversionTypes = $state<string[]>([]);
public downloadFns = $state<(() => void)[]>([]);
public beenToConverterPage = $state(false);
public shouldShowAlert = $derived(
!this.beenToConverterPage && this.files.length > 0,
);
}
export const files = new Files();

View File

@ -1,7 +1,105 @@
<script lang="ts">
import { onMount } from "svelte";
import "../app.css";
let { children } = $props();
import { goto } from "$app/navigation";
import clsx from "clsx";
import { blur, duration, transition } from "$lib/animation";
import { quintOut } from "svelte/easing";
import { files } from "$lib/store/index.svelte";
let { children, data } = $props();
let navWidth = $state(1);
let shouldGoBack = $state(false);
const links = $derived<{
[key: string]: {
href: string;
alert?: boolean;
};
}>({
Upload: { href: "/" },
[files.files.length > 0
? `Convert ${files.files.length} file${files.files.length > 1 ? "s" : ""}`
: `Convert`]: { href: "/convert", alert: files.shouldShowAlert },
});
const linkCount = $derived(Object.keys(links).length);
const linkIndex = $derived(
Object.keys(links).findIndex(
(link) => links[link].href === data.pathname,
),
);
</script>
{@render children()}
<div class="w-full h-full flex items-center pt-72 flex-col gap-4">
<div
bind:clientWidth={navWidth}
class="bg-background relative w-full h-16 max-w-screen-lg border-2 p-1 border-solid border-foreground-muted-alt rounded-2xl flex"
>
<div
class="absolute pointer-events-none top-1 bg-foreground h-[calc(100%-8px)] rounded-xl"
style="width: {navWidth / linkCount - 8}px; left: {(navWidth /
linkCount) *
linkIndex +
4}px; transition: {duration - 200}ms ease left;"
></div>
{#each Object.entries(links) as [name, link] (link)}
<button
class="w-1/2 h-full flex items-center justify-center rounded-xl relative"
onclick={() => {
const keys = Object.keys(links);
const currentIndex = keys.findIndex(
(key) => links[key].href === data.pathname,
);
const nextIndex = keys.findIndex(
(key) => links[key] === link,
);
shouldGoBack = nextIndex < currentIndex;
console.log({ shouldGoBack });
goto(link.href);
}}
>
<span class="mix-blend-difference invert">
{name}
</span>
</button>
{/each}
</div>
<div class="w-full grid grid-cols-1 grid-rows-1 relative">
{#key data.pathname}
<div class="w-full">
<div
class="absolute top-0 left-0 w-full h-full flex justify-center"
in:blur={{
duration,
easing: quintOut,
blurMultiplier: 12,
x: {
start: !shouldGoBack ? 250 : -250,
end: 0,
},
scale: {
start: 0.75,
end: 1,
},
}}
out:blur={{
duration,
easing: quintOut,
blurMultiplier: 12,
x: {
start: 0,
end: !shouldGoBack ? -250 : 250,
},
scale: {
start: 1,
end: 0.75,
},
}}
>
{@render children()}
</div>
</div>
{/key}
</div>
</div>

6
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,6 @@
export const load = ({ url }) => {
const { pathname } = url;
return {
pathname,
};
};

View File

@ -1,18 +1,15 @@
<script lang="ts">
import Uploader from "$lib/components/visual/Uploader.svelte";
import { converters } from "$lib/converters";
let conversionTypes = $state<string[]>([]);
let downloadFns = $state<(() => void)[]>([]);
let files = $state<File[]>();
import { files } from "$lib/store/index.svelte";
$effect(() => {
$inspect(files);
});
const convertAllFiles = async () => {
const promises = files?.map(async (file, i) => {
let conversionType = conversionTypes[i];
const promises = files.files?.map(async (file, i) => {
let conversionType = files.conversionTypes[i];
const converter = converters[0];
const convertedFile = await converter.convert(
{
@ -21,7 +18,7 @@
},
conversionType,
);
downloadFns[i] = () => {
files.downloadFns[i] = () => {
const url = URL.createObjectURL(
new Blob([convertedFile.buffer]),
);
@ -39,9 +36,7 @@
};
</script>
<div class="w-full h-full flex items-center justify-center">
<Uploader bind:files />
</div>
<Uploader bind:files={files.files} />
<style>
/* for this page specifically */

View File

@ -0,0 +1,33 @@
<script>
import { converters } from "$lib/converters";
import { files } from "$lib/store/index.svelte";
if (files.files.length > 0) files.beenToConverterPage = true;
</script>
<div class="flex flex-col gap-4 w-full items-center">
{#if files.files.length === 0}
<p class="text-foreground-muted">
No files uploaded. Head to the Upload tab to begin!
</p>
{/if}
{#each files.files as file, i}
<div
class="flex items-center w-full max-w-screen-lg border-2 border-solid border-foreground-muted-alt rounded-xl px-4 py-2"
>
<div class="flex items-center flex-grow">
{file.name}
</div>
<div class="flex gap-4 flex-shrink-0">
<select
class="border-2 border-solid border-foreground-muted-alt rounded-xl px-4 py-2 focus:!outline-none"
bind:value={files.conversionTypes[i]}
>
{#each converters[0].supportedFormats as conversionType}
<option value={conversionType}>{conversionType}</option>
{/each}
</select>
</div>
</div>
{/each}
</div>

View File

@ -4,16 +4,17 @@ export default {
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {},
colors: {
background: "var(--bg)",
foreground: "var(--fg)",
"foreground-muted": "var(--fg-muted)",
"foreground-muted-alt": "var(--fg-muted-alt)",
},
fontFamily: {
display: "var(--font-display)",
body: "var(--font-body)",
extend: {
colors: {
background: "var(--bg)",
foreground: "var(--fg)",
"foreground-muted": "var(--fg-muted)",
"foreground-muted-alt": "var(--fg-muted-alt)",
},
fontFamily: {
display: "var(--font-display)",
body: "var(--font-body)",
},
},
},