Skip to content

Commit 77d9edb

Browse files
committed
Add particle sphere background and stack section
Introduces a Three.js-powered interactive particle sphere as a background, with scroll and mouse parallax effects using GSAP. Adds Marquee and StackSection components to showcase testimonials and technology stack. Updates layout to include the new background and sections, and installs 'three' and 'gsap' dependencies.
1 parent 8903777 commit 77d9edb

9 files changed

Lines changed: 381 additions & 98 deletions

File tree

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
</template>
88

99
<script setup></script>
10-
10+
useHead({ htmlAttrs: { class: 'dark' } })
1111
<style></style>

app/components/Marquee.vue

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<template>
2+
<UMarquee
3+
pause-on-hover
4+
:overlay="false"
5+
:repeat="3"
6+
:ui="{
7+
root: 'gap-0',
8+
content: 'py-1',
9+
}"
10+
>
11+
<div class="space-y-3 px-4 py-3 border-x" v-for="item in items" :key="item">
12+
<p>{{ item.title }}</p>
13+
<div class="flex items-center gap-2">
14+
<UAvatar :src="item.avatar" />
15+
<div class="flex flex-col gap-1">
16+
<p class="text-sm leading-4 font-semibold text-foreground">
17+
{{ item.name }}
18+
</p>
19+
<p class="text-xs leading-4 text-muted-foreground">{{ item.desc }}</p>
20+
</div>
21+
</div>
22+
</div>
23+
</UMarquee>
24+
</template>
25+
<script setup>
26+
const items = [
27+
{
28+
id: "tm-001",
29+
title: "This portfolio feels polished and thoughtfully crafted.",
30+
avatar: "https://api.dicebear.com/9.x/thumbs/svg?seed=tm-001",
31+
name: "Alex Mercer",
32+
desc: "Product designer at a fictional studio",
33+
},
34+
{
35+
id: "tm-002",
36+
title: "Smooth animations, clear layout, and great attention to detail.",
37+
avatar: "https://api.dicebear.com/9.x/thumbs/svg?seed=tm-002",
38+
name: "Jordan Lee",
39+
desc: "Front-end engineer at Imaginary Labs",
40+
},
41+
{
42+
id: "tm-003",
43+
title: "Exactly the kind of portfolio I’d expect from a modern developer.",
44+
avatar: "https://api.dicebear.com/9.x/thumbs/svg?seed=tm-003",
45+
name: "Riley Santos",
46+
desc: "Fictional hiring manager",
47+
},
48+
{
49+
id: "tm-004",
50+
title: "Clean, fast, and visually engaging from start to finish.",
51+
avatar: "https://api.dicebear.com/9.x/thumbs/svg?seed=tm-004",
52+
name: "Morgan Patel",
53+
desc: "Creative director at Nowhere Studio",
54+
},
55+
];
56+
</script>

