Initial commit
31
src/App.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Global Reset */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
background: #0f0f0f;
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
BIN
src/assets/myself/15.jpg
Executable file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/myself/27-2.jpg
Executable file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
src/assets/myself/DSCN1921.jpg
Executable file
|
After Width: | Height: | Size: 728 KiB |
BIN
src/assets/myself/GOPR0521.png
Executable file
|
After Width: | Height: | Size: 14 MiB |
BIN
src/assets/myself/HarzUrlaub.jpeg
Executable file
|
After Width: | Height: | Size: 474 KiB |
BIN
src/assets/myself/SmartTom.jpeg
Executable file
|
After Width: | Height: | Size: 155 KiB |
BIN
src/assets/myself/TOm.jpg
Executable file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/assets/myself/ski.png
Executable file
|
After Width: | Height: | Size: 5.9 MiB |
BIN
src/assets/myself/surf.png
Executable file
|
After Width: | Height: | Size: 5.1 MiB |
66
src/components/sections/HeroSection.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue"
|
||||
import gsap from "gsap"
|
||||
import ScrollTrigger from "gsap/ScrollTrigger"
|
||||
import { ScrollSmoother } from 'gsap/ScrollSmoother';
|
||||
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
const section = ref<HTMLElement | null>(null)
|
||||
let ctx
|
||||
|
||||
onMounted(() => {
|
||||
ctx = gsap.context(() => {
|
||||
gsap.from(".hero-title", {
|
||||
y: 100,
|
||||
opacity: 0,
|
||||
duration: 1.2,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: section.value,
|
||||
start: "top 80%",
|
||||
end: "top 50%",
|
||||
scrub: true
|
||||
}
|
||||
})
|
||||
|
||||
gsap.from(".hero-sub", {
|
||||
y: 40,
|
||||
opacity: 0,
|
||||
duration: 1,
|
||||
delay: 0.3,
|
||||
scrollTrigger: {
|
||||
trigger: section.value,
|
||||
start: "top 80%",
|
||||
end: "top 50%",
|
||||
scrub: true
|
||||
}
|
||||
})
|
||||
}, section)
|
||||
})
|
||||
|
||||
onUnmounted(() => ctx.revert())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section ref="section" class="hero">
|
||||
<h1 class="hero-title">Welcome</h1>
|
||||
<p class="hero-sub">Tom Herpel</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
background: #0f0f0f;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.hero-title {
|
||||
font-size: clamp(3rem, 6vw, 6rem);
|
||||
}
|
||||
</style>
|
||||
151
src/components/sections/ImageSection.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import ski from '../../assets/myself/ski.png'
|
||||
import book from '../../assets/myself/SmartTom.jpeg'
|
||||
import surf from '../../assets/myself/surf.png'
|
||||
import boxen from '../../assets/myself/TOm.jpg'
|
||||
import florencs from '../../assets/myself/15.jpg'
|
||||
import schnee from '../../assets/myself/DSCN1921.jpg'
|
||||
import water from '../../assets/myself/27-2.jpg'
|
||||
import gopro from '../../assets/myself/GOPR0521.png'
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<section class="image-section">
|
||||
<div class="gallery-wrap">
|
||||
<div class="gallery gallery--bento gallery--switch" id="gallery-8">
|
||||
<div class="gallery__item">
|
||||
<img :src=boxen alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src=gopro alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src=schnee alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src=surf alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src="florencs" alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src=book alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src=ski alt="" />
|
||||
</div>
|
||||
<div class="gallery__item">
|
||||
<img :src=water alt="" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h2>Here is some content</h2>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gallery-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.gallery__item {
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
flex: none;
|
||||
position: relative;
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.gallery--bento {
|
||||
display: grid;
|
||||
gap: 1vh;
|
||||
grid-template-columns: repeat(3, 32.5vw);
|
||||
grid-template-rows: repeat(4, 23vh);
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.gallery--final.gallery--bento {
|
||||
grid-template-columns: repeat(3, 100vw);
|
||||
grid-template-rows: repeat(4, 49.5vh);
|
||||
gap: 1vh;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(1) {
|
||||
grid-area: 1 / 1 / 3 / 2;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(2) {
|
||||
grid-area: 1 / 2 / 2 / 3;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(3) {
|
||||
grid-area: 2 / 2 / 4 / 3;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(4) {
|
||||
grid-area: 1 / 3 / 3 / 3;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(5) {
|
||||
grid-area: 3 / 1 / 3 / 2;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(6) {
|
||||
grid-area: 3 / 3 / 5 / 4;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(7) {
|
||||
grid-area: 4 / 1 / 5 / 2;
|
||||
}
|
||||
|
||||
.gallery--bento .gallery__item:nth-child(8) {
|
||||
grid-area: 4 / 2 / 5 / 3;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 2rem 5rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
99
src/components/sections/TextSection.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue"
|
||||
import gsap from "gsap"
|
||||
import ScrollTrigger from "gsap/ScrollTrigger"
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
const section = ref(null)
|
||||
let ctx
|
||||
|
||||
onMounted(() => {
|
||||
ctx = gsap.context(() => {
|
||||
|
||||
gsap.from(".fade-text", {
|
||||
opacity: 0,
|
||||
y: 50,
|
||||
duration: 1,
|
||||
scrollTrigger: {
|
||||
trigger: section.value,
|
||||
start: "top 70%"
|
||||
}
|
||||
})
|
||||
|
||||
gsap.to(".highlight", {
|
||||
backgroundSize: "100% 100%",
|
||||
scrollTrigger: {
|
||||
trigger: ".highlight",
|
||||
start: "top 80%",
|
||||
end: "top 50%",
|
||||
scrub: true
|
||||
}
|
||||
})
|
||||
|
||||
}, section)
|
||||
})
|
||||
|
||||
onUnmounted(() => ctx.revert())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section ref="section" class="text-section">
|
||||
<h2 class="fade-text">
|
||||
Do you want to see where I used to study?
|
||||
<!-- A blog that <span class="highlight">moves with you</span> -->
|
||||
</h2>
|
||||
<p class="fade-text">
|
||||
</p>
|
||||
<router-link to="/experience">
|
||||
<button class="btn">Zur Experience</button>
|
||||
</router-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-section {
|
||||
min-height: 100vh;
|
||||
background: #111;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 6rem 10%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background: linear-gradient(90deg, #ffffff, #ffffff);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% 100%;
|
||||
background-position: left;
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
.btn {
|
||||
background-color: white;
|
||||
color: #1a1a2e; /* dunkler Text */
|
||||
border: none;
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||
}
|
||||
</style>
|
||||
40
src/composables/useScrollAnimations.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import gsap from 'gsap'
|
||||
import ScrollTrigger from 'gsap/ScrollTrigger'
|
||||
import { useSectionStore } from '../store/useActiveSection'
|
||||
|
||||
|
||||
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
|
||||
export function useActiveSection(sectionIds: string[]) {
|
||||
//const uiStore = useSectionStore();
|
||||
const currentSection = ref<string | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
sectionIds.forEach(id => {
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: 'top center',
|
||||
end: 'bottom center',
|
||||
onEnter: () => {
|
||||
if (currentSection.value !== id) {
|
||||
//uiStore.currentSection = id
|
||||
console.log(`Section aktiv: ${id} (nach unten gescrollt)`)
|
||||
}
|
||||
},
|
||||
onEnterBack: () => (currentSection.value = id),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
ScrollTrigger.getAll().forEach(t => t.kill())
|
||||
})
|
||||
|
||||
return { currentSection }
|
||||
}
|
||||
9
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// vite erkennt sonst .vue nicht als import an
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
8
src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
18
src/router/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import ExperienceView from '../views/ExperienceView.vue'
|
||||
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{ path: '/', component: HomeView },
|
||||
{ path: '/experience',
|
||||
component: ExperienceView,
|
||||
props: { modelUrl: '/models/LuebeckRoom_materials-draco.glb', dracoPath: '/draco/' } }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
8
src/store/useActiveSection.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// stores/sections.ts
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useSectionStore = defineStore('sections', () => {
|
||||
const currentSection = ref<string | null>(null)
|
||||
return { currentSection }
|
||||
})
|
||||
79
src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
161
src/views/ExperienceView.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<!-- 3DModelViewer.vue -->
|
||||
<template>
|
||||
<div ref="containerRef" class="three-container"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
// Beispiel: '/models/dein-modell.glb' oder '/models/dein-modell.gltf'
|
||||
},
|
||||
// Optional – falls du die Decoder-Dateien woanders hast
|
||||
dracoPath: {
|
||||
type: String,
|
||||
default: '/draco/' // ← WICHTIG: Passe den Pfad an!
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const containerRef = ref(null)
|
||||
let scene, camera, renderer, controls, model
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
animate()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
watch(() => props.modelUrl, (newUrl) => {
|
||||
if (newUrl) loadModel(newUrl)
|
||||
})
|
||||
|
||||
function init() {
|
||||
if (!containerRef.value) return
|
||||
|
||||
// Scene
|
||||
scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(0x1a1a2e)
|
||||
|
||||
// Camera
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
50,
|
||||
containerRef.value.clientWidth / containerRef.value.clientHeight,
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
camera.position.set(0, 1.5, 4)
|
||||
|
||||
// Renderer
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
containerRef.value.appendChild(renderer.domElement)
|
||||
|
||||
// Controls
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.05
|
||||
controls.minDistance = 1
|
||||
controls.maxDistance = 50
|
||||
|
||||
// Licht
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 0.8)
|
||||
scene.add(ambient)
|
||||
|
||||
const directional = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directional.position.set(5, 8, 4)
|
||||
scene.add(directional)
|
||||
|
||||
window.addEventListener('resize', onWindowResize)
|
||||
|
||||
loadModel(props.modelUrl)
|
||||
}
|
||||
|
||||
function loadModel(url) {
|
||||
if (model) {
|
||||
scene.remove(model)
|
||||
}
|
||||
|
||||
const dracoLoader = new DRACOLoader()
|
||||
dracoLoader.setDecoderPath(props.dracoPath)
|
||||
|
||||
const loader = new GLTFLoader()
|
||||
loader.setDRACOLoader(dracoLoader)
|
||||
|
||||
loader.load(
|
||||
url,
|
||||
(gltf) => {
|
||||
model = gltf.scene
|
||||
|
||||
const box = new THREE.Box3().setFromObject(model)
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
|
||||
model.position.sub(center)
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const scale = 2 / maxDim
|
||||
model.scale.multiplyScalar(scale)
|
||||
|
||||
scene.add(model)
|
||||
|
||||
document.title = 'Model geladen' // Fertigmeldung im Tab
|
||||
},
|
||||
(progress) => {
|
||||
const percent = (progress.loaded / progress.total * 100).toFixed(1)
|
||||
console.log(`Loading: ${percent}%`)
|
||||
document.title = `${percent}%`
|
||||
},
|
||||
(error) => {
|
||||
console.error('Fehler beim Laden des Modells:', error)
|
||||
document.title = 'Fehler beim Laden!'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function onWindowResize() {
|
||||
if (!containerRef.value || !camera || !renderer) return
|
||||
|
||||
camera.aspect = containerRef.value.clientWidth / containerRef.value.clientHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
if (controls) controls.update()
|
||||
if (renderer && scene && camera) renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
window.removeEventListener('resize', onWindowResize)
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose()
|
||||
containerRef.value?.removeChild(renderer.domElement)
|
||||
}
|
||||
|
||||
if (controls) controls.dispose()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.three-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
background: #0f0f1a;
|
||||
}
|
||||
</style>
|
||||
18
src/views/HomeView.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import HeroSection from '../components/sections/HeroSection.vue'
|
||||
import ImageSection from '../components/sections/ImageSection.vue'
|
||||
import TextSection from '../components/sections/TextSection.vue'
|
||||
import { useActiveSection } from '../composables/useScrollAnimations'
|
||||
|
||||
const { currentSection } = useActiveSection(['hero', 'image', 'text'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeroSection id="hero" />
|
||||
<ImageSection id="image" />
|
||||
<TextSection id="text" />
|
||||
|
||||
<div class="indicator">
|
||||
Aktuelle Section: {{ currentSection }}
|
||||
</div>
|
||||
</template>
|
||||