Skip to content

Commit 149231d

Browse files
authored
Merge pull request #7 from trsctr/feat/threejsscene
Feat/threejsscene
2 parents bd51199 + d067724 commit 149231d

14 files changed

+432
-721
lines changed

package-lock.json

Lines changed: 110 additions & 689 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
"clsx": "^2.1.1",
1616
"react": "^18.3.1",
1717
"react-dom": "^18.3.1",
18-
"three": "^0.171.0"
18+
"three": "^0.171.0",
19+
"vite-plugin-glsl": "^1.3.1"
1920
},
2021
"devDependencies": {
2122
"@eslint/js": "^9.15.0",
23+
"@types/node": "^22.10.2",
2224
"@types/react": "^18.3.12",
2325
"@types/react-dom": "^18.3.1",
2426
"@types/three": "^0.170.0",
@@ -33,6 +35,6 @@
3335
"tailwindcss": "^3.4.16",
3436
"typescript": "~5.6.2",
3537
"typescript-eslint": "^8.15.0",
36-
"vite": "^6.0.1"
38+
"vite": "^6.0.6"
3739
}
3840
}

src/App.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
11
import React from 'react';
22
import About from './components/About';
33
import Header from './components/Header';
4+
import Scene from './components/Scene';
5+
import BackgroundShader from './components/BackgroundShader';
6+
import { Canvas } from '@react-three/fiber';
47

58
const App: React.FC = () => {
69
return (
10+
<div className="w-full h-full bg-gradient-to-b from-background-top to-black">
11+
{/* Main container with flex layout */}
12+
<div className=" top-0 left-0 w-full h-full fixed">
13+
<Canvas className="w-full h-full opacity-30">
14+
<BackgroundShader />
15+
</Canvas>
16+
</div>
717

8-
<div className="bg-black min-h-screen mt-0 flex items-center justify-center w-full h-full">
9-
10-
<div className="absolute top-0 left-0 p-5 hidden md:block">
11-
<Header text="trsctr.github.io" textSize="text-2xl" className="tracking-widest font-mono" hasGradient/>
12-
</div>
13-
<About title="Hello, my name is Otto" imageUrl="/assets/photo.jpg" imageOnRight>
14-
<p className="mb-2">
15-
This is my webpage.
16-
</p>
17-
<p>
18-
There are many like it, but this one is mine.
19-
</p>
20-
<p className="mt-2">
21-
Something something something about something and I like cats and good music.</p>
22-
<p className="mt-2">I'm beginning to like Tailwind. It's useful and makes CSS less nervewracking than it could be.</p>
23-
24-
<p className="mt-2">Which is nice.</p>
25-
</About>
26-
</div>
18+
<div className="min-h-screen flex flex-col lg:flex-row items-center justify-center w-full">
2719

20+
{/* Header section (visible on medium screens and up) */}
21+
<div className="absolute top-0 left-0 p-5 hidden md:block">
22+
<Header text="trsctr.github.io" textSize="text-2xl" className="tracking-widest font-mono z-20" hasGradient />
23+
</div>
24+
25+
26+
<div className="flex flex-col lg:flex-row items-center justify-center lg:justify-between w-full h-full relative">
27+
28+
{/* Textbox section */}
29+
<div className="flex-1 p-5 z-30">
30+
<About title="Hello, my name is Otto" imageUrl="/assets/photo.jpg" imageOnRight>
31+
<p className="mb-2">This is my webpage.</p>
32+
<p>There are many like it, but this one is mine.</p>
33+
<p className="mt-2">Something something something about something and I like cats and good music.</p>
34+
<p className="mt-2">Lorem ipsum dolor shit Valmet. Shiggity shiggity schwa. Hello world. Bla bla bla. Is this shader heavy.</p>
35+
<p className="mt-2">Lässyn lässyn lää läpäti lää</p>
36+
</About>
37+
</div>
38+
39+
{/* Canvas section */}
40+
<div className="flex-1 p-5 flex justify-center -mt-20 lg:mt-0 z-0">
41+
<div className="relative min-w-[200px] w-full max-w-[800px] h-auto aspect-[1/1]">
42+
<Scene />
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
48+
</div>
2849
);
2950
};
3051

