Skip to content

Commit 57f7871

Browse files
authored
Merge pull request #27 from hackclub/bkend_second
Bkend second
2 parents 2d8cfa0 + d96c6d4 commit 57f7871

12 files changed

Lines changed: 101736 additions & 40 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { useEffect, useRef } from "react";
2+
import * as THREE from "three";
3+
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
4+
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
5+
6+
/**
7+
* OBJ/MTL preview with the same drag-to-rotate behaviour as {@link BrickModel}.
8+
* Call `onInteractDrag` when the user rotates (suppress parent `<a>` navigation).
9+
*/
10+
export function MainNavBrickModel({ mtlUrl, objUrl, onInteractDrag }) {
11+
const containerRef = useRef(null);
12+
const onInteractDragRef = useRef(onInteractDrag);
13+
onInteractDragRef.current = onInteractDrag;
14+
15+
useEffect(() => {
16+
const container = containerRef.current;
17+
if (!container) return undefined;
18+
19+
const scene = new THREE.Scene();
20+
const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);
21+
camera.position.set(0, 0.05, 4.9);
22+
23+
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
24+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
25+
renderer.setClearColor(0x000000, 0);
26+
renderer.outputColorSpace = THREE.SRGBColorSpace;
27+
renderer.domElement.style.display = "block";
28+
renderer.domElement.style.width = "100%";
29+
renderer.domElement.style.height = "100%";
30+
container.appendChild(renderer.domElement);
31+
32+
const modelRoot = new THREE.Group();
33+
modelRoot.position.set(-0.28, 0.22, 0);
34+
modelRoot.rotation.set(-0.18, -0.38, 0.02);
35+
scene.add(modelRoot);
36+
37+
const ambient = new THREE.AmbientLight(0xffffff, 1.25);
38+
scene.add(ambient);
39+
40+
const keyLight = new THREE.DirectionalLight(0xffffff, 2.2);
41+
keyLight.position.set(3, 5, 4);
42+
scene.add(keyLight);
43+
44+
const fillLight = new THREE.DirectionalLight(0xffe6c7, 1.1);
45+
fillLight.position.set(-4, 1.5, 3);
46+
scene.add(fillLight);
47+
48+
const targetRotation = new THREE.Vector2(-0.38, -0.52);
49+
const currentRotation = new THREE.Vector2(-0.38, -0.52);
50+
const dragState = {
51+
active: false,
52+
pointerId: null,
53+
x: 0,
54+
y: 0,
55+
};
56+
let dragDistance = 0;
57+
let frameId = 0;
58+
let disposed = false;
59+
60+
const resize = () => {
61+
const { width, height } = container.getBoundingClientRect();
62+
if (!width || !height) return;
63+
64+
camera.aspect = width / height;
65+
camera.updateProjectionMatrix();
66+
renderer.setSize(width, height, false);
67+
};
68+
69+
const resizeObserver = new ResizeObserver(resize);
70+
resizeObserver.observe(container);
71+
resize();
72+
73+
const capture = true;
74+
75+
const handlePointerDown = (event) => {
76+
if (event.button !== 0 && event.button !== undefined) return;
77+
dragState.active = true;
78+
dragState.pointerId = event.pointerId;
79+
dragState.x = event.clientX;
80+
dragState.y = event.clientY;
81+
dragDistance = 0;
82+
container.setPointerCapture(event.pointerId);
83+
};
84+
85+
const handlePointerMove = (event) => {
86+
if (!dragState.active || dragState.pointerId !== event.pointerId) return;
87+
88+
const deltaX = event.clientX - dragState.x;
89+
const deltaY = event.clientY - dragState.y;
90+
91+
dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
92+
93+
if (dragDistance > 4) {
94+
event.preventDefault();
95+
}
96+
97+
targetRotation.x += deltaX * 0.012;
98+
targetRotation.y += deltaY * 0.012;
99+
dragState.x = event.clientX;
100+
dragState.y = event.clientY;
101+
};
102+
103+
const handlePointerUp = (event) => {
104+
const pid = event.pointerId;
105+
if (dragState.pointerId !== pid) return;
106+
107+
if (dragDistance > 8 && typeof onInteractDragRef.current === "function") {
108+
onInteractDragRef.current();
109+
}
110+
111+
try {
112+
if (container.hasPointerCapture(pid)) {
113+
container.releasePointerCapture(pid);
114+
}
115+
} catch {
116+
// ignore duplicate release
117+
}
118+
119+
dragState.active = false;
120+
dragState.pointerId = null;
121+
};
122+
123+
container.addEventListener("pointerdown", handlePointerDown, { capture });
124+
container.addEventListener("pointermove", handlePointerMove, { capture, passive: false });
125+
container.addEventListener("pointerup", handlePointerUp, { capture });
126+
container.addEventListener("pointercancel", handlePointerUp, { capture });
127+
128+
const materialsLoader = new MTLLoader();
129+
materialsLoader.load(
130+
mtlUrl,
131+
(materials) => {
132+
if (disposed) return;
133+
134+
materials.preload();
135+
136+
const objectLoader = new OBJLoader();
137+
objectLoader.setMaterials(materials);
138+
objectLoader.load(objUrl, (object) => {
139+
if (disposed) return;
140+
141+
const box = new THREE.Box3().setFromObject(object);
142+
const center = box.getCenter(new THREE.Vector3());
143+
const size = box.getSize(new THREE.Vector3());
144+
const maxAxis = Math.max(size.x, size.y, size.z) || 1;
145+
146+
object.position.sub(center);
147+
object.scale.setScalar(2.72 / maxAxis);
148+
object.traverse((child) => {
149+
if (child.isMesh) {
150+
child.castShadow = false;
151+
child.receiveShadow = false;
152+
}
153+
});
154+
155+
modelRoot.add(object);
156+
});
157+
},
158+
undefined,
159+
(err) => {
160+
console.error("[MainNavBrickModel] Failed to load materials:", err);
161+
}
162+
);
163+
164+
const animate = () => {
165+
currentRotation.x += (targetRotation.x - currentRotation.x) * 0.08;
166+
currentRotation.y += (targetRotation.y - currentRotation.y) * 0.08;
167+
modelRoot.rotation.y = currentRotation.x;
168+
modelRoot.rotation.x = currentRotation.y;
169+
170+
renderer.render(scene, camera);
171+
frameId = window.requestAnimationFrame(animate);
172+
};
173+
174+
animate();
175+
176+
return () => {
177+
disposed = true;
178+
window.cancelAnimationFrame(frameId);
179+
resizeObserver.disconnect();
180+
container.removeEventListener("pointerdown", handlePointerDown, { capture: true });
181+
container.removeEventListener("pointermove", handlePointerMove, { capture: true });
182+
container.removeEventListener("pointerup", handlePointerUp, { capture: true });
183+
container.removeEventListener("pointercancel", handlePointerUp, { capture: true });
184+
renderer.dispose();
185+
renderer.domElement.remove();
186+
scene.traverse((object) => {
187+
if (object.geometry) object.geometry.dispose();
188+
if (object.material) {
189+
const materials = Array.isArray(object.material) ? object.material : [object.material];
190+
materials.forEach((material) => material.dispose());
191+
}
192+
});
193+
};
194+
}, [mtlUrl, objUrl]);
195+
196+
return (
197+
<div
198+
ref={containerRef}
199+
className="platform-main__brick-model"
200+
aria-hidden="true"
201+
style={{ touchAction: "none" }}
202+
/>
203+
);
204+
}

