1:"$Sreact.fragment" 2:I[88754,["/_next/static/chunks/0a3f498ef71c618e.js","/_next/static/chunks/61bc785dc6bd3109.js","/_next/static/chunks/a676f0ced11453e9.js","/_next/static/chunks/4b778b2e48718709.js","/_next/static/chunks/95a5e382a1e8200a.js","/_next/static/chunks/06fe5961b16f81d0.js","/_next/static/chunks/8c955ce35598c7b8.js","/_next/static/chunks/15c613f0f258455e.js"],"ClientPageWrapper"] 3:I[79520,["/_next/static/chunks/0a3f498ef71c618e.js","/_next/static/chunks/61bc785dc6bd3109.js","/_next/static/chunks/a676f0ced11453e9.js","/_next/static/chunks/4b778b2e48718709.js","/_next/static/chunks/95a5e382a1e8200a.js","/_next/static/chunks/06fe5961b16f81d0.js","/_next/static/chunks/8c955ce35598c7b8.js","/_next/static/chunks/15c613f0f258455e.js"],""] 5:I[73552,["/_next/static/chunks/0a3f498ef71c618e.js","/_next/static/chunks/61bc785dc6bd3109.js","/_next/static/chunks/a676f0ced11453e9.js","/_next/static/chunks/4b778b2e48718709.js","/_next/static/chunks/95a5e382a1e8200a.js","/_next/static/chunks/06fe5961b16f81d0.js","/_next/static/chunks/8c955ce35598c7b8.js","/_next/static/chunks/15c613f0f258455e.js"],"AnimatedText"] 6:I[22016,["/_next/static/chunks/0a3f498ef71c618e.js","/_next/static/chunks/61bc785dc6bd3109.js","/_next/static/chunks/a676f0ced11453e9.js","/_next/static/chunks/4b778b2e48718709.js","/_next/static/chunks/95a5e382a1e8200a.js","/_next/static/chunks/06fe5961b16f81d0.js","/_next/static/chunks/8c955ce35598c7b8.js","/_next/static/chunks/15c613f0f258455e.js"],""] 7:I[3601,["/_next/static/chunks/0a3f498ef71c618e.js","/_next/static/chunks/61bc785dc6bd3109.js","/_next/static/chunks/a676f0ced11453e9.js","/_next/static/chunks/4b778b2e48718709.js","/_next/static/chunks/95a5e382a1e8200a.js","/_next/static/chunks/06fe5961b16f81d0.js","/_next/static/chunks/8c955ce35598c7b8.js","/_next/static/chunks/15c613f0f258455e.js"],"AnimatedSection"] 13:I[85437,["/_next/static/chunks/0a3f498ef71c618e.js","/_next/static/chunks/61bc785dc6bd3109.js","/_next/static/chunks/a676f0ced11453e9.js","/_next/static/chunks/4b778b2e48718709.js","/_next/static/chunks/95a5e382a1e8200a.js","/_next/static/chunks/06fe5961b16f81d0.js","/_next/static/chunks/8c955ce35598c7b8.js","/_next/static/chunks/15c613f0f258455e.js"],"Image"] 15:I[97367,["/_next/static/chunks/ff1a16fafef87110.js","/_next/static/chunks/247eb132b7f7b574.js"],"OutletBoundary"] 16:"$Sreact.suspense" 4:T488,{"@context":"https://schema.org","@type":"BlogPosting","headline":"3D Card with Embedded Links Using Three.js and Blender","description":"Ever thought your traditional business card could use a serious upgrade? Here's how I used Three.js and Blender to transform a plain business card into an interactive 3D masterpiece with embedded clickable links.","datePublished":"2023-11-01T00:00:00.000Z","dateModified":"2023-11-01T00:00:00.000Z","author":{"@type":"Person","name":"Hussein Maghrabi","url":"https://h-maghrabi.tech/","sameAs":["https://www.linkedin.com/in/hussein-maghrabi/","https://www.linkedin.com/in/hussein-maghrabi/"]},"publisher":{"@type":"Person","name":"Hussein Maghrabi","url":"https://h-maghrabi.tech/"},"url":"https://h-maghrabi.tech//blogs/3d-card-threejs-blender","mainEntityOfPage":{"@type":"WebPage","@id":"https://h-maghrabi.tech//blogs/3d-card-threejs-blender"},"image":"https://h-maghrabi.tech//projects/card/card_2.webp","keywords":"Three.js, Blender, 3D, JavaScript, WebGL","wordCount":914,"timeRequired":"PT7M","inLanguage":"en-US","isPartOf":{"@type":"Blog","name":"Hussein Maghrabi's Blog","url":"https://h-maghrabi.tech//blogs"}}0:{"buildId":"pOVubU_Kt6m3SrfLgqzM-","rsc":["$","$1","c",{"children":[["$","$L2",null,{"children":[["$","$L3",null,{"id":"schema-blog-post","type":"application/ld+json","dangerouslySetInnerHTML":{"__html":"$4"}}],["$","$L3",null,{"id":"schema-breadcrumb-post","type":"application/ld+json","dangerouslySetInnerHTML":{"__html":"{\"@context\":\"https://schema.org\",\"@type\":\"BreadcrumbList\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https://h-maghrabi.tech/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Blogs\",\"item\":\"https://h-maghrabi.tech//blogs\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"3D Card with Embedded Links Using Three.js and Blender\",\"item\":\"https://h-maghrabi.tech//blogs/3d-card-threejs-blender\"}]}"}}],["$","article",null,{"className":"max-w-3xl mx-auto px-4 sm:px-6 py-8","children":[["$","$L5",null,{"delay":0,"children":["$","nav",null,{"aria-label":"Breadcrumb","className":"mb-6","children":["$","ol",null,{"className":"flex items-center gap-1.5 text-sm text-muted-foreground","children":[["$","li",null,{"children":["$","$L6",null,{"href":"/","className":"hover:text-foreground transition-colors","children":"Home"}]}],["$","li",null,{"aria-hidden":"true","children":["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-chevron-right w-3.5 h-3.5","children":[["$","path","mthhwq",{"d":"m9 18 6-6-6-6"}],"$undefined"]}]}],["$","li",null,{"children":["$","$L6",null,{"href":"/blogs","className":"hover:text-foreground transition-colors","children":"Blogs"}]}],["$","li",null,{"aria-hidden":"true","children":["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-chevron-right w-3.5 h-3.5","children":[["$","path","mthhwq",{"d":"m9 18 6-6-6-6"}],"$undefined"]}]}],["$","li",null,{"className":"text-foreground font-medium truncate max-w-[200px] sm:max-w-[300px]","aria-current":"page","children":"3D Card with Embedded Links Using Three.js and Blender"}]]}]}]}],["$","$L7",null,{"direction":"up","children":["$","header",null,{"className":"mb-8","children":[["$","div",null,{"className":"flex flex-wrap gap-2 mb-4","children":[["$","span","Three.js",{"className":"inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20","children":"Three.js"}],["$","span","Blender",{"className":"inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20","children":"Blender"}],"$L8","$L9","$La"]}],"$Lb","$Lc","$Ld"]}]}],"$Le","$Lf","$L10"]}]]}],["$L11"],"$L12"]}],"loading":null,"isPartial":false} 8:["$","span","3D",{"className":"inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20","children":"3D"}] 9:["$","span","JavaScript",{"className":"inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20","children":"JavaScript"}] a:["$","span","WebGL",{"className":"inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20","children":"WebGL"}] b:["$","h1",null,{"className":"font-heading text-3xl sm:text-4xl md:text-5xl leading-tight text-foreground mb-4","children":"3D Card with Embedded Links Using Three.js and Blender"}] c:["$","p",null,{"className":"text-lg text-muted-foreground leading-relaxed mb-6","children":"Ever thought your traditional business card could use a serious upgrade? Here's how I used Three.js and Blender to transform a plain business card into an interactive 3D masterpiece with embedded clickable links."}] d:["$","div",null,{"className":"flex flex-wrap items-center gap-4 text-sm text-muted-foreground pb-6 border-b border-border","children":[["$","address",null,{"className":"flex items-center gap-1.5 not-italic","children":[["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-user w-4 h-4","children":[["$","path","975kel",{"d":"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"}],["$","circle","17ys0d",{"cx":"12","cy":"7","r":"4"}],"$undefined"]}],["$","a",null,{"rel":"author","href":"https://h-maghrabi.tech/","className":"hover:text-foreground transition-colors","children":"Hussein Maghrabi"}]]}],["$","time",null,{"dateTime":"2023-11-01T00:00:00.000Z","className":"flex items-center gap-1.5","children":[["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-calendar w-4 h-4","children":[["$","path","1cmpym",{"d":"M8 2v4"}],["$","path","4m81vk",{"d":"M16 2v4"}],["$","rect","1hopcy",{"width":"18","height":"18","x":"3","y":"4","rx":"2"}],["$","path","8toen8",{"d":"M3 10h18"}],"$undefined"]}],"November 1, 2023"]}],["$","span",null,{"className":"flex items-center gap-1.5","children":[["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-clock w-4 h-4","children":[["$","circle","1mglay",{"cx":"12","cy":"12","r":"10"}],["$","polyline","68esgv",{"points":"12 6 12 12 16 14"}],"$undefined"]}],7," min read"]}]]}] e:["$","$L7",null,{"direction":"up","delay":0.05,"children":["$","figure",null,{"className":"mb-10","children":["$","$L13",null,{"src":"/projects/card/card_2.webp","alt":"3D Card with Embedded Links Using Three.js and Blender","width":768,"height":400,"className":"w-full h-auto rounded-lg border border-border object-cover","priority":true}]}]}] 14:T252e,
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. ๐โจ
Live Demo: card.nbarkiya.xyz ยท Source Code: GitHub
Here's a step-by-step guide on how to create this project.
We'll use ViteJS with the Vanilla JS template for a fast, modern dev environment:
npm create vite@latest my-3d-card -- --template vanilla
We need two packages: gsap for smooth animations and three for the 3D engine.
npm i three gsap
Set up your index.html file. This is the canvas for the 3D scene and links to our script:
<!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>
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";
A good 3D scene needs layered lighting to look realistic. We set up a front light, back light, mid light and a PointLight:
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 jumping back into the JavaScript, we need the actual 3D model:
card.png and logo.png in the Shading section with your files.glb) formatThe key idea โ we define invisible clickable planes/circles that are positioned precisely over the areas on the card where links should exist.
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://nbarkiya.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",
},
};
const loader = new GLTFLoader();
let mesh;
loader.load("/naman_card.glb", (gltf) => {
mesh = gltf.scene;
mesh.traverse((child) => {
if (child.isMesh) child.name = "ClickablePart1";
});
const scaleFactor = 5;
mesh.scale.set(scaleFactor, scaleFactor, scaleFactor);
scene.add(mesh);
// Invisible box over the resume link area
const box1 = new THREE.Mesh(
new THREE.PlaneGeometry(0.5, 0.08),
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 })
);
box1.position.set(linkPos.box1.x, linkPos.box1.y, linkPos.box1.z);
box1.name = linkPos.box1.name;
mesh.add(box1);
// Invisible circle over the GitHub icon
const circle1 = new THREE.Mesh(
new THREE.CircleGeometry(0.16, 32),
new THREE.MeshBasicMaterial({ transparent: true, opacity: 0 })
);
circle1.position.set(linkPos.circle1.x, linkPos.circle1.y, linkPos.circle1.z);
circle1.name = linkPos.circle1.name;
mesh.add(circle1);
// ... repeat for circle2, circle3, box2
mesh.rotation.y = 0;
mesh.rotation.z = 0;
loop();
});
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;
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);
};
This is the secret sauce. We use a THREE.Raycaster to check if a click intersects with any of the invisible hit meshes, then open the corresponding link:
let autoRotate = false;
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 each clickable object and open its link
["circle1", "circle2", "circle3", "box1", "box2"].forEach((key) => {
const obj = scene.getObjectByName(linkPos[key].name);
if (obj && raycaster.intersectObject(obj).length > 0) {
window.open(linkPos[key].link, "_blank");
}
});
});
:root {
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
-webkit-font-smoothing: antialiased;
}
body {
margin: 0;
display: flex;
place-items: center;
}
.clickable-box {
cursor: pointer;
}
Well folks, that's a wrap! We've gone from a plain business card to a jaw-dropping 3D Portfolio Card with embedded links โ all using the power of Three.js and Blender. The invisible raycasting approach for links is the key insight: you can overlay any clickable geometry on your 3D model without modifying the model itself.
Grab the assets, customize the card with your own links and branding, and ship something that'll genuinely wow people.