A minimalistic, interactive portfolio website built with React, featuring an infinite draggable photo grid, custom Spotify player, and smooth animations.
An infinitely scrollable photo gallery with advanced physics and optimizations:
- Uses Framer Motion's
useSpringfor buttery-smooth dragging - Spring configuration:
damping: 40, stiffness: 200, mass: 0.5 - Creates natural, fluid motion with realistic inertia
- Eliminates jitter and provides smooth deceleration
const x = useSpring(rawX, { damping: 40, stiffness: 200, mass: 0.5 })
const y = useSpring(rawY, { damping: 40, stiffness: 200, mass: 0.5 })- Implements infinite scrolling using modular arithmetic
- Grid wraps seamlessly at boundaries (torus topology)
- Formula:
mod(n, m) = ((n % m) + m) % m - Creates the illusion of infinite space with finite grid items
const tx = useTransform(x, (v) =>
mod((item.relX * TOTAL_CELL) + v + TOTAL_CELL, gridWidth) - TOTAL_CELL
)- Images scale up when cursor/finger is near (within 350px)
- Uses distance calculation with squared optimization
- Spring physics applied to scale for smooth transitions
- Formula:
scale = 1 + (1 - distance / 350) * 0.12
- React.memo: Prevents unnecessary re-renders of grid items
- GPU Acceleration: Uses
translate3dfor hardware-accelerated transforms - Image Preloading: All images fully loaded and decoded before display
- Eager Loading: Images decoded synchronously for instant rendering
- No Duplicates: Fisher-Yates shuffle ensures unique images per viewport
A fully functional music player with real audio playback:
- Real MP3 playback with 4 curated tracks
- Expandable/collapsible interface
- Custom progress bar with seek functionality
- Animated audio visualizer (5 bars)
- Previous/Next track navigation
- Time display (current/duration)
- Auto-plays next track on completion
- Over My Dead Body - Drake
- I Feel It Coming - The Weeknd
- Past Life - Tame Impala
- P2 - Lil Uzi Vert
- Hover state expands player with spring animation
- Playing indicator: 5 animated bars with staggered timing
- Smooth transitions using Framer Motion
- Progress bar updates in real-time
Clean, minimalistic landing page with:
- About section with personal introduction
- Links to Instagram and TikTok (@imdonghakim)
- Professional contact information
- Social media icons (Email, LinkedIn, GitHub, X/Twitter)
- Headers: Gowun Batang (serif) - elegant, traditional feel
- Body: Karla (sans-serif) - clean, modern readability
- Responsive font sizing for all devices
- Minimalistic layout with ample whitespace
- Smooth fade-in animations on load
- Glass morphism navbar
- Dark/Light theme toggle
Showcases portfolio projects with:
- Project cards with tech stack badges
- Clean descriptions
- Consistent typography and spacing
- Responsive grid layout
Dynamic theme switching with:
- Dark mode:
#1e1e1ebackground - Light mode:
#ffffffbackground - Smooth 500ms transitions
- Persists in localStorage
- Icon changes (Sun/Moon)
Elegant loading experience:
- Grey circular spinner (LoadingIndicator component)
- Shows progress during image preloading
- Full-screen overlay prevents blank states
- Smooth fade-out transition
- Grid appears only when 100% ready
- React 18 - UI framework
- Vite - Build tool and dev server
- React Router - Client-side routing
- Framer Motion - Spring physics, animations, gestures
- useSpring - Smooth interpolation for dragging
- useTransform - Efficient value transformations
- useMotionValue - High-performance animated values
- Tailwind CSS - Utility-first styling
- Custom CSS - Glass morphism effects
- PostCSS - CSS processing
- React Icons - Icon library (bi, fa, md, wi)
- WebP Images - Optimized 77 unique photos
- MP3 Audio - Real music files
const mod = (n, m) => ((n % m) + m) % mThis ensures values wrap correctly in both positive and negative directions, creating seamless infinite scrolling.
const distanceSq = (mx - centerX) ** 2 + (my - centerY) ** 2
if (distanceSq > 122500) return 1 // 350Β² optimization
const distance = Math.sqrt(distanceSq)Calculates squared distance first to avoid expensive square root operation when possible.
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}Ensures unbiased randomization of image order for variety.
const GridItem = memo(({ item, ... }) => {
// Component logic
}, (prevProps, nextProps) =>
prevProps.item.id === nextProps.item.id
)Prevents re-renders when theme changes, dramatically improving performance.
useMotionValue- Reactive animated valuesuseSpring- Physics-based animationsuseTransform- Value transformationsuseCallback- Memoized functionsuseMemo- Memoized computations
// Preload and decode all images
await img.decode()
// Store in memory
preloadedImagesRef.current = loadedImages
// Then show grid
setIsReady(true)- Custom
onTouchMovehandler for hover effects touchAction: 'none'prevents browser gestures-webkit-user-select: noneprevents text selection- Viewport meta prevents zoom during drag
- Flexbox and Grid layouts
- Mobile-first approach
- Touch-friendly hit targets
- Adaptive font sizes
-
Image Loading
- All images preloaded before grid appears
- Decoded in memory for instant rendering
- No progressive loading or blank states
-
GPU Acceleration
translate3dfor hardware accelerationwillChange: 'transform'optimizationtransformTemplatefor efficient updates
-
React Optimizations
- Memo prevents unnecessary re-renders
- Callback functions memoized
- Grid config memoized with dependencies
-
Render Optimization
- Only visible items + buffer rendered
- Theme changes don't re-render grid items
- Smooth 500ms color transitions
portfolio3/
βββ src/
β βββ App.jsx # Main app & routing
β βββ Navbar.jsx # Glass navigation bar
β βββ Projects.jsx # Projects showcase
β βββ InfiniteGrid.jsx # Photo grid (core feature)
β βββ SpotifyPlayer.jsx # Music player
β βββ components/
β β βββ application/
β β βββ loading-indicator/ # Loading spinner
β βββ public/
β βββ toWEBP/ # 77 unique photos
β βββ currents.jpg # Album art
β βββ ea.jpeg
β βββ starboy.jpg
β βββ takecare.jpg
β βββ *.mp3 # Music files
βββ index.html # Entry point
βββ tailwind.config.js # Tailwind configuration
βββ vite.config.js # Vite configuration
# Install dependencies
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run previewBuilt with Vite for optimal production builds:
- Code splitting
- Tree shaking
- Asset optimization
- Minification
- Calculate viewport size and required cells
- Create image pool (shuffled, no duplicates in viewport)
- Assign images sequentially to grid positions
- Apply toroidal wrapping for infinite scrolling
- Capture raw delta from pan gesture
- Update motion values
- Spring interpolates to target position
- Transform applies modular wrapping
- GPU renders final position
- Create Image objects for all photos
- Wait for
onloadevent - Call
img.decode()for full decode - Store in memory reference
- Display grid when 100% complete
Design & Development: Dongha Kim
Built with: React, Framer Motion, Tailwind CSS
Photography: Personal collection (77 photos)
Music: Curated playlist with proper audio files
Personal portfolio project - All rights reserved.
Made with β€οΈ by Dongha Kim