From our sponsor: Meco is a distraction-free space for reading and discovering newsletters, separate from the inbox.
Why Create a Personal Portfolio?
Considering I am already the co-founder of Overpx studio, this is a question many might ask. The reasons are mainly two:
1. The need for a personal space: from 2008 to 2014 I had a blog, fedeweb.net, where I used to write articles about WordPress, JavaScript, Gimp (does it still exist?), and more. After closing it, I’ve missed having a space of my own to share ideas and projects.
2. A personal challenge: I wanted to challenge myself by handling by myself every step of the process, from concept to execution. I took care of each stage: ideation, moodboard, design and development, without relying on anyone else.
Before starting the design process, I took the Obys course on Awwwards, which I highly recommend. It provided useful insights and helped me refine my design approach.
Inspiration
I’ve always been drawn towards geometry, especially circles, precision, and cleanliness in design. While gathering elements for my moodboard, I found two particular images on Savee that had a significant impact on the overall design of the site, shaping its visual direction.
Try to guess what elements have been influenced by these images 🙂
Development
The primary goal during the development phase was to ensure a smooth, uninterrupted experience while navigating between pages. I wanted to avoid any pauses or delays, creating a seamless flow throughout the entire site.
The tech stack
I used my usual development stack, with one key exception: instead of using a CMS for content management, I opted to handle everything directly via a JSON file.
- Nuxt for static site generation
- GSAP for animation
- TresJs for Webgl
- Tailwindcss for CSS
- Pinia for state management
Page transitions
To handle the transitions between pages and projects, I started with this Stackblitz provided by GSAP, specifically developed for Nuxt 3.
The transitions vary depending on both the current page and the destination page. To manage the different cases, I compare the value of the clicked element with the current route.
The clicked element value is managed through the routeStore state, created with Pinia, where a custom value is passed.
// methods navigate for the next project link, inside a single project page
const navigate = (e) => {
const route = e.currentTarget.dataset.url
routeStore.setUrl('/projects-single') // set the routeStore value
navigateTo({ path: route })
}
The current route is retrieved using the useRoute() composable provided by Nuxt.
These two values are compared during the onLeave event of the page. Let’s take a closer look at the transition between projects:
onLeave: (el, done) => {
const routeStore = useUrlStore(),
route = useRoute()
// other conditions
// from single to single
if (route.name === 'projects-single' && routeStore.url === '/projects-single') {
// ...
const elementToFlip = el.querySelector('[data-next-project]')
useFlipState().value = Flip.getState(elementToFlip)
// ...
}
// other conditions
},
As you can see, I used Flip, a GSAP plugin that allows to manage seamlessly transition between two states.
Specifically, the element to flip is passed to the getState() function. The value is then assigned to the useFlipState composable so it can be reused on the destination page.
// composable useFlipState.js
export const useFlipState = () => useState('flip-state');
Finally, within the single project page, the transition between the two states is performed using Flip.from:
const flipTl = Flip.from(useFlipState().value, {
targets: pictureEls[0], // get the first image of the gallery
duration: 1.2,
z: 0,
ease: 'power3.out',
// ...
})
// border radius animation
flipTl.fromTo(
pictureEls[0], // first gallery item
{ '--borderRadius': isDesktop ? '20rem' : '10rem' },
{ '--borderRadius': isDesktop ? '2rem' : '2rem', duration: 0.6, ease: 'sine.out' },
0
)
// ...
Flip.from() returns a timeline where you can add all the other animations you need in the transition; in the code example there is the border-radius animation.
Text effect
The goal was to incorporate the concept of a diagonal into the text animations, creating a dynamic and visually interesting movement.
To achieve this effect, I used the SplitText plugin to split the text into individual characters, and then I applied a clipPath in combination with a diagonal transition (both x and y) for the all pages except the homepage, where there is a horizontal-only transition.
Specifically, I created a global animation, clipTitle, which was then called wherever needed:
gsap.registerEffect({
name: 'clipTitle',
effect: (targets, config) => {
const tl = gsap.timeline({
defaults: { duration: config.duration, ease: config.ease },
})
// Check if the text has already been split, if not, split it and mark it as done
const chars = targets[0].classList.contains('text-split-done')
? targets[0].querySelectorAll('.char')
: new SplitText(targets, { type: 'chars', charsClass: 'char' }).chars
if (!targets[0].classList.contains('text-split-done')) {
targets[0].classList.add('text-split-done')
}
tl.fromTo(
chars,
{
x: config.x,
yPercent: config.yPercent,
clipPath: 'inset(0% 100% 120% -5%)',
transformOrigin: '0% 50%',
},
{
willChange: 'transform',
clipPath: 'inset(0% -100% -100% -5%)',
x: 0,
yPercent: 0,
stagger: config.stagger,
duration: config.duration,
ease: config.ease,
},
0.05
)
return tl
},
defaults: { yPercent: 30, x: -30, duration: 0.8, ease: 'power3.out', stagger: -0.05 },
extendTimeline: true,
})
Background animation
For the background animation, I used TresJs, a library that allows creating ThreeJS scenes with Vue components in a declarative way. While I could have used ThreeJS directly or another WebGL library, I decided to go with TresJs to test it out and explore its capabilities.
This is the fragment shader for the background distortion:
float circle_s(vec2 dist,float radius){
return smoothstep(0.,radius,pow(dot(dist,dist),.6)*.1);
}
void main(){
vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.);
vec2 uv=vUv*aspect;
vec2 mouse=vUv-u_point;
mouse.y/=u_ratio;
float noise=snoise(vec3(uv,u_time * 3.));
float noise1=snoise(vec3(uv+.1,u_time * 3.));
float noise2=snoise(vec3(uv-.1,u_time * 3.));
float alpha=(noise+noise1+noise2)/3.;
alpha*=circle_s(mouse,.015 * u_mouseInteraction);
float x=1.-noise;
vec3 color1=vec3(u_color1.x/255.,u_color1.y/255.,u_color1.z/255.);
vec3 color2=vec3(u_color2.x/255.,u_color2.y/255.,u_color2.z/255.);
// Blending based on combined noise
float blendFactor=smoothstep(.1,1.,x * 1.);
vec3 blendedColor=mix(color1, color2, blendFactor);
gl_FragColor.rgb=blendedColor;
gl_FragColor.a=alpha;
}
The snoise function can be found in this gist, in particular I used the Simplex3D noise by Ian McEwan and Stefan Gustavson and it was used to create a sort of color distortion effect by manipulating the alpha component.
The colors are managed through a state created with Pinia, which receives the colors in rgb format, from a JSON file.
Keyboard-only usage
One thing I’m particularly proud of is that the entire site is fully navigable using only the keyboard. This includes the project page, where you can access individual projects using digit numbers, and within the single project pages, you can navigate from one project to the next using the right arrow key.
Other tech aspects
For the mouse trail effect, I started with this Codrops playground (thanks, Manoela! 😊) and adapted it to suit my specific use case.
For the scrolling effect on the projects page, I based it on this CodePen created by GSAP, further customizing it to match the site’s flow and interaction.
404 page
I attempted to simulate the effect of Newton’s cradle, with the colors of the shader changing randomly each time.
Sounds effects
In 2022, I came across this sound library and promised myself I would use it in a project someday. What better opportunity than to use it for my own site?
The library contains three sound collections, and on the site, I specifically used a few sounds from the “sine” collection.
Conclusions
This project has been a fulfilling experience of self-expression and technical exploration. I learned a lot about crafting seamless web experiences, and I’m excited to apply these lessons to future projects.
I’m very grateful for all the appreciation and awards received. Winning my first SOTD on Awwwards has been a true highlight of this journey, it feels kind of incredible.
I hope you enjoyed this behind-the-scenes look at my portfolio project, perhaps it would inspire you for your own work.