app/components/StackSection.vue

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<template>
2+
<section
3+
data-slot="panel"
4+
class="screen-line-before screen-line-after border-x border-edge"
5+
id="stack"
6+
>
7+
<header data-slot="panel-header" class="screen-line-after px-4">
8+
<h2 data-slot="panel-title" class="text-3xl font-semibold">Stack</h2>
9+
</header>
10+
<div
11+
data-slot="panel-body"
12+
class="p-4 [--pattern-foreground:var(--color-zinc-950)]/5 dark:[--pattern-foreground:var(--color-white)]/5 bg-[radial-gradient(var(--pattern-foreground)_1px,transparent_0)] bg-size-[10px_10px] bg-center bg-zinc-950/0.75 dark:bg-white/0.75"
13+
>
14+
<ul class="flex flex-wrap gap-4 select-none">
15+
<li v-for="item in stackItems" :key="item.label" class="flex">
16+
<UTooltip :text="item.label">
17+
<UIcon :name="item.icon" class="size-10" />
18+
</UTooltip>
19+
</li>
20+
</ul>
21+
</div>
22+
</section>
23+
</template>
24+
25+
<script setup lang="ts">
26+
interface StackItem {
27+
label: string;
28+
icon?: string;
29+
}
30+
31+
const stackItems: StackItem[] = [
32+
{
33+
label: "TypeScript",
34+
icon: "devicon:typescript",
35+
},
36+
{
37+
label: "JavaScript",
38+
icon: "devicon:javascript",
39+
},
40+
{
41+
label: "HTML",
42+
icon: "devicon:html5",
43+
},
44+
{
45+
label: "CSS",
46+
icon: "devicon:css3",
47+
},
48+
{
49+
label: "Vue.js",
50+
icon: "devicon:vuejs",
51+
},
52+
{
53+
label: "Nuxt.js",
54+
icon: "devicon:nuxtjs",
55+
},
56+
{
57+
label: "React",
58+
icon: "devicon:react",
59+
},
60+
{
61+
label: "Next.js",
62+
icon: "devicon:nextjs",
63+
},
64+
{
65+
label: "Tailwind CSS",
66+
icon: "devicon:tailwindcss",
67+
},
68+
69+
{
70+
label: "Git",
71+
icon: "devicon:git",
72+
},
73+
{
74+
label: "GitHub",
75+
icon: "devicon:github",
76+
},
77+
78+
{
79+
label: "Node.js",
80+
icon: "devicon:nodejs",
81+
},
82+
{
83+
label: "Vite",
84+
icon: "devicon:vitejs",
85+
},
86+
{
87+
label: "Vitest",
88+
icon: "devicon:vitest",
89+
},
90+
{
91+
label: "Figma",
92+
icon: "devicon:figma",
93+
},
94+
{
95+
label: "Adobe Photoshop",
96+
icon: "devicon:photoshop",
97+
},
98+
{
99+
label: "ChatGPT",
100+
icon: "hugeicons:chat-gpt",
101+
},
102+
];
103+
</script>

app/components/ThreeCanvas.vue

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,51 @@
11
<template>
2-
<div ref="canvasContainer" class="canvas-container"></div>
2+
<div ref="canvasContainer" class="fixed inset-0 w-full h-full -z-10 bg-neutral-950">
3+
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-black/20 to-black/80 pointer-events-none"></div>
4+
</div>
35
</template>
46

