Skip to content

Commit 74c1f5a

Browse files
committed
app: abstract waves and topo maps
1 parent 89ad996 commit 74c1f5a

File tree

5 files changed

+722
-0
lines changed

5 files changed

+722
-0
lines changed

public/apps/apps.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,24 @@
297297
"pinned_order": 18,
298298
"created_at": "2025-11-29T15:00:00+03:00"
299299
},
300+
{
301+
"slug": "abstract-waves",
302+
"to": "/apps/abstract-waves",
303+
"title": "Abstract Waves",
304+
"description": "Generate mesmerizing black and white abstract wave patterns.",
305+
"icon": "WaveSine",
306+
"pinned_order": 19,
307+
"created_at": "2025-12-01T12:00:00+03:00"
308+
},
309+
{
310+
"slug": "topographic-maps",
311+
"to": "/apps/topographic-maps",
312+
"title": "Topographic Map Generator",
313+
"description": "Generate seamless topographic contour maps.",
314+
"icon": "MapTrifold",
315+
"pinned_order": 20,
316+
"created_at": "2025-12-01T12:30:00+03:00"
317+
},
300318
{
301319
"slug": "color-palette-generator",
302320
"to": "/apps/color-palette-generator",

src/components/AnimatedRoutes.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ const NotepadPage = lazy(() => import('../pages/apps/NotepadPage'));
126126
const CozyAppPage = lazy(() => import('../pages/apps/CozyAppPage'));
127127
const SpirographPage = lazy(() => import('../pages/apps/SpirographPage'));
128128
const FractalFloraPage = lazy(() => import('../pages/apps/FractalFloraPage'));
129+
const AbstractWavesPage = lazy(() => import('../pages/apps/AbstractWavesPage'));
130+
const TopographicMapPage = lazy(() => import('../pages/apps/TopographicMapPage'));
129131
const FezynthPage = lazy(() => import('../pages/apps/FezynthPage'));
130132
const CodeSeancePage = lazy(() => import('../pages/apps/CodeSeancePage'));
131133
const RoadmapViewerPage = lazy(() => import('../pages/roadmap/FezzillaPage'));
@@ -808,6 +810,14 @@ function AnimatedRoutes() {
808810
path="/apps::flora"
809811
element={<Navigate to="/apps/fractal-flora" replace />}
810812
/>
813+
<Route
814+
path="/apps::aw"
815+
element={<Navigate to="/apps/abstract-waves" replace />}
816+
/>
817+
<Route
818+
path="/apps::topo"
819+
element={<Navigate to="/apps/topographic-maps" replace />}
820+
/>
811821
{/* End of hardcoded redirects */}
812822
<Route
813823
path="/apps/ip"
@@ -1754,6 +1764,38 @@ function AnimatedRoutes() {
17541764
</motion.div>
17551765
}
17561766
/>
1767+
<Route
1768+
path="/apps/abstract-waves"
1769+
element={
1770+
<motion.div
1771+
initial="initial"
1772+
animate="in"
1773+
exit="out"
1774+
variants={pageVariants}
1775+
transition={pageTransition}
1776+
>
1777+
<Suspense fallback={<Loading />}>
1778+
<AbstractWavesPage />
1779+
</Suspense>
1780+
</motion.div>
1781+
}
1782+
/>
1783+
<Route
1784+
path="/apps/topographic-maps"
1785+
element={
1786+
<motion.div
1787+
initial="initial"
1788+
animate="in"
1789+
exit="out"
1790+
variants={pageVariants}
1791+
transition={pageTransition}
1792+
>
1793+
<Suspense fallback={<Loading />}>
1794+
<TopographicMapPage />
1795+
</Suspense>
1796+
</motion.div>
1797+
}
1798+
/>
17571799
<Route
17581800
path="/roadmap"
17591801
element={
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import React, {useState, useEffect, useRef, useCallback} from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
DownloadSimple,
6+
ArrowsClockwise,
7+
} from '@phosphor-icons/react';
8+
import useSeo from '../../hooks/useSeo';
9+
import {useToast} from '../../hooks/useToast';
10+
import BreadcrumbTitle from '../../components/BreadcrumbTitle';
11+
12+
const AbstractWavesPage = () => {
13+
useSeo({
14+
title: 'Abstract Waves | Fezcodex',
15+
description: 'Generate mesmerizing black and white abstract wave patterns.',
16+
keywords: ['waves', 'generative art', 'abstract', 'black and white', 'canvas'],
17+
});
18+
19+
const {addToast} = useToast();
20+
const canvasRef = useRef(null);
21+
22+
// --- Parameters ---
23+
const [lineCount, setLineCount] = useState(50);
24+
const [amplitude, setAmplitude] = useState(50);
25+
const [frequency, setFrequency] = useState(0.02);
26+
const [perspective, setPerspective] = useState(10); // Spacing
27+
const [noise, setNoise] = useState(10); // Random offset
28+
const [phase, setPhase] = useState(0);
29+
const [lineWidth, setLineWidth] = useState(2);
30+
const [fill, setFill] = useState(true); // Fill below line to hide lines behind
31+
const [inverted, setInverted] = useState(false);
32+
33+
const drawWaves = useCallback(
34+
(ctx, w, h) => {
35+
const bgColor = inverted ? '#000000' : '#ffffff';
36+
const lineColor = inverted ? '#ffffff' : '#000000';
37+
38+
ctx.fillStyle = bgColor;
39+
ctx.fillRect(0, 0, w, h);
40+
41+
ctx.lineWidth = lineWidth;
42+
ctx.strokeStyle = lineColor;
43+
ctx.fillStyle = bgColor; // For hiding lines behind
44+
45+
// Center vertically roughly
46+
const totalHeight = lineCount * perspective;
47+
const startY = (h - totalHeight) / 2;
48+
49+
for (let i = 0; i < lineCount; i++) {
50+
const yBase = startY + i * perspective;
51+
52+
ctx.beginPath();
53+
for (let x = 0; x <= w; x += 5) { // Step size 5 for performance
54+
// Create a complex wave by combining sine waves and noise
55+
const distFromCenter = Math.abs(x - w / 2);
56+
const dampener = Math.max(0, 1 - distFromCenter / (w / 2)); // 1 at center, 0 at edges
57+
58+
// Main wave
59+
let y = yBase + Math.sin(x * frequency + phase + (i * 0.1)) * amplitude * dampener;
60+
61+
// Add some "noise" or irregularity
62+
y += Math.cos(x * frequency * 2.5 + (i * 0.5)) * (noise * dampener);
63+
64+
if (x === 0) {
65+
ctx.moveTo(x, y);
66+
} else {
67+
ctx.lineTo(x, y);
68+
}
69+
}
70+
71+
if (fill) {
72+
// Close the path for filling to hide lines behind
73+
ctx.lineTo(w, w); // Down to bottom right (roughly) - actually just needs to be low enough
74+
ctx.lineTo(0, w); // Back to bottom left
75+
ctx.closePath();
76+
ctx.fill();
77+
78+
// Re-stroke the top line
79+
ctx.stroke();
80+
} else {
81+
ctx.stroke();
82+
}
83+
}
84+
},
85+
[lineCount, amplitude, frequency, perspective, noise, phase, lineWidth, fill, inverted]
86+
);
87+
88+
// Effect to redraw
89+
useEffect(() => {
90+
const canvas = canvasRef.current;
91+
if (!canvas) return;
92+
const ctx = canvas.getContext('2d');
93+
const width = canvas.width;
94+
const height = canvas.height;
95+
96+
let rafId = requestAnimationFrame(() => drawWaves(ctx, width, height));
97+
return () => cancelAnimationFrame(rafId);
98+
}, [drawWaves]);
99+
100+
// Init Canvas Size
101+
useEffect(() => {
102+
const canvas = canvasRef.current;
103+
if (canvas) {
104+
canvas.width = 1200;
105+
canvas.height = 800;
106+
}
107+
}, []);
108+
109+
const handleDownload = () => {
110+
const canvas = canvasRef.current;
111+
const link = document.createElement('a');
112+
link.download = `abstract_waves_${Date.now()}.png`;
113+
link.href = canvas.toDataURL();
114+
link.click();
115+
addToast({
116+
title: 'Saved',
117+
message: 'Wave pattern saved to device.',
118+
duration: 3000,
119+
});
120+
};
121+
122+
const randomizeParams = () => {
123+
setLineCount(30 + Math.floor(Math.random() * 50));
124+
setAmplitude(20 + Math.random() * 80);
125+
setFrequency(0.005 + Math.random() * 0.04);
126+
setPerspective(5 + Math.random() * 20);
127+
setNoise(Math.random() * 40);
128+
setPhase(Math.random() * Math.PI * 2);
129+
setInverted(Math.random() > 0.5);
130+
};
131+
132+
return (
133+
<div className="min-h-screen bg-gray-50 flex flex-col">
134+
<div className="container mx-auto px-4 py-8 flex-grow flex flex-col max-w-6xl">
135+
<Link
136+
to="/apps"
137+
className="group text-primary-400 hover:underline flex items-center justify-center gap-2 text-lg mb-4"
138+
>
139+
<ArrowLeftIcon className="text-xl transition-transform group-hover:-translate-x-1"/>{' '}
140+
Back to Apps
141+
</Link>
142+
<BreadcrumbTitle title="Abstract Waves" slug="aw"/>
143+
144+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 flex-grow">
145+
{/* Controls */}
146+
<div className="space-y-6 bg-white p-6 rounded-xl shadow-md border border-gray-200 h-fit">
147+
<div className="flex gap-2 mb-4">
148+
<button
149+
onClick={randomizeParams}
150+
className="flex-1 py-2 px-4 bg-gray-800 text-white rounded-lg hover:bg-gray-900 flex items-center justify-center gap-2 transition-colors"
151+
>
152+
<ArrowsClockwise size={20}/> Randomize
153+
</button>
154+
<button
155+
onClick={handleDownload}
156+
className="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200"
157+
title="Download"
158+
>
159+
<DownloadSimple size={24}/>
160+
</button>
161+
</div>
162+
163+
{/* Toggles */}
164+
<div className="flex gap-4">
165+
<label className="flex items-center gap-2 cursor-pointer">
166+
<input
167+
type="checkbox"
168+
checked={inverted}
169+
onChange={(e) => setInverted(e.target.checked)}
170+
className="w-4 h-4 accent-black"
171+
/>
172+
<span className="text-sm font-medium text-gray-700">Invert Colors</span>
173+
</label>
174+
<label className="flex items-center gap-2 cursor-pointer">
175+
<input
176+
type="checkbox"
177+
checked={fill}
178+
onChange={(e) => setFill(e.target.checked)}
179+
className="w-4 h-4 accent-black"
180+
/>
181+
<span className="text-sm font-medium text-gray-700">Opaque Lines</span>
182+
</label>
183+
</div>
184+
185+
<div className="space-y-4">
186+
<div>
187+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
188+
<span>Line Count</span>
189+
<span className="text-gray-500">{lineCount}</span>
190+
</label>
191+
<input
192+
type="range"
193+
min="10"
194+
max="100"
195+
value={lineCount}
196+
onChange={(e) => setLineCount(Number(e.target.value))}
197+
className="w-full accent-black"
198+
/>
199+
</div>
200+
<div>
201+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
202+
<span>Amplitude (Height)</span>
203+
<span className="text-gray-500">{Math.round(amplitude)}</span>
204+
</label>
205+
<input
206+
type="range"
207+
min="0"
208+
max="150"
209+
value={amplitude}
210+
onChange={(e) => setAmplitude(Number(e.target.value))}
211+
className="w-full accent-black"
212+
/>
213+
</div>
214+
<div>
215+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
216+
<span>Frequency (Width)</span>
217+
<span className="text-gray-500">{frequency.toFixed(3)}</span>
218+
</label>
219+
<input
220+
type="range"
221+
min="0.001"
222+
max="0.05"
223+
step="0.001"
224+
value={frequency}
225+
onChange={(e) => setFrequency(Number(e.target.value))}
226+
className="w-full accent-black"
227+
/>
228+
</div>
229+
<div>
230+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
231+
<span>Spacing</span>
232+
<span className="text-gray-500">{perspective}px</span>
233+
</label>
234+
<input
235+
type="range"
236+
min="2"
237+
max="40"
238+
value={perspective}
239+
onChange={(e) => setPerspective(Number(e.target.value))}
240+
className="w-full accent-black"
241+
/>
242+
</div>
243+
<div>
244+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
245+
<span>Noise / Distortion</span>
246+
<span className="text-gray-500">{Math.round(noise)}</span>
247+
</label>
248+
<input
249+
type="range"
250+
min="0"
251+
max="100"
252+
value={noise}
253+
onChange={(e) => setNoise(Number(e.target.value))}
254+
className="w-full accent-black"
255+
/>
256+
</div>
257+
<div>
258+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
259+
<span>Phase Shift</span>
260+
<span className="text-gray-500">{phase.toFixed(1)}</span>
261+
</label>
262+
<input
263+
type="range"
264+
min="0"
265+
max="10"
266+
step="0.1"
267+
value={phase}
268+
onChange={(e) => setPhase(Number(e.target.value))}
269+
className="w-full accent-black"
270+
/>
271+
</div>
272+
<div>
273+
<label className="flex justify-between text-sm font-medium text-gray-700 mb-1">
274+
<span>Line Width</span>
275+
<span className="text-gray-500">{lineWidth}px</span>
276+
</label>
277+
<input
278+
type="range"
279+
min="1"
280+
max="10"
281+
value={lineWidth}
282+
onChange={(e) => setLineWidth(Number(e.target.value))}
283+
className="w-full accent-black"
284+
/>
285+
</div>
286+
</div>
287+
<div className="bg-gray-100 p-3 rounded-lg text-xs text-gray-600">
288+
<strong>Tip:</strong> "Opaque Lines" hides the waves behind the current one, creating a 3D landscape
289+
effect similar to the famous "Unknown Pleasures" album cover.
290+
</div>
291+
</div>
292+
293+
{/* Canvas */}
294+
<div
295+
className="lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-200 p-4 flex items-center justify-center overflow-hidden">
296+
<canvas
297+
ref={canvasRef}
298+
className="max-w-full h-auto rounded-lg shadow-inner border border-gray-100"
299+
style={{maxHeight: '600px'}}
300+
/>
301+
</div>
302+
</div>
303+
</div>
304+
</div>
305+
);
306+
};
307+
308+
export default AbstractWavesPage;

0 commit comments

Comments
 (0)