Showcase: Portfolio 3D — Creative Web

Showcase: Portfolio 3D Saatnya menggabungkan semua yang sudah dipelajari untuk membuat sebuah mini portfolio 3D — halaman web interaktif yang menampilkan…

Showcase: Portfolio 3D

Saatnya menggabungkan semua yang sudah dipelajari untuk membuat sebuah mini portfolio 3D — halaman web interaktif yang menampilkan karya-karyamu dalam lingkungan 3D.

Konsep Portfolio 3D

Bayangkan portfolio yang bukan halaman biasa, tapi ruang 3D di mana pengunjung bisa:

Struktur Project

portfolio-3d/
  src/
    App.jsx
    components/
      Scene.jsx          # Scene utama
      FloatingCard.jsx    # Kartu project yang melayang
      HeroText.jsx        # Teks 3D judul
      ParticleField.jsx   # Background partikel
      ContactSection.jsx  # Section kontak
    data/
      projects.js         # Data portfolio
    styles/
      global.css
  public/
    textures/
    models/
  package.json

Data Projects

// src/data/projects.js
export const projects = [
  {
    id: 1,
    title: "E-Commerce App",
    description: "Toko online modern dengan React dan Node.js",
    color: "#3498db",
    image: "/textures/project1.jpg",
    url: "https://project1.example.com",
  },
  {
    id: 2,
    title: "Weather Dashboard",
    description: "Dashboard cuaca real-time dengan data API",
    color: "#2ecc71",
    image: "/textures/project2.jpg",
    url: "https://project2.example.com",
  },
  {
    id: 3,
    title: "Chat Application",
    description: "Aplikasi chat real-time dengan WebSocket",
    color: "#e74c3c",
    image: "/textures/project3.jpg",
    url: "https://project3.example.com",
  },
];

FloatingCard — Kartu Project 3D

import { useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
import { Text, RoundedBox } from "@react-three/drei";

function FloatingCard({ project, position, index }) {
  const groupRef = useRef();
  const [hovered, setHovered] = useState(false);

  useFrame((state) => {
    const t = state.clock.elapsedTime;
    groupRef.current.position.y =
      position[1] + Math.sin(t + index * 0.5) * 0.2;
    groupRef.current.rotation.y = Math.sin(t * 0.3 + index) * 0.1;
  });

  return (
    <group
      ref={groupRef}
      position={position}
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
      onClick={() => window.open(project.url, "_blank")}
    >
      <RoundedBox
        args={[2.5, 1.5, 0.1]}
        radius={0.1}
        smoothness={4}
      >
        <meshStandardMaterial
          color={hovered ? "#ffffff" : project.color}
          roughness={0.3}
          metalness={0.1}
        />
      </RoundedBox>

      <Text
        position={[0, 0.3, 0.06]}
        fontSize={0.18}
        color={hovered ? project.color : "#ffffff"}
        anchorX="center"
        anchorY="middle"
        maxWidth={2}
      >
        {project.title}
      </Text>

      <Text
        position={[0, -0.1, 0.06]}
        fontSize={0.1}
        color="#cccccc"
        anchorX="center"
        anchorY="middle"
        maxWidth={2}
      >
        {project.description}
      </Text>
    </group>
  );
}

ParticleField — Background Atmosferik

import { useRef, useMemo } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";

function ParticleField({ count = 500 }) {
  const meshRef = useRef();

  const positions = useMemo(() => {
    const pos = new Float32Array(count * 3);
    for (let i = 0; i < count * 3; i++) {
      pos[i] = (Math.random() - 0.5) * 20;
    }
    return pos;
  }, [count]);

  useFrame((state) => {
    meshRef.current.rotation.y = state.clock.elapsedTime * 0.02;
    meshRef.current.rotation.x = state.clock.elapsedTime * 0.01;
  });

  return (
    <points ref={meshRef}>
      <bufferGeometry>
        <bufferAttribute
          attach="attributes-position"
          count={count}
          array={positions}
          itemSize={3}
        />
      </bufferGeometry>
      <pointsMaterial
        size={0.02}
        color="#ffffff"
        transparent
        opacity={0.6}
        sizeAttenuation
      />
    </points>
  );
}

Scene Utama — Menyatukan Semua

import { Canvas } from "@react-three/fiber";
import { OrbitControls, Environment, Float } from "@react-three/drei";
import { projects } from "./data/projects";

function Scene() {
  return (
    <>
      <Environment preset="night" />
      <ambientLight intensity={0.3} />
      <directionalLight position={[5, 5, 5]} intensity={0.8} />
      <pointLight position={[-3, 3, -3]} color="#3498db" intensity={0.5} />

      <OrbitControls
        enableZoom={false}
        maxPolarAngle={Math.PI / 2}
        minPolarAngle={Math.PI / 4}
      />

      <ParticleField count={800} />

      {projects.map((project, i) => (
        <FloatingCard
          key={project.id}
          project={project}
          position={[(i - 1) * 3.5, 0, 0]}
          index={i}
        />
      ))}

      {/* Lantai reflektif */}
      <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -2, 0]}>
        <planeGeometry args={[50, 50]} />
        <meshStandardMaterial
          color="#0a0a1a"
          roughness={0.8}
          metalness={0.2}
        />
      </mesh>
    </>
  );
}

