DEV Community

Cover image for 3D Card with Embedded Links Using Three.js and Blender
Naman Barkiya
Naman Barkiya

Posted on

3D Card with Embedded Links Using Three.js and Blender

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
Enter fullscreen mode Exit fullscreen mode
npm i three gsap
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

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 card

  • Feel 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:
    Shading Section 1
    Shading Section 2

  • 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):

UV Editing Section

  • 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",
    },
Enter fullscreen mode Exit fullscreen mode

this object defines each link position on the canvas as you can see in the image:

links embedding layout

  • 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);
    }
);

Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode
  • 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
        }
    }
});

Enter fullscreen mode Exit fullscreen mode
  • 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;
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
rd273001 profile image
Ravi Dubey

Great 3d Portfolio card and well-written blog

Collapse
 
yinmrsir profile image
eagle

It's great. I love it