client/src/components/MainPage.css

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,69 +23,85 @@
2323

2424
.platform-main__block {
2525
position: absolute;
26-
display: inline-flex;
27-
align-items: center;
28-
justify-content: center;
29-
text-decoration: none;
30-
color: #f7cf57;
31-
font-size: clamp(1.6rem, 3vw, 3rem);
32-
letter-spacing: 0.02em;
33-
text-shadow: 0 2px 0 #8d1f38;
34-
border-radius: 16px;
35-
background: linear-gradient(140deg, #ffe9bd 0%, #d8d2c8 100%);
36-
border: 4px dashed rgba(109, 104, 98, 0.85);
37-
box-shadow: 0 14px 0 rgba(85, 81, 76, 0.4);
26+
display: block;
27+
padding: 0;
28+
margin: 0;
29+
overflow: visible;
30+
touch-action: none;
31+
user-select: none;
32+
-webkit-user-drag: none;
33+
cursor: grab;
3834
transform: rotate(-14deg);
39-
transition: transform 150ms ease, box-shadow 150ms ease;
35+
transition: transform 150ms ease;
36+
background: transparent;
37+
border: none;
38+
outline: none;
39+
box-shadow: none;
4040
}
4141

42-
.platform-main__block span {
43-
transform: rotate(0deg);
42+
.platform-main__block:active {
43+
cursor: grabbing;
44+
}
45+
46+
.platform-main__brick-model {
47+
position: absolute;
48+
inset: 0;
49+
z-index: 1;
50+
touch-action: none;
51+
}
52+
53+
.platform-main__block-sr-only {
54+
position: absolute;
55+
width: 1px;
56+
height: 1px;
57+
padding: 0;
58+
margin: -1px;
59+
overflow: hidden;
60+
clip: rect(0, 0, 0, 0);
61+
white-space: nowrap;
62+
border: 0;
63+
pointer-events: none;
4464
}
4565

4666
.platform-main__block:hover {
4767
transform: rotate(-14deg) translateY(-4px);
48-
box-shadow: 0 18px 0 rgba(85, 81, 76, 0.36);
4968
}
5069

5170
.platform-main__block:focus-visible {
5271
outline: 3px solid #1d55c9;
53-
outline-offset: 4px;
72+
outline-offset: 6px;
73+
border-radius: 4px;
5474
}
5575

5676
.platform-main__block--shop {
5777
top: 30%;
5878
left: 20%;
5979
width: min(18rem, 24vw);
60-
height: min(10rem, 15vw);
80+
height: min(11.5rem, 17vw);
6181
}
6282

6383
.platform-main__block--user {
6484
top: 28%;
6585
left: 57%;
6686
width: min(18rem, 24vw);
67-
height: min(10rem, 15vw);
87+
height: min(11.5rem, 17vw);
6888
}
6989

7090
.platform-main__block--projects {
7191
top: 56%;
7292
left: 39%;
7393
width: min(18rem, 24vw);
74-
height: min(10rem, 15vw);
94+
height: min(11.5rem, 17vw);
7595
}
7696

7797
.platform-main__block--faq {
7898
top: 55%;
7999
left: 73%;
80100
width: min(18rem, 24vw);
81-
height: min(10rem, 15vw);
101+
height: min(11.5rem, 17vw);
82102
}
83103

84104
@media (max-width: 900px) {
85-
.platform-main__block {
86-
font-size: clamp(1.3rem, 5vw, 2.4rem);
87-
}
88-
89105
.platform-main__block--shop {
90106
top: 29%;
91107
left: 11%;
@@ -120,11 +136,6 @@
120136
min-height: 100dvh;
121137
}
122138

123-
.platform-main__block {
124-
transform: rotate(-9deg);
125-
box-shadow: 0 10px 0 rgba(85, 81, 76, 0.4);
126-
}
127-
128139
.platform-main__block:hover {
129140
transform: rotate(-9deg) translateY(-2px);
130141
}

0 commit comments

Comments
 (0)