function App() {
  return (
    <div style={{ width: "100vw", height: "100vh", background: "#000" }}>
      <Canvas camera={{ position: [0, 2, 8], fov: 60 }}>
        <Scene />
      </Canvas>

      {/* HTML overlay */}
      <div style={{
        position: "absolute", top: 40, left: 0, right: 0,
        textAlign: "center", color: "white", pointerEvents: "none"
      }}>
        <h1 style={{ fontSize: "2rem", margin: 0 }}>My Portfolio</h1>
        <p style={{ opacity: 0.6 }}>Klik kartu untuk melihat project</p>
      </div>
    </div>
  );
}

Tips Deployment

  1. Optimasi performa:

    • Gunakan <Suspense> untuk lazy loading model 3D
    • Kurangi polygon count untuk mobile
    • Gunakan <Preload all /> dari drei untuk preload asset
  2. Responsif:

    <Canvas
      dpr={[1, 2]}  // device pixel ratio range
      camera={{ position: [0, 2, 8], fov: 60 }}
    >
    
  3. Fallback untuk device lemah:

    import { useDetectGPU } from "@react-three/drei";
    
    function Scene() {
      const gpu = useDetectGPU();
      const isLowEnd = gpu.tier < 2;
    
      return (
        <>
          <ParticleField count={isLowEnd ? 100 : 800} />
          {/* kurangi quality untuk device lemah */}
        </>
      );
    }
    
  4. Deploy:

    • Vercel, Netlify, atau GitHub Pages
    • Pastikan static assets (textures, models) terinclude
    • Gunakan gzip/brotli compression untuk file 3D

Aksesibilitas: Jangan Lupa Pengguna yang Sensitif terhadap Gerakan

Scene 3D yang keren bisa bikin sebagian orang motion sickness, pusing, atau kesulitan fokus — terutama pengguna dengan gangguan vestibular. Respek preferensi mereka:

import { useEffect, useState } from "react";

function useReducedMotion() {
  const [reduced, setReduced] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    setReduced(mq.matches);
    const handler = (e) => setReduced(e.matches);
    mq.addEventListener("change", handler);
    return () => mq.removeEventListener("change", handler);
  }, []);

  return reduced;
}

function Scene() {
  const reduced = useReducedMotion();

  return (
    <>
      {/* Matikan auto-rotate & animasi aggressive kalau user minta reduced motion */}
      <FloatingCard float={!reduced} />
      <ParticleField count={reduced ? 0 : 800} />
      <OrbitControls autoRotate={!reduced} />
    </>
  );
}

Checklist aksesibilitas 3D:

<Canvas>
  {/* Fallback untuk screen reader dan browser tanpa WebGL */}
  <mesh>...</mesh>
</Canvas>
<p className="sr-only">
  Portfolio interaktif 3D dengan 3 proyek: Aplikasi X, Website Y, Game Z.
  Gunakan tombol panah untuk navigasi, atau klik tombol "Versi Teks" di bawah.
</p>

Prinsip: 3D adalah enhancement, bukan requirement. Semua informasi penting harus tetap bisa diakses tanpa WebGL.

Selamat! Kamu sudah menguasai fondasi creative web development — dari Canvas 2D sampai React Three Fiber. Sekarang waktunya bereksperimen dan membangun pengalaman web yang memukau!