basic setup
This commit is contained in:
42
composables/useDetectOS.js
Normal file
42
composables/useDetectOS.js
Normal 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
14
composables/useLenis.js
Normal 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
|
||||
}
|
||||
29
composables/useRelativeSize.js
Normal file
29
composables/useRelativeSize.js
Normal 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,
|
||||
}
|
||||
}
|
||||
46
composables/useScrollProgress.js
Normal file
46
composables/useScrollProgress.js
Normal 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 }
|
||||
}
|
||||
89
composables/useSmoothMouse.js
Normal file
89
composables/useSmoothMouse.js
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user