feat: prettier animations

This commit is contained in:
not-nullptr 2024-11-12 01:44:55 +00:00
parent db5d5414db
commit cd91b30119
4 changed files with 183 additions and 53 deletions

View File

@ -1,4 +1,6 @@
import type { EasingFunction, TransitionConfig } from "svelte/transition";
import type { AnimationConfig, FlipParams } from "svelte/animate";
import { cubicOut } from "svelte/easing";
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)";
@ -58,13 +60,15 @@ export const blur = (
"(prefers-reduced-motion: reduce)",
).matches;
if (typeof config?.opacity === "undefined" && config) config.opacity = true;
const isUsingTranslate = !!config?.x || !!config?.y || !!config?.scale;
console.log(isUsingTranslate);
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(
css: (t) => {
if (prefersReducedMotion) return "";
const translate = isUsingTranslate
? `translate(${remap(
t,
0,
1,
@ -112,7 +116,57 @@ export const blur = (
config?.scale?.end,
config?.scale?.start,
),
)});`,
)})`
: ``;
return `filter: blur(${(1 - t) * (config?.blurMultiplier || 1)}px); opacity: ${config?.opacity ? t : 1}; transform: ${
translate
};`;
},
easing: config?.easing,
};
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function is_function(thing: unknown): thing is Function {
return typeof thing === "function";
}
type Params = FlipParams & {};
/**
* The flip function calculates the start and end position of an element and animates between them, translating the x and y values.
* `flip` stands for [First, Last, Invert, Play](https://aerotwist.com/blog/flip-your-animations/).
*
* https://svelte.dev/docs/svelte-animate#flip
*/
export function flip(
node: HTMLElement,
{ from, to }: { from: DOMRect; to: DOMRect },
params: Params = {},
): AnimationConfig {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const [ox, oy] = style.transformOrigin.split(" ").map(parseFloat);
const dx = from.left + (from.width * ox) / to.width - (to.left + ox);
const dy = from.top + (from.height * oy) / to.height - (to.top + oy);
const {
delay = 0,
duration = (d) => Math.sqrt(d) * 120,
easing = cubicOut,
} = params;
return {
delay,
duration: is_function(duration)
? duration(Math.sqrt(dx * dx + dy * dy))
: duration,
easing,
css: (t, u) => {
const x = u * dx;
const y = u * dy;
// const sx = scale ? t + (u * from.width) / to.width : 1;
// const sy = scale ? t + (u * from.height) / to.height : 1;
return `transform: ${transform} translate(${x}px, ${y}px);`;
},
};
}

View File

@ -4,6 +4,7 @@ class Files {
file: File;
to: string;
blobUrl: string;
id: string;
}[]
>([]);
public conversionTypes = $state<string[]>([]);

View File

@ -13,6 +13,7 @@
file: f,
to: converters[0].supportedFormats[0],
blobUrl: URL.createObjectURL(f),
id: Math.random().toString(36).substring(2),
})),
];

View File

@ -1,64 +1,138 @@
<script>
<script lang="ts">
import { blur, duration, flip, transition } from "$lib/animation";
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
import { converters } from "$lib/converters";
import { files } from "$lib/store/index.svelte";
import clsx from "clsx";
import { XIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { quintOut } from "svelte/easing";
if (files.files.length > 0) files.beenToConverterPage = true;
const reversed = $derived(files.files.slice().reverse());
let finisheds = $state(
Array.from({ length: files.files.length }, () => false),
);
onMount(() => {
finisheds.forEach((_, i) => {
const duration = 750 + i * 50 - 8;
setTimeout(() => {
finisheds[i] = true;
console.log(`finished ${i}`);
}, duration);
});
});
</script>
<div class="flex flex-col gap-4 w-full items-center">
<div class="grid grid-cols-1 grid-rows-1 w-full">
{#if files.files.length === 0}
<p class="text-foreground-muted">
<p class="text-foreground-muted col-start-1 row-start-1 text-center">
No files uploaded. Head to the Upload tab to begin!
</p>
{/if}
{#each files.files.reverse() as file, i}
<div
class="flex relative items-center w-full border-2 border-solid border-foreground-muted-alt rounded-xl pl-4 pr-2 py-2"
>
<div class="flex items-center w-full z-50 relative">
<div
class="flex items-center flex-grow"
style="text-shadow: 0px 0px 6px white, 0px 0px 12px white"
>
{file.file.name}
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span>from</span>
<span
class="py-2 px-3 font-display bg-foreground text-background rounded-xl"
>.{file.file.name.split(".").slice(-1)}</span
>
<span>to</span>
<select
class="font-display border-2 border-solid border-foreground-muted-alt rounded-xl p-2 focus:!outline-none"
bind:value={files.conversionTypes[i]}
>
{#each converters[0].supportedFormats as conversionType}
<option value={conversionType}
>{conversionType}</option
>
{/each}
</select>
</div>
</div>
<!-- god knows why, but setting opacity > 0.98 causes a z-ordering issue in firefox ??? -->
<div
class="flex flex-col gap-4 w-full items-center col-start-1 row-start-1"
>
{#each reversed as file, i (file.id)}
<div
class="absolute top-0 -z-50 left-0 w-full h-full rounded-[10px] overflow-hidden opacity-[0.98]"
animate:flip={{ duration, easing: quintOut }}
out:blur={{
duration,
easing: quintOut,
blurMultiplier: 16,
}}
class={clsx(
"flex relative items-center w-full border-2 border-solid border-foreground-muted-alt rounded-xl pl-4 pr-2 py-2",
{
"initial-fade": !finisheds[i],
},
)}
style="--delay: {i *
50}ms; --transition: {transition}; --duration: {duration}ms;"
>
<div class="flex items-center w-full z-50 relative">
<div
class="flex items-center flex-grow"
style="text-shadow: 0px 0px 6px white, 0px 0px 12px white"
>
{file.file.name}
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span>from</span>
<span
class="py-2 px-3 font-display bg-foreground text-background rounded-xl"
>.{file.file.name.split(".").slice(-1)}</span
>
<span>to</span>
<select
class="font-display border-2 border-solid border-foreground-muted-alt rounded-xl p-2 focus:!outline-none"
bind:value={files.conversionTypes[i]}
>
{#each converters[0].supportedFormats as conversionType}
<option value={conversionType}
>{conversionType}</option
>
{/each}
</select>
<button
onclick={() => {
// delete the file from the list
files.files = files.files.filter(
(f) => f !== file,
);
}}
class="ml-2 mr-1"
>
<XIcon size="18" />
</button>
</div>
</div>
<!-- god knows why, but setting opacity > 0.98 causes a z-ordering issue in firefox ??? -->
<div
class="bg-cover bg-center w-full h-full"
style="background-image: url({file.blobUrl});"
></div>
<div class="absolute top-0 right-0 w-5/6 h-full">
<ProgressiveBlur
direction="right"
endIntensity={64}
iterations={6}
fadeTo="rgba(255, 255, 255, 0.8)"
/>
class="absolute top-0 -z-50 left-0 w-full h-full rounded-[10px] overflow-hidden opacity-[0.98]"
>
<div
class="bg-cover bg-center w-full h-full"
style="background-image: url({file.blobUrl});"
></div>
<div class="absolute top-0 right-0 w-5/6 h-full">
<ProgressiveBlur
direction="right"
endIntensity={64}
iterations={6}
fadeTo="rgba(255, 255, 255, 0.8)"
/>
</div>
</div>
</div>
</div>
{/each}
{/each}
</div>
</div>
<style>
@keyframes initial-transition {
0% {
transform: translateY(50px);
filter: blur(16px);
opacity: 0;
}
100% {
transform: translateY(0);
filter: blur(0);
opacity: 1;
}
}
.initial-fade {
animation: initial-transition 750ms var(--delay) ease-out;
animation-timing-function: var(--transition);
opacity: 0;
}
.initial-fade.finished {
animation: none;
opacity: 1 !important;
}
</style>