Merge branch 'main' of https://github.com/AshwaniNITR/ML4EWeb1
This commit is contained in:
@@ -10,6 +10,7 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "res.cloudinary.com",
|
hostname: "res.cloudinary.com",
|
||||||
},
|
},
|
||||||
|
{ protocol: "https", hostname: "picsum.photos" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "ml4e",
|
"name": "ml4e",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/orbitron": "^5.2.8",
|
||||||
"@react-three/drei": "^10.7.6",
|
"@react-three/drei": "^10.7.6",
|
||||||
"@react-three/fiber": "^9.3.0",
|
"@react-three/fiber": "^9.3.0",
|
||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"@tsparticles/slim": "^3.9.1",
|
"@tsparticles/slim": "^3.9.1",
|
||||||
"cloudinary": "^2.7.0",
|
"cloudinary": "^2.7.0",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
|
"gl-matrix": "^3.4.4",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"mongoose": "^8.19.1",
|
"mongoose": "^8.19.1",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
@@ -241,6 +243,15 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource-variable/orbitron": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/orbitron/-/orbitron-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-Jazvuj2cgBq663z1j/+JdwH2ztqQ8RaGhJgdHYphFbtfowiFaR74R8UgZl7x1skbXmUMWWH1jXfG7mrYFIYK0A==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -4301,6 +4312,12 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gl-matrix": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource-variable/orbitron": "^5.2.8",
|
||||||
"@react-three/drei": "^10.7.6",
|
"@react-three/drei": "^10.7.6",
|
||||||
"@react-three/fiber": "^9.3.0",
|
"@react-three/fiber": "^9.3.0",
|
||||||
"@react-three/postprocessing": "^3.0.4",
|
"@react-three/postprocessing": "^3.0.4",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"@tsparticles/slim": "^3.9.1",
|
"@tsparticles/slim": "^3.9.1",
|
||||||
"cloudinary": "^2.7.0",
|
"cloudinary": "^2.7.0",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
|
"gl-matrix": "^3.4.4",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"mongoose": "^8.19.1",
|
"mongoose": "^8.19.1",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
|
|||||||
121
src/app/components/InfiniteMenu/InfiniteMenu.css
Normal file
121
src/app/components/InfiniteMenu/InfiniteMenu.css
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
Installed from https://reactbits.dev/default/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Note: this CSS is only an example, you can overlay whatever you want using the activeItem logic */
|
||||||
|
|
||||||
|
#infinite-grid-menu-canvas {
|
||||||
|
cursor: grab;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#infinite-grid-menu-canvas:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: #5227ff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 5px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-title {
|
||||||
|
user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 4rem;
|
||||||
|
left: 1.6em;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button-icon {
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
color: #fff;
|
||||||
|
top: 2px;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-title {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(20%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-title.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(20%, -50%);
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-title.inactive {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-description {
|
||||||
|
user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
max-width: 10ch;
|
||||||
|
top: 50%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
right: 1%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-description.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-90%, -50%);
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face-description.inactive {
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-60%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.active {
|
||||||
|
bottom: 3.8em;
|
||||||
|
transform: translateX(-50%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button.inactive {
|
||||||
|
bottom: -80px;
|
||||||
|
transform: translateX(-50%) scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1500px) {
|
||||||
|
.face-title,
|
||||||
|
.face-description {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
988
src/app/components/InfiniteMenu/InfiniteMenu.jsx
Normal file
988
src/app/components/InfiniteMenu/InfiniteMenu.jsx
Normal file
@@ -0,0 +1,988 @@
|
|||||||
|
/*
|
||||||
|
Installed from https://reactbits.dev/default/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { mat4, quat, vec2, vec3 } from 'gl-matrix';
|
||||||
|
import './InfiniteMenu.css';
|
||||||
|
|
||||||
|
const discVertShaderSource = `#version 300 es
|
||||||
|
|
||||||
|
uniform mat4 uWorldMatrix;
|
||||||
|
uniform mat4 uViewMatrix;
|
||||||
|
uniform mat4 uProjectionMatrix;
|
||||||
|
uniform vec3 uCameraPosition;
|
||||||
|
uniform vec4 uRotationAxisVelocity;
|
||||||
|
|
||||||
|
in vec3 aModelPosition;
|
||||||
|
in vec3 aModelNormal;
|
||||||
|
in vec2 aModelUvs;
|
||||||
|
in mat4 aInstanceMatrix;
|
||||||
|
|
||||||
|
out vec2 vUvs;
|
||||||
|
out float vAlpha;
|
||||||
|
flat out int vInstanceId;
|
||||||
|
|
||||||
|
#define PI 3.141593
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 worldPosition = uWorldMatrix * aInstanceMatrix * vec4(aModelPosition, 1.);
|
||||||
|
|
||||||
|
vec3 centerPos = (uWorldMatrix * aInstanceMatrix * vec4(0., 0., 0., 1.)).xyz;
|
||||||
|
float radius = length(centerPos.xyz);
|
||||||
|
|
||||||
|
if (gl_VertexID > 0) {
|
||||||
|
vec3 rotationAxis = uRotationAxisVelocity.xyz;
|
||||||
|
float rotationVelocity = min(.15, uRotationAxisVelocity.w * 15.);
|
||||||
|
vec3 stretchDir = normalize(cross(centerPos, rotationAxis));
|
||||||
|
vec3 relativeVertexPos = normalize(worldPosition.xyz - centerPos);
|
||||||
|
float strength = dot(stretchDir, relativeVertexPos);
|
||||||
|
float invAbsStrength = min(0., abs(strength) - 1.);
|
||||||
|
strength = rotationVelocity * sign(strength) * abs(invAbsStrength * invAbsStrength * invAbsStrength + 1.);
|
||||||
|
worldPosition.xyz += stretchDir * strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldPosition.xyz = radius * normalize(worldPosition.xyz);
|
||||||
|
|
||||||
|
gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
|
||||||
|
|
||||||
|
vAlpha = smoothstep(0.5, 1., normalize(worldPosition.xyz).z) * .9 + .1;
|
||||||
|
vUvs = aModelUvs;
|
||||||
|
vInstanceId = gl_InstanceID;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const discFragShaderSource = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D uTex;
|
||||||
|
uniform int uItemCount;
|
||||||
|
uniform int uAtlasSize;
|
||||||
|
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
in vec2 vUvs;
|
||||||
|
in float vAlpha;
|
||||||
|
flat in int vInstanceId;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
int itemIndex = vInstanceId % uItemCount;
|
||||||
|
int cellsPerRow = uAtlasSize;
|
||||||
|
int cellX = itemIndex % cellsPerRow;
|
||||||
|
int cellY = itemIndex / cellsPerRow;
|
||||||
|
vec2 cellSize = vec2(1.0) / vec2(float(cellsPerRow));
|
||||||
|
vec2 cellOffset = vec2(float(cellX), float(cellY)) * cellSize;
|
||||||
|
|
||||||
|
ivec2 texSize = textureSize(uTex, 0);
|
||||||
|
float imageAspect = float(texSize.x) / float(texSize.y);
|
||||||
|
float containerAspect = 1.0;
|
||||||
|
|
||||||
|
float scale = max(imageAspect / containerAspect,
|
||||||
|
containerAspect / imageAspect);
|
||||||
|
|
||||||
|
vec2 st = vec2(vUvs.x, 1.0 - vUvs.y);
|
||||||
|
st = (st - 0.5) * scale + 0.5;
|
||||||
|
|
||||||
|
st = clamp(st, 0.0, 1.0);
|
||||||
|
|
||||||
|
st = st * cellSize + cellOffset;
|
||||||
|
|
||||||
|
outColor = texture(uTex, st);
|
||||||
|
outColor.a *= vAlpha;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
class Face {
|
||||||
|
constructor(a, b, c) {
|
||||||
|
this.a = a;
|
||||||
|
this.b = b;
|
||||||
|
this.c = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Vertex {
|
||||||
|
constructor(x, y, z) {
|
||||||
|
this.position = vec3.fromValues(x, y, z);
|
||||||
|
this.normal = vec3.create();
|
||||||
|
this.uv = vec2.create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Geometry {
|
||||||
|
constructor() {
|
||||||
|
this.vertices = [];
|
||||||
|
this.faces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addVertex(...args) {
|
||||||
|
for (let i = 0; i < args.length; i += 3) {
|
||||||
|
this.vertices.push(new Vertex(args[i], args[i + 1], args[i + 2]));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFace(...args) {
|
||||||
|
for (let i = 0; i < args.length; i += 3) {
|
||||||
|
this.faces.push(new Face(args[i], args[i + 1], args[i + 2]));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastVertex() {
|
||||||
|
return this.vertices[this.vertices.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
subdivide(divisions = 1) {
|
||||||
|
const midPointCache = {};
|
||||||
|
let f = this.faces;
|
||||||
|
|
||||||
|
for (let div = 0; div < divisions; ++div) {
|
||||||
|
const newFaces = new Array(f.length * 4);
|
||||||
|
|
||||||
|
f.forEach((face, ndx) => {
|
||||||
|
const mAB = this.getMidPoint(face.a, face.b, midPointCache);
|
||||||
|
const mBC = this.getMidPoint(face.b, face.c, midPointCache);
|
||||||
|
const mCA = this.getMidPoint(face.c, face.a, midPointCache);
|
||||||
|
|
||||||
|
const i = ndx * 4;
|
||||||
|
newFaces[i + 0] = new Face(face.a, mAB, mCA);
|
||||||
|
newFaces[i + 1] = new Face(face.b, mBC, mAB);
|
||||||
|
newFaces[i + 2] = new Face(face.c, mCA, mBC);
|
||||||
|
newFaces[i + 3] = new Face(mAB, mBC, mCA);
|
||||||
|
});
|
||||||
|
|
||||||
|
f = newFaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.faces = f;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
spherize(radius = 1) {
|
||||||
|
this.vertices.forEach(vertex => {
|
||||||
|
vec3.normalize(vertex.normal, vertex.position);
|
||||||
|
vec3.scale(vertex.position, vertex.normal, radius);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
return {
|
||||||
|
vertices: this.vertexData,
|
||||||
|
indices: this.indexData,
|
||||||
|
normals: this.normalData,
|
||||||
|
uvs: this.uvData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get vertexData() {
|
||||||
|
return new Float32Array(this.vertices.flatMap(v => Array.from(v.position)));
|
||||||
|
}
|
||||||
|
|
||||||
|
get normalData() {
|
||||||
|
return new Float32Array(this.vertices.flatMap(v => Array.from(v.normal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
get uvData() {
|
||||||
|
return new Float32Array(this.vertices.flatMap(v => Array.from(v.uv)));
|
||||||
|
}
|
||||||
|
|
||||||
|
get indexData() {
|
||||||
|
return new Uint16Array(this.faces.flatMap(f => [f.a, f.b, f.c]));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMidPoint(ndxA, ndxB, cache) {
|
||||||
|
const cacheKey = ndxA < ndxB ? `k_${ndxB}_${ndxA}` : `k_${ndxA}_${ndxB}`;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(cache, cacheKey)) {
|
||||||
|
return cache[cacheKey];
|
||||||
|
}
|
||||||
|
const a = this.vertices[ndxA].position;
|
||||||
|
const b = this.vertices[ndxB].position;
|
||||||
|
const ndx = this.vertices.length;
|
||||||
|
cache[cacheKey] = ndx;
|
||||||
|
this.addVertex((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5);
|
||||||
|
return ndx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IcosahedronGeometry extends Geometry {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const t = Math.sqrt(5) * 0.5 + 0.5;
|
||||||
|
this.addVertex(
|
||||||
|
-1,
|
||||||
|
t,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
t,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
-t,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
-t,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
t,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
t,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
-t,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
-t,
|
||||||
|
t,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
t,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
-t,
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
-t,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
).addFace(
|
||||||
|
0,
|
||||||
|
11,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
7,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
9,
|
||||||
|
5,
|
||||||
|
11,
|
||||||
|
4,
|
||||||
|
11,
|
||||||
|
10,
|
||||||
|
2,
|
||||||
|
10,
|
||||||
|
7,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
1,
|
||||||
|
8,
|
||||||
|
3,
|
||||||
|
9,
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
6,
|
||||||
|
8,
|
||||||
|
3,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
4,
|
||||||
|
9,
|
||||||
|
5,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
11,
|
||||||
|
6,
|
||||||
|
2,
|
||||||
|
10,
|
||||||
|
8,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
9,
|
||||||
|
8,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscGeometry extends Geometry {
|
||||||
|
constructor(steps = 4, radius = 1) {
|
||||||
|
super();
|
||||||
|
steps = Math.max(4, steps);
|
||||||
|
|
||||||
|
const alpha = (2 * Math.PI) / steps;
|
||||||
|
|
||||||
|
this.addVertex(0, 0, 0);
|
||||||
|
this.lastVertex.uv[0] = 0.5;
|
||||||
|
this.lastVertex.uv[1] = 0.5;
|
||||||
|
|
||||||
|
for (let i = 0; i < steps; ++i) {
|
||||||
|
const x = Math.cos(alpha * i);
|
||||||
|
const y = Math.sin(alpha * i);
|
||||||
|
this.addVertex(radius * x, radius * y, 0);
|
||||||
|
this.lastVertex.uv[0] = x * 0.5 + 0.5;
|
||||||
|
this.lastVertex.uv[1] = y * 0.5 + 0.5;
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
this.addFace(0, i, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.addFace(0, steps, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createShader(gl, type, source) {
|
||||||
|
const shader = gl.createShader(type);
|
||||||
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(gl.getShaderInfoLog(shader));
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgram(gl, shaderSources, transformFeedbackVaryings, attribLocations) {
|
||||||
|
const program = gl.createProgram();
|
||||||
|
|
||||||
|
[gl.VERTEX_SHADER, gl.FRAGMENT_SHADER].forEach((type, ndx) => {
|
||||||
|
const shader = createShader(gl, type, shaderSources[ndx]);
|
||||||
|
if (shader) gl.attachShader(program, shader);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (transformFeedbackVaryings) {
|
||||||
|
gl.transformFeedbackVaryings(program, transformFeedbackVaryings, gl.SEPARATE_ATTRIBS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribLocations) {
|
||||||
|
for (const attrib in attribLocations) {
|
||||||
|
gl.bindAttribLocation(program, attribLocations[attrib], attrib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.linkProgram(program);
|
||||||
|
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(gl.getProgramInfoLog(program));
|
||||||
|
gl.deleteProgram(program);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeVertexArray(gl, bufLocNumElmPairs, indices) {
|
||||||
|
const va = gl.createVertexArray();
|
||||||
|
gl.bindVertexArray(va);
|
||||||
|
|
||||||
|
for (const [buffer, loc, numElem] of bufLocNumElmPairs) {
|
||||||
|
if (loc === -1) continue;
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
gl.enableVertexAttribArray(loc);
|
||||||
|
gl.vertexAttribPointer(loc, numElem, gl.FLOAT, false, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indices) {
|
||||||
|
const indexBuffer = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.bindVertexArray(null);
|
||||||
|
return va;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize(canvas) {
|
||||||
|
const dpr = Math.min(2, window.devicePixelRatio);
|
||||||
|
const displayWidth = Math.round(canvas.clientWidth * dpr);
|
||||||
|
const displayHeight = Math.round(canvas.clientHeight * dpr);
|
||||||
|
const needResize = canvas.width !== displayWidth || canvas.height !== displayHeight;
|
||||||
|
if (needResize) {
|
||||||
|
canvas.width = displayWidth;
|
||||||
|
canvas.height = displayHeight;
|
||||||
|
}
|
||||||
|
return needResize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBuffer(gl, sizeOrData, usage) {
|
||||||
|
const buf = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, usage);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAndSetupTexture(gl, minFilter, magFilter, wrapS, wrapT) {
|
||||||
|
const texture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArcballControl {
|
||||||
|
isPointerDown = false;
|
||||||
|
orientation = quat.create();
|
||||||
|
pointerRotation = quat.create();
|
||||||
|
rotationVelocity = 0;
|
||||||
|
rotationAxis = vec3.fromValues(1, 0, 0);
|
||||||
|
snapDirection = vec3.fromValues(0, 0, -1);
|
||||||
|
snapTargetDirection;
|
||||||
|
EPSILON = 0.1;
|
||||||
|
IDENTITY_QUAT = quat.create();
|
||||||
|
|
||||||
|
constructor(canvas, updateCallback) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.updateCallback = updateCallback || (() => null);
|
||||||
|
|
||||||
|
this.pointerPos = vec2.create();
|
||||||
|
this.previousPointerPos = vec2.create();
|
||||||
|
this._rotationVelocity = 0;
|
||||||
|
this._combinedQuat = quat.create();
|
||||||
|
|
||||||
|
canvas.addEventListener('pointerdown', e => {
|
||||||
|
vec2.set(this.pointerPos, e.clientX, e.clientY);
|
||||||
|
vec2.copy(this.previousPointerPos, this.pointerPos);
|
||||||
|
this.isPointerDown = true;
|
||||||
|
});
|
||||||
|
canvas.addEventListener('pointerup', () => {
|
||||||
|
this.isPointerDown = false;
|
||||||
|
});
|
||||||
|
canvas.addEventListener('pointerleave', () => {
|
||||||
|
this.isPointerDown = false;
|
||||||
|
});
|
||||||
|
canvas.addEventListener('pointermove', e => {
|
||||||
|
if (this.isPointerDown) {
|
||||||
|
vec2.set(this.pointerPos, e.clientX, e.clientY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.style.touchAction = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime, targetFrameDuration = 16) {
|
||||||
|
const timeScale = deltaTime / targetFrameDuration + 0.00001;
|
||||||
|
let angleFactor = timeScale;
|
||||||
|
let snapRotation = quat.create();
|
||||||
|
|
||||||
|
if (this.isPointerDown) {
|
||||||
|
const INTENSITY = 0.3 * timeScale;
|
||||||
|
const ANGLE_AMPLIFICATION = 5 / timeScale;
|
||||||
|
|
||||||
|
const midPointerPos = vec2.sub(vec2.create(), this.pointerPos, this.previousPointerPos);
|
||||||
|
vec2.scale(midPointerPos, midPointerPos, INTENSITY);
|
||||||
|
|
||||||
|
if (vec2.sqrLen(midPointerPos) > this.EPSILON) {
|
||||||
|
vec2.add(midPointerPos, this.previousPointerPos, midPointerPos);
|
||||||
|
|
||||||
|
const p = this.#project(midPointerPos);
|
||||||
|
const q = this.#project(this.previousPointerPos);
|
||||||
|
const a = vec3.normalize(vec3.create(), p);
|
||||||
|
const b = vec3.normalize(vec3.create(), q);
|
||||||
|
|
||||||
|
vec2.copy(this.previousPointerPos, midPointerPos);
|
||||||
|
|
||||||
|
angleFactor *= ANGLE_AMPLIFICATION;
|
||||||
|
|
||||||
|
this.quatFromVectors(a, b, this.pointerRotation, angleFactor);
|
||||||
|
} else {
|
||||||
|
quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const INTENSITY = 0.1 * timeScale;
|
||||||
|
quat.slerp(this.pointerRotation, this.pointerRotation, this.IDENTITY_QUAT, INTENSITY);
|
||||||
|
|
||||||
|
if (this.snapTargetDirection) {
|
||||||
|
const SNAPPING_INTENSITY = 0.2;
|
||||||
|
const a = this.snapTargetDirection;
|
||||||
|
const b = this.snapDirection;
|
||||||
|
const sqrDist = vec3.squaredDistance(a, b);
|
||||||
|
const distanceFactor = Math.max(0.1, 1 - sqrDist * 10);
|
||||||
|
angleFactor *= SNAPPING_INTENSITY * distanceFactor;
|
||||||
|
this.quatFromVectors(a, b, snapRotation, angleFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedQuat = quat.multiply(quat.create(), snapRotation, this.pointerRotation);
|
||||||
|
this.orientation = quat.multiply(quat.create(), combinedQuat, this.orientation);
|
||||||
|
quat.normalize(this.orientation, this.orientation);
|
||||||
|
|
||||||
|
const RA_INTENSITY = 0.8 * timeScale;
|
||||||
|
quat.slerp(this._combinedQuat, this._combinedQuat, combinedQuat, RA_INTENSITY);
|
||||||
|
quat.normalize(this._combinedQuat, this._combinedQuat);
|
||||||
|
|
||||||
|
const rad = Math.acos(this._combinedQuat[3]) * 2.0;
|
||||||
|
const s = Math.sin(rad / 2.0);
|
||||||
|
let rv = 0;
|
||||||
|
if (s > 0.000001) {
|
||||||
|
rv = rad / (2 * Math.PI);
|
||||||
|
this.rotationAxis[0] = this._combinedQuat[0] / s;
|
||||||
|
this.rotationAxis[1] = this._combinedQuat[1] / s;
|
||||||
|
this.rotationAxis[2] = this._combinedQuat[2] / s;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RV_INTENSITY = 0.5 * timeScale;
|
||||||
|
this._rotationVelocity += (rv - this._rotationVelocity) * RV_INTENSITY;
|
||||||
|
this.rotationVelocity = this._rotationVelocity / timeScale;
|
||||||
|
|
||||||
|
this.updateCallback(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
quatFromVectors(a, b, out, angleFactor = 1) {
|
||||||
|
const axis = vec3.cross(vec3.create(), a, b);
|
||||||
|
vec3.normalize(axis, axis);
|
||||||
|
const d = Math.max(-1, Math.min(1, vec3.dot(a, b)));
|
||||||
|
const angle = Math.acos(d) * angleFactor;
|
||||||
|
quat.setAxisAngle(out, axis, angle);
|
||||||
|
return { q: out, axis, angle };
|
||||||
|
}
|
||||||
|
|
||||||
|
#project(pos) {
|
||||||
|
const r = 2;
|
||||||
|
const w = this.canvas.clientWidth;
|
||||||
|
const h = this.canvas.clientHeight;
|
||||||
|
const s = Math.max(w, h) - 1;
|
||||||
|
|
||||||
|
const x = (2 * pos[0] - w - 1) / s;
|
||||||
|
const y = (2 * pos[1] - h - 1) / s;
|
||||||
|
let z = 0;
|
||||||
|
const xySq = x * x + y * y;
|
||||||
|
const rSq = r * r;
|
||||||
|
|
||||||
|
if (xySq <= rSq / 2.0) {
|
||||||
|
z = Math.sqrt(rSq - xySq);
|
||||||
|
} else {
|
||||||
|
z = rSq / Math.sqrt(xySq);
|
||||||
|
}
|
||||||
|
return vec3.fromValues(-x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InfiniteGridMenu {
|
||||||
|
TARGET_FRAME_DURATION = 1000 / 60;
|
||||||
|
SPHERE_RADIUS = 2;
|
||||||
|
|
||||||
|
#time = 0;
|
||||||
|
#deltaTime = 0;
|
||||||
|
#deltaFrames = 0;
|
||||||
|
#frames = 0;
|
||||||
|
|
||||||
|
camera = {
|
||||||
|
matrix: mat4.create(),
|
||||||
|
near: 0.1,
|
||||||
|
far: 40,
|
||||||
|
fov: Math.PI / 4,
|
||||||
|
aspect: 1,
|
||||||
|
position: vec3.fromValues(0, 0, 3),
|
||||||
|
up: vec3.fromValues(0, 1, 0),
|
||||||
|
matrices: {
|
||||||
|
view: mat4.create(),
|
||||||
|
projection: mat4.create(),
|
||||||
|
inversProjection: mat4.create()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
nearestVertexIndex = null;
|
||||||
|
smoothRotationVelocity = 0;
|
||||||
|
scaleFactor = 1.0;
|
||||||
|
movementActive = false;
|
||||||
|
|
||||||
|
constructor(canvas, items, onActiveItemChange, onMovementChange, onInit = null) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.items = items || [];
|
||||||
|
this.onActiveItemChange = onActiveItemChange || (() => {});
|
||||||
|
this.onMovementChange = onMovementChange || (() => {});
|
||||||
|
this.#init(onInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
this.viewportSize = vec2.set(this.viewportSize || vec2.create(), this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
|
||||||
|
const gl = this.gl;
|
||||||
|
const needsResize = resizeCanvasToDisplaySize(gl.canvas);
|
||||||
|
if (needsResize) {
|
||||||
|
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#updateProjectionMatrix(gl);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(time = 0) {
|
||||||
|
this.#deltaTime = Math.min(32, time - this.#time);
|
||||||
|
this.#time = time;
|
||||||
|
this.#deltaFrames = this.#deltaTime / this.TARGET_FRAME_DURATION;
|
||||||
|
this.#frames += this.#deltaFrames;
|
||||||
|
|
||||||
|
this.#animate(this.#deltaTime);
|
||||||
|
this.#render();
|
||||||
|
|
||||||
|
requestAnimationFrame(t => this.run(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
#init(onInit) {
|
||||||
|
this.gl = this.canvas.getContext('webgl2', { antialias: true, alpha: false });
|
||||||
|
const gl = this.gl;
|
||||||
|
if (!gl) {
|
||||||
|
throw new Error('No WebGL 2 context!');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.viewportSize = vec2.fromValues(this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
this.drawBufferSize = vec2.clone(this.viewportSize);
|
||||||
|
|
||||||
|
this.discProgram = createProgram(gl, [discVertShaderSource, discFragShaderSource], null, {
|
||||||
|
aModelPosition: 0,
|
||||||
|
aModelNormal: 1,
|
||||||
|
aModelUvs: 2,
|
||||||
|
aInstanceMatrix: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
this.discLocations = {
|
||||||
|
aModelPosition: gl.getAttribLocation(this.discProgram, 'aModelPosition'),
|
||||||
|
aModelUvs: gl.getAttribLocation(this.discProgram, 'aModelUvs'),
|
||||||
|
aInstanceMatrix: gl.getAttribLocation(this.discProgram, 'aInstanceMatrix'),
|
||||||
|
uWorldMatrix: gl.getUniformLocation(this.discProgram, 'uWorldMatrix'),
|
||||||
|
uViewMatrix: gl.getUniformLocation(this.discProgram, 'uViewMatrix'),
|
||||||
|
uProjectionMatrix: gl.getUniformLocation(this.discProgram, 'uProjectionMatrix'),
|
||||||
|
uCameraPosition: gl.getUniformLocation(this.discProgram, 'uCameraPosition'),
|
||||||
|
uScaleFactor: gl.getUniformLocation(this.discProgram, 'uScaleFactor'),
|
||||||
|
uRotationAxisVelocity: gl.getUniformLocation(this.discProgram, 'uRotationAxisVelocity'),
|
||||||
|
uTex: gl.getUniformLocation(this.discProgram, 'uTex'),
|
||||||
|
uFrames: gl.getUniformLocation(this.discProgram, 'uFrames'),
|
||||||
|
uItemCount: gl.getUniformLocation(this.discProgram, 'uItemCount'),
|
||||||
|
uAtlasSize: gl.getUniformLocation(this.discProgram, 'uAtlasSize')
|
||||||
|
};
|
||||||
|
|
||||||
|
this.discGeo = new DiscGeometry(56, 1);
|
||||||
|
this.discBuffers = this.discGeo.data;
|
||||||
|
this.discVAO = makeVertexArray(
|
||||||
|
gl,
|
||||||
|
[
|
||||||
|
[makeBuffer(gl, this.discBuffers.vertices, gl.STATIC_DRAW), this.discLocations.aModelPosition, 3],
|
||||||
|
[makeBuffer(gl, this.discBuffers.uvs, gl.STATIC_DRAW), this.discLocations.aModelUvs, 2]
|
||||||
|
],
|
||||||
|
this.discBuffers.indices
|
||||||
|
);
|
||||||
|
|
||||||
|
this.icoGeo = new IcosahedronGeometry();
|
||||||
|
this.icoGeo.subdivide(1).spherize(this.SPHERE_RADIUS);
|
||||||
|
this.instancePositions = this.icoGeo.vertices.map(v => v.position);
|
||||||
|
this.DISC_INSTANCE_COUNT = this.icoGeo.vertices.length;
|
||||||
|
this.#initDiscInstances(this.DISC_INSTANCE_COUNT);
|
||||||
|
|
||||||
|
this.worldMatrix = mat4.create();
|
||||||
|
this.#initTexture();
|
||||||
|
|
||||||
|
this.control = new ArcballControl(this.canvas, deltaTime => this.#onControlUpdate(deltaTime));
|
||||||
|
|
||||||
|
this.#updateCameraMatrix();
|
||||||
|
this.#updateProjectionMatrix(gl);
|
||||||
|
this.resize();
|
||||||
|
|
||||||
|
if (onInit) onInit(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#initTexture() {
|
||||||
|
const gl = this.gl;
|
||||||
|
this.tex = createAndSetupTexture(gl, gl.LINEAR, gl.LINEAR, gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
const itemCount = Math.max(1, this.items.length);
|
||||||
|
this.atlasSize = Math.ceil(Math.sqrt(itemCount));
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const cellSize = 512;
|
||||||
|
|
||||||
|
canvas.width = this.atlasSize * cellSize;
|
||||||
|
canvas.height = this.atlasSize * cellSize;
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
this.items.map(
|
||||||
|
item =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.src = item.image;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).then(images => {
|
||||||
|
images.forEach((img, i) => {
|
||||||
|
const x = (i % this.atlasSize) * cellSize;
|
||||||
|
const y = Math.floor(i / this.atlasSize) * cellSize;
|
||||||
|
ctx.drawImage(img, x, y, cellSize, cellSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.tex);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
||||||
|
gl.generateMipmap(gl.TEXTURE_2D);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#initDiscInstances(count) {
|
||||||
|
const gl = this.gl;
|
||||||
|
this.discInstances = {
|
||||||
|
matricesArray: new Float32Array(count * 16),
|
||||||
|
matrices: [],
|
||||||
|
buffer: gl.createBuffer()
|
||||||
|
};
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
const instanceMatrixArray = new Float32Array(this.discInstances.matricesArray.buffer, i * 16 * 4, 16);
|
||||||
|
instanceMatrixArray.set(mat4.create());
|
||||||
|
this.discInstances.matrices.push(instanceMatrixArray);
|
||||||
|
}
|
||||||
|
gl.bindVertexArray(this.discVAO);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, this.discInstances.matricesArray.byteLength, gl.DYNAMIC_DRAW);
|
||||||
|
const mat4AttribSlotCount = 4;
|
||||||
|
const bytesPerMatrix = 16 * 4;
|
||||||
|
for (let j = 0; j < mat4AttribSlotCount; ++j) {
|
||||||
|
const loc = this.discLocations.aInstanceMatrix + j;
|
||||||
|
gl.enableVertexAttribArray(loc);
|
||||||
|
gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, bytesPerMatrix, j * 4 * 4);
|
||||||
|
gl.vertexAttribDivisor(loc, 1);
|
||||||
|
}
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||||
|
gl.bindVertexArray(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#animate(deltaTime) {
|
||||||
|
const gl = this.gl;
|
||||||
|
this.control.update(deltaTime, this.TARGET_FRAME_DURATION);
|
||||||
|
|
||||||
|
let positions = this.instancePositions.map(p => vec3.transformQuat(vec3.create(), p, this.control.orientation));
|
||||||
|
const scale = 0.25;
|
||||||
|
const SCALE_INTENSITY = 0.6;
|
||||||
|
positions.forEach((p, ndx) => {
|
||||||
|
const s = (Math.abs(p[2]) / this.SPHERE_RADIUS) * SCALE_INTENSITY + (1 - SCALE_INTENSITY);
|
||||||
|
const finalScale = s * scale;
|
||||||
|
const matrix = mat4.create();
|
||||||
|
mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), vec3.negate(vec3.create(), p)));
|
||||||
|
mat4.multiply(matrix, matrix, mat4.targetTo(mat4.create(), [0, 0, 0], p, [0, 1, 0]));
|
||||||
|
mat4.multiply(matrix, matrix, mat4.fromScaling(mat4.create(), [finalScale, finalScale, finalScale]));
|
||||||
|
mat4.multiply(matrix, matrix, mat4.fromTranslation(mat4.create(), [0, 0, -this.SPHERE_RADIUS]));
|
||||||
|
|
||||||
|
mat4.copy(this.discInstances.matrices[ndx], matrix);
|
||||||
|
});
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.discInstances.buffer);
|
||||||
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.discInstances.matricesArray);
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
||||||
|
|
||||||
|
this.smoothRotationVelocity = this.control.rotationVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
#render() {
|
||||||
|
const gl = this.gl;
|
||||||
|
gl.useProgram(this.discProgram);
|
||||||
|
|
||||||
|
gl.enable(gl.CULL_FACE);
|
||||||
|
gl.enable(gl.DEPTH_TEST);
|
||||||
|
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
gl.uniformMatrix4fv(this.discLocations.uWorldMatrix, false, this.worldMatrix);
|
||||||
|
gl.uniformMatrix4fv(this.discLocations.uViewMatrix, false, this.camera.matrices.view);
|
||||||
|
gl.uniformMatrix4fv(this.discLocations.uProjectionMatrix, false, this.camera.matrices.projection);
|
||||||
|
gl.uniform3f(
|
||||||
|
this.discLocations.uCameraPosition,
|
||||||
|
this.camera.position[0],
|
||||||
|
this.camera.position[1],
|
||||||
|
this.camera.position[2]
|
||||||
|
);
|
||||||
|
gl.uniform4f(
|
||||||
|
this.discLocations.uRotationAxisVelocity,
|
||||||
|
this.control.rotationAxis[0],
|
||||||
|
this.control.rotationAxis[1],
|
||||||
|
this.control.rotationAxis[2],
|
||||||
|
this.smoothRotationVelocity * 1.1
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.uniform1i(this.discLocations.uItemCount, this.items.length);
|
||||||
|
gl.uniform1i(this.discLocations.uAtlasSize, this.atlasSize);
|
||||||
|
|
||||||
|
gl.uniform1f(this.discLocations.uFrames, this.#frames);
|
||||||
|
gl.uniform1f(this.discLocations.uScaleFactor, this.scaleFactor);
|
||||||
|
gl.uniform1i(this.discLocations.uTex, 0);
|
||||||
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.tex);
|
||||||
|
|
||||||
|
gl.bindVertexArray(this.discVAO);
|
||||||
|
gl.drawElementsInstanced(
|
||||||
|
gl.TRIANGLES,
|
||||||
|
this.discBuffers.indices.length,
|
||||||
|
gl.UNSIGNED_SHORT,
|
||||||
|
0,
|
||||||
|
this.DISC_INSTANCE_COUNT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateCameraMatrix() {
|
||||||
|
mat4.targetTo(this.camera.matrix, this.camera.position, [0, 0, 0], this.camera.up);
|
||||||
|
mat4.invert(this.camera.matrices.view, this.camera.matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateProjectionMatrix(gl) {
|
||||||
|
this.camera.aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
|
||||||
|
const height = this.SPHERE_RADIUS * 0.35;
|
||||||
|
const distance = this.camera.position[2];
|
||||||
|
if (this.camera.aspect > 1) {
|
||||||
|
this.camera.fov = 2 * Math.atan(height / distance);
|
||||||
|
} else {
|
||||||
|
this.camera.fov = 2 * Math.atan(height / this.camera.aspect / distance);
|
||||||
|
}
|
||||||
|
mat4.perspective(
|
||||||
|
this.camera.matrices.projection,
|
||||||
|
this.camera.fov,
|
||||||
|
this.camera.aspect,
|
||||||
|
this.camera.near,
|
||||||
|
this.camera.far
|
||||||
|
);
|
||||||
|
mat4.invert(this.camera.matrices.inversProjection, this.camera.matrices.projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
#onControlUpdate(deltaTime) {
|
||||||
|
const timeScale = deltaTime / this.TARGET_FRAME_DURATION + 0.0001;
|
||||||
|
let damping = 5 / timeScale;
|
||||||
|
let cameraTargetZ = 3;
|
||||||
|
|
||||||
|
const isMoving = this.control.isPointerDown || Math.abs(this.smoothRotationVelocity) > 0.01;
|
||||||
|
|
||||||
|
if (isMoving !== this.movementActive) {
|
||||||
|
this.movementActive = isMoving;
|
||||||
|
this.onMovementChange(isMoving);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.control.isPointerDown) {
|
||||||
|
const nearestVertexIndex = this.#findNearestVertexIndex();
|
||||||
|
const itemIndex = nearestVertexIndex % Math.max(1, this.items.length);
|
||||||
|
this.onActiveItemChange(itemIndex);
|
||||||
|
const snapDirection = vec3.normalize(vec3.create(), this.#getVertexWorldPosition(nearestVertexIndex));
|
||||||
|
this.control.snapTargetDirection = snapDirection;
|
||||||
|
} else {
|
||||||
|
cameraTargetZ += this.control.rotationVelocity * 80 + 2.5;
|
||||||
|
damping = 7 / timeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.camera.position[2] += (cameraTargetZ - this.camera.position[2]) / damping;
|
||||||
|
this.#updateCameraMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
#findNearestVertexIndex() {
|
||||||
|
const n = this.control.snapDirection;
|
||||||
|
const inversOrientation = quat.conjugate(quat.create(), this.control.orientation);
|
||||||
|
const nt = vec3.transformQuat(vec3.create(), n, inversOrientation);
|
||||||
|
|
||||||
|
let maxD = -1;
|
||||||
|
let nearestVertexIndex;
|
||||||
|
for (let i = 0; i < this.instancePositions.length; ++i) {
|
||||||
|
const d = vec3.dot(nt, this.instancePositions[i]);
|
||||||
|
if (d > maxD) {
|
||||||
|
maxD = d;
|
||||||
|
nearestVertexIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearestVertexIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#getVertexWorldPosition(index) {
|
||||||
|
const nearestVertexPos = this.instancePositions[index];
|
||||||
|
return vec3.transformQuat(vec3.create(), nearestVertexPos, this.control.orientation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultItems = [
|
||||||
|
{
|
||||||
|
image: 'https://picsum.photos/900/900?grayscale',
|
||||||
|
link: 'https://google.com/',
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function InfiniteMenu({ items = [], onSelect }) {
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
const [activeItem, setActiveItem] = useState(null);
|
||||||
|
const [isMoving, setIsMoving] = useState(false);
|
||||||
|
const onSelectRef = useRef(onSelect);
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectRef.current = onSelect;
|
||||||
|
}, [onSelect]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
// stable list for this instance
|
||||||
|
const list = items.length ? items : defaultItems;
|
||||||
|
let sketch;
|
||||||
|
|
||||||
|
const handleActiveItem = (index) => {
|
||||||
|
const itemIndex = ((index % list.length) + list.length) % list.length;
|
||||||
|
const item = list[itemIndex];
|
||||||
|
setActiveItem(item);
|
||||||
|
// notify parent without causing this effect to re-run
|
||||||
|
onSelectRef.current?.(item, itemIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
sketch = new InfiniteGridMenu(
|
||||||
|
canvas,
|
||||||
|
list,
|
||||||
|
handleActiveItem,
|
||||||
|
setIsMoving,
|
||||||
|
(sk) => sk.run()
|
||||||
|
);
|
||||||
|
|
||||||
|
// initialize bottom panel without telling the WebGL to “snap” to 0
|
||||||
|
setActiveItem(list[0]);
|
||||||
|
onSelectRef.current?.(list[0], 0);
|
||||||
|
|
||||||
|
const handleResize = () => sketch?.resize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
// IMPORTANT: do NOT depend on `onSelect` here
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
if (!activeItem?.link) return;
|
||||||
|
if (activeItem.link.startsWith('http')) {
|
||||||
|
window.open(activeItem.link, '_blank');
|
||||||
|
} else {
|
||||||
|
console.log('Internal route:', activeItem.link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||||
|
<canvas id="infinite-grid-menu-canvas" ref={canvasRef} />
|
||||||
|
|
||||||
|
{activeItem && (
|
||||||
|
<>
|
||||||
|
<h2 className={`face-title ${isMoving ? 'inactive' : 'active'}`}>{activeItem.title}</h2>
|
||||||
|
|
||||||
|
<p className={`face-description ${isMoving ? 'inactive' : 'active'}`}> {activeItem.description}</p>
|
||||||
|
|
||||||
|
<div onClick={handleButtonClick} className={`action-button ${isMoving ? 'inactive' : 'active'}`}>
|
||||||
|
<p className="action-button-icon">↗</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -558,4 +558,11 @@
|
|||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
}
|
}
|
||||||
|
/* Disable rainbow-only on hover when disableAura is true */
|
||||||
|
.no-hover-rainbow .pc-card:hover .pc-shine,
|
||||||
|
.no-hover-rainbow .pc-card.active .pc-shine {
|
||||||
|
filter: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
background-blend-mode: normal !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef, useCallback, useMemo } from "react";
|
import React, { useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import { FaLinkedin, FaEnvelope } from "react-icons/fa"; // ✅ icons
|
import { FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||||
import "./ProfileCard.css";
|
import "./ProfileCard.css";
|
||||||
|
|
||||||
const DEFAULT_BEHIND_GRADIENT =
|
const DEFAULT_BEHIND_GRADIENT =
|
||||||
@@ -21,21 +21,16 @@ const ANIMATION_CONFIG = {
|
|||||||
DEVICE_BETA_OFFSET: 20,
|
DEVICE_BETA_OFFSET: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
const clamp = (value, min = 0, max = 100) =>
|
const clamp = (v, min = 0, max = 100) => Math.min(Math.max(v, min), max);
|
||||||
Math.min(Math.max(value, min), max);
|
const round = (v, p = 3) => parseFloat(v.toFixed(p));
|
||||||
|
const adjust = (v, a, b, c, d) => round(c + ((d - c) * (v - a)) / (b - a));
|
||||||
const round = (value, precision = 3) => parseFloat(value.toFixed(precision));
|
|
||||||
|
|
||||||
const adjust = (value, fromMin, fromMax, toMin, toMax) =>
|
|
||||||
round(toMin + ((toMax - toMin) * (value - fromMin)) / (fromMax - fromMin));
|
|
||||||
|
|
||||||
const easeInOutCubic = (x) =>
|
const easeInOutCubic = (x) =>
|
||||||
x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
||||||
|
|
||||||
const ProfileCardComponent = ({
|
const ProfileCardComponent = ({
|
||||||
avatarUrl = "<Placeholder for avatar URL>",
|
avatarUrl,
|
||||||
iconUrl = "<Placeholder for icon URL>",
|
iconUrl,
|
||||||
grainUrl = "<Placeholder for grain URL>",
|
grainUrl,
|
||||||
behindGradient,
|
behindGradient,
|
||||||
innerGradient,
|
innerGradient,
|
||||||
showBehindGradient = true,
|
showBehindGradient = true,
|
||||||
@@ -48,155 +43,119 @@ const ProfileCardComponent = ({
|
|||||||
title = "Software Engineer",
|
title = "Software Engineer",
|
||||||
handle = "javicodes",
|
handle = "javicodes",
|
||||||
status = "Online",
|
status = "Online",
|
||||||
|
linkedinUrl,
|
||||||
// NEW: Social links inside the card footer
|
email,
|
||||||
linkedinUrl, // e.g. "https://linkedin.com/in/..."
|
|
||||||
email, // e.g. "user@site.com" or "mailto:user@site.com"
|
|
||||||
|
|
||||||
// Back-compat: if no social links passed, we can still show a button
|
|
||||||
contactText = "Contact",
|
contactText = "Contact",
|
||||||
showUserInfo = true,
|
showUserInfo = true,
|
||||||
onContactClick,
|
onContactClick,
|
||||||
|
zoom = 1,
|
||||||
|
disableAura = false, // keep tilt, but swap hover rainbow → static cyan glow
|
||||||
}) => {
|
}) => {
|
||||||
const wrapRef = useRef(null);
|
const wrapRef = useRef(null);
|
||||||
const cardRef = useRef(null);
|
const cardRef = useRef(null);
|
||||||
|
|
||||||
const animationHandlers = useMemo(() => {
|
const animationHandlers = useMemo(() => {
|
||||||
if (!enableTilt) return null;
|
if (!enableTilt) return null;
|
||||||
|
|
||||||
let rafId = null;
|
let rafId = null;
|
||||||
|
|
||||||
const updateCardTransform = (offsetX, offsetY, card, wrap) => {
|
const updateCardTransform = (x, y, card, wrap) => {
|
||||||
const width = card.clientWidth;
|
const w = card.clientWidth,
|
||||||
const height = card.clientHeight;
|
h = card.clientHeight;
|
||||||
|
const px = clamp((100 / w) * x);
|
||||||
const percentX = clamp((100 / width) * offsetX);
|
const py = clamp((100 / h) * y);
|
||||||
const percentY = clamp((100 / height) * offsetY);
|
const cx = px - 50,
|
||||||
|
cy = py - 50;
|
||||||
const centerX = percentX - 50;
|
const props = {
|
||||||
const centerY = percentY - 50;
|
"--pointer-x": `${px}%`,
|
||||||
|
"--pointer-y": `${py}%`,
|
||||||
const properties = {
|
"--background-x": `${adjust(px, 0, 100, 35, 65)}%`,
|
||||||
"--pointer-x": `${percentX}%`,
|
"--background-y": `${adjust(py, 0, 100, 35, 65)}%`,
|
||||||
"--pointer-y": `${percentY}%`,
|
"--pointer-from-center": `${clamp(Math.hypot(py - 50, px - 50) / 50, 0, 1)}`,
|
||||||
"--background-x": `${adjust(percentX, 0, 100, 35, 65)}%`,
|
"--pointer-from-top": `${py / 100}`,
|
||||||
"--background-y": `${adjust(percentY, 0, 100, 35, 65)}%`,
|
"--pointer-from-left": `${px / 100}`,
|
||||||
"--pointer-from-center": `${clamp(
|
"--rotate-x": `${round(-(cx / 5))}deg`,
|
||||||
Math.hypot(percentY - 50, percentX - 50) / 50,
|
"--rotate-y": `${round(cy / 4)}deg`,
|
||||||
0,
|
};
|
||||||
1
|
Object.entries(props).forEach(([k, v]) => wrap.style.setProperty(k, v));
|
||||||
)}`,
|
|
||||||
"--pointer-from-top": `${percentY / 100}`,
|
|
||||||
"--pointer-from-left": `${percentX / 100}`,
|
|
||||||
"--rotate-x": `${round(-(centerX / 5))}deg`,
|
|
||||||
"--rotate-y": `${round(centerY / 4)}deg`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(properties).forEach(([property, value]) => {
|
const createSmoothAnimation = (dur, sx, sy, card, wrap) => {
|
||||||
wrap.style.setProperty(property, value);
|
const t0 = performance.now();
|
||||||
});
|
const tx = wrap.clientWidth / 2,
|
||||||
|
ty = wrap.clientHeight / 2;
|
||||||
|
const loop = (t) => {
|
||||||
|
const e = t - t0;
|
||||||
|
const p = clamp(e / dur);
|
||||||
|
const ep = easeInOutCubic(p);
|
||||||
|
const cx = adjust(ep, 0, 1, sx, tx);
|
||||||
|
const cy = adjust(ep, 0, 1, sy, ty);
|
||||||
|
updateCardTransform(cx, cy, card, wrap);
|
||||||
|
if (p < 1) rafId = requestAnimationFrame(loop);
|
||||||
};
|
};
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
const createSmoothAnimation = (duration, startX, startY, card, wrap) => {
|
|
||||||
const startTime = performance.now();
|
|
||||||
const targetX = wrap.clientWidth / 2;
|
|
||||||
const targetY = wrap.clientHeight / 2;
|
|
||||||
|
|
||||||
const animationLoop = (currentTime) => {
|
|
||||||
const elapsed = currentTime - startTime;
|
|
||||||
const progress = clamp(elapsed / duration);
|
|
||||||
const easedProgress = easeInOutCubic(progress);
|
|
||||||
|
|
||||||
const currentX = adjust(easedProgress, 0, 1, startX, targetX);
|
|
||||||
const currentY = adjust(easedProgress, 0, 1, startY, targetY);
|
|
||||||
|
|
||||||
updateCardTransform(currentX, currentY, card, wrap);
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
rafId = requestAnimationFrame(animationLoop);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
rafId = requestAnimationFrame(animationLoop);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateCardTransform,
|
updateCardTransform,
|
||||||
createSmoothAnimation,
|
createSmoothAnimation,
|
||||||
cancelAnimation: () => {
|
cancelAnimation: () => {
|
||||||
if (rafId) {
|
if (rafId) cancelAnimationFrame(rafId);
|
||||||
cancelAnimationFrame(rafId);
|
|
||||||
rafId = null;
|
rafId = null;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [enableTilt]);
|
}, [enableTilt]);
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
(event) => {
|
(e) => {
|
||||||
const card = cardRef.current;
|
const c = cardRef.current,
|
||||||
const wrap = wrapRef.current;
|
w = wrapRef.current;
|
||||||
|
if (!c || !w || !animationHandlers) return;
|
||||||
if (!card || !wrap || !animationHandlers) return;
|
const r = c.getBoundingClientRect();
|
||||||
|
animationHandlers.updateCardTransform(e.clientX - r.left, e.clientY - r.top, c, w);
|
||||||
const rect = card.getBoundingClientRect();
|
|
||||||
animationHandlers.updateCardTransform(
|
|
||||||
event.clientX - rect.left,
|
|
||||||
event.clientY - rect.top,
|
|
||||||
card,
|
|
||||||
wrap
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[animationHandlers]
|
[animationHandlers]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(() => {
|
const handlePointerEnter = useCallback(() => {
|
||||||
const card = cardRef.current;
|
const c = cardRef.current,
|
||||||
const wrap = wrapRef.current;
|
w = wrapRef.current;
|
||||||
|
if (!c || !w || !animationHandlers) return;
|
||||||
if (!card || !wrap || !animationHandlers) return;
|
|
||||||
|
|
||||||
animationHandlers.cancelAnimation();
|
animationHandlers.cancelAnimation();
|
||||||
wrap.classList.add("active");
|
w.classList.add("active");
|
||||||
card.classList.add("active");
|
c.classList.add("active");
|
||||||
}, [animationHandlers]);
|
}, [animationHandlers]);
|
||||||
|
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
(event) => {
|
(e) => {
|
||||||
const card = cardRef.current;
|
const c = cardRef.current,
|
||||||
const wrap = wrapRef.current;
|
w = wrapRef.current;
|
||||||
|
if (!c || !w || !animationHandlers) return;
|
||||||
if (!card || !wrap || !animationHandlers) return;
|
|
||||||
|
|
||||||
animationHandlers.createSmoothAnimation(
|
animationHandlers.createSmoothAnimation(
|
||||||
ANIMATION_CONFIG.SMOOTH_DURATION,
|
ANIMATION_CONFIG.SMOOTH_DURATION,
|
||||||
event.offsetX,
|
e.offsetX,
|
||||||
event.offsetY,
|
e.offsetY,
|
||||||
card,
|
c,
|
||||||
wrap
|
w
|
||||||
);
|
);
|
||||||
wrap.classList.remove("active");
|
w.classList.remove("active");
|
||||||
card.classList.remove("active");
|
c.classList.remove("active");
|
||||||
},
|
},
|
||||||
[animationHandlers]
|
[animationHandlers]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeviceOrientation = useCallback(
|
const handleDeviceOrientation = useCallback(
|
||||||
(event) => {
|
(e) => {
|
||||||
const card = cardRef.current;
|
const c = cardRef.current,
|
||||||
const wrap = wrapRef.current;
|
w = wrapRef.current;
|
||||||
|
if (!c || !w || !animationHandlers) return;
|
||||||
if (!card || !wrap || !animationHandlers) return;
|
const { beta, gamma } = e;
|
||||||
|
|
||||||
const { beta, gamma } = event;
|
|
||||||
if (!beta || !gamma) return;
|
if (!beta || !gamma) return;
|
||||||
|
|
||||||
animationHandlers.updateCardTransform(
|
animationHandlers.updateCardTransform(
|
||||||
card.clientHeight / 2 + gamma * mobileTiltSensitivity,
|
c.clientHeight / 2 + gamma * mobileTiltSensitivity,
|
||||||
card.clientWidth / 2 +
|
c.clientWidth / 2 +
|
||||||
(beta - ANIMATION_CONFIG.DEVICE_BETA_OFFSET) * mobileTiltSensitivity,
|
(beta - ANIMATION_CONFIG.DEVICE_BETA_OFFSET) * mobileTiltSensitivity,
|
||||||
card,
|
c,
|
||||||
wrap
|
w
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[animationHandlers, mobileTiltSensitivity]
|
[animationHandlers, mobileTiltSensitivity]
|
||||||
@@ -204,58 +163,47 @@ const ProfileCardComponent = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableTilt || !animationHandlers) return;
|
if (!enableTilt || !animationHandlers) return;
|
||||||
|
const c = cardRef.current,
|
||||||
|
w = wrapRef.current;
|
||||||
|
if (!c || !w) return;
|
||||||
|
|
||||||
const card = cardRef.current;
|
const move = handlePointerMove;
|
||||||
const wrap = wrapRef.current;
|
const enter = handlePointerEnter;
|
||||||
|
const leave = handlePointerLeave;
|
||||||
if (!card || !wrap) return;
|
const orient = handleDeviceOrientation;
|
||||||
|
|
||||||
const pointerMoveHandler = handlePointerMove;
|
|
||||||
const pointerEnterHandler = handlePointerEnter;
|
|
||||||
const pointerLeaveHandler = handlePointerLeave;
|
|
||||||
const deviceOrientationHandler = handleDeviceOrientation;
|
|
||||||
|
|
||||||
|
c.addEventListener("pointerenter", enter);
|
||||||
|
c.addEventListener("pointermove", move);
|
||||||
|
c.addEventListener("pointerleave", leave);
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!enableMobileTilt || location.protocol !== "https:") return;
|
if (!enableMobileTilt || location.protocol !== "https:") return;
|
||||||
if (typeof window.DeviceMotionEvent.requestPermission === "function") {
|
if (typeof window.DeviceMotionEvent?.requestPermission === "function") {
|
||||||
window.DeviceMotionEvent.requestPermission()
|
window.DeviceMotionEvent.requestPermission().then((s) => {
|
||||||
.then((state) => {
|
if (s === "granted")
|
||||||
if (state === "granted") {
|
window.addEventListener("deviceorientation", orient);
|
||||||
window.addEventListener(
|
});
|
||||||
"deviceorientation",
|
|
||||||
deviceOrientationHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => console.error(err));
|
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener("deviceorientation", deviceOrientationHandler);
|
window.addEventListener("deviceorientation", orient);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
c.addEventListener("click", handleClick);
|
||||||
|
|
||||||
card.addEventListener("pointerenter", pointerEnterHandler);
|
const ix = w.clientWidth - ANIMATION_CONFIG.INITIAL_X_OFFSET;
|
||||||
card.addEventListener("pointermove", pointerMoveHandler);
|
const iy = ANIMATION_CONFIG.INITIAL_Y_OFFSET;
|
||||||
card.addEventListener("pointerleave", pointerLeaveHandler);
|
animationHandlers.updateCardTransform(ix, iy, c, w);
|
||||||
card.addEventListener("click", handleClick);
|
|
||||||
|
|
||||||
const initialX = wrap.clientWidth - ANIMATION_CONFIG.INITIAL_X_OFFSET;
|
|
||||||
const initialY = ANIMATION_CONFIG.INITIAL_Y_OFFSET;
|
|
||||||
|
|
||||||
animationHandlers.updateCardTransform(initialX, initialY, card, wrap);
|
|
||||||
animationHandlers.createSmoothAnimation(
|
animationHandlers.createSmoothAnimation(
|
||||||
ANIMATION_CONFIG.INITIAL_DURATION,
|
ANIMATION_CONFIG.INITIAL_DURATION,
|
||||||
initialX,
|
ix,
|
||||||
initialY,
|
iy,
|
||||||
card,
|
c,
|
||||||
wrap
|
w
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
card.removeEventListener("pointerenter", pointerEnterHandler);
|
c.removeEventListener("pointerenter", enter);
|
||||||
card.removeEventListener("pointermove", pointerMoveHandler);
|
c.removeEventListener("pointermove", move);
|
||||||
card.removeEventListener("pointerleave", pointerLeaveHandler);
|
c.removeEventListener("pointerleave", leave);
|
||||||
card.removeEventListener("click", handleClick);
|
window.removeEventListener("deviceorientation", orient);
|
||||||
window.removeEventListener("deviceorientation", deviceOrientationHandler);
|
|
||||||
animationHandlers.cancelAnimation();
|
animationHandlers.cancelAnimation();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
@@ -280,21 +228,17 @@ const ProfileCardComponent = ({
|
|||||||
[iconUrl, grainUrl, showBehindGradient, behindGradient, innerGradient]
|
[iconUrl, grainUrl, showBehindGradient, behindGradient, innerGradient]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleContactClick = useCallback(() => {
|
const emailHref =
|
||||||
onContactClick?.();
|
email && (email.startsWith("mailto:") ? email : `mailto:${email}`);
|
||||||
}, [onContactClick]);
|
|
||||||
|
|
||||||
// normalize mailto
|
const avatarTransform = `translateX(-50%) scale(${zoom || 1})`;
|
||||||
const emailHref = email
|
|
||||||
? email.startsWith("mailto:")
|
|
||||||
? email
|
|
||||||
: `mailto:${email}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={wrapRef}
|
ref={wrapRef}
|
||||||
className={`pc-card-wrapper ${className}`.trim()}
|
className={`pc-card-wrapper ${className} ${
|
||||||
|
disableAura ? "cyan-hover" : ""
|
||||||
|
}`.trim()}
|
||||||
style={cardStyle}
|
style={cardStyle}
|
||||||
>
|
>
|
||||||
<section ref={cardRef} className="pc-card">
|
<section ref={cardRef} className="pc-card">
|
||||||
@@ -302,20 +246,15 @@ const ProfileCardComponent = ({
|
|||||||
<div className="pc-shine" />
|
<div className="pc-shine" />
|
||||||
<div className="pc-glare" />
|
<div className="pc-glare" />
|
||||||
|
|
||||||
{/* Top image/content */}
|
|
||||||
<div className="pc-content pc-avatar-content">
|
<div className="pc-content pc-avatar-content">
|
||||||
<img
|
<img
|
||||||
className="avatar"
|
className="avatar"
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={`${name || "User"} avatar`}
|
alt={`${name || "User"} avatar`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={(e) => {
|
style={{ transform: avatarTransform }}
|
||||||
const target = e.target;
|
|
||||||
target.style.display = "none";
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Bottom glass bar with handle + icons */}
|
|
||||||
{showUserInfo && (
|
{showUserInfo && (
|
||||||
<div className="pc-user-info">
|
<div className="pc-user-info">
|
||||||
<div className="pc-user-details">
|
<div className="pc-user-details">
|
||||||
@@ -324,22 +263,15 @@ const ProfileCardComponent = ({
|
|||||||
src={miniAvatarUrl || avatarUrl}
|
src={miniAvatarUrl || avatarUrl}
|
||||||
alt={`${name || "User"} mini avatar`}
|
alt={`${name || "User"} mini avatar`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={(e) => {
|
|
||||||
const target = e.target;
|
|
||||||
target.style.opacity = "0.5";
|
|
||||||
target.src = avatarUrl;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pc-user-text">
|
<div className="pc-user-text">
|
||||||
<div className="pc-handle">@{handle}</div>
|
<div className="pc-handle">@{handle}</div>
|
||||||
{status ? <div className="pc-status">{status}</div> : null}
|
{status && <div className="pc-status">{status}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 👉 Right side: social icons (prefer these over button) */}
|
{(linkedinUrl || emailHref) && (
|
||||||
{(linkedinUrl || emailHref) ? (
|
|
||||||
<div className="pc-socials" style={{ pointerEvents: "auto" }}>
|
<div className="pc-socials" style={{ pointerEvents: "auto" }}>
|
||||||
{linkedinUrl && (
|
{linkedinUrl && (
|
||||||
<a
|
<a
|
||||||
@@ -364,23 +296,11 @@ const ProfileCardComponent = ({
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
// Fallback to button if no social links were provided
|
|
||||||
<button
|
|
||||||
className="pc-contact-btn"
|
|
||||||
onClick={handleContactClick}
|
|
||||||
style={{ pointerEvents: "auto" }}
|
|
||||||
type="button"
|
|
||||||
aria-label={`Contact ${name || "user"}`}
|
|
||||||
>
|
|
||||||
{contactText}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name / Title */}
|
|
||||||
<div className="pc-content">
|
<div className="pc-content">
|
||||||
<div className="pc-details">
|
<div className="pc-details">
|
||||||
<h3>{name}</h3>
|
<h3>{name}</h3>
|
||||||
@@ -393,5 +313,4 @@ const ProfileCardComponent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileCard = React.memo(ProfileCardComponent);
|
export default React.memo(ProfileCardComponent);
|
||||||
export default ProfileCard;
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Orbitron } from "next/font/google";
|
import { Orbitron } from "next/font/google";
|
||||||
import { FaGithub } from "react-icons/fa";
|
import { FaGithub } from "react-icons/fa";
|
||||||
import NeuralBackground from "@/app/components/NeuralBackground";
|
|
||||||
|
|
||||||
const orbitron = Orbitron({
|
const orbitron = Orbitron({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -17,62 +17,89 @@ const cardThemes = [
|
|||||||
headerBg: "bg-[#1E3A8A]",
|
headerBg: "bg-[#1E3A8A]",
|
||||||
accent: "#60A5FA",
|
accent: "#60A5FA",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
bg: "from-[#0A0F1E] to-[#112240]",
|
|
||||||
headerBg: "bg-[#0EA5E9]",
|
|
||||||
accent: "#38BDF8",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bg: "from-[#1E293B] to-[#0F172A]",
|
|
||||||
headerBg: "bg-[#0284C7]",
|
|
||||||
accent: "#7DD3FC",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const ProjectsPage: React.FC = () => {
|
// ===== Loading Screen =====
|
||||||
const [projects, setProjects] = useState<any[]>([]);
|
function LoadingScreen() {
|
||||||
const [loading, setLoading] = useState(true);
|
return (
|
||||||
const [error, setError] = useState("");
|
<div className="relative grid min-h-screen place-items-center overflow-hidden bg-[#0b1117] text-slate-200">
|
||||||
|
<div className="pointer-events-none absolute inset-0 -z-10 blur-2xl opacity-70 [background:radial-gradient(50%_50%_at_20%_0%,rgba(34,211,238,.16),transparent_60%),radial-gradient(45%_45%_at_85%_15%,rgba(20,184,166,.14),transparent_60%),radial-gradient(40%_40%_at_50%_120%,rgba(56,189,248,.12),transparent_60%)]" />
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<div className="relative h-20 w-20">
|
||||||
|
<div className="absolute inset-0 animate-[spin_1.2s_linear_infinite] rounded-full border-4 border-cyan-300/30 border-t-cyan-300 shadow-[0_0_24px_rgba(34,211,238,.35)]" />
|
||||||
|
<div className="absolute inset-2 grid place-items-center rounded-full bg-black/40 backdrop-blur-xl">
|
||||||
|
<Image
|
||||||
|
src="/ml4e.svg"
|
||||||
|
alt="ML4E"
|
||||||
|
width={44}
|
||||||
|
height={44}
|
||||||
|
priority
|
||||||
|
className="opacity-95 drop-shadow-[0_0_12px_rgba(34,211,238,.45)] animate-[pulse_1.8s_ease-in-out_infinite]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-semibold tracking-wide text-cyan-200 drop-shadow-[0_0_16px_rgba(0,255,255,.35)]">
|
||||||
|
Loading Projects
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-slate-400">
|
||||||
|
Fetching the latest curated projects…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ViewProjectsPage() {
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [usingFallback, setUsingFallback] = useState(false);
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
|
||||||
|
// Fetch from API or fallback
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProjects = async () => {
|
let alive = true;
|
||||||
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/projects", { cache: "no-store" });
|
const res = await fetch("/api/projects", { cache: "no-store" });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || "Failed to fetch projects");
|
if (!res.ok) throw new Error(data?.error || "Failed to load projects");
|
||||||
setProjects(data);
|
if (!alive) return;
|
||||||
} catch (err: any) {
|
setProjects(data as Project[]);
|
||||||
console.error("Error fetching projects:", err);
|
setUsingFallback(false);
|
||||||
setError(err.message || "Something went wrong");
|
} catch {
|
||||||
|
if (!alive) return;
|
||||||
|
setProjects(DUMMY_PROJECTS);
|
||||||
|
setUsingFallback(true);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (alive) setLoading(false);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
};
|
};
|
||||||
fetchProjects();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
const menuItems: MenuItem[] = useMemo(
|
||||||
return (
|
() =>
|
||||||
<div className="min-h-screen flex items-center justify-center text-cyan-400 text-2xl font-bold">
|
projects.map((p) => ({
|
||||||
Loading Projects...
|
image: p.imageUrl || "https://picsum.photos/600/600?grayscale",
|
||||||
</div>
|
link: p.deployedLink || p.githubLink || "#",
|
||||||
|
title: p.name || "Untitled",
|
||||||
|
description:
|
||||||
|
p.description ||
|
||||||
|
(p.techStack ? `Built with ${p.techStack}` : "Explore this project"),
|
||||||
|
})),
|
||||||
|
[projects]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
const activeItem = menuItems[activeIndex] || menuItems[0];
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center text-red-500 text-xl font-semibold">
|
if (loading) return <LoadingScreen />;
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen w-full overflow-hidden">
|
<div className="min-h-screen w-full bg-gradient-to-b from-[#050816] via-[#0A0F1E] to-[#0F172A] py-16 px-4 sm:px-8">
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-[#020617] via-[#0A0F1E] to-[#0F172A]" />
|
|
||||||
<NeuralBackground />
|
|
||||||
|
|
||||||
<div className="relative z-10 py-16 px-4 sm:px-8">
|
|
||||||
<h1
|
<h1
|
||||||
className={`text-4xl sm:text-5xl font-bold text-center text-cyan-400 mb-12 ${orbitron.className}`}
|
className={`text-4xl sm:text-5xl font-bold text-center text-cyan-400 mb-12 ${orbitron.className}`}
|
||||||
>
|
>
|
||||||
@@ -85,11 +112,9 @@ const ProjectsPage: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={project._id || index}
|
key={project._id || index}
|
||||||
className={`rounded-xl overflow-hidden border border-cyan-500/20 shadow-[0_0_25px_rgba(0,255,255,0.1)] bg-gradient-to-br ${theme.bg} transform transition-all duration-300 hover:-translate-y-2 hover:shadow-[0_0_40px_rgba(0,255,255,0.3)] hover:border-cyan-400/40`}
|
className={`rounded-xl overflow-hidden border border-cyan-500/20 shadow-[0_0_25px_rgba(0,255,255,0.1)] hover:shadow-[0_0_35px_rgba(0,255,255,0.25)] transition duration-300 bg-gradient-to-br ${theme.bg}`}
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${theme.headerBg} py-4 px-6 text-center border-b border-cyan-400/30`}
|
|
||||||
>
|
>
|
||||||
|
<div className={`${theme.headerBg} py-4 px-6 text-center border-b border-cyan-400/30`}>
|
||||||
<h2
|
<h2
|
||||||
className={`text-2xl font-bold text-white tracking-wide ${orbitron.className}`}
|
className={`text-2xl font-bold text-white tracking-wide ${orbitron.className}`}
|
||||||
>
|
>
|
||||||
@@ -116,9 +141,7 @@ const ProjectsPage: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{project.description && (
|
{project.description && (
|
||||||
<p className="text-gray-400 text-sm italic">
|
<p className="text-gray-400 text-sm italic">{project.description}</p>
|
||||||
{project.description}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-4">
|
<div className="flex items-center justify-between mt-4">
|
||||||
@@ -152,7 +175,6 @@ const ProjectsPage: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
149
src/app/projects_legacy/page.tsx
Normal file
149
src/app/projects_legacy/page.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Orbitron } from "next/font/google";
|
||||||
|
import { FaGithub } from "react-icons/fa";
|
||||||
|
|
||||||
|
const orbitron = Orbitron({
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "700"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardThemes = [
|
||||||
|
{
|
||||||
|
bg: "from-[#0F172A] to-[#1E293B]",
|
||||||
|
headerBg: "bg-[#1E3A8A]",
|
||||||
|
accent: "#60A5FA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "from-[#0A0F1E] to-[#112240]",
|
||||||
|
headerBg: "bg-[#0EA5E9]",
|
||||||
|
accent: "#38BDF8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bg: "from-[#1E293B] to-[#0F172A]",
|
||||||
|
headerBg: "bg-[#0284C7]",
|
||||||
|
accent: "#7DD3FC",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ProjectsPage: React.FC = () => {
|
||||||
|
const [projects, setProjects] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/projects", { cache: "no-store" });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || "Failed to fetch projects");
|
||||||
|
setProjects(data);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Error fetching projects:", err);
|
||||||
|
setError(err.message || "Something went wrong");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchProjects();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center text-cyan-400 text-2xl font-bold">
|
||||||
|
Loading Projects...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center text-red-500 text-xl font-semibold">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen w-full bg-gradient-to-b from-[#050816] via-[#0A0F1E] to-[#0F172A] py-16 px-4 sm:px-8">
|
||||||
|
<h1
|
||||||
|
className={`text-4xl sm:text-5xl font-bold text-center text-cyan-400 mb-12 ${orbitron.className}`}
|
||||||
|
>
|
||||||
|
PROJECTS
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
|
||||||
|
{projects.map((project, index) => {
|
||||||
|
const theme = cardThemes[index % cardThemes.length];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={project._id || index}
|
||||||
|
className={`rounded-xl overflow-hidden border border-cyan-500/20 shadow-[0_0_25px_rgba(0,255,255,0.1)] hover:shadow-[0_0_35px_rgba(0,255,255,0.25)] transition duration-300 bg-gradient-to-br ${theme.bg}`}
|
||||||
|
>
|
||||||
|
<div className={`${theme.headerBg} py-4 px-6 text-center border-b border-cyan-400/30`}>
|
||||||
|
<h2
|
||||||
|
className={`text-2xl font-bold text-white tracking-wide ${orbitron.className}`}
|
||||||
|
>
|
||||||
|
{project.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{project.imageUrl && (
|
||||||
|
<div className="relative w-full h-40 sm:h-48 border-b border-cyan-400/20">
|
||||||
|
<Image
|
||||||
|
src={project.imageUrl}
|
||||||
|
alt={project.name}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
|
priority={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-6 text-white flex flex-col gap-3">
|
||||||
|
<p className="text-gray-300">
|
||||||
|
<strong>Tech Stack:</strong> {project.techStack}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{project.description && (
|
||||||
|
<p className="text-gray-400 text-sm italic">{project.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
{project.deployedLink && (
|
||||||
|
<a
|
||||||
|
href={project.deployedLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: theme.accent }}
|
||||||
|
className="underline hover:opacity-80 transition duration-200"
|
||||||
|
>
|
||||||
|
View Project
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{project.githubLink && (
|
||||||
|
<a
|
||||||
|
href={project.githubLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-2xl hover:scale-110 transition-transform"
|
||||||
|
style={{ color: theme.accent }}
|
||||||
|
>
|
||||||
|
<FaGithub />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectsPage;
|
||||||
@@ -20,6 +20,8 @@ const members = [
|
|||||||
avatarUrl: "/team/president.png",
|
avatarUrl: "/team/president.png",
|
||||||
linkedin: "https://linkedin.com/in/kunal",
|
linkedin: "https://linkedin.com/in/kunal",
|
||||||
gmail: "mailto:kunal@nitrkl.ac.in",
|
gmail: "mailto:kunal@nitrkl.ac.in",
|
||||||
|
zoom: 1, // 🔹 Normal size
|
||||||
|
disableAura: true, // 🔹 Keep glow animation
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Rishi Das",
|
name: "Rishi Das",
|
||||||
@@ -28,6 +30,8 @@ const members = [
|
|||||||
avatarUrl: "/team/vp.png",
|
avatarUrl: "/team/vp.png",
|
||||||
linkedin: "https://linkedin.com/in/rishi",
|
linkedin: "https://linkedin.com/in/rishi",
|
||||||
gmail: "mailto:rishi@nitrkl.ac.in",
|
gmail: "mailto:rishi@nitrkl.ac.in",
|
||||||
|
zoom: 1,
|
||||||
|
disableAura: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Bibhu",
|
name: "Bibhu",
|
||||||
@@ -36,6 +40,8 @@ const members = [
|
|||||||
avatarUrl: "/team/secretary.png",
|
avatarUrl: "/team/secretary.png",
|
||||||
linkedin: "https://linkedin.com/in/bibhu",
|
linkedin: "https://linkedin.com/in/bibhu",
|
||||||
gmail: "mailto:bibhu@nitrkl.ac.in",
|
gmail: "mailto:bibhu@nitrkl.ac.in",
|
||||||
|
zoom: 1,
|
||||||
|
disableAura: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Arko Pravo Dey",
|
name: "Arko Pravo Dey",
|
||||||
@@ -44,6 +50,8 @@ const members = [
|
|||||||
avatarUrl: "/team/treasurer.png",
|
avatarUrl: "/team/treasurer.png",
|
||||||
linkedin: "https://linkedin.com/in/arko",
|
linkedin: "https://linkedin.com/in/arko",
|
||||||
gmail: "mailto:arko@nitrkl.ac.in",
|
gmail: "mailto:arko@nitrkl.ac.in",
|
||||||
|
zoom: 1.5, // 🔹 Slightly larger photo
|
||||||
|
disableAura: true, // 🔹 Turn off rainbow animation
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Risabh Anand",
|
name: "Risabh Anand",
|
||||||
@@ -52,6 +60,8 @@ const members = [
|
|||||||
avatarUrl: "/team/mllead.png",
|
avatarUrl: "/team/mllead.png",
|
||||||
linkedin: "https://linkedin.com/in/risabh",
|
linkedin: "https://linkedin.com/in/risabh",
|
||||||
gmail: "mailto:risabh@nitrkl.ac.in",
|
gmail: "mailto:risabh@nitrkl.ac.in",
|
||||||
|
zoom: 1.5,
|
||||||
|
disableAura: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -69,13 +79,8 @@ export default function TeamPage() {
|
|||||||
<div className="relative text-center pt-12 pb-8">
|
<div className="relative text-center pt-12 pb-8">
|
||||||
<h1
|
<h1
|
||||||
className="text-4xl md:text-5xl font-extrabold tracking-wide text-cyan-200 drop-shadow-[0_0_18px_rgba(0,255,255,.45)]">
|
className="text-4xl md:text-5xl font-extrabold tracking-wide text-cyan-200 drop-shadow-[0_0_18px_rgba(0,255,255,.45)]">
|
||||||
|
|
||||||
<TextType
|
<TextType
|
||||||
text={[
|
text={["Executive Body", "Team ML4E"]}
|
||||||
"Executive Body",
|
|
||||||
"Team ML4E",
|
|
||||||
"Meet Our Leaders",
|
|
||||||
]}
|
|
||||||
className="text-cyan-200 drop-shadow-[0_0_18px_rgba(0,255,255,.45)]"
|
className="text-cyan-200 drop-shadow-[0_0_18px_rgba(0,255,255,.45)]"
|
||||||
typingSpeed={75}
|
typingSpeed={75}
|
||||||
pauseDuration={1500}
|
pauseDuration={1500}
|
||||||
@@ -105,10 +110,11 @@ export default function TeamPage() {
|
|||||||
status=""
|
status=""
|
||||||
contactText=""
|
contactText=""
|
||||||
avatarUrl={m.avatarUrl}
|
avatarUrl={m.avatarUrl}
|
||||||
enableTilt
|
enableTilt={false}
|
||||||
showUserInfo={false}
|
showUserInfo={false}
|
||||||
enableMobileTilt
|
showBehindGradient={false}
|
||||||
showBehindGradient={true}
|
zoom={m.zoom} // ✅ Apply per-member zoom
|
||||||
|
disableAura={m.disableAura} // ✅ Apply per-member aura toggle
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Social Links */}
|
{/* Social Links */}
|
||||||
|
|||||||
Reference in New Issue
Block a user