add heart animation
This commit is contained in:
@ -3,8 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link href="/src/assets/main.css" rel="stylesheet">
|
||||||
<title>Vite + Vue + TS</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>I LOVE YOU</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
77
package-lock.json
generated
77
package-lock.json
generated
@ -17,6 +17,8 @@
|
|||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"@types/three": "^0.178.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.5.0",
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
@ -540,6 +542,13 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.8",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||||
@ -1875,6 +1884,13 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/draco3d": {
|
"node_modules/@types/draco3d": {
|
||||||
"version": "1.4.10",
|
"version": "1.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||||
@ -1894,12 +1910,52 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "24.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||||
|
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/offscreencanvas": {
|
"node_modules/@types/offscreencanvas": {
|
||||||
"version": "2019.7.3",
|
"version": "2019.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||||
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.178.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.178.1.tgz",
|
||||||
|
"integrity": "sha512-WSabew1mgWgRx2RfLfKY+9h4wyg6U94JfLbZEGU245j/WY2kXqU0MUfghS+3AYMV5ET1VlILAgpy77cB6a3Itw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": "*",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~0.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/three/node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/webxr": {
|
"node_modules/@types/webxr": {
|
||||||
"version": "0.5.22",
|
"version": "0.5.22",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
|
||||||
@ -2515,6 +2571,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.64",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz",
|
||||||
|
"integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@ -4503,6 +4566,13 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "0.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
||||||
|
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
@ -5544,6 +5614,13 @@
|
|||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
|
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unicorn-magic": {
|
"node_modules/unicorn-magic": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"@types/three": "^0.178.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.5.0",
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
|
31
src/App.vue
31
src/App.vue
@ -1,27 +1,10 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<Default>
|
||||||
<a href="https://vite.dev" target="_blank">
|
<RouterView />
|
||||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
</Default>
|
||||||
</a>
|
|
||||||
<a href="https://vuejs.org/" target="_blank">
|
|
||||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<script setup lang="ts">
|
||||||
.logo {
|
import Default from './layout/UiDefault.vue'
|
||||||
height: 6em;
|
import { RouterView } from 'vue-router'
|
||||||
padding: 1.5em;
|
</script>
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.vue:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #42b883aa);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
48
src/components/typography/UiHeadering.vue
Normal file
48
src/components/typography/UiHeadering.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="tag"
|
||||||
|
class="font-semibold text-twilight-900"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'text-3xl md:text-4xl lg:text-5xl 2xl:text-6xl': size === '600',
|
||||||
|
}, // H1
|
||||||
|
{
|
||||||
|
'text-3xl md:text-4xl 2xl:text-5xl': size === '500',
|
||||||
|
}, // H2
|
||||||
|
{
|
||||||
|
'text-2xl md:text-3xl 2xl:text-4xl': size === '400',
|
||||||
|
}, // H3
|
||||||
|
{
|
||||||
|
'text-xl lg:text-2xl 2xl:text-3xl': size === '300',
|
||||||
|
}, // H4
|
||||||
|
{
|
||||||
|
'text-lg md:text-xl': size === '200',
|
||||||
|
}, // H5
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRefs } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* Tag name: h1 to h6
|
||||||
|
*/
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
default: 'h2',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Heading size: '600' (largest) to '200' (smallest)
|
||||||
|
*/
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '500',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { size, tag } = toRefs(props)
|
||||||
|
</script>
|
45
src/components/typography/UiParagraph.vue
Normal file
45
src/components/typography/UiParagraph.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<component
|
||||||
|
:is="as"
|
||||||
|
class="text-gray-900"
|
||||||
|
:class="[
|
||||||
|
{
|
||||||
|
'text-2xl lg:text-3xl 2xl:text-4xl': size === '600',
|
||||||
|
}, // 24px
|
||||||
|
{
|
||||||
|
'text-xl lg:text-2xl 2xl:text-3xl': size === '500',
|
||||||
|
}, // 20px
|
||||||
|
{
|
||||||
|
'text-lg lg:text-xl 2xl:text-2xl': size === '400',
|
||||||
|
}, // 18px
|
||||||
|
{
|
||||||
|
'text-base lg:text-lg': size === '300',
|
||||||
|
}, // 16px
|
||||||
|
{
|
||||||
|
'text-sm': size === '200',
|
||||||
|
}, // 14px
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRefs } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
as: {
|
||||||
|
type: String,
|
||||||
|
default: 'p',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Tailwind text size from 600 to 200
|
||||||
|
*/
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: '300',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { size, as } = toRefs(props)
|
||||||
|
</script>
|
5
src/constants/heart-section.ts
Normal file
5
src/constants/heart-section.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
SCALE: 1,
|
||||||
|
DIRECTION: 1,
|
||||||
|
ANIMATION_ID: 0,
|
||||||
|
}
|
1
src/constants/index.ts
Normal file
1
src/constants/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as heartSectionConst } from './heart-section.ts'
|
66
src/declarations.d.ts
vendored
Normal file
66
src/declarations.d.ts
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
declare module 'three/examples/jsm/loaders/GLTFLoader' {
|
||||||
|
import { Loader, LoadingManager, Group, AnimationClip, Object3D } from 'three'
|
||||||
|
|
||||||
|
export interface GLTF {
|
||||||
|
scene: Group
|
||||||
|
scenes: Group[]
|
||||||
|
animations: AnimationClip[]
|
||||||
|
cameras: Object3D[]
|
||||||
|
asset: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GLTFLoader extends Loader {
|
||||||
|
constructor(manager?: LoadingManager)
|
||||||
|
load(
|
||||||
|
url: string,
|
||||||
|
onLoad: (gltf: GLTF) => void,
|
||||||
|
onProgress?: (event: ProgressEvent<EventTarget>) => void,
|
||||||
|
onError?: (event: ErrorEvent | unknown) => void
|
||||||
|
): void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'three/examples/jsm/controls/OrbitControls' {
|
||||||
|
import { Camera, EventDispatcher, MOUSE, TOUCH, Vector3 } from 'three'
|
||||||
|
|
||||||
|
export class OrbitControls extends EventDispatcher {
|
||||||
|
constructor(object: Camera, domElement?: HTMLElement)
|
||||||
|
object: Camera
|
||||||
|
target: Vector3
|
||||||
|
update(): boolean
|
||||||
|
dispose(): void
|
||||||
|
|
||||||
|
enabled: boolean
|
||||||
|
autoRotate: boolean
|
||||||
|
autoRotateSpeed: number
|
||||||
|
|
||||||
|
minDistance: number
|
||||||
|
maxDistance: number
|
||||||
|
|
||||||
|
minZoom: number
|
||||||
|
maxZoom: number
|
||||||
|
|
||||||
|
minPolarAngle: number
|
||||||
|
maxPolarAngle: number
|
||||||
|
|
||||||
|
minAzimuthAngle: number
|
||||||
|
maxAzimuthAngle: number
|
||||||
|
|
||||||
|
enableDamping: boolean
|
||||||
|
dampingFactor: number
|
||||||
|
|
||||||
|
enableZoom: boolean
|
||||||
|
zoomSpeed: number
|
||||||
|
|
||||||
|
enableRotate: boolean
|
||||||
|
rotateSpeed: number
|
||||||
|
|
||||||
|
enablePan: boolean
|
||||||
|
panSpeed: number
|
||||||
|
screenSpacePanning: boolean
|
||||||
|
keyPanSpeed: number
|
||||||
|
|
||||||
|
mouseButtons: { LEFT: MOUSE; MIDDLE: MOUSE; RIGHT: MOUSE }
|
||||||
|
touches: { ONE: TOUCH; TWO: TOUCH }
|
||||||
|
}
|
||||||
|
}
|
17
src/layout/UiDefault.vue
Normal file
17
src/layout/UiDefault.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen">
|
||||||
|
<Header></Header>
|
||||||
|
<Main class="min-h-screen h-full">
|
||||||
|
<Suspense>
|
||||||
|
<slot />
|
||||||
|
</Suspense>
|
||||||
|
</Main>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Header from '../layout/UiHeader.vue'
|
||||||
|
import Main from '../layout/UiMain.vue'
|
||||||
|
import Footer from '../layout/UiFooter.vue'
|
||||||
|
</script>
|
7
src/layout/UiFooter.vue
Normal file
7
src/layout/UiFooter.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<footer>
|
||||||
|
<div class="flex items-center justify-between hidden">hidden</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
7
src/layout/UiHeader.vue
Normal file
7
src/layout/UiHeader.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<header>
|
||||||
|
<div class="flex items-center justify-between hidden">hidden</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
8
src/layout/UiMain.vue
Normal file
8
src/layout/UiMain.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<Suspense>
|
||||||
|
<slot />
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts"></script>
|
@ -1,5 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import '../src/assets/main.css'
|
import '../src/assets/main.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
app.mount('#app')
|
||||||
|
88
src/pages/index/_ui/heartSection.vue
Normal file
88
src/pages/index/_ui/heartSection.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<div ref="container" class="w-full h-screen" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||||
|
import { heartSectionConst } from '../../../constants'
|
||||||
|
|
||||||
|
const container = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
let scene: THREE.Scene
|
||||||
|
let camera: THREE.PerspectiveCamera
|
||||||
|
let renderer: THREE.WebGLRenderer
|
||||||
|
let controls: OrbitControls
|
||||||
|
let heart: THREE.Group | null = null
|
||||||
|
|
||||||
|
let { SCALE: scale, ANIMATION_ID: animationId, DIRECTION: direction } = heartSectionConst
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
animationId = requestAnimationFrame(animate)
|
||||||
|
if (heart) {
|
||||||
|
scale += 0.003 * direction
|
||||||
|
if (scale > 1.05 || scale < 0.95) return (direction *= -1)
|
||||||
|
heart.scale.set(scale, scale, scale)
|
||||||
|
heart.rotation.y += 0.005
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update()
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!container.value) return
|
||||||
|
|
||||||
|
scene = new THREE.Scene()
|
||||||
|
scene.background = new THREE.Color(0x000000)
|
||||||
|
|
||||||
|
camera = new THREE.PerspectiveCamera(
|
||||||
|
75,
|
||||||
|
container.value.clientWidth / container.value.clientHeight,
|
||||||
|
0.1,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
camera.position.set(0, 1, 5)
|
||||||
|
camera.position.set(0, 1, 5)
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||||
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
||||||
|
container.value.appendChild(renderer.domElement)
|
||||||
|
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 3)
|
||||||
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
const pointLight = new THREE.PointLight(0xff3366, 1.2)
|
||||||
|
pointLight.position.set(5, 5, 5)
|
||||||
|
scene.add(pointLight)
|
||||||
|
|
||||||
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
|
|
||||||
|
const loader = new GLTFLoader()
|
||||||
|
loader.load('/models/heart.glb', (gltf) => {
|
||||||
|
heart = gltf.scene
|
||||||
|
heart?.scale.set(1, 1, 1)
|
||||||
|
scene.add(heart)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('resize', onWindowResize)
|
||||||
|
|
||||||
|
animate()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cancelAnimationFrame(animationId)
|
||||||
|
window.removeEventListener('resize', onWindowResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onWindowResize = () => {
|
||||||
|
if (!container.value) return
|
||||||
|
camera.aspect = container.value.clientWidth / container.value.clientHeight
|
||||||
|
camera.updateProjectionMatrix()
|
||||||
|
renderer.setSize(container.value.clientWidth, container.value.clientHeight)
|
||||||
|
}
|
||||||
|
</script>
|
9
src/pages/index/rootPage.vue
Normal file
9
src/pages/index/rootPage.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import HeartSection from './_ui/heartSection.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<HeartSection />
|
||||||
|
</section>
|
||||||
|
</template>
|
7
src/router/config/_type/TRoutes.ts
Normal file
7
src/router/config/_type/TRoutes.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
|
export type TRoute = {
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
component: () => Promise<Component>
|
||||||
|
}
|
10
src/router/config/routes.ts
Normal file
10
src/router/config/routes.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type { TRoute } from './_type/TRoutes.ts'
|
||||||
|
|
||||||
|
const routes: TRoute[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: '/',
|
||||||
|
component: () => import('../../pages/index/rootPage.vue'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
export default routes
|
8
src/router/index.ts
Normal file
8
src/router/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import routes from './config/routes.ts'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
export default router
|
@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["./src/**/*", "./src/**/*.vue"],
|
||||||
|
"exclude": ["./src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"~": ["../src/*"],
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@ -11,8 +15,4 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user