feat(docs): Happy Halloween! 🎃
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 MiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="4em" height="4em" viewBox="0 0 24 24">
|
||||
<g fill="none" fill-rule="evenodd" transform="rotate(30) translate(10 -5) scale(0.7)">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/>
|
||||
<path fill="currentColor" d="M12 2a9 9 0 0 1 9 9v8.62c0 1.83-1.966 2.987-3.565 2.099l-.363-.195c-1-.512-1.784-.68-2.889-.114l-.198.108a4 4 0 0 1-3.762.11l-.208-.11c-1.277-.73-2.166-.512-3.45.2c-1.6.89-3.565-.267-3.565-2.097V11a9 9 0 0 1 9-9M8.5 9a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m7 0a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -6,7 +6,14 @@ import { createAnimatable } from 'animejs'
|
||||
import { onMounted, shallowRef, useTemplateRef, watch, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// NOTICE: for unknown reasons, without `?no-inline`, the inlined SVG use in the SFC template
|
||||
// of `url(...)` doesn't work properly in Vite/Vue, the entire SVG content either got incorrectly
|
||||
// cached or erased in dev/prod, causing the mask pattern to not show up.
|
||||
import homeBackgroundPatternGhost from '../assets/home-patterns-ghost.svg?no-inline'
|
||||
import ParallaxCover from './ParallaxCover.vue'
|
||||
import ParallaxCoverHalloween20251029 from './ParallaxCoverHalloween20251029.vue'
|
||||
|
||||
import { isBetweenHalloweenAndHalfOfNovember } from '../composables/date'
|
||||
|
||||
const heroRef = useTemplateRef<HTMLDivElement>('hero')
|
||||
|
||||
@@ -83,12 +90,30 @@ watchEffect((onCleanup) => {
|
||||
<section class="mx-auto h-full max-w-[1440px] w-full flex flex-1 flex-col">
|
||||
<div class="z-10 h-full w-full flex flex-1 flex-col items-center justify-start gap-4 overflow-hidden px-16 pb-16 pt-20 md:pt-36">
|
||||
<div ref="hero" flex="~ col items-center gap-4 justify-start">
|
||||
<div class="relative w-full justify-center text-center font-extrabold font-sans-rounded" text="4xl md:5xl">
|
||||
<div>
|
||||
Project AIRI
|
||||
</div>
|
||||
<div class="relative w-full flex flex-col items-center justify-center text-center font-extrabold font-sans-rounded" text="4xl md:5xl">
|
||||
<ClientOnly>
|
||||
<div
|
||||
v-if="isBetweenHalloweenAndHalfOfNovember(new Date())"
|
||||
:class="[
|
||||
'w-fit',
|
||||
'flex items-center gap-2',
|
||||
'px-4 py-1',
|
||||
'mb-3 sm:mb-4 lg:mb-8',
|
||||
'border-1 rounded-full border-orange-500 bg-orange-100/20 text-orange-700 dark:border-orange-400 dark:bg-orange-900/20 dark:text-orange-300',
|
||||
'text-sm sm:text-base align-middle',
|
||||
]"
|
||||
>
|
||||
<div i-twemoji:jack-o-lantern />Happy Halloween!<div i-twemoji:jack-o-lantern />
|
||||
</div>
|
||||
<div :class="[isBetweenHalloweenAndHalfOfNovember(new Date()) ? 'font-sans-serif-halloween' : '']">
|
||||
Project AIRI
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<div class="relative max-w-prose text-center text-slate-900 dark:text-white">
|
||||
<div
|
||||
class="relative max-w-prose text-center text-slate-900 dark:text-white"
|
||||
:class="[isBetweenHalloweenAndHalfOfNovember(new Date()) ? 'font-sans-serif-halloween-secondary' : '']"
|
||||
>
|
||||
{{ t('docs.theme.home.subtitle') }}
|
||||
</div>
|
||||
<div class="relative z-10 w-full flex justify-center gap-4">
|
||||
@@ -134,10 +159,17 @@ watchEffect((onCleanup) => {
|
||||
</section>
|
||||
|
||||
<div class="absolute inset-0 overflow-hidden -z-10">
|
||||
<!-- Repeating, slowly scrolling SVG pattern background -->
|
||||
<div class="bg-ghost-pattern pointer-events-none absolute inset-0 z-0 opacity-10 dark:opacity-10" :style="{ '--bg-mask-ghost-pattern': `url(${homeBackgroundPatternGhost})` }" />
|
||||
<!-- Flickering red (even within the color space range) if to top in oklch (UnoCSS or tailwind css default), have to force to use srgb color space to prevent this -->
|
||||
<div class="absolute bottom-0 left-0 right-0 top-0 z-2 h-80% from-transparent to-white bg-gradient-to-t dark:to-[hsl(207_15%_5%)]" style="--un-gradient-shape: to top in srgb;" />
|
||||
<ClientOnly>
|
||||
<ParallaxCover />
|
||||
<template v-if="isBetweenHalloweenAndHalfOfNovember(new Date())">
|
||||
<ParallaxCoverHalloween20251029 />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ParallaxCover />
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
|
||||
@@ -148,3 +180,27 @@ watchEffect((onCleanup) => {
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Infinite scrolling background pattern via mask */
|
||||
.bg-ghost-pattern {
|
||||
background-color: white;
|
||||
-webkit-mask-image: var(--bg-mask-ghost-pattern);
|
||||
mask-image: var(--bg-mask-ghost-pattern);
|
||||
-webkit-mask-repeat: repeat;
|
||||
mask-repeat: repeat;
|
||||
-webkit-mask-size: 256px 256px;
|
||||
mask-size: 256px 256px;
|
||||
-webkit-mask-position: 0 0;
|
||||
mask-position: 0 0;
|
||||
animation: ghost-mask-scroll 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ghost-mask-scroll {
|
||||
100% { -webkit-mask-position: 256px 256px; mask-position: 256px 256px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bg-ghost-pattern { animation: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,8 +8,8 @@ import { onMounted, shallowRef, useTemplateRef, watchEffect } from 'vue'
|
||||
import homeCover from '../assets/home-cover-2025-07-23.avif'
|
||||
|
||||
const surfaceRef = useTemplateRef<HTMLImageElement>('surface')
|
||||
const silhouettePinkRef = useTemplateRef<HTMLDivElement>('silhouettePink')
|
||||
const silhouettePurpleRef = useTemplateRef<HTMLDivElement>('silhouettePurple')
|
||||
const silhouetteLayer1Ref = useTemplateRef<HTMLDivElement>('silhouetteLayer1')
|
||||
const silhouetteLayer2Ref = useTemplateRef<HTMLDivElement>('silhouetteLayer2')
|
||||
|
||||
const shouldReduceMotion = useLocalStorage('docs:settings/reduce-motion', false)
|
||||
|
||||
@@ -17,8 +17,8 @@ const DURATION = 1200
|
||||
const EASE = 'outSine'
|
||||
|
||||
const surfaceAnimatable = shallowRef<AnimatableObject>()
|
||||
const silhouettePinkAnimatable = shallowRef<AnimatableObject>()
|
||||
const silhouettePurpleAnimatable = shallowRef<AnimatableObject>()
|
||||
const silhouetteLayer1Animatable = shallowRef<AnimatableObject>()
|
||||
const silhouetteLayer2Animatable = shallowRef<AnimatableObject>()
|
||||
|
||||
function animateCover(xOffsetRatio: number, yOffsetRatio: number) {
|
||||
const referenceWidth = window.innerWidth
|
||||
@@ -27,13 +27,13 @@ function animateCover(xOffsetRatio: number, yOffsetRatio: number) {
|
||||
surfaceAnimatable.value?.y?.(-yOffsetRatio * 0.02 * referenceWidth)
|
||||
surfaceAnimatable.value?.z?.(0)
|
||||
|
||||
silhouettePinkAnimatable.value?.x?.(0.01 * referenceWidth - yOffsetRatio * 0.015 * referenceWidth)
|
||||
silhouettePinkAnimatable.value?.y?.(0.02 * referenceWidth + xOffsetRatio * 0.015 * referenceWidth)
|
||||
silhouettePinkAnimatable.value?.z?.(0)
|
||||
silhouetteLayer1Animatable.value?.x?.(0.01 * referenceWidth - yOffsetRatio * 0.015 * referenceWidth)
|
||||
silhouetteLayer1Animatable.value?.y?.(0.02 * referenceWidth + xOffsetRatio * 0.015 * referenceWidth)
|
||||
silhouetteLayer1Animatable.value?.z?.(0)
|
||||
|
||||
silhouettePurpleAnimatable.value?.x?.(0.01 * referenceWidth + yOffsetRatio * 0.01 * referenceWidth)
|
||||
silhouettePurpleAnimatable.value?.y?.(-0.01 * referenceWidth - yOffsetRatio * 0.01 * referenceWidth)
|
||||
silhouettePurpleAnimatable.value?.z?.(0)
|
||||
silhouetteLayer2Animatable.value?.x?.(0.01 * referenceWidth + yOffsetRatio * 0.01 * referenceWidth)
|
||||
silhouetteLayer2Animatable.value?.y?.(-0.01 * referenceWidth - yOffsetRatio * 0.01 * referenceWidth)
|
||||
silhouetteLayer2Animatable.value?.z?.(0)
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
@@ -55,8 +55,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
surfaceAnimatable.value = createAnimatable(surfaceRef.value!, animatableConfig)
|
||||
silhouettePinkAnimatable.value = createAnimatable(silhouettePinkRef.value!, animatableConfig)
|
||||
silhouettePurpleAnimatable.value = createAnimatable(silhouettePurpleRef.value!, animatableConfig)
|
||||
silhouetteLayer1Animatable.value = createAnimatable(silhouetteLayer1Ref.value!, animatableConfig)
|
||||
silhouetteLayer2Animatable.value = createAnimatable(silhouetteLayer2Ref.value!, animatableConfig)
|
||||
})
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
@@ -88,8 +88,8 @@ const maskImageURL = `url(${homeCover})`
|
||||
]"
|
||||
>
|
||||
<img ref="surface" :src="homeCover" alt="Project AIRI Cover Image" class="w-full object-cover">
|
||||
<div ref="silhouettePink" class="silhouette absolute left-0 top-0 z--1 h-full w-full bg-[oklch(0.8105_0.1267_350.84)]" />
|
||||
<div ref="silhouettePurple" class="silhouette absolute left-0 top-0 z--2 h-full w-full bg-[oklch(0.5712_0.2396_278.59)]" />
|
||||
<div ref="silhouetteLayer2" class="silhouette absolute left-0 top-0 z--1 h-full w-full bg-[oklch(0.8105_0.1267_350.84)]" />
|
||||
<div ref="silhouetteLayer2" class="silhouette absolute left-0 top-0 z--2 h-full w-full bg-[oklch(0.5712_0.2396_278.59)]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import type { AnimatableObject } from 'animejs'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { createAnimatable } from 'animejs'
|
||||
import { onMounted, shallowRef, useTemplateRef, watchEffect } from 'vue'
|
||||
|
||||
import homeCover from '../assets/home-cover-2025-10-29.png'
|
||||
|
||||
const surfaceRef = useTemplateRef<HTMLImageElement>('surface')
|
||||
const silhouetteLayer1Ref = useTemplateRef<HTMLDivElement>('silhouetteLayer1')
|
||||
const silhouetteLayer2Ref = useTemplateRef<HTMLDivElement>('silhouetteLayer2')
|
||||
|
||||
const shouldReduceMotion = useLocalStorage('docs:settings/reduce-motion', false)
|
||||
|
||||
const DURATION = 1200
|
||||
const EASE = 'outSine'
|
||||
|
||||
const surfaceAnimatable = shallowRef<AnimatableObject>()
|
||||
const silhouetteLayer1Animatable = shallowRef<AnimatableObject>()
|
||||
const silhouetteLayer2Animatable = shallowRef<AnimatableObject>()
|
||||
|
||||
function animateCover(xOffsetRatio: number, yOffsetRatio: number) {
|
||||
const referenceWidth = window.innerWidth
|
||||
|
||||
surfaceAnimatable.value?.x?.(-xOffsetRatio * 0.02 * referenceWidth)
|
||||
surfaceAnimatable.value?.y?.(-yOffsetRatio * 0.02 * referenceWidth)
|
||||
surfaceAnimatable.value?.z?.(0)
|
||||
|
||||
silhouetteLayer1Animatable.value?.x?.(0.01 * referenceWidth - yOffsetRatio * 0.015 * referenceWidth)
|
||||
silhouetteLayer1Animatable.value?.y?.(0.02 * referenceWidth + xOffsetRatio * 0.015 * referenceWidth)
|
||||
silhouetteLayer1Animatable.value?.z?.(0)
|
||||
|
||||
silhouetteLayer2Animatable.value?.x?.(0.01 * referenceWidth + yOffsetRatio * 0.01 * referenceWidth)
|
||||
silhouetteLayer2Animatable.value?.y?.(-0.01 * referenceWidth - yOffsetRatio * 0.01 * referenceWidth)
|
||||
silhouetteLayer2Animatable.value?.z?.(0)
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
|
||||
const xOffsetRatio = (x - window.innerWidth / 2) / window.innerWidth
|
||||
const yOffsetRatio = (y - window.innerHeight / 2) / window.innerHeight
|
||||
|
||||
animateCover(xOffsetRatio, yOffsetRatio)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const animatableConfig = {
|
||||
x: DURATION,
|
||||
y: DURATION,
|
||||
z: 0,
|
||||
ease: EASE,
|
||||
}
|
||||
|
||||
surfaceAnimatable.value = createAnimatable(surfaceRef.value!, animatableConfig)
|
||||
silhouetteLayer1Animatable.value = createAnimatable(silhouetteLayer1Ref.value!, animatableConfig)
|
||||
silhouetteLayer2Animatable.value = createAnimatable(silhouetteLayer2Ref.value!, animatableConfig)
|
||||
})
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
if (shouldReduceMotion.value) {
|
||||
animateCover(0, 0)
|
||||
}
|
||||
else {
|
||||
if (!import.meta.env.SSR) {
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const maskImageURL = `url(${homeCover})`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'relative left-1/2 -translate-x-1/2 max-w-none z-1',
|
||||
'w-[160%] translate-y-[50%] translate-x-[-75%] scale-150 -rotate-0 top-8rem',
|
||||
'md:w-[150%] md:translate-y-[40%] md:rotate-[0deg] scale-125 md:top-8dvh',
|
||||
'lg:w-[95%] lg:translate-y-[2%] lg:translate-x-[-55%] lg:rotate-[0deg] lg:scale-95 lg:top-32dvh',
|
||||
'xl:top-18dvh',
|
||||
'2xl:w-[100%] 2xl:translate-y-[-4%] 2xl:translate-x-[-60%] 2xl:rotate-[0deg] 2xl:top-16dvh',
|
||||
]"
|
||||
>
|
||||
<img ref="surface" :src="homeCover" alt="Project AIRI Cover Image" class="w-full object-cover">
|
||||
<div ref="silhouetteLayer2" class="silhouette absolute left-0 top-0 z--1 h-full w-full bg-[oklch(0.89_0.08_67.49)]" />
|
||||
<div ref="silhouetteLayer2" class="silhouette absolute left-0 top-0 z--2 h-full w-full bg-[oklch(0.69_0.16_295.04)]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.silhouette {
|
||||
-webkit-mask-image: v-bind(maskImageURL);
|
||||
mask-image: v-bind(maskImageURL);
|
||||
-webkit-mask-mode: alpha;
|
||||
mask-mode: alpha;
|
||||
-webkit-mask-size: contain;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center; /* Center the mask */
|
||||
mask-position: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { isAfter, isBefore } from 'date-fns'
|
||||
|
||||
export function isBetweenHalloweenAndHalfOfNovember(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const halloween = new Date(year, 9, 25) // October 25
|
||||
const halfOfNovember = new Date(year, 10, 15) // November 15
|
||||
|
||||
return isAfter(date, halloween) && isBefore(date, halfOfNovember)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import DocOutline from '../components/DocOutline.vue'
|
||||
import DocSidebar from '../components/DocSidebar.vue'
|
||||
import DocTopbar from '../components/DocTopbar.vue'
|
||||
|
||||
import { isBetweenHalloweenAndHalfOfNovember } from '../composables/date'
|
||||
import { flatten } from '../utils/flatten'
|
||||
|
||||
const { theme, frontmatter } = useData()
|
||||
@@ -51,12 +52,22 @@ const isCharactersPage = computed(() => path.value.includes('characters'))
|
||||
class="pointer-events-none absolute inset-0 left-0 top-0 z-0 h-max w-full flex justify-center overflow-hidden"
|
||||
>
|
||||
<div class="w-[108rem] flex flex-none justify-end">
|
||||
<img
|
||||
class="max-w-none w-[90rem] flex-none"
|
||||
decoding="async"
|
||||
src="/new-bg.avif"
|
||||
alt="backdrop"
|
||||
>
|
||||
<ClientOnly>
|
||||
<img
|
||||
v-if="isBetweenHalloweenAndHalfOfNovember(new Date())"
|
||||
class="max-w-none w-[90rem] flex-none"
|
||||
decoding="async"
|
||||
src="/new-bg-halloween.png"
|
||||
alt="backdrop"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="max-w-none w-[90rem] flex-none"
|
||||
decoding="async"
|
||||
src="/new-bg.avif"
|
||||
alt="backdrop"
|
||||
>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
+3
-1
@@ -23,6 +23,7 @@
|
||||
"@proj-airi/i18n": "workspace:^",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"colorjs.io": "^0.5.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"mark.js": "^8.11.1",
|
||||
"motion-v": "^1.7.2",
|
||||
"pathe": "^2.0.3",
|
||||
@@ -33,6 +34,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.69",
|
||||
"@iconify-json/mingcute": "^1.2.5",
|
||||
"@iconify-json/twemoji": "^1.2.4",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||
"@mdit/plugin-footnote": "^0.22.2",
|
||||
@@ -45,7 +48,6 @@
|
||||
"@unocss/reset": "^66.5.3",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"animejs": "^4.2.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"gray-matter": "^4.0.3",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
|
||||
@@ -64,6 +64,12 @@ export default defineConfig({
|
||||
name: 'Comfortaa Variable',
|
||||
provider: 'none',
|
||||
},
|
||||
'mystery-quest': {
|
||||
name: 'Mystery Quest',
|
||||
},
|
||||
'grandstander': {
|
||||
name: 'Grandstander',
|
||||
},
|
||||
},
|
||||
}),
|
||||
presetIcons(),
|
||||
@@ -100,6 +106,8 @@ export default defineConfig({
|
||||
fontFamily: {
|
||||
'sans': `ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-rounded': `"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-serif-halloween-secondary': `"Grandstander", "DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
'sans-serif-halloween': `"Mystery Quest", "DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";`,
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
|
||||
Reference in New Issue
Block a user