Ever thought your traditional business card could use a serious upgrade?
Well, I sure did! Buckle up because I'm about to take you on a mind-bending journey into the world of creativity, innovation, and 3D magic. Welcome to the behind-the-scenes tale of my 'Dynamic Portfolio Card' project β where Three.js meets Blender to transform a plain ol' business card into an interactive work of art that'll leave everyone you meet absolutely awestruck! πβ¨
Live Demo: card.namanbarkiya.xyz
Source Code: Github link
Let's jump right in with a step-by-step guide on how to create this project:
- Setting Up the Project: ViteJS and Vanilla JS Template
npm create vite@latest my-3d-card -- --template vanilla
npm i three gsap
- Creating the HTML Canvas and Script File: Now that we've got the essentials covered, let's set up our index.html file within the project directory. This file will serve as the canvas for our 3D masterpiece and will link to our main script file, main.js.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Card | Naman Barkiya</title>
</head>
<body>
<canvas class="webgl"></canvas>
<script type="module" src="./main.js"></script>
</body>
</html>
Moving Forward with main.js File:
- import all necessary packages:
import "./card-style.css";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
- Now we create a scene for our threejs (Customizing Light Positions):
const scene = new THREE.Scene();
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
const lightFront = new THREE.DirectionalLight(0xffffff, 0.7);
lightFront.position.set(0, 10, 30);
scene.add(lightFront);
const lightBack = new THREE.DirectionalLight(0xffffff, 0.7);
lightBack.position.set(-30, 10, -30);
scene.add(lightBack);
const lightMid = new THREE.DirectionalLight(0xffffff, 0.7);
lightMid.position.set(30, 10, -30);
scene.add(lightMid);
const pointLight = new THREE.PointLight(0xffffff, 1, 60);
pointLight.position.set(10, 10, 30);
scene.add(pointLight);
const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height);
camera.position.z = 30;
scene.add(camera);
const canvas = document.querySelector(".webgl");
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setClearColor(0x030712, 1);
renderer.setPixelRatio(2);
Before delving into the JavaScript file, let's first craft the star of our show β the 3D model. We'll be using Blender for this creative endeavor. You'll find the Blender file included in the source code. Let's jump into Blender and start bringing our vision to life!
First download the blend file from the link:
drive link for assets of 3d cardFeel free to take your pick: you can either whip up your very own card from scratch or just grab mine and give it your own spin using this Figma file link: figma file link
Once all the files are ready you can go to blender and replace card.png and logo.png in the shading section:
Make the adjustments from the UV Editing Section and set the height and width accordingly (It will already be adjusted if you've downloaded my assets):
- Once everything is ready let's export the model in glTF 2.0 (.glb/.gltf) format and you are good to go!
Let's get back to our main.js file
Now that we have our 3D model and three.js scene ready, It's time to now load the model and embed links π₯
- We need a object containing all the links and position on the canvas:
const linkPos = {
box1: {
x: 0.7,
y: 1.21,
z: 0.03,
name: "ClickableBox1",
link: "/naman_barkiya_resume.pdf",
},
box2: {
x: 0.06,
y: -0.4,
z: 0.03,
name: "ClickableBox2",
link: "https://namanbarkiya.xyz",
},
circle1: {
x: -0.46,
y: -1.06,
z: 0.03,
name: "ClickableCircle1",
link: "https://github.com/namanbarkiya",
},
circle2: {
x: 0.05,
y: -1.06,
z: 0.03,
name: "ClickableCircle2",
link: "https://www.linkedin.com/in/naman-barkiya-015323200/",
},
circle3: {
x: 0.55,
y: -1.06,
z: 0.03,
name: "ClickableCircle3",
link: "mailto:naman.barkiya02@gmail.com",
},
this object defines each link position on the canvas as you can see in the image:
- Let's load the model and align all the link positions with respect to card:
// Load the 3D model
const loader = new GLTFLoader();
let mesh;
loader.load(
"/naman_card.glb",
(gltf) => {
mesh = gltf.scene;
mesh.traverse((child) => {
if (child.isMesh) {
child.name = "ClickablePart1"; // Replace with a meaningful name
}
});
// Optionally, you can set the position, rotation, or scale of the mesh here
// For example:
// mesh.position.set(x, y, z);
// mesh.rotation.set(rx, ry, rz);
// Increase the size of the mesh
const scaleFactor = 5;
mesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
scene.add(mesh);
// BOX 1
const box1Geometry = new THREE.PlaneGeometry(0.5, 0.08);
const box1Material = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0,
});
const box1 = new THREE.Mesh(box1Geometry, box1Material);
box1.position.x = linkPos.box1.x;
box1.position.y = linkPos.box1.y;
box1.position.z = linkPos.box1.z;
box1.name = linkPos.box1.name;
mesh.add(box1); // Add the circle as a child of the loaded model
// BOX 1
const box2Geometry = new THREE.PlaneGeometry(1.2, 0.2);
const box2Material = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0,
});
const box2 = new THREE.Mesh(box2Geometry, box2Material);
box2.position.x = linkPos.box2.x;
box2.position.y = linkPos.box2.y;
box2.position.z = linkPos.box2.z;
box2.name = linkPos.box2.name;
mesh.add(box2); // Add the circle as a child of the loaded model
// CIRCLE 1
const circle1Geometry = new THREE.CircleGeometry(0.16, 32);
const circle1Material = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0,
});
const circle1 = new THREE.Mesh(circle1Geometry, circle1Material);
circle1.position.x = linkPos.circle1.x;
circle1.position.y = linkPos.circle1.y;
circle1.position.z = linkPos.circle1.z;
circle1.name = linkPos.circle1.name;
mesh.add(circle1); // Add the circle as a child of the loaded model
// CIRCLE 2
const circle2Geometry = new THREE.CircleGeometry(0.16, 32);
const circle2Material = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0,
});
const circle2 = new THREE.Mesh(circle2Geometry, circle2Material);
circle2.position.x = linkPos.circle2.x;
circle2.position.y = linkPos.circle2.y;
circle2.position.z = linkPos.circle2.z;
circle2.name = linkPos.circle2.name;
mesh.add(circle2); // Add the circle as a child of the loaded model
// CIRCLE 3
const circle3Geometry = new THREE.CircleGeometry(0.16, 32);
const circle3Material = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0,
});
const circle3 = new THREE.Mesh(circle3Geometry, circle3Material);
circle3.position.x = linkPos.circle3.x;
circle3.position.y = linkPos.circle3.y;
circle3.position.z = linkPos.circle3.z;
circle3.name = linkPos.circle3.name;
mesh.add(circle3); // Add the circle as a child of the loaded model
mesh.rotation.y = 0;
mesh.rotation.z = 0;
loop();
},
undefined,
(error) => {
console.error("Error loading 3D model:", error);
}
);
- Then we make the card interactive by adding controls and setting auto rotation play/plause on click:
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.enablePan = false;
controls.enableZoom = false;
controls.autoRotate = false;
controls.minPolarAngle = 1.5;
controls.maxPolarAngle = 1.5;
controls.autoRotateSpeed = 3;
- Making the card responsive:
window.addEventListener("resize", () => {
sizes.height = window.innerHeight;
sizes.width = window.innerWidth;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
});
const loop = () => {
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(loop);
};
let autoRotate = false;
- Opening the links in new tab:
canvas.addEventListener("click", (event) => {
controls.autoRotate = !autoRotate;
autoRotate = !autoRotate;
const mouse = {
x: (event.clientX / sizes.width) * 2 - 1,
y: -(event.clientY / sizes.height) * 2 + 1,
};
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// Check for intersections with the clickable circle
const clickableBox1 = scene.getObjectByName(linkPos.box1.name);
const clickableBox2 = scene.getObjectByName(linkPos.box2.name);
const clickableCircle1 = scene.getObjectByName(linkPos.circle1.name);
const clickableCircle2 = scene.getObjectByName(linkPos.circle2.name);
const clickableCircle3 = scene.getObjectByName(linkPos.circle3.name);
if (clickableCircle1) {
const intersects = raycaster.intersectObject(clickableCircle1);
if (intersects.length > 0) {
// Replace 'YOUR_HYPERLINK_URL' with the desired URL
window.open(linkPos.circle1.link, "_blank"); // Opens the link in a new tab
}
}
if (clickableCircle2) {
const intersects = raycaster.intersectObject(clickableCircle2);
if (intersects.length > 0) {
// Replace 'YOUR_HYPERLINK_URL' with the desired URL
window.open(linkPos.circle2.link, "_blank"); // Opens the link in a new tab
}
}
if (clickableCircle3) {
const intersects = raycaster.intersectObject(clickableCircle3);
if (intersects.length > 0) {
// Replace 'YOUR_HYPERLINK_URL' with the desired URL
window.open(linkPos.circle3.link, "_blank"); // Opens the link in a new tab
}
}
if (clickableBox1) {
const intersects = raycaster.intersectObject(clickableBox1);
if (intersects.length > 0) {
window.open(linkPos.box1.link, "_blank");
}
}
if (clickableBox2) {
const intersects = raycaster.intersectObject(clickableBox2);
if (intersects.length > 0) {
// Replace 'YOUR_HYPERLINK_URL' with the desired URL
window.open(linkPos.box2.link, "_blank"); // Opens the link in a new tab
}
}
});
-
Now the last part, adding styles:
- card-style.css:
:root {
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
display: flex;
place-items: center;
}
.clickable-box {
cursor: pointer;
}
Conclusion
Well folks, that's a wrap! We've reached the end of this wild ride through the world of 3D creativity and innovation. From a plain old business card to a jaw-dropping "Portfolio Card with embedded links" we've seen it all.
Useful Links
Live Demo: card.namanbarkiya.xyz
Source Code: Github link
Portfolio: namanbarkiya.xyz
Github: namanbarkiya
Linkedin: Naman Barkiya
Top comments (2)
Great 3d Portfolio card and well-written blog
It's great. I love it