Skip to content

Commit a73cf0c

Browse files
committed
Add 404 page components and 3D model for Not Found experience
- Introduced a new 404 page with a custom Not Found component. - Added a 3D model (`404.glb`) for enhanced visual representation on the 404 page. - Implemented an animated Hero component to display the 3D model. - Created an overlay component with a link to navigate back to the home page. - Defined metadata for the 404 page to improve SEO.
1 parent 5516431 commit a73cf0c

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed

public/404.glb

22.2 KB
Binary file not shown.

src/app/not-found.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { HeroASCIINotFound } from "@/components/hero-404";
2+
import { Metadata } from "next";
3+
4+
export const metadata: Metadata = {
5+
title: "404",
6+
description: "404"
7+
};
8+
9+
export default function NotFound() {
10+
return (
11+
<div className="h-full w-full relative">
12+
<HeroASCIINotFound />
13+
</div>
14+
);
15+
}

src/components/hero-404/index.tsx

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
"use client";
3+
import {
4+
Box,
5+
Camera,
6+
GLTFLoader,
7+
Mesh,
8+
Plane,
9+
Program,
10+
Renderer,
11+
RenderTarget,
12+
Transform,
13+
Vec3
14+
} from "ogl";
15+
import { useEffect, useRef } from "react";
16+
import { Overlay404 } from "../overlay-404";
17+
18+
const DESKTOP_HEIGHT = 256;
19+
const MOBILE_HEIGHT = 260;
20+
21+
const DESKTOP_CAMERA_POSITION = new Vec3(8, -4, 15);
22+
const MOBILE_CAMERA_POSITION = new Vec3(2, -2, 1);
23+
24+
const MODEL_POSITION = new Vec3(0, 0, 0);
25+
26+
export const HeroASCIINotFound = () => {
27+
const containerRef = useRef<HTMLDivElement>(null);
28+
useEffect(() => {
29+
const container = containerRef.current;
30+
const vertex = /*glsl*/ `#version 300 es
31+
in vec3 position;
32+
in vec3 normal;
33+
in vec2 uv;
34+
uniform mat4 modelViewMatrix;
35+
uniform mat4 projectionMatrix;
36+
uniform mat3 normalMatrix;
37+
out vec2 vUv;
38+
out vec3 vNormal;
39+
void main() {
40+
vUv = uv;
41+
vNormal = normalize(normalMatrix * normal);
42+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
43+
}`;
44+
const fragment = /*glsl*/ `#version 300 es
45+
precision mediump float;
46+
uniform float uTime;
47+
in vec2 vUv;
48+
in vec3 vNormal;
49+
out vec4 fragColor;
50+
void main() {
51+
// Simple lighting from the top-left
52+
vec3 lightDir = normalize(vec3(-1.0, 1.0, 1.0));
53+
float light = max(0.1, dot(vNormal, lightDir)) * 0.9 + 0.2;
54+
// Basic white color with light intensity
55+
fragColor = vec4(light, light, light, 1.0);
56+
}`;
57+
const asciiVertex = /*glsl*/ `#version 300 es
58+
in vec2 uv;
59+
in vec2 position;
60+
out vec2 vUv;
61+
void main() {
62+
vUv = uv;
63+
gl_Position = vec4(position, 0., 1.);
64+
}`;
65+
const asciiFragment = /*glsl*/ `#version 300 es
66+
precision highp float;
67+
uniform vec2 uResolution;
68+
uniform sampler2D uTexture;
69+
out vec4 fragColor;
70+
float character(int n, vec2 p) {
71+
// character grid scale
72+
float scale = uResolution.x < 768.0 ? 6.0 : 6.0;
73+
p = floor(p * vec2(-scale, scale) + 2.5);
74+
if(clamp(p.x, 0.0, 6.0) == p.x && clamp(p.y, 0.0, 6.0) == p.y) {
75+
int a = int(round(p.x) + 5.0 * round(p.y));
76+
if(((n >> a) & 1) == 1) return 0.8;
77+
}
78+
return 0.0;
79+
}
80+
void main() {
81+
vec2 pix = gl_FragCoord.xy;
82+
// pixel size
83+
float pixelSize = uResolution.x < 768.0 ? 12.0 : 14.0;
84+
vec3 col = texture(uTexture, floor(pix / pixelSize) * pixelSize / uResolution.xy).rgb;
85+
float gray = 0.3 * col.r + 0.59 * col.g + 0.11 * col.b;
86+
int n = 2048;
87+
if(gray > 0.2) n = 65600;
88+
if(gray > 0.3) n = 163153;
89+
if(gray > 0.4) n = 15255086;
90+
if(gray > 0.5) n = 13121101;
91+
if(gray > 0.6) n = 15252014;
92+
if(gray > 0.7) n = 13195790;
93+
if(gray > 0.8) n = 11512810;
94+
// char size
95+
float charSize = uResolution.x < 768.0 ? 6.0 : 4.0;
96+
vec2 p = mod(pix / charSize, 2.0) - vec2(1.0);
97+
col = vec3(character(n, p));
98+
if (gray < 0.2) {
99+
col *= 0.4;
100+
}
101+
fragColor = vec4(col, 1.0);
102+
}`;
103+
104+
const isMobile = () => window.innerWidth < 768;
105+
106+
const renderer = new Renderer({ antialias: true });
107+
const gl = renderer.gl;
108+
containerRef.current?.appendChild(gl.canvas);
109+
const camera = new Camera(gl, { near: 0.1, far: 100, fov: 10 });
110+
camera.position.set(
111+
isMobile() ? MOBILE_CAMERA_POSITION : DESKTOP_CAMERA_POSITION
112+
);
113+
camera.lookAt(MODEL_POSITION);
114+
115+
const scene = new Transform();
116+
scene.position.copy(MODEL_POSITION);
117+
118+
let gltf: any;
119+
async function loadModel() {
120+
gltf = await GLTFLoader.load(gl, "/404.glb");
121+
122+
const s = gltf.scene || gltf.scenes[0];
123+
s.forEach((root: any) => {
124+
root.setParent(scene);
125+
root.traverse((node: any) => {
126+
if (node.program) {
127+
node.program = createProgram();
128+
}
129+
});
130+
});
131+
}
132+
133+
function createProgram() {
134+
return new Program(gl, {
135+
vertex,
136+
fragment
137+
});
138+
}
139+
140+
loadModel();
141+
142+
const updateSize = () => {
143+
const height = isMobile() ? MOBILE_HEIGHT : DESKTOP_HEIGHT;
144+
renderer.setSize(window.innerWidth - 32, window.innerHeight - height);
145+
camera.perspective({
146+
aspect: (gl.canvas.width + 32) / gl.canvas.height
147+
});
148+
};
149+
150+
window.addEventListener("resize", updateSize);
151+
updateSize();
152+
const boxProgram = new Program(gl, {
153+
vertex: vertex,
154+
fragment: fragment,
155+
uniforms: {
156+
uTime: { value: 0 }
157+
},
158+
cullFace: false
159+
});
160+
const boxMesh = new Mesh(gl, {
161+
geometry: new Box(gl, { width: 1, height: 2, depth: 1 }),
162+
program: boxProgram
163+
});
164+
const renderTarget = new RenderTarget(gl);
165+
const asciiProgram = new Program(gl, {
166+
vertex: asciiVertex,
167+
fragment: asciiFragment,
168+
uniforms: {
169+
uResolution: { value: [gl.canvas.width, gl.canvas.height] },
170+
uTexture: { value: renderTarget.texture }
171+
}
172+
});
173+
const asciiMesh = new Mesh(gl, {
174+
geometry: new Plane(gl, { width: 2, height: 2 }),
175+
program: asciiProgram
176+
});
177+
const boxScene = new Transform();
178+
boxMesh.setParent(boxScene);
179+
const asciiScene = new Transform();
180+
asciiMesh.setParent(asciiScene);
181+
182+
const MAX_TILT = 0.2;
183+
const LERP_FACTOR = 0.05;
184+
185+
let targetRotationX = 0;
186+
let targetRotationY = 0;
187+
let targetRotationZ = 0;
188+
let currentRotationX = 0;
189+
let currentRotationY = 0;
190+
let currentRotationZ = 0;
191+
let isDragging = false;
192+
193+
function onMouseMove(e: MouseEvent) {
194+
const x = (e.clientX / window.innerWidth) * 2 - 1;
195+
const y = (e.clientY / window.innerHeight) * 2 - 1;
196+
197+
targetRotationX = y * MAX_TILT;
198+
targetRotationY = x * MAX_TILT;
199+
targetRotationZ = -y * MAX_TILT;
200+
}
201+
202+
function onTouchStart(e: TouchEvent) {
203+
isDragging = true;
204+
e.preventDefault();
205+
}
206+
207+
function onTouchMove(e: TouchEvent) {
208+
if (!isDragging) return;
209+
210+
// Prevent default touch behavior
211+
e.preventDefault();
212+
213+
const touchX = e.touches[0].clientX;
214+
const touchY = e.touches[0].clientY;
215+
216+
const x = (touchX / window.innerWidth) * 2 - 1;
217+
const y = (touchY / window.innerHeight) * 2 - 1;
218+
219+
targetRotationX = y * MAX_TILT;
220+
targetRotationY = x * MAX_TILT;
221+
targetRotationZ = -y * MAX_TILT;
222+
}
223+
224+
function onTouchEnd() {
225+
isDragging = false;
226+
}
227+
228+
window.addEventListener("mousemove", onMouseMove);
229+
window.addEventListener("touchstart", onTouchStart, { passive: false });
230+
window.addEventListener("touchmove", onTouchMove, { passive: false });
231+
window.addEventListener("touchend", onTouchEnd);
232+
233+
function update(time: number) {
234+
// Animate model if it has animations
235+
if (gltf?.animations?.length) {
236+
const { animation } = gltf.animations[0];
237+
animation.elapsed += 0.01;
238+
animation.update();
239+
}
240+
241+
const elapsedTime = time * 0.001;
242+
boxProgram.uniforms.uTime.value = elapsedTime;
243+
244+
currentRotationX += (targetRotationX - currentRotationX) * LERP_FACTOR;
245+
currentRotationY += (targetRotationY - currentRotationY) * LERP_FACTOR;
246+
currentRotationZ += (targetRotationZ - currentRotationZ) * LERP_FACTOR;
247+
248+
scene.rotation.x = currentRotationX;
249+
scene.rotation.y = currentRotationY;
250+
scene.rotation.z = currentRotationZ;
251+
252+
renderer.render({ scene, camera, target: renderTarget });
253+
asciiProgram.uniforms.uResolution.value = [
254+
gl.canvas.width,
255+
gl.canvas.height
256+
];
257+
renderer.render({ scene: asciiScene, camera });
258+
}
259+
260+
let animationId: number;
261+
function animate(time: number) {
262+
update(time);
263+
animationId = requestAnimationFrame(animate);
264+
}
265+
animationId = requestAnimationFrame(animate);
266+
return () => {
267+
window.removeEventListener("resize", updateSize);
268+
window.removeEventListener("mousemove", onMouseMove);
269+
window.removeEventListener("touchstart", onTouchStart);
270+
window.removeEventListener("touchmove", onTouchMove);
271+
window.removeEventListener("touchend", onTouchEnd);
272+
cancelAnimationFrame(animationId);
273+
renderer.gl.getExtension("WEBGL_lose_context")?.loseContext();
274+
if (container?.contains(gl.canvas)) {
275+
container.removeChild(gl.canvas);
276+
}
277+
};
278+
}, []);
279+
280+
return (
281+
<div className="w-full overflow-clip h-[calc(100dvh-260px)] md:h-[calc(100dvh-256px)]">
282+
<div
283+
ref={containerRef}
284+
className="w-[calc(100%-2rem)] mx-4 h-full overflow-clip"
285+
></div>
286+
<Overlay404 />
287+
</div>
288+
);
289+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Link } from "../link";
2+
3+
export const Overlay404 = () => {
4+
return (
5+
<div className="absolute inset-0 z-50 flex items-center justify-center">
6+
<div className="w-full h-full relative">
7+
<div className="absolute top-4 md:top-12 left-1/2 -translate-x-1/2 md:left-auto md:right-12 md:translate-x-0 text-white bg-black w-[174px] lg:w-[204px] text-right font-mono text-xs md:text-sm lg:pl-4 py-1 px-3 md:py-2">
8+
<Link href="/" className="z-10">
9+
Go back home
10+
</Link>
11+
</div>
12+
</div>
13+
</div>
14+
);
15+
};

0 commit comments

Comments
 (0)