July 17th, 2024
The Project
The goal of this project was to create a website where visitors can explore some of the major projects I've worked on. Beyond serving as a web portfolio, I aimed to make the website itself a noteworthy project, showcasing my frontend development capabilities through various innovative elements.
My Role
I developed this project entirely by hand, coding every part of the website in HTML, CSS, and JavaScript without using any website creation services.
3D Element
The most interesting part of this website is on the landing page, where an interactive 3D model of myself appears behind the main title. This model was created using a Gaussian splatting scan, a process that involves taking multiple photos and generating a model with software like Polycam.
To embed the model into the website, I used the JavaScript library three.js. This required several steps:
Creating a scene to place the model.
-
Setting up a camera to view the model within the scene.
-
Ensuring the renderer for the scene adjusted to the size of the container holding it.
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// Get container that will hold 3D render
var myContainer = document.getElementById("container3D");
// Set up the required elements for the renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// Declare 3D object
let object;
// Set initial mouse positions
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
// Declare loader for .glb 3D files
const loader = new GLTFLoader();
// Load 3D file
loader.load(
"self_portrait.glb",
function (gltf) {
object = gltf.scene;
scene.add(object);
// Adjusting object position in container
object.position.y = -0.8;
object.position.x = -0.43;
},
// If failure send to console
undefined,
function (error) {
console.error(error);
}
);
// Declare renderer
const renderer = new THREE.WebGLRenderer({
alpha: true, // Set background to transparent
antialias: true, // Makes edges smooth
});
// Set size of renderer to the container width and height
renderer.setSize(myContainer.clientWidth * 1.5, myContainer.clientHeight);
// renderer.setSize(window.innerWidth, window.innerHeight);
// Add renderer to container
document.getElementById("container3D").appendChild(renderer.domElement);
// Distance from camera to subject
camera.position.z = 1;
// Light settings, ambient light gives equal lighting
const light = new THREE.AmbientLight(0xffffff); // soft white light
scene.add(light);
What makes this element particularly engaging is that it follows mouse movements. This was achieved by tracking mouse movement in the script and rotating the model accordingly, using events and functions.
function animate() {
requestAnimationFrame(animate);
// Horizontal movement
object.rotation.y = -2.9 + (mouseX / window.innerWidth) * 3;
// Verticle movement, with lower stop
if ((mouseY * 0.5) / window.innerHeight >= 1.4) {
object.rotation.x = 1.4;
} else {
object.rotation.x = (mouseY * 0.5) / window.innerHeight;
}
// Render frame
renderer.render(scene, camera);
}
// Event for mouse movement
document.onmousemove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
};
Animated Features
One prominent feature on both the main page and the portfolio gallery page is the flip animation on the project cards. This was implemented using primarily CSS and HTML:
-
The HTML specifies the content for the front and back of the cards.
-
In CSS, the cards have a class that rotates them 180°, using an attribute called preserve-3d. Here is a snippet of the styling for the cards:
.project { display: flex; height: 25rem; width: 100%; position: relative; transition: 0.9s; transform-style: preserve-3d; } .flip { transform: rotateY(180deg); } .front, .back { position: absolute; top: 0; left: 0; width: 100%; height: 100%; color: #ffffff; text-align: center; backface-visibility: hidden; transition: 0.9s; transform-style: preserve-3d; }
In JavaScript, an event listener on the button adds the flip class to the card when clicked. The code is streamlined so that instead of each card needing its own event listener, the function identifies the button number and matches it to the corresponding card.
document.addEventListener("DOMContentLoaded", function () {
// Select all buttons
const buttons = document.querySelectorAll(".card-btn");
// Add click event listener to each button
buttons.forEach((button) => {
button.addEventListener("click", function () {
// Get the associated project using the data attribute
const projectNumber = this.getAttribute("data-btn");
const project = document.querySelector(
`.project[data-project="${projectNumber}"]`
);
// Toggle the 'flip' class on the associated project
project.classList.toggle("flip");
});
});
});
Final Product
Everything here is the final product.