57
<script setup lang="ts">
6-
import { ref, onMounted, onBeforeUnmount } from 'vue'
7-
import { ThreeScene } from '~/utils/ThreeScene'
8+
import { ref, onMounted, onBeforeUnmount } from "vue";
9+
import { ThreeScene } from "~/utils/ThreeScene";
810
9-
const canvasContainer = ref<HTMLCanvasElement | null>(null)
10-
let threeScene: ThreeScene | null = null
11+
const canvasContainer = ref<HTMLElement | null>(null);
12+
let threeScene: ThreeScene | null = null;
13+
let resizeObserver: ResizeObserver | null = null;
1114
12-
onMounted(() => {
13-
if (canvasContainer.value) {
14-
// Instantiate our class
15-
threeScene = new ThreeScene()
16-
17-
// Initialize the scene and pass the container element
18-
threeScene.init(canvasContainer.value)
19-
20-
// Add a resize listener
21-
const resizeObserver = new ResizeObserver((entries) => {
22-
for (const entry of entries) {
23-
const { width, height } = entry.contentRect
24-
threeScene?.updateSize(width, height)
25-
}
26-
})
27-
28-
resizeObserver.observe(canvasContainer.value)
29-
30-
// Cleanup on unmount
31-
onBeforeUnmount(() => {
32-
resizeObserver.disconnect()
33-
threeScene?.cleanup()
34-
})
15+
const handleScroll = () => {
16+
if (threeScene) {
17+
threeScene.onScroll(window.scrollY, document.body.scrollHeight);
3518
}
36-
})
37-
</script>
19+
};
20+
21+
const handleMouseMove = (event: MouseEvent) => {
22+
if (threeScene) {
23+
threeScene.onMouseMove(event.clientX, event.clientY);
24+
}
25+
};
3826
39-
<style scoped>
40-
.canvas-container {
41-
width: 100%;
42-
height: 100%;
43-
outline: none;
44-
}
45-
</style>
27+
onMounted(() => {
28+
if (!canvasContainer.value) return;
29+
30+
threeScene = new ThreeScene();
31+
threeScene.init(canvasContainer.value);
32+
33+
// 1. Observe Window Resize
34+
resizeObserver = new ResizeObserver((entries) => {
35+
const { width, height } = entries[0].contentRect;
36+
threeScene?.updateSize(width, height);
37+
});
38+
resizeObserver.observe(canvasContainer.value);
39+
40+
// 2. Add Event Listeners
41+
window.addEventListener("scroll", handleScroll);
42+
window.addEventListener("mousemove", handleMouseMove);
43+
});
44+
45+
onBeforeUnmount(() => {
46+
window.removeEventListener("scroll", handleScroll);
47+
window.removeEventListener("mousemove", handleMouseMove);
48+
resizeObserver?.disconnect();
49+
threeScene?.cleanup();
50+
});
51+
</script>

app/layouts/default.vue

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,38 @@
11
<template>
22
<div>
3-
<header class="border-panel" style="border-bottom:1px solid var(--border)">
4-
<div class="container flex items-center justify-between border-r border-l py-3">
5-
<div class="flex items-center gap-4">
6-
<nav class="small-muted">
7-
<a class="px-3" href="#projects">Projects</a>
8-
<a class="px-3" href="#about">About</a>
9-
<a class="px-3" href="#contact">Contact</a>
10-
</nav>
3+
<ThreeCanvas />
4+
<div class="relative z-10 bg-black/50">
5+
<header
6+
class="border-panel"
7+
style="border-bottom: 1px solid var(--border)"
8+
>
9+
<div
10+
class="container flex items-center justify-between border-r border-l py-3"
11+
>
12+
<div class="flex items-center gap-4">
13+
<nav class="small-muted">
14+
<a class="px-3" href="#projects">Projects</a>
15+
<a class="px-3" href="#about">About</a>
16+
<a class="px-3" href="#contact">Contact</a>
17+
</nav>
18+
</div>
1119
</div>
12-
<nav class="small-muted px-2">
13-
<ColorModeButton />
14-
</nav>
15-
</div>
16-
</header>
20+
</header>
1721

18-
<UMain>
19-
<div class="w-5 hidden border-x border-x-(--pattern-fg) bg-[image:repeating-linear-gradient(315deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)] bg-[size:10px_10px] bg-fixed [--pattern-fg:var(--color-black)]/5 md:block dark:[--pattern-fg:var(--color-white)]/10"></div>
20-
<slot />
21-
</UMain>
22+
<UMain>
23+
<div
24+
class="w-5 hidden border-x border-x-(--pattern-fg) bg-[image:repeating-linear-gradient(315deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)] bg-[size:10px_10px] bg-fixed [--pattern-fg:var(--color-black)]/5 md:block dark:[--pattern-fg:var(--color-white)]/10"
25+
></div>
26+
<slot />
27+
</UMain>
2228

23-
<footer class="border-panel small-muted">
24-
<div class="container border-l border-r p-2">
25-
© {{ new Date().getFullYear() }} — Iman Mohammadi — Built with Nuxt
26-
</div>
27-
</footer>
29+
<footer class="border-panel small-muted">
30+
<div class="container border-l border-r p-2">
31+
© {{ new Date().getFullYear() }} — Iman Mohammadi — Built with Nuxt
32+
</div>
33+
</footer>
34+
</div>
2835
</div>
2936
</template>
3037

31-
<script setup>
32-
</script>
38+
<script setup></script>

app/pages/index.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,15 @@
118118
<div class="container">
119119
<AboutTextSection />
120120
</div>
121+
<Divider />
122+
<div class="container">
123+
<div class="border-x">
124+
<Marquee />
125+
</div>
126+
</div>
127+
<Divider />
128+
<div class="container">
129+
<StackSection />
130+
</div>
121131
</div>
122132
</template>

0 commit comments

Comments
 (0)