Skip to content

Commit 5db8ff9

Browse files
Final Commit (ive spent too long on this)
1 parent dca0fe3 commit 5db8ff9

File tree

14 files changed

+530
-6
lines changed

14 files changed

+530
-6
lines changed

apps/blade/src/app/jesusgonzalez/_components/CustomHero.tsx renamed to apps/blade/src/app/jesusgonzalez/components/CustomHero.tsx

File renamed without changes.

apps/blade/src/app/jesusgonzalez/_components/Footer.tsx renamed to apps/blade/src/app/jesusgonzalez/components/Footer.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ const Footer = () => {
4545
},
4646
];
4747

48-
const currentDate: Date = new Date();
49-
5048
return (
5149
<footer className="flex justify-center bg-background p-5">
5250
<div className="flex max-w-fit items-center justify-between space-x-4 rounded-2xl border bg-card p-5 px-6 py-3">
@@ -55,7 +53,7 @@ const Footer = () => {
5553
<p>-</p>
5654
<Badge className="flex gap-1" variant='outline' >
5755
<ClockIcon />
58-
<p>{currentDate.toLocaleDateString()}</p>
56+
<p>01/07/2006</p>
5957
</Badge>
6058
<Separator orientation="vertical" />
6159

apps/blade/src/app/jesusgonzalez/_components/NavBar.tsx renamed to apps/blade/src/app/jesusgonzalez/components/NavBar.tsx

File renamed without changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from "react";
2+
import Link from "next/link";
3+
import { Badge } from "@forge/ui/badge";
4+
import {
5+
Card,
6+
CardDescription,
7+
CardFooter,
8+
CardHeader,
9+
CardTitle,
10+
} from "@forge/ui/card";
11+
12+
export interface Project {
13+
title: string,
14+
href: string,
15+
desc: string;
16+
stack: string[];
17+
}
18+
19+
const ProjectCard = ({ title, href, desc, stack }: Project) => {
20+
return (
21+
<div>
22+
<Card>
23+
<CardHeader>
24+
<CardTitle>
25+
<Link className='hover:underline cursor-pointer' href={href}>{title}</Link>
26+
</CardTitle>
27+
<CardDescription>{desc}</CardDescription>
28+
</CardHeader>
29+
<CardFooter className="flex flex-wrap gap-2">
30+
{stack.map((val, index) => (
31+
<Badge key={index} className="whitespace-nowrap">
32+
{val}
33+
</Badge>
34+
))}
35+
</CardFooter>
36+
</Card>
37+
</div>
38+
);
39+
};
40+
41+
export default ProjectCard;

apps/blade/src/app/jesusgonzalez/_components/TextScroll.tsx renamed to apps/blade/src/app/jesusgonzalez/components/TextScroll.tsx

File renamed without changes.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from "react";
2+
3+
import type { Vector2 } from "./types";
4+
5+
interface ArrowProps {
6+
start: Vector2;
7+
vector: Vector2;
8+
scale?: number;
9+
color?: string;
10+
}
11+
12+
const Arrow = ({
13+
start,
14+
vector,
15+
scale = 10,
16+
color = "#8b5cf6",
17+
}: ArrowProps) => {
18+
const end = {
19+
x: start.x + vector.x * scale,
20+
y: start.y - vector.y * scale,
21+
};
22+
const angle: number = Math.atan2(-vector.y, vector.x);
23+
const headLength = 4;
24+
25+
const head1 = {
26+
x: end.x - headLength * Math.cos(angle - Math.PI / 6),
27+
y: end.y - headLength * Math.sin(angle - Math.PI / 6),
28+
};
29+
const head2 = {
30+
x: end.x - headLength * Math.cos(angle + Math.PI / 6),
31+
y: end.y - headLength * Math.sin(angle + Math.PI / 6),
32+
};
33+
34+
return (
35+
<g>
36+
<line
37+
x1={start.x}
38+
y1={start.y}
39+
x2={end.x}
40+
y2={end.y}
41+
stroke={color}
42+
strokeWidth={1.5}
43+
/>
44+
<polygon
45+
points={`${end.x},${end.y} ${head1.x} ${head1.y} ${head2.x} ${head2.y}`}
46+
fill={color}
47+
/>
48+
</g>
49+
);
50+
};
51+
52+
export default Arrow;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import type { FieldMode } from './types';
5+
6+
interface FieldModeSelectorProps {
7+
mode: FieldMode;
8+
onModeChange: (mode: FieldMode) => void;
9+
}
10+
11+
const modes: { value: FieldMode; label: string }[] = [
12+
{ value: 'mouse', label: 'Mouse Follow' },
13+
{ value: 'swirl', label: 'Swirl' },
14+
{ value: 'radial', label: 'Radial' },
15+
{ value: 'sink', label: 'Sink' },
16+
];
17+
18+
const FieldModeSelector = ({ mode, onModeChange }: FieldModeSelectorProps) => {
19+
return (
20+
<div className="bg-slate-800/50 rounded-lg p-1 flex gap-1">
21+
{modes.map((m) => (
22+
<button
23+
key={m.value}
24+
onClick={() => onModeChange(m.value)}
25+
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
26+
mode === m.value
27+
? 'bg-violet-500 text-white'
28+
: 'text-gray-400 hover:bg-slate-700/50'
29+
}`}
30+
>
31+
{m.label}
32+
</button>
33+
))}
34+
</div>
35+
);
36+
};
37+
38+
export default FieldModeSelector;
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
'use client';
2+
3+
import React, { useState, useRef, useEffect, useCallback } from 'react';
4+
import type { Bounds, VectorFieldFn, Vector2 } from './types';
5+
import {
6+
generateGridPoint,
7+
mapToScreen,
8+
screenToMath,
9+
sinkField,
10+
createMouseFollowField,
11+
} from './fields';
12+
13+
interface InteractiveVectorFieldProps {
14+
height?: number;
15+
bounds?: Bounds;
16+
step?: number;
17+
arrowScale?: number;
18+
color?: string;
19+
}
20+
21+
const lerpAngle = (current: number, target: number, t: number): number => {
22+
let diff = target - current;
23+
while (diff > Math.PI) diff -= 2 * Math.PI;
24+
while (diff < -Math.PI) diff += 2 * Math.PI;
25+
return current + diff * t;
26+
};
27+
28+
const drawArrow = (
29+
ctx: CanvasRenderingContext2D,
30+
start: Vector2,
31+
vector: Vector2,
32+
scale: number,
33+
color: string
34+
) => {
35+
const end = {
36+
x: start.x + vector.x * scale,
37+
y: start.y - vector.y * scale,
38+
};
39+
40+
const dx = end.x - start.x;
41+
const dy = end.y - start.y;
42+
const angle = Math.atan2(dy, dx);
43+
const headLength = 4;
44+
45+
ctx.beginPath();
46+
ctx.moveTo(start.x, start.y);
47+
ctx.lineTo(end.x, end.y);
48+
ctx.strokeStyle = color;
49+
ctx.lineWidth = 1.5;
50+
ctx.stroke();
51+
52+
ctx.beginPath();
53+
ctx.moveTo(end.x, end.y);
54+
ctx.lineTo(
55+
end.x - headLength * Math.cos(angle - Math.PI / 6),
56+
end.y - headLength * Math.sin(angle - Math.PI / 6)
57+
);
58+
ctx.lineTo(
59+
end.x - headLength * Math.cos(angle + Math.PI / 6),
60+
end.y - headLength * Math.sin(angle + Math.PI / 6)
61+
);
62+
ctx.closePath();
63+
ctx.fillStyle = color;
64+
ctx.fill();
65+
};
66+
67+
const BORDER_PADDING = 20;
68+
69+
const InteractiveVectorField = ({
70+
height = 250,
71+
bounds = { xMin: -5, xMax: 5, yMin: -5, yMax: 5 },
72+
step = 0.8,
73+
arrowScale = 12,
74+
color = '#a78bfa',
75+
}: InteractiveVectorFieldProps) => {
76+
const [mousePos, setMousePos] = useState<Vector2 | null>(null);
77+
const [dimensions, setDimensions] = useState({ width: 800, height });
78+
const totalHeight = height + BORDER_PADDING * 2;
79+
80+
const canvasRef = useRef<HTMLCanvasElement>(null);
81+
const containerRef = useRef<HTMLDivElement>(null);
82+
const animationRef = useRef<number>(0);
83+
const currentAnglesRef = useRef<Map<string, number>>(new Map());
84+
85+
const getField = useCallback((): VectorFieldFn => {
86+
if (mousePos) {
87+
return createMouseFollowField(mousePos.x, mousePos.y);
88+
}
89+
return sinkField;
90+
}, [mousePos]);
91+
92+
useEffect(() => {
93+
const container = containerRef.current;
94+
if (!container) return;
95+
96+
const resizeObserver = new ResizeObserver((entries) => {
97+
const entry = entries[0];
98+
if (!entry) return;
99+
const width = entry.contentRect.width;
100+
setDimensions({ width, height });
101+
});
102+
103+
resizeObserver.observe(container);
104+
return () => resizeObserver.disconnect();
105+
}, [height]);
106+
107+
useEffect(() => {
108+
const canvas = canvasRef.current;
109+
if (!canvas) return;
110+
111+
const ctx = canvas.getContext('2d');
112+
if (!ctx) return;
113+
114+
const dpr = window.devicePixelRatio || 1;
115+
canvas.width = dimensions.width * dpr;
116+
canvas.height = totalHeight * dpr;
117+
ctx.scale(dpr, dpr);
118+
119+
const points = generateGridPoint(
120+
bounds.xMin,
121+
bounds.xMax,
122+
bounds.yMin,
123+
bounds.yMax,
124+
step
125+
);
126+
127+
const lerpSpeed = 0.04;
128+
129+
const render = () => {
130+
ctx.clearRect(0, 0, dimensions.width, totalHeight);
131+
132+
// Draw dashed border lines at outer edges
133+
ctx.setLineDash([8, 6]);
134+
ctx.strokeStyle = '#4b5563';
135+
ctx.lineWidth = 1;
136+
137+
// Top border
138+
ctx.beginPath();
139+
ctx.moveTo(0, 0.5);
140+
ctx.lineTo(dimensions.width, 0.5);
141+
ctx.stroke();
142+
143+
// Bottom border
144+
ctx.beginPath();
145+
ctx.moveTo(0, totalHeight - 0.5);
146+
ctx.lineTo(dimensions.width, totalHeight - 0.5);
147+
ctx.stroke();
148+
149+
ctx.setLineDash([]);
150+
151+
const field = getField();
152+
153+
points.forEach((point) => {
154+
const key = `${point.x},${point.y}`;
155+
const screenPos = mapToScreen(point, bounds, dimensions.width, height);
156+
// Offset Y position for border padding
157+
screenPos.y += BORDER_PADDING;
158+
const vector = field(point.x, point.y);
159+
160+
const mag = Math.sqrt(vector.x ** 2 + vector.y ** 2) || 1;
161+
const normalized = { x: vector.x / mag, y: vector.y / mag };
162+
163+
// Calculate target angle (in screen coordinates, y is flipped)
164+
const targetAngle = Math.atan2(-normalized.y, normalized.x);
165+
166+
// Get or initialize current angle
167+
let currentAngle = currentAnglesRef.current.get(key);
168+
if (currentAngle === undefined) {
169+
currentAngle = targetAngle;
170+
}
171+
172+
// Lerp toward target
173+
currentAngle = lerpAngle(currentAngle, targetAngle, lerpSpeed);
174+
currentAnglesRef.current.set(key, currentAngle);
175+
176+
// Convert back to vector
177+
const smoothedVector = {
178+
x: Math.cos(currentAngle),
179+
y: -Math.sin(currentAngle),
180+
};
181+
182+
drawArrow(ctx, screenPos, smoothedVector, arrowScale, color);
183+
});
184+
185+
animationRef.current = requestAnimationFrame(render);
186+
};
187+
188+
render();
189+
190+
return () => {
191+
cancelAnimationFrame(animationRef.current);
192+
};
193+
}, [dimensions, bounds, step, arrowScale, color, getField, height, totalHeight]);
194+
195+
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
196+
const canvas = canvasRef.current;
197+
if (!canvas) return;
198+
199+
const rect = canvas.getBoundingClientRect();
200+
const screenX = e.clientX - rect.left;
201+
const screenY = e.clientY - rect.top - BORDER_PADDING;
202+
203+
const mathPos = screenToMath(screenX, screenY, bounds, dimensions.width, height);
204+
setMousePos(mathPos);
205+
};
206+
207+
const handleMouseLeave = () => {
208+
setMousePos(null);
209+
};
210+
211+
return (
212+
<div ref={containerRef} className="w-full">
213+
<canvas
214+
ref={canvasRef}
215+
style={{ width: dimensions.width, height: totalHeight }}
216+
className="cursor-none"
217+
onMouseMove={handleMouseMove}
218+
onMouseLeave={handleMouseLeave}
219+
/>
220+
</div>
221+
);
222+
};
223+
224+
export default InteractiveVectorField;

0 commit comments

Comments
 (0)