diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 06da873..fe736c7 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -263,6 +263,18 @@ body { font-size: 16px; line-height: 1.65; font-weight: var(--body-weight); + transition: opacity 0.4s ease; +} + +html.is-loading, +html.is-loading body { + background: var(--bg); +} + +html.is-loading body { + opacity: 0; + overflow: hidden; + transition: none; } strong, diff --git a/app/plugins/page-ready.client.ts b/app/plugins/page-ready.client.ts new file mode 100644 index 0000000..97aa5d7 --- /dev/null +++ b/app/plugins/page-ready.client.ts @@ -0,0 +1,45 @@ +import blendSrc from '~/assets/images/blend.jpeg'; + +const MAX_WAIT_MS = 2500; + +function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function loadSrc(src: string) { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => resolve(); + img.onerror = () => resolve(); + img.src = src; + }); +} + +function settleImage(img: HTMLImageElement) { + if (img.complete) { + return img.decode?.().catch(() => undefined) ?? Promise.resolve(); + } + return new Promise((resolve) => { + img.addEventListener('load', () => resolve(), { once: true }); + img.addEventListener('error', () => resolve(), { once: true }); + }); +} + +async function whenReady() { + const tasks: Promise[] = [loadSrc(blendSrc)]; + if (document.fonts?.ready) tasks.push(document.fonts.ready); + for (const img of document.images) { + tasks.push(settleImage(img)); + } + await Promise.race([Promise.all(tasks), sleep(MAX_WAIT_MS)]); + await new Promise((resolve) => requestAnimationFrame(() => resolve())); +} + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('app:mounted', async () => { + await whenReady(); + document.documentElement.classList.remove('is-loading'); + }); +}); diff --git a/nuxt.config.ts b/nuxt.config.ts index 3bd4e3f..b5aa292 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -60,7 +60,7 @@ export default defineNuxtConfig({ link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }], script: [ { - innerHTML: `(function(){try{var t=localStorage.getItem('bg-theme')||'dusk';var f=localStorage.getItem('body-font')||'plantin';document.documentElement.classList.add('theme-'+t,'font-'+f);}catch(e){document.documentElement.classList.add('theme-dusk','font-plantin');}})();`, + innerHTML: `(function(){try{var t=localStorage.getItem('bg-theme')||'dusk';var f=localStorage.getItem('body-font')||'plantin';var m=t==='paper'?'light-mode':'dark-mode';document.documentElement.classList.add('theme-'+t,'font-'+f,m,'is-loading');}catch(e){document.documentElement.classList.add('theme-dusk','font-plantin','dark-mode','is-loading');}setTimeout(function(){document.documentElement.classList.remove('is-loading')},4000);})();`, tagPosition: 'head', }, ],