src/components/About.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ interface AboutProps {
2121
*/
2222
const About: React.FC<AboutProps> = ( {title, imageUrl, children, imageOnRight = false}) => {
2323
return (
24-
<div className="absolute flex left-0 top-0 md:top-1/4 md:left-32 w-full md:w-9/12 max-w-[900px]">
24+
<div className="md:absolute flex left-0 top-0 md:top-1/4 md:left-32 w-full md:w-9/12 max-w-[900px]">
2525
<ContentBox className="rounded-lg shadow-lg">
2626
{title && <Header text={title} hasGradient/>}
27+
28+
2729
<TextImageBox imageUrl={imageUrl} imageOnRight={imageOnRight}>
30+
2831
{children}
2932
</TextImageBox>
3033
<Footer/>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useFrame } from '@react-three/fiber';
2+
import { ShaderMaterial, Vector2 } from 'three';
3+
import { useMemo, useRef } from 'react';
4+
import vertexShader from '@/shaders/BackgroundVertex.glsl';
5+
import fragmentShader from '@/shaders/BackgroundFragment.glsl';
6+
7+
const BackgroundShader = () => {
8+
const materialRef = useRef<ShaderMaterial | null>(null);
9+
const random = Math.random();
10+
11+
const uniforms = useMemo(() => ({
12+
uTime: { value: 0 },
13+
uResolution: { value: new Vector2(window.innerWidth, window.innerHeight) },
14+
uRandom : { value: random }
15+
}), [random]);
16+
17+
useFrame((_, delta) => {
18+
if (materialRef.current) {
19+
materialRef.current.uniforms.uTime.value += delta;
20+
}
21+
});
22+
23+
return (
24+
<mesh position={[0, 0, -1]}>
25+
<planeGeometry args={[2, 2]} />
26+
<shaderMaterial
27+
ref={materialRef}
28+
uniforms={uniforms}
29+
depthWrite={false}
30+
depthTest={false}
31+
vertexShader={vertexShader}
32+
fragmentShader={fragmentShader}
33+
34+
/>
35+
</mesh>
36+
);
37+
};
38+
39+
export default BackgroundShader;

src/components/ContentBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface ContentBoxProps {
1515
*/
1616
const ContentBox: React.FC<ContentBoxProps> = ({ background = "bg-background", className, children }) => {
1717
return (
18-
<section className={`${background} relative p-4 flex justify-center ${className}`}>
18+
<section className={`${background} opacity-80 relative p-4 flex justify-center ${className}`}>
1919
{/* Content Box */}
2020
<div className="w-full flex-col">
2121
{children}

src/components/MorphingMesh.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, { useRef, useMemo } from "react";
2+
import { BufferGeometry, Float32BufferAttribute, Color, BufferAttribute, InterleavedBufferAttribute, MathUtils, Mesh, DoubleSide } from "three";
3+
import { useFrame } from "@react-three/fiber";
4+
5+
// Helper function to generate random vertices
6+
const generateVertices = (points: number = 55, size: number = 4): number[] => {
7+
const vertices: number[] = [];
8+
for (let i = 0; i < points; i++) {
9+
const x = Math.random() * size - size * 0.5;
10+
const y = Math.random() * size - size * 0.5;
11+
const z = Math.random() * size - size * 0.5;
12+
vertices.push(x, y, z);
13+
}
14+
return vertices;
15+
};
16+
17+
// Helper function to generate colors based on vertices
18+
const generateColors = (vertices: number[], colormod: number = 1): number[] => {
19+
const colors: number[] = [];
20+
for (let i = 0; i < vertices.length; i += 3) {
21+
const x = vertices[i];
22+
const y = vertices[i + 1];
23+
const z = vertices[i + 2];
24+
const distance = Math.sqrt(x * x + y * y + z * z);
25+
const color = new Color();
26+
const hue = distance * colormod % 1;
27+
color.setHSL(hue, 1, 0.5);
28+
colors.push(color.r, color.g, color.b);
29+
}
30+
return colors;
31+
};
32+
33+
// Helper function to interpolate mesh attributes
34+
const interpolateAttributes = (attr: BufferAttribute | InterleavedBufferAttribute, target: number[], delta: number) => {
35+
for (let i = 0; i < attr.count * 3; i++) {
36+
attr.array[i] = MathUtils.lerp(attr.array[i] as number, target[i], 2 * delta);
37+
}
38+
attr.needsUpdate = true;
39+
};
40+
41+
// Helper function to rotate a mesh
42+
const rotateMesh = (mesh: Mesh | null, delta: number) => {
43+
if (mesh) {
44+
mesh.rotation.y += 0.05 * delta;
45+
mesh.rotation.x += 0.05 * delta;
46+
}
47+
};
48+
49+
// MorphingMesh component
50+
const MorphingMesh: React.FC = () => {
51+
const solidMeshRef = useRef<Mesh>(null);
52+
const wireframeMeshRef = useRef<Mesh>(null);
53+
54+
// Use refs instead of state for tracking morphing progress and targets
55+
const morphProgressRef = useRef(0);
56+
const morphTargetRef = useRef<number[] | null>(null);
57+
const targetColorsRef = useRef<number[]>([]);
58+
59+
// Memoizing the initial vertices and colors
60+
const initialVertices = useMemo(() => generateVertices(), []);
61+
const initialColors = useMemo(() => generateColors(initialVertices), [initialVertices]);
62+
63+
// Create the geometry with position and color attributes
64+
const geometry = useMemo(() => {
65+
const geo = new BufferGeometry();
66+
geo.setAttribute("position", new Float32BufferAttribute(initialVertices, 3));
67+
geo.setAttribute("color", new Float32BufferAttribute(initialColors, 3));
68+
return geo;
69+
}, [initialVertices, initialColors]);
70+
71+
// handleClick generates new random geometry and colors
72+
// and sets the morph target to the new geometry
73+
// click position is use as modifier for color generation
74+
const handleClick = (event: any) => {
75+
const { point } = event;
76+
const colormod = Math.abs(point.x + point.y - point.z) * Math.random();
77+
const newVertices = generateVertices();
78+
const newColors = generateColors(newVertices, colormod);
79+
morphTargetRef.current = newVertices;
80+
targetColorsRef.current = newColors;
81+
morphProgressRef.current = 0;
82+
};
83+
84+
useFrame((_, delta) => {
85+
if (solidMeshRef.current && wireframeMeshRef.current) {
86+
87+
if (morphTargetRef.current) {
88+
const positionAttr = solidMeshRef.current.geometry.attributes.position;
89+
const colorAttr = solidMeshRef.current.geometry.attributes.color;
90+
91+
interpolateAttributes(positionAttr, morphTargetRef.current, delta);
92+
interpolateAttributes(colorAttr, targetColorsRef.current, delta);
93+
94+
morphProgressRef.current += delta;
95+
if (morphProgressRef.current >= 1) {
96+
morphProgressRef.current = 1;
97+
morphTargetRef.current = null;
98+
targetColorsRef.current = [];
99+
}
100+
101+
solidMeshRef.current.geometry.computeVertexNormals();
102+
wireframeMeshRef.current!.geometry = solidMeshRef.current.geometry;
103+
}
104+
105+
rotateMesh(solidMeshRef.current, delta);
106+
rotateMesh(wireframeMeshRef.current, delta);
107+
}
108+
});
109+
110+
return (
111+
<>
112+
{/* Solid Mesh */}
113+
<mesh ref={solidMeshRef} geometry={geometry} onClick={handleClick}>
114+
<meshBasicMaterial vertexColors transparent opacity={0.3} side={DoubleSide} />
115+
</mesh>
116+
117+
{/* Wireframe Mesh */}
118+
<mesh ref={wireframeMeshRef} geometry={geometry}>
119+
<meshBasicMaterial vertexColors depthWrite={false} opacity={.2} transparent side={DoubleSide} wireframe />
120+
</mesh>
121+
</>
122+
);
123+
};
124+
125+
export default MorphingMesh;

src/components/Scene.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from "react";
2+
import { Canvas } from "@react-three/fiber";
3+
import MorphingMesh from "./MorphingMesh";
4+
import { PerspectiveCamera } from "@react-three/drei";
5+
6+
const Scene: React.FC = () => {
7+
return (
8+
<Canvas
9+
gl={{ antialias: true, alpha: true }}
10+
>
11+
<PerspectiveCamera makeDefault position={[0, 0, 7]} />
12+
<ambientLight intensity={0.5} />
13+
<directionalLight position={[5, 5, 5]} intensity={1} />
14+
<MorphingMesh />
15+
</Canvas>
16+
17+
);
18+
};
19+
20+
export default Scene;

src/components/TextImageBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface TextImageBoxProps {
1010
const TextImage: React.FC<Pick<TextImageBoxProps, 'imageUrl' | 'altText'>> = ({ imageUrl, altText = "Image"}) => {
1111
return (
1212
<div className="w-full sm:flex flex-shrink-0 justify-center md:block md:w-1/4 mx-4 mb-0 lg:mb-4">
13-
<img src={`${imageUrl}`} alt={altText} className="w-full max-w-[250px] md:max-w-[200px] mx-auto justify-center rounded" />
13+
<img src={`${imageUrl}`} alt={altText} className="w-full max-w-[250px] md:max-w-[200px] mx-auto justify-center rounded-md opacity-60" />
1414
</div>
1515
)
1616
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
uniform float uTime;
2+
uniform vec2 uResolution;
3+
uniform float uRandom;
4+
5+
#define TWO_PI 6.28318
6+
const float MAX_ITERATIONS = 1.5;
7+
const float UV_OFFSET = 0.001;
8+
const float IT_STEP = 0.1;
9+
10+
11+
// Function to generate a color palette based on sinusoidal variation of RGB channels based on input value
12+
vec3 generateColorPalette(float value) {
13+
vec3 baseColor = vec3(0.0, 0.0, 0.0); // Base color for the palette
14+
vec3 amplitude = vec3(0.0, 0.1, 0.1); // Amplitude/range of oscillation for each color channel
15+
vec3 frequency = vec3(0.0, 0.1, 0.1); // Frequency of oscillation for each color channel
16+
vec3 phaseShift = vec3(0.0, uRandom, uRandom); // Phase shift to offset each channel's oscillation
17+
18+
// Return the color based on oscillation
19+
return baseColor + amplitude * cos(TWO_PI * (frequency * value + phaseShift));
20+
}
21+
22+
void main() {
23+
// Calculate UV coordinates based on fragment coordinates (gl_FragCoord) and resolution
24+
// We normalize the UVs by dividing by the resolution, and then modify them slightly for animation
25+
vec2 uv = (gl_FragCoord.xy + uResolution.xy) / uResolution.y;
26+
uv.x += uTime * UV_OFFSET; // Modifying the x-coordinate of UV over time
27+
uv.y -= uv.x * 0.1; // Modifying the y-coordinate of UV based on x for a slanted effect
28+
29+
// Store the original UV coordinates to be referred to in the loop (this is the 'reference' UV)
30+
vec2 referenceUv = uv;
31+
32+
// Initialize the final color output
33+
vec3 finalcolor = vec3(0.0);
34+
35+
// Time modifier, affecting oscillations or movement over time
36+
float timemod = uTime * 0.01;
37+
float sinTimemod = sin(timemod); // Precompute sine of time for reuse
38+
float cosTimemod = cos(timemod); // Precompute cosine of time for reuse
39+
40+
// Loop to create dynamic visual effects
41+
for (float i = 0.0; i < MAX_ITERATIONS; i += IT_STEP) { // Iterate with a step of 0.1
42+
// Distort UV coordinates for visual effects based on sine, fract, and length
43+
uv -= fract(uv * i) - length(referenceUv * i) - sinTimemod;
44+
45+
// Calculate a distance metric based on the UV and reference UV, multiplied by randomness factor
46+
// This distance is used to modulate the effect
47+
float distMetric = length(uv - referenceUv + i) * uRandom;
48+
49+
distMetric -= cos(i + timemod); // Apply additional cosine-based transformation for added dynamic effect
50+
51+
// Modify UV based on the sine of the distance metric to introduce more distortion
52+
uv -= sin(i * distMetric);
53+
54+
// Generate color from the current UV coordinates and time, and adjust the intensity based on the distance metric and random uniform
55+
vec3 col = generateColorPalette(length(uv * referenceUv - distMetric) - uTime + uRandom);
56+
57+
// Smooth the distance metric to create softer transitions
58+
distMetric = smoothstep(0.0, 0.9, distMetric);
59+
60+
// Accumulate the color, weighted by the smooth distance metric
61+
finalcolor += col * distMetric;
62+
}
63+
64+
// Output the final computed color for this fragment
65+
gl_FragColor = vec4(finalcolor, 1.0);
66+
}

0 commit comments

Comments
 (0)