feat(about): implement 90-degree orthogonal isometric river with large-screen overflow and responsive timeline
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
.journeySection {
|
.journeySection {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1050px;
|
height: 65.625rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-page);
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
@@ -87,11 +87,11 @@
|
|||||||
transform var(--motion-duration-default) var(--motion-ease-out);
|
transform var(--motion-duration-default) var(--motion-ease-out);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step05 { top: 116px; right: 46px; }
|
.step05 { top: 7.25rem; right: 2.875rem; }
|
||||||
.step04 { top: 334px; right: 250px; }
|
.step04 { top: 20.875rem; right: 15.625rem; }
|
||||||
.step03 { top: 500px; left: 48%; }
|
.step03 { top: 31.25rem; left: 48%; }
|
||||||
.step02 { top: 690px; left: 25%; }
|
.step02 { top: 43.125rem; left: 25%; }
|
||||||
.step01 { top: 826px; left: 70px; }
|
.step01 { top: 51.625rem; left: 4.375rem; }
|
||||||
|
|
||||||
.stepNumber {
|
.stepNumber {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -124,11 +124,11 @@
|
|||||||
width: 14.375rem;
|
width: 14.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step05 { top: 90px; right: 32px; }
|
.step05 { top: 5.625rem; right: 2rem; }
|
||||||
.step04 { top: 300px; right: 10%; }
|
.step04 { top: 18.75rem; right: 10%; }
|
||||||
.step03 { top: 490px; left: 48%; }
|
.step03 { top: 30.625rem; left: 48%; }
|
||||||
.step02 { top: 690px; left: 24%; }
|
.step02 { top: 43.125rem; left: 24%; }
|
||||||
.step01 { top: 850px; left: 32px; }
|
.step01 { top: 53.125rem; left: 2rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
margin-top: 13.75rem;
|
margin-top: 13.75rem;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepsList::before {
|
.stepsList::before {
|
||||||
@@ -180,8 +180,8 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1.25rem;
|
top: 1.25rem;
|
||||||
bottom: 1.25rem;
|
bottom: 1.25rem;
|
||||||
left: 0.3125rem;
|
left: 0.5rem;
|
||||||
width: 1px;
|
width: 2px;
|
||||||
background: var(--border-strong);
|
background: var(--border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,18 +203,33 @@
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1.375rem;
|
top: 1.375rem;
|
||||||
left: -1.25rem;
|
left: -1rem;
|
||||||
width: 0.5625rem;
|
width: 0.625rem;
|
||||||
height: 0.5625rem;
|
height: 0.625rem;
|
||||||
|
transform: translateX(-50%);
|
||||||
border: 3px solid var(--bg-page);
|
border: 3px solid var(--bg-page);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--accent-default);
|
background: var(--accent-default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1441px) {
|
||||||
|
.journeySection {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journeySection::after {
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
.stepCard:hover {
|
.stepCard:hover {
|
||||||
border-color: var(--accent-default);
|
border-color: var(--accent-default);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { AboutRiverScene } from './scenes/AboutRiverScene';
|
|||||||
import styles from './RiverJourney.module.css';
|
import styles from './RiverJourney.module.css';
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ number: '05', title: 'Product Engineer', description: 'Merging product strategy with engineering to ship impactful products.' },
|
|
||||||
{ number: '04', title: 'Software Engineer', description: 'Engineered scalable applications and delivered real-world solutions.' },
|
|
||||||
{ number: '03', title: 'Computer Science', description: 'Built strong foundations in algorithms and systems thinking.' },
|
|
||||||
{ number: '02', title: 'UI/UX Designer', description: 'Designed intuitive interfaces and user-centered experiences.' },
|
|
||||||
{ number: '01', title: 'Digital Artist', description: 'Explored visual design, illustration and storytelling.' },
|
{ number: '01', title: 'Digital Artist', description: 'Explored visual design, illustration and storytelling.' },
|
||||||
|
{ number: '02', title: 'UI/UX Designer', description: 'Designed intuitive interfaces and user-centered experiences.' },
|
||||||
|
{ number: '03', title: 'Computer Science', description: 'Built strong foundations in algorithms and systems thinking.' },
|
||||||
|
{ number: '04', title: 'Software Engineer', description: 'Engineered scalable applications and delivered real-world solutions.' },
|
||||||
|
{ number: '05', title: 'Product Engineer', description: 'Merging product strategy with engineering to ship impactful products.' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RiverJourney: React.FC = () => {
|
export const RiverJourney: React.FC = () => {
|
||||||
|
|||||||
@@ -12,16 +12,15 @@ import {
|
|||||||
} from './scenePrimitives';
|
} from './scenePrimitives';
|
||||||
import styles from './Scenes.module.css';
|
import styles from './Scenes.module.css';
|
||||||
|
|
||||||
|
// Sharp 90-degree orthogonal isometric river path:
|
||||||
|
// Enters from bottom-left along the isometric Z axis,
|
||||||
|
// takes a sharp 90-degree orthogonal step across the isometric X axis at the center,
|
||||||
|
// and turns 90 degrees to continue along the isometric Z axis towards the top-right.
|
||||||
const riverPath = [
|
const riverPath = [
|
||||||
new THREE.Vector2(4.5, -13),
|
new THREE.Vector2(-1.3, -16),
|
||||||
new THREE.Vector2(2.1, -7.4),
|
new THREE.Vector2(-1.3, 0),
|
||||||
new THREE.Vector2(0.2, -3.8),
|
new THREE.Vector2(1.3, 0),
|
||||||
new THREE.Vector2(0.2, -1.2),
|
new THREE.Vector2(1.3, 16),
|
||||||
new THREE.Vector2(1.5, 0.2),
|
|
||||||
new THREE.Vector2(-0.8, 2.4),
|
|
||||||
new THREE.Vector2(-0.1, 4.1),
|
|
||||||
new THREE.Vector2(-2.5, 8.1),
|
|
||||||
new THREE.Vector2(-3.3, 13),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||||
@@ -42,7 +41,7 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ground = new THREE.Mesh(
|
const ground = new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(34, 34),
|
new THREE.PlaneGeometry(60, 60),
|
||||||
new THREE.MeshStandardMaterial({ color: palette.ground, roughness: 1 }),
|
new THREE.MeshStandardMaterial({ color: palette.ground, roughness: 1 }),
|
||||||
);
|
);
|
||||||
ground.rotation.x = -Math.PI / 2;
|
ground.rotation.x = -Math.PI / 2;
|
||||||
@@ -51,9 +50,9 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
|||||||
world.add(ground);
|
world.add(ground);
|
||||||
|
|
||||||
const gridVertices: number[] = [];
|
const gridVertices: number[] = [];
|
||||||
for (let value = -17; value <= 17; value += 1) {
|
for (let value = -30; value <= 30; value += 1) {
|
||||||
gridVertices.push(-17, -0.3, value, 17, -0.3, value);
|
gridVertices.push(-30, -0.3, value, 30, -0.3, value);
|
||||||
gridVertices.push(value, -0.3, -17, value, -0.3, 17);
|
gridVertices.push(value, -0.3, -30, value, -0.3, 30);
|
||||||
}
|
}
|
||||||
const gridGeometry = new THREE.BufferGeometry();
|
const gridGeometry = new THREE.BufferGeometry();
|
||||||
gridGeometry.setAttribute('position', new THREE.Float32BufferAttribute(gridVertices, 3));
|
gridGeometry.setAttribute('position', new THREE.Float32BufferAttribute(gridVertices, 3));
|
||||||
@@ -96,8 +95,8 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
|||||||
world.add(primaryPlates, secondaryPlates, tertiaryPlates);
|
world.add(primaryPlates, secondaryPlates, tertiaryPlates);
|
||||||
|
|
||||||
[
|
[
|
||||||
[-7.5, -7.7, 9.5, 5.1], [-6.2, -2.7, 8.5, 4.2], [-6.5, 2.2, 8.7, 4.3],
|
[-7.5, -7.7, 9.5, 5.1], [-6.6, -2.7, 8.5, 4.2], [-6.8, 2.2, 8.7, 4.3],
|
||||||
[-7.1, 7.2, 9.2, 5.0], [7.4, -8.0, 9.4, 5.4], [6.2, -2.9, 8.0, 4.1],
|
[-7.1, 7.2, 9.2, 5.0], [7.4, -8.0, 9.4, 5.4], [6.4, -2.9, 8.0, 4.1],
|
||||||
[6.6, 2.0, 8.4, 4.2], [6.3, 7.0, 9.1, 5.0],
|
[6.6, 2.0, 8.4, 4.2], [6.3, 7.0, 9.1, 5.0],
|
||||||
].forEach(([x, z, width, depth]) => addPlate(primaryPlates, x, z, width, depth));
|
].forEach(([x, z, width, depth]) => addPlate(primaryPlates, x, z, width, depth));
|
||||||
|
|
||||||
@@ -108,7 +107,7 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
|||||||
addPlate(secondaryPlates, x, z, width, depth, 0.5, theme === 'dark' ? 0x0b2f5f : 0xe6f0fe),
|
addPlate(secondaryPlates, x, z, width, depth, 0.5, theme === 'dark' ? 0x0b2f5f : 0xe6f0fe),
|
||||||
);
|
);
|
||||||
|
|
||||||
const riverGeometry = new THREE.ShapeGeometry(createRibbonShape(riverPath, 2.2));
|
const riverGeometry = new THREE.ShapeGeometry(createRibbonShape(riverPath, 1.8));
|
||||||
const waterMaterial = new THREE.MeshStandardMaterial({
|
const waterMaterial = new THREE.MeshStandardMaterial({
|
||||||
color: palette.water,
|
color: palette.water,
|
||||||
emissive: palette.water,
|
emissive: palette.water,
|
||||||
@@ -130,15 +129,15 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
|||||||
const direction = end.clone().sub(start);
|
const direction = end.clone().sub(start);
|
||||||
const length = direction.length();
|
const length = direction.length();
|
||||||
direction.normalize();
|
direction.normalize();
|
||||||
for (let distance = 1; distance < length; distance += 1.15) {
|
for (let distance = 1.3; distance < length - 0.7; distance += 1.35) {
|
||||||
const at = start.clone().addScaledVector(direction, distance);
|
const at = start.clone().addScaledVector(direction, distance);
|
||||||
const seam = new THREE.Mesh(
|
const seam = new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(2.08, 0.014),
|
new THREE.PlaneGeometry(1.72, 0.016),
|
||||||
new THREE.MeshBasicMaterial({ color: palette.edge, transparent: true, opacity: 0.35 }),
|
new THREE.MeshBasicMaterial({ color: palette.edge, transparent: true, opacity: 0.32 }),
|
||||||
);
|
);
|
||||||
seam.rotation.x = -Math.PI / 2;
|
seam.rotation.x = -Math.PI / 2;
|
||||||
seam.rotation.z = Math.atan2(direction.y, direction.x) + Math.PI / 2;
|
seam.rotation.z = Math.atan2(direction.y, direction.x) + Math.PI / 2;
|
||||||
seam.position.set(at.x, -0.2, at.y);
|
seam.position.set(at.x, -0.21, -at.y);
|
||||||
tertiaryPlates.add(seam);
|
tertiaryPlates.add(seam);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,10 +50,28 @@
|
|||||||
|
|
||||||
.aboutScene {
|
.aboutScene {
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
height: 640px;
|
height: 40rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aboutScene canvas {
|
.aboutScene canvas {
|
||||||
opacity: 0.68;
|
opacity: 0.68;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1441px) {
|
||||||
|
.aboutScene {
|
||||||
|
overflow: visible;
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aboutScene::after {
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,19 +60,52 @@ export function fitOrthographicCamera(
|
|||||||
|
|
||||||
export function createRibbonShape(points: THREE.Vector2[], width: number) {
|
export function createRibbonShape(points: THREE.Vector2[], width: number) {
|
||||||
const halfWidth = width / 2;
|
const halfWidth = width / 2;
|
||||||
const offsetPoint = (index: number, direction: 1 | -1) => {
|
const n = points.length;
|
||||||
const current = points[index].clone();
|
if (n < 2) return new THREE.Shape();
|
||||||
const previous = points[Math.max(0, index - 1)];
|
|
||||||
const next = points[Math.min(points.length - 1, index + 1)];
|
const left: THREE.Vector2[] = [];
|
||||||
const tangent = next.clone().sub(previous).normalize();
|
const right: THREE.Vector2[] = [];
|
||||||
return current.add(new THREE.Vector2(-tangent.y, tangent.x).multiplyScalar(halfWidth * direction));
|
|
||||||
};
|
for (let i = 0; i < n; i++) {
|
||||||
|
let normal: THREE.Vector2;
|
||||||
|
let miterScale = 1;
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
const d = points[1].clone().sub(points[0]).normalize();
|
||||||
|
normal = new THREE.Vector2(-d.y, d.x);
|
||||||
|
} else if (i === n - 1) {
|
||||||
|
const d = points[n - 1].clone().sub(points[n - 2]).normalize();
|
||||||
|
normal = new THREE.Vector2(-d.y, d.x);
|
||||||
|
} else {
|
||||||
|
const d1 = points[i].clone().sub(points[i - 1]).normalize();
|
||||||
|
const d2 = points[i + 1].clone().sub(points[i]).normalize();
|
||||||
|
const n1 = new THREE.Vector2(-d1.y, d1.x);
|
||||||
|
const n2 = new THREE.Vector2(-d2.y, d2.x);
|
||||||
|
const miter = n1.clone().add(n2);
|
||||||
|
const len = miter.length();
|
||||||
|
if (len > 0.0001) {
|
||||||
|
miter.normalize();
|
||||||
|
const dot = Math.max(miter.dot(n1), 0.25);
|
||||||
|
miterScale = Math.min(1 / dot, 2.5);
|
||||||
|
normal = miter;
|
||||||
|
} else {
|
||||||
|
normal = n1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = normal.clone().multiplyScalar(halfWidth * miterScale);
|
||||||
|
left.push(points[i].clone().add(offset));
|
||||||
|
right.push(points[i].clone().sub(offset));
|
||||||
|
}
|
||||||
|
|
||||||
const left = points.map((_, index) => offsetPoint(index, 1));
|
|
||||||
const right = points.map((_, index) => offsetPoint(index, -1)).reverse();
|
|
||||||
const shape = new THREE.Shape();
|
const shape = new THREE.Shape();
|
||||||
shape.moveTo(left[0].x, left[0].y);
|
shape.moveTo(left[0].x, left[0].y);
|
||||||
[...left.slice(1), ...right].forEach((point) => shape.lineTo(point.x, point.y));
|
for (let i = 1; i < left.length; i++) {
|
||||||
|
shape.lineTo(left[i].x, left[i].y);
|
||||||
|
}
|
||||||
|
for (let i = right.length - 1; i >= 0; i--) {
|
||||||
|
shape.lineTo(right[i].x, right[i].y);
|
||||||
|
}
|
||||||
shape.closePath();
|
shape.closePath();
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
@@ -92,7 +125,7 @@ export function createMesa(
|
|||||||
halfWidth - inset, height, halfDepth - inset, -halfWidth + inset, height, halfDepth - inset,
|
halfWidth - inset, height, halfDepth - inset, -halfWidth + inset, height, halfDepth - inset,
|
||||||
];
|
];
|
||||||
const indices = [
|
const indices = [
|
||||||
4, 5, 6, 4, 6, 7,
|
4, 6, 5, 4, 7, 6,
|
||||||
0, 1, 5, 0, 5, 4,
|
0, 1, 5, 0, 5, 4,
|
||||||
1, 2, 6, 1, 6, 5,
|
1, 2, 6, 1, 6, 5,
|
||||||
2, 3, 7, 2, 7, 6,
|
2, 3, 7, 2, 7, 6,
|
||||||
@@ -101,15 +134,33 @@ export function createMesa(
|
|||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
geometry.setIndex(indices);
|
geometry.setIndex(indices);
|
||||||
|
geometry.clearGroups();
|
||||||
|
geometry.addGroup(0, 6, 0);
|
||||||
|
geometry.addGroup(6, 6, 1);
|
||||||
|
geometry.addGroup(12, 6, 2);
|
||||||
|
geometry.addGroup(18, 6, 1);
|
||||||
|
geometry.addGroup(24, 6, 2);
|
||||||
geometry.computeVertexNormals();
|
geometry.computeVertexNormals();
|
||||||
|
|
||||||
const mesa = new THREE.Mesh(
|
const mesa = new THREE.Mesh(
|
||||||
geometry,
|
geometry,
|
||||||
|
[
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
color: palette.mountain,
|
color: palette.mountainLight,
|
||||||
flatShading: true,
|
flatShading: true,
|
||||||
roughness: 0.9,
|
roughness: 0.9,
|
||||||
}),
|
}),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: palette.mountain,
|
||||||
|
flatShading: true,
|
||||||
|
roughness: 0.92,
|
||||||
|
}),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: palette.mountainDark,
|
||||||
|
flatShading: true,
|
||||||
|
roughness: 0.94,
|
||||||
|
}),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
mesa.castShadow = true;
|
mesa.castShadow = true;
|
||||||
mesa.receiveShadow = true;
|
mesa.receiveShadow = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user