From 1f67bb749313d482ff902eabf8b997e1167c97c8 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Thu, 1 Oct 2026 21:28:22 +0200 Subject: [PATCH] feat: update authorization animation slightly --- frontend/src/lib/icons/connect-arrow.svelte | 141 ++++++++++---- .../components/client-provider-images.svelte | 179 ++++++++++++++---- 2 files changed, 242 insertions(+), 78 deletions(-) diff --git a/frontend/src/lib/icons/connect-arrow.svelte b/frontend/src/lib/icons/connect-arrow.svelte index 63c29a7d..73aa2f8e 100644 --- a/frontend/src/lib/icons/connect-arrow.svelte +++ b/frontend/src/lib/icons/connect-arrow.svelte @@ -1,43 +1,106 @@ - - - + + + + + {#each strokes as points, i (i)} + + {/each} + + + diff --git a/frontend/src/routes/authorize/components/client-provider-images.svelte b/frontend/src/routes/authorize/components/client-provider-images.svelte index 046f6fff..38645d09 100644 --- a/frontend/src/routes/authorize/components/client-provider-images.svelte +++ b/frontend/src/routes/authorize/components/client-provider-images.svelte @@ -4,9 +4,15 @@ import ConnectArrow from '$lib/icons/connect-arrow.svelte'; import CrossAnimated from '$lib/icons/cross-animated.svelte'; import { m } from '$lib/paraglide/messages'; + import appConfigStore from '$lib/stores/application-configuration-store'; import type { OidcClientMetaData } from '$lib/types/oidc.type'; import { cachedOidcClientLogo } from '$lib/utils/cached-image-util'; import { mode } from 'mode-watcher'; + import { untrack } from 'svelte'; + import { cubicIn, cubicOut } from 'svelte/easing'; + import { prefersReducedMotion, Tween } from 'svelte/motion'; + + type Outcome = 'success' | 'error'; const { success, @@ -18,58 +24,153 @@ client?: OidcClientMetaData; } = $props(); - let animationDone = $state(false); + // Each tile travels this far so they meet in the middle + const TRAVEL = 108; + // The tiles start 152px apart, so they touch once each has covered this share of its travel + const CONTACT = 152 / (2 * TRAVEL); + // Kept below the previous 500ms slide so the animation never delays the redirect + const PULL_MS = 350; - $effect(() => { - if (success || error) { - setTimeout(() => (animationDone = true), client ? 500 : 0); - } else { - animationDone = false; - } - }); + const SQUASH = [ + { transform: 'scale(1.18, .84)' }, + { transform: 'scale(.94, 1.06)', offset: 0.45 }, + { transform: 'scale(1)' } + ]; + const SHAKE = [ + { transform: 'scale(1.18, .84)' }, + { transform: 'scale(.94, 1.06)', offset: 0.2 }, + { transform: 'scale(1)', offset: 0.3 }, + { transform: 'translateX(-7px)', offset: 0.44 }, + { transform: 'translateX(6px)', offset: 0.58 }, + { transform: 'translateX(-4px)', offset: 0.72 }, + { transform: 'translateX(2px)', offset: 0.86 }, + { transform: 'translateX(0)' } + ]; const isLightMode = $derived(mode.current === 'light'); + + // The tiles accelerate into each other while the arrow snaps taut during the first third of the pull + const pull = new Tween(0, { duration: PULL_MS }); + const travelled = $derived(cubicIn(pull.current)); + const taut = $derived(cubicOut(Math.min(1, pull.current / 0.32))); + + let result = $state(null); + let animated = $state(false); + let resultTile = $state(); + let hasMounted = false; + + $effect(() => { + const outcome = success ? 'success' : error ? 'error' : null; + untrack(() => show(outcome)); + }); + + async function show(outcome: Outcome | null) { + // An outcome that is already set when the page loads is shown without replaying the animation + const animate = + hasMounted && !$appConfigStore.disableAnimations && !prefersReducedMotion.current; + hasMounted = true; + + resultTile?.getAnimations().forEach((animation) => animation.cancel()); + result = null; + + if (!outcome) { + pull.set(0, { duration: 0 }); + return; + } + + // The pull only starts from rest, so an outcome that arrives mid-pull jumps straight to the merged tile + // Any newer outcome interrupts the pull, and an interrupted tween never resolves, so a stale outcome is never shown + if (animate && pull.current === 0) await pull.set(1); + else pull.set(1, { duration: 0 }); + + animated = animate; + result = outcome; + if (animate) { + resultTile?.animate(outcome === 'success' ? SQUASH : SHAKE, { + duration: outcome === 'success' ? 380 : 620, + easing: 'ease-out' + }); + } + }
-
- {#if animationDone && success} -
- + +
+
+ +
+ +
+ {#if client?.hasLogo} + {m.client_logo()} + {:else if client?.name} +
+ {client.name.charAt(0).toUpperCase()} +
+ {/if}
- {:else if animationDone && error} -
- -
- {:else if client?.hasLogo} - {m.client_logo()} - {:else if client?.name} -
- {client.name.charAt(0).toUpperCase()} -
- {/if} + + {#if result} +
+ {#if result === 'success'} + + {:else} + + {/if} +
+ {/if} +
+ +