Compare commits
1 Commits
main
...
abb7651e41
| Author | SHA1 | Date | |
|---|---|---|---|
| abb7651e41 |
@@ -67,7 +67,7 @@ const AboutPage = () => {
|
|||||||
const age = calculateAge("2002-02-27");
|
const age = calculateAge("2002-02-27");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-center gap-8 p-4 pt-24 text-white">
|
<div className="flex w-full flex-col items-center gap-8 p-4 pt-24 text-white gravity-body">
|
||||||
<div className="flex flex-col items-center justify-center gap-2 p-4 backdrop-blur-sm glass-effect glossy-effect bottom gloss-highlight rounded-lg shadow-lg">
|
<div className="flex flex-col items-center justify-center gap-2 p-4 backdrop-blur-sm glass-effect glossy-effect bottom gloss-highlight rounded-lg shadow-lg">
|
||||||
<Image
|
<Image
|
||||||
src="/images/ja.avif"
|
src="/images/ja.avif"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
|
import GravityToggle from "@/components/gravity-toggle";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -98,6 +99,7 @@ export default function RootLayout({
|
|||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
|
<GravityToggle />
|
||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { skills, jobs } from "@/lib/data";
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full">
|
<div className="flex flex-col items-center w-full gravity-body">
|
||||||
<Hero />
|
<Hero />
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<AboutSummary />
|
<AboutSummary />
|
||||||
|
|||||||
36
components/gravity-toggle.tsx
Normal file
36
components/gravity-toggle.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { GravityEngine } from "@/lib/gravity-engine";
|
||||||
|
import { Magnet } from "lucide-react";
|
||||||
|
|
||||||
|
export default function GravityToggle() {
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const engineRef = useRef<GravityEngine | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
engineRef.current = new GravityEngine();
|
||||||
|
return () => engineRef.current?.stop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleGravity = () => {
|
||||||
|
if (!engineRef.current) return;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
engineRef.current.stop();
|
||||||
|
} else {
|
||||||
|
engineRef.current.start();
|
||||||
|
}
|
||||||
|
setIsActive(!isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleGravity}
|
||||||
|
className="fixed bottom-4 right-4 p-3 bg-yellow-400 text-black rounded-full shadow-lg z-50 hover:bg-yellow-500 transition-colors"
|
||||||
|
title="Toggle Gravity"
|
||||||
|
>
|
||||||
|
<Magnet size={24} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
223
lib/gravity-engine.ts
Normal file
223
lib/gravity-engine.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
interface PhysicsBody {
|
||||||
|
el: HTMLElement;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
vx: number;
|
||||||
|
vy: number;
|
||||||
|
isDragging: boolean;
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GravityEngine {
|
||||||
|
private bodies: PhysicsBody[] = [];
|
||||||
|
private animationFrameId: number | null = null;
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
INITIAL_FORCE = 10;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
this.isRunning = true;
|
||||||
|
|
||||||
|
const containers = document.querySelectorAll(".gravity-body");
|
||||||
|
const targetElements: HTMLElement[] = [];
|
||||||
|
|
||||||
|
// Helper to recursively drill down to leaf nodes
|
||||||
|
const extractLeaves = (el: HTMLElement) => {
|
||||||
|
// 1. Define "solid" elements that should fall as one single piece
|
||||||
|
const isSolid = ["SVG", "IMG", "BUTTON", "IFRAME", "A"].includes(
|
||||||
|
el.tagName.toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Base case: If it's solid, or has no children, it's a target
|
||||||
|
if (isSolid || el.children.length === 0) {
|
||||||
|
// Only extract elements that actually take up visual space
|
||||||
|
if (el.offsetWidth > 0 && el.offsetHeight > 0) {
|
||||||
|
targetElements.push(el);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.width = `${el.offsetWidth}px`;
|
||||||
|
el.style.height = `${el.offsetHeight}px`;
|
||||||
|
|
||||||
|
Array.from(el.children).forEach((child) => {
|
||||||
|
extractLeaves(child as HTMLElement);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const htmlContainer = container as HTMLElement;
|
||||||
|
|
||||||
|
// Lock the main container's dimensions
|
||||||
|
htmlContainer.style.width = `${htmlContainer.offsetWidth}px`;
|
||||||
|
htmlContainer.style.height = `${htmlContainer.offsetHeight}px`;
|
||||||
|
|
||||||
|
// Start the recursive extraction on its direct children
|
||||||
|
Array.from(htmlContainer.children).forEach((child) => {
|
||||||
|
extractLeaves(child as HTMLElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialStates = targetElements.map((el) => {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return { el, rect };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bodies = initialStates.map(({ el, rect }) => {
|
||||||
|
// Lock dimensions so it doesn't warp when pulled out of flex/grid
|
||||||
|
el.style.width = `${rect.width}px`;
|
||||||
|
el.style.height = `${rect.height}px`;
|
||||||
|
el.style.margin = "0px";
|
||||||
|
|
||||||
|
// Snap to fixed positioning at its exact current visual location
|
||||||
|
el.style.position = "fixed";
|
||||||
|
el.style.left = "0px";
|
||||||
|
el.style.top = "0px";
|
||||||
|
el.style.transform = `translate(${rect.left}px, ${rect.top}px)`;
|
||||||
|
|
||||||
|
const body: PhysicsBody = {
|
||||||
|
el: el,
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.top,
|
||||||
|
vx: (Math.random() - 0.5) * 8, // slight explosion outward
|
||||||
|
vy: (Math.random() - 0.5) * 5, // slight pop upward
|
||||||
|
isDragging: false,
|
||||||
|
startX: 0,
|
||||||
|
startY: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.attachMouseEvents(body);
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId);
|
||||||
|
|
||||||
|
// Reset all children back to their normal document flow
|
||||||
|
this.bodies.forEach((body) => {
|
||||||
|
body.el.style.transform = "";
|
||||||
|
body.el.style.position = "";
|
||||||
|
body.el.style.left = "";
|
||||||
|
body.el.style.top = "";
|
||||||
|
body.el.style.width = "";
|
||||||
|
body.el.style.height = "";
|
||||||
|
body.el.style.margin = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset parent containers
|
||||||
|
const containers = document.querySelectorAll(".gravity-body");
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const htmlContainer = container as HTMLElement;
|
||||||
|
htmlContainer.style.width = "";
|
||||||
|
htmlContainer.style.height = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bodies = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachMouseEvents(body: PhysicsBody) {
|
||||||
|
// Prevent default drag behaviors that interfere with physics
|
||||||
|
body.el.ondragstart = () => false;
|
||||||
|
|
||||||
|
body.el.addEventListener("pointerdown", (e) => {
|
||||||
|
body.isDragging = true;
|
||||||
|
body.startX = e.clientX;
|
||||||
|
body.startY = e.clientY;
|
||||||
|
body.vx = 0;
|
||||||
|
body.vy = 0;
|
||||||
|
body.el.setPointerCapture(e.pointerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
body.el.addEventListener("pointermove", (e) => {
|
||||||
|
if (!body.isDragging) return;
|
||||||
|
|
||||||
|
// Calculate velocity based on mouse movement for the "throw"
|
||||||
|
body.vx = e.movementX * 0.5;
|
||||||
|
body.vy = e.movementY * 0.5;
|
||||||
|
|
||||||
|
body.x += e.movementX;
|
||||||
|
body.y += e.movementY;
|
||||||
|
});
|
||||||
|
|
||||||
|
body.el.addEventListener("pointerup", (e) => {
|
||||||
|
body.isDragging = false;
|
||||||
|
body.el.releasePointerCapture(e.pointerId);
|
||||||
|
|
||||||
|
// The Drag vs. Click Resolver
|
||||||
|
const deltaX = Math.abs(e.clientX - body.startX);
|
||||||
|
const deltaY = Math.abs(e.clientY - body.startY);
|
||||||
|
|
||||||
|
// If the user moved the mouse less than 5px, treat it as a click
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
// It was a drag. Prevent links from firing.
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Catch clicks at the capture phase to stop them if we were dragging
|
||||||
|
body.el.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
|
const deltaX = Math.abs(e.clientX - body.startX);
|
||||||
|
const deltaY = Math.abs(e.clientY - body.startY);
|
||||||
|
if (deltaX > 5 || deltaY > 5) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tick = () => {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
const gravity = 0.5;
|
||||||
|
const bounce = -0.7;
|
||||||
|
const floorY = window.innerHeight;
|
||||||
|
|
||||||
|
this.bodies.forEach((body) => {
|
||||||
|
if (body.isDragging) {
|
||||||
|
// Only update visually while dragging, physics are paused
|
||||||
|
body.el.style.position = "fixed";
|
||||||
|
body.el.style.left = "0px";
|
||||||
|
body.el.style.top = "0px";
|
||||||
|
body.el.style.transform = `translate(${body.x}px, ${body.y}px)`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Gravity
|
||||||
|
body.vy += gravity;
|
||||||
|
body.x += body.vx;
|
||||||
|
body.y += body.vy;
|
||||||
|
|
||||||
|
// Apply Air Friction
|
||||||
|
body.vx *= 0.99;
|
||||||
|
body.vy *= 0.99;
|
||||||
|
|
||||||
|
const rect = body.el.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Floor Collision
|
||||||
|
if (body.y + rect.height > floorY) {
|
||||||
|
body.y = floorY - rect.height;
|
||||||
|
body.vy *= bounce;
|
||||||
|
body.vx *= 0.9; // Ground friction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Step
|
||||||
|
body.el.style.position = "fixed";
|
||||||
|
body.el.style.left = "0px";
|
||||||
|
body.el.style.top = "0px";
|
||||||
|
body.el.style.transform = `translate(${body.x}px, ${body.y}px)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(this.tick);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user