diff --git a/src/lib/animations/index.ts b/src/lib/animations/index.ts index 341ef1872a..7317585936 100644 --- a/src/lib/animations/index.ts +++ b/src/lib/animations/index.ts @@ -1,36 +1,85 @@ -export function write(text: string, cb: (v: string) => void, duration = 500) { +export function write( + text: string, + cb: (v: string) => void, + duration = 500, + { signal, startIndex = 0 }: { signal?: AbortSignal; startIndex?: number } = {} +) { if (text.length === 0) { cb(''); return Promise.resolve(); } + if (startIndex >= text.length) { + cb(text); + return Promise.resolve(); + } const step = duration / text.length; - let i = 0; - return new Promise((resolve) => { + let i = startIndex; + + return new Promise((resolve, reject) => { const interval = setInterval(() => { + if (signal?.aborted) { + clearInterval(interval); + return reject(new Error('Aborted')); + } + cb(text.slice(0, ++i)); if (i === text.length) { clearInterval(interval); resolve(); } }, step); + + signal?.addEventListener( + 'abort', + () => { + clearInterval(interval); + reject(new Error('Aborted')); + }, + { once: true } + ); }); } -export function unwrite(text: string, cb: (v: string) => void, duration = 500) { +export function unwrite( + text: string, + cb: (v: string) => void, + duration = 500, + { signal, startIndex }: { signal?: AbortSignal; startIndex?: number } = {} +) { if (text.length === 0) { cb(''); return Promise.resolve(); } const step = duration / text.length; - let i = text.length; - return new Promise((resolve) => { + let i = startIndex ?? text.length; + + if (i <= 0) { + cb(''); + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { const interval = setInterval(() => { + if (signal?.aborted) { + clearInterval(interval); + return reject(new Error('Aborted')); + } + cb(text.slice(0, --i)); if (i === 0) { clearInterval(interval); resolve(); } }, step); + + signal?.addEventListener( + 'abort', + () => { + clearInterval(interval); + reject(new Error('Aborted')); + }, + { once: true } + ); }); } diff --git a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte index f1b2ebf338..206bda3d32 100644 --- a/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte +++ b/src/routes/(marketing)/(components)/bento/(animations)/auth.svelte @@ -14,17 +14,37 @@ let password = $state(''); let button: HTMLButtonElement; + let controller: AbortController | null = null; + $effect(() => { inView( container, () => { if (!isMobile()) return; - write('•••••••••••••', (v) => (password = v), 1000).then(() => { - animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); - }); + controller?.abort(); + controller = new AbortController(); + + write('•••••••••••••', (v) => (password = v), 1000, { + signal: controller.signal, + startIndex: password.length + }) + .then(() => { + animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); + }) + .catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); + return () => { - unwrite('•••••••••••••', (v) => (password = v)); + controller?.abort(); + controller = new AbortController(); + unwrite('•••••••••••••', (v) => (password = v), 500, { + signal: controller.signal, + startIndex: password.length + }).catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); }; }, { amount: 'all' } @@ -33,13 +53,35 @@ hover(container, () => { if (isMobile()) return; - write('•••••••••••••', (v) => (password = v), 1000).then(() => { - animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); - }); + controller?.abort(); + controller = new AbortController(); + + write('•••••••••••••', (v) => (password = v), 1000, { + signal: controller.signal, + startIndex: password.length + }) + .then(() => { + animate(button, { scale: [1, 0.95, 1] }, { duration: 0.25 }); + }) + .catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); + return () => { - unwrite('•••••••••••••', (v) => (password = v)); + controller?.abort(); + controller = new AbortController(); + unwrite('•••••••••••••', (v) => (password = v), 500, { + signal: controller.signal, + startIndex: password.length + }).catch((err: unknown) => { + if (err instanceof Error && err.message !== 'Aborted') console.error(err); + }); }; }); + + return () => { + controller?.abort(); + }; });