feat: Convert product showcase to personal portfolio with dynamic 3D terrain and updated content.
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"gsap": "^3.14.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"simplex-noise": "^4.0.3",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3100,6 +3101,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/simplex-noise": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/simplex-noise/-/simplex-noise-4.0.3.tgz",
|
||||
"integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"gsap": "^3.14.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"simplex-noise": "^4.0.3",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
27
src/App.jsx
27
src/App.jsx
@@ -54,33 +54,34 @@ function App() {
|
||||
position: 'relative',
|
||||
paddingTop: '60px'
|
||||
}}>
|
||||
<HeroModel />
|
||||
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '20%',
|
||||
left: '10%',
|
||||
position: 'relative', // Changed to relative to sit on top of canvas if needed, or maintain z-index structure
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none'
|
||||
pointerEvents: 'none',
|
||||
textAlign: 'center',
|
||||
mixBlendMode: 'difference', // Ensure text pops against wireframe
|
||||
color: '#fff'
|
||||
}}>
|
||||
<h1 style={{
|
||||
fontSize: '8rem',
|
||||
fontWeight: '900',
|
||||
lineHeight: '0.8',
|
||||
color: '#000',
|
||||
letterSpacing: '-0.05em'
|
||||
letterSpacing: '-0.05em',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
OP-1 <br /> FIELD
|
||||
Matiss <br /> Jurevics
|
||||
</h1>
|
||||
<p style={{
|
||||
marginTop: '20px',
|
||||
marginTop: '30px',
|
||||
fontSize: '1.2rem',
|
||||
maxWidth: '300px',
|
||||
fontFamily: 'monospace'
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.1em'
|
||||
}}>
|
||||
The portable synthesizer. Refined, reshaped, reborn.
|
||||
Creative Developer / Designer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HeroModel />
|
||||
</section>
|
||||
|
||||
{/* Product Grid Section */}
|
||||
|
||||
@@ -1,77 +1,65 @@
|
||||
import React, { useRef, useState, Suspense } from 'react';
|
||||
import React, { useRef, useMemo, Suspense } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrthographicCamera, Environment, ContactShadows, Float } from '@react-three/drei';
|
||||
import { createNoise3D } from 'simplex-noise';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const Device = (props) => {
|
||||
const Terrain = () => {
|
||||
const mesh = useRef();
|
||||
const noise3D = useMemo(() => createNoise3D(), []);
|
||||
|
||||
// Create geometry with high segment count for smooth wave
|
||||
const geometry = useMemo(() => new THREE.PlaneGeometry(15, 15, 64, 64), []);
|
||||
|
||||
// Rotate based on mouse position (simplified for now, logic can be in global state or event)
|
||||
useFrame((state) => {
|
||||
if (mesh.current) {
|
||||
// Gentle idle rotation
|
||||
mesh.current.rotation.y += 0.002;
|
||||
const time = state.clock.getElapsedTime();
|
||||
const positions = mesh.current.geometry.attributes.position;
|
||||
|
||||
// Mouse interaction (lerp to mouse x/y)
|
||||
const t = state.clock.getElapsedTime();
|
||||
mesh.current.rotation.x = THREE.MathUtils.lerp(mesh.current.rotation.x, Math.cos(t / 2) / 8 + state.mouse.y / 4, 0.1);
|
||||
mesh.current.rotation.z = THREE.MathUtils.lerp(mesh.current.rotation.z, Math.sin(t / 4) / 8, 0.1);
|
||||
mesh.current.rotation.y = THREE.MathUtils.lerp(mesh.current.rotation.y, state.mouse.x / 4, 0.1);
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
const x = positions.getX(i);
|
||||
const y = positions.getY(i);
|
||||
// Animate z-axis with noise based on x, y and time
|
||||
// Frequency: how "zoomed in" the noise is (0.3)
|
||||
// Amplitude: height of waves (1.5)
|
||||
const z = noise3D(x * 0.3, y * 0.3, time * 0.2) * 1.5;
|
||||
positions.setZ(i, z);
|
||||
}
|
||||
positions.needsUpdate = true;
|
||||
|
||||
// Slight rotation for perspective
|
||||
mesh.current.rotation.x = -Math.PI / 3;
|
||||
mesh.current.rotation.z += 0.001;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group {...props} dispose={null}>
|
||||
<Float speed={2} rotationIntensity={0.5} floatIntensity={0.5}>
|
||||
{/* Main Body - Industrial Grey Box */}
|
||||
<mesh ref={mesh}>
|
||||
<boxGeometry args={[2, 3, 0.5]} />
|
||||
<meshStandardMaterial color="#d4d4d4" roughness={0.4} metalness={0.1} />
|
||||
|
||||
{/* Screen / Detail */}
|
||||
<mesh position={[0, 0.8, 0.26]}>
|
||||
<planeGeometry args={[1.5, 0.8]} />
|
||||
<meshBasicMaterial color="#111" />
|
||||
</mesh>
|
||||
|
||||
{/* Orange Dial -- Teenage Engineering signature */}
|
||||
<mesh position={[0.5, -0.5, 0.3]} rotation={[1.57, 0, 0]}>
|
||||
<cylinderGeometry args={[0.2, 0.2, 0.2, 32]} />
|
||||
<meshStandardMaterial color="#ff4d00" />
|
||||
</mesh>
|
||||
|
||||
{/* Buttons */}
|
||||
<mesh position={[-0.5, -0.5, 0.3]} rotation={[1.57, 0, 0]}>
|
||||
<cylinderGeometry args={[0.15, 0.15, 0.1, 32]} />
|
||||
<meshStandardMaterial color="#333" />
|
||||
</mesh>
|
||||
<mesh position={[-0.5, -0.9, 0.3]} rotation={[1.57, 0, 0]}>
|
||||
<cylinderGeometry args={[0.15, 0.15, 0.1, 32]} />
|
||||
<meshStandardMaterial color="#333" />
|
||||
</mesh>
|
||||
</mesh>
|
||||
</Float>
|
||||
</group>
|
||||
<mesh ref={mesh} geometry={geometry}>
|
||||
{/* Wireframe material options */}
|
||||
<meshStandardMaterial
|
||||
color="#333"
|
||||
wireframe={true}
|
||||
wireframeLinewidth={1.5}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
const HeroModel = () => {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '80vh', position: 'relative', marginTop: 0, zIndex: 0 }}>
|
||||
<Canvas shadows dpr={[1, 2]}>
|
||||
<div style={{ width: '100%', height: '100vh', position: 'absolute', top: 0, left: 0, zIndex: 0 }}>
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{ position: [0, 5, 10], fov: 45 }}
|
||||
dpr={[1, 2]}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
{/* Orthographic Camera for schematic look */}
|
||||
<OrthographicCamera makeDefault position={[0, 0, 10]} zoom={150} />
|
||||
|
||||
<ambientLight intensity={1.5} />
|
||||
<ambientLight intensity={0.5} />
|
||||
<pointLight position={[10, 10, 10]} intensity={1} />
|
||||
<spotLight position={[-10, 15, 10]} angle={0.3} penumbra={1} castShadow intensity={2} shadow-bias={-0.0001} />
|
||||
|
||||
<Device position={[0, 0, 0]} />
|
||||
<Terrain />
|
||||
|
||||
{/* Environment for reflections */}
|
||||
<Environment preset="city" />
|
||||
<ContactShadows resolution={1024} scale={10} blur={2} opacity={0.25} far={10} color="#000" />
|
||||
{/* Fog to fade edges into background color */}
|
||||
<fog attach="fog" args={['#e4e4e4', 5, 20]} />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
@@ -28,13 +28,13 @@ const Header = () => {
|
||||
return (
|
||||
<header style={headerStyle}>
|
||||
<div className="logo" style={{ fontSize: '18px', fontWeight: 'bold' }}>
|
||||
te
|
||||
mj
|
||||
</div>
|
||||
|
||||
<nav style={navStyle}>
|
||||
<a href="#products">Products</a>
|
||||
<a href="#about">Now</a>
|
||||
<a href="#store">Store</a>
|
||||
<a href="#work">Work</a>
|
||||
<a href="#about">About</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
|
||||
<div className="cart">
|
||||
|
||||
@@ -6,12 +6,10 @@ import '../styles/variables.css';
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const products = [
|
||||
{ id: 1, name: 'TX-6', desc: 'Portable Mixer', price: '$1199' },
|
||||
{ id: 2, name: 'CM-15', desc: 'Field Microphone', price: '$1199' },
|
||||
{ id: 3, name: 'TP-7', desc: 'Field Recorder', price: '$1499' },
|
||||
{ id: 4, name: 'OB-4', desc: 'Magic Radio', price: '$649' },
|
||||
{ id: 5, name: 'OD-11', desc: 'Cloud Speaker', price: '$999' },
|
||||
{ id: 6, name: 'EP-133', desc: 'K.O. II', price: '$299' },
|
||||
{ id: 1, name: 'Portfolio V1', desc: 'Web Design', price: '2023' },
|
||||
{ id: 2, name: 'Neon Dreams', desc: 'WebGL Experience', price: '2024' },
|
||||
{ id: 3, name: 'Type Lab', desc: 'Typography Tool', price: '2024' },
|
||||
{ id: 4, name: 'Audio Vis', desc: 'Sound Reactive', price: '2023' },
|
||||
];
|
||||
|
||||
const ProductGrid = () => {
|
||||
@@ -67,7 +65,7 @@ const ProductGrid = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="products" ref={gridRef} style={{
|
||||
<section id="work" ref={gridRef} style={{
|
||||
padding: '100px 20px',
|
||||
background: '#fff',
|
||||
minHeight: '100vh'
|
||||
@@ -81,8 +79,8 @@ const ProductGrid = () => {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline'
|
||||
}} ref={titleRef}>
|
||||
<h2 className="uppercase" style={{ fontSize: '2rem', margin: 0 }}>Field System</h2>
|
||||
<span className="mono" style={{ fontSize: '0.9rem', color: '#666' }}>ENGINEERING / AUDIO</span>
|
||||
<h2 className="uppercase" style={{ fontSize: '2rem', margin: 0 }}>Selected Work</h2>
|
||||
<span className="mono" style={{ fontSize: '0.9rem', color: '#666' }}>DESIGN / CODE</span>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
@@ -112,7 +110,7 @@ const ProductGrid = () => {
|
||||
onMouseLeave={onLeave}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%', zIndex: 2 }}>
|
||||
<span className="mono" style={{ fontSize: '0.8rem', color: '#ff4d00' }}>00{p.id}</span>
|
||||
<span className="mono" style={{ fontSize: '0.8rem', color: '#ff4d00' }}>0{p.id}</span>
|
||||
<div className="indicator" style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
@@ -132,7 +130,7 @@ const ProductGrid = () => {
|
||||
fontWeight: 800,
|
||||
userSelect: 'none'
|
||||
}}>
|
||||
TE
|
||||
MJ
|
||||
</div>
|
||||
|
||||
<div style={{ zIndex: 2 }}>
|
||||
|
||||
Reference in New Issue
Block a user