basic setup

This commit is contained in:
nicwands
2026-05-18 15:13:23 -04:00
parent c4113658f7
commit a54e63323f
33 changed files with 2563 additions and 43 deletions

View File

@@ -0,0 +1,42 @@
import { ref } from 'vue'
export default () => {
const os = ref('Unknown')
const isMobile = ref(false)
const isAndroid = ref(false)
const isIOS = ref(false)
const isMacOS = ref(false)
const isWindows = ref(false)
const isLinux = ref(false)
if (typeof navigator !== 'undefined') {
const userAgent = navigator.userAgent.toLowerCase()
const platform = navigator.platform.toLowerCase()
const detectedAndroid = /android/.test(userAgent)
const detectedIOS = /iphone|ipad|ipod/.test(userAgent)
const detectedMacOS = /mac/.test(platform) && !detectedIOS
const detectedWindows = /win/.test(platform)
const detectedLinux = /linux/.test(platform) && !detectedAndroid
os.value = detectedAndroid
? 'Android'
: detectedIOS
? 'iOS'
: detectedMacOS
? 'macOS'
: detectedWindows
? 'Windows'
: detectedLinux
? 'Linux'
: 'Unknown'
isMobile.value = detectedAndroid || detectedIOS
isAndroid.value = detectedAndroid
isIOS.value = detectedIOS
isMacOS.value = detectedMacOS
isWindows.value = detectedWindows
isLinux.value = detectedLinux
}
return { os, isMobile, isAndroid, isIOS, isMacOS, isWindows, isLinux }
}

14
composables/useLenis.js Normal file
View File

@@ -0,0 +1,14 @@
import { inject, onBeforeUnmount } from 'vue'
export default (callback = () => {}, instanceId) => {
const instanceKey = `lenis${instanceId ? `-${instanceId}` : ''}`
const lenis = inject(instanceKey)
if (lenis.value) {
lenis.value.on('scroll', callback)
}
onBeforeUnmount(() => lenis.value?.off('scroll', callback))
return lenis
}

View File

@@ -0,0 +1,29 @@
import { useWindowSize } from '@vueuse/core'
import { viewports } from '@/libs/theme'
const { width: wWidth, height: wHeight } = useWindowSize()
export default () => {
// Desktop
const dvw = (pixels) => {
return (pixels / viewports.desktop.width) * wWidth.value
}
const dvh = (pixels) => {
return (pixels / viewports.desktop.height) * wHeight.value
}
// Mobile
const mvw = (pixels) => {
return (pixels / viewports.mobile.width) * wWidth.value
}
const mvh = (pixels) => {
return (pixels / viewports.mobile.height) * wHeight.value
}
return {
dvw,
dvh,
mvw,
mvh,
}
}

View File

@@ -0,0 +1,46 @@
import {
useElementBounding,
useIntersectionObserver,
useWindowSize,
} from '@vueuse/core'
import { ref } from 'vue'
import { mapRange, clamp } from '@/libs/math'
import useLenis from '@/composables/useLenis'
const { height: wHeight } = useWindowSize()
export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
const isActive = ref(true)
const smoothProgress = ref(0)
const { height, top } = useElementBounding(el)
const isIntersected = ref(false)
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
isIntersected.value = isIntersecting
})
useLenis(({ scroll }) => {
if (!isActive.value) return
if (!height.value || !wHeight.value) return
if (!isIntersected.value) return
const pageTop = scroll + top.value
const start = pageTop - wHeight.value * entry
const end = pageTop + height.value - wHeight.value * exit
let rawProgress = mapRange(start, end, scroll, 0, 1)
rawProgress = clamp(0, rawProgress, 1)
smoothProgress.value += (rawProgress - smoothProgress.value) * 0.1
callback?.(smoothProgress.value)
})
const destroy = () => {
isActive.value = false
stop?.()
}
return { destroy }
}

View File

@@ -0,0 +1,89 @@
import gsap from 'gsap'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useWindowSize, useEventListener } from '@vueuse/core'
/**
* Shared global raw mouse state (only set up once)
*/
const rawMouse = {
x: ref(0),
y: ref(0),
initialized: false,
cleanup: null,
}
const useGlobalMouseListener = () => {
if (!rawMouse.initialized && !import.meta.env.SSR) {
rawMouse.initialized = true
rawMouse.cleanup = useEventListener(window, 'mousemove', (e) => {
rawMouse.x.value = e.clientX
rawMouse.y.value = e.clientY
})
}
}
/**
* Composable for smoothed mouse position with customizable smoothing and normalization.
*/
export default (options) => {
const smoothFactor = options?.smoothFactor ?? 0.1
const normalize = options?.normalize ?? false
const callback = options?.onUpdate
const { width: wWidth, height: wHeight } = useWindowSize()
const sx = ref(0)
const sy = ref(0)
useGlobalMouseListener()
const getTargetX = () =>
normalize ? rawMouse.x.value / wWidth.value : rawMouse.x.value
const getTargetY = () =>
normalize ? rawMouse.y.value / wHeight.value : rawMouse.y.value
let tween
onMounted(() => {
if (tween) tween.kill
// Start smoothing tween
tween = gsap.to(
{ x: sx.value, y: sy.value },
{
duration: 1,
ease: 'linear',
repeat: -1,
onUpdate: () => {
const newX = gsap.utils.interpolate(
sx.value,
getTargetX(),
smoothFactor,
)
const newY = gsap.utils.interpolate(
sy.value,
getTargetY(),
smoothFactor,
)
sx.value = newX
sy.value = newY
callback?.({ sx: sx.value, sy: sy.value })
},
},
)
})
onBeforeUnmount(() => {
tween?.kill()
rawMouse.cleanup()
rawMouse.initialized = false
})
return {
sx,
sy,
rawX: rawMouse.x,
rawY: rawMouse.y,
}
}