feat: reworked HeroSection background

This commit is contained in:
Melvin Ragusa
2026-01-22 00:23:52 +01:00
parent f2250ab65e
commit 4f07eef844
31 changed files with 665 additions and 4234 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
build/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#0F1410"/>
<text x="50" y="62" font-family="system-ui, sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#E1E3DF">R</text>
<text x="68" y="62" font-family="system-ui, sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#7FD998">IT</text>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

View File

@@ -2,7 +2,7 @@
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Ragusa IT-Consulting - Professionelle Webentwicklung, IT-Beratung und technischer Support" />
<meta name="theme-color" content="#0F1410" />
@@ -10,10 +10,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-DpUrPOvK.js"></script>
<link rel="modulepreload" crossorigin href="/assets/three-A7r-9XvZ.js">
<script type="module" crossorigin src="/assets/index-VO0wVEDp.js"></script>
<link rel="modulepreload" crossorigin href="/assets/three-eMwtdKkp.js">
<link rel="modulepreload" crossorigin href="/assets/motion-BIOHP8Ul.js">
<link rel="stylesheet" crossorigin href="/assets/index-D4N8_we_.css">
<link rel="stylesheet" crossorigin href="/assets/index-BAFm1LaZ.css">
</head>
<body>
<div id="root"></div>

BIN
build/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

9
build/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 224 KiB

View File

@@ -2,7 +2,7 @@
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Ragusa IT-Consulting - Professionelle Webentwicklung, IT-Beratung und technischer Support" />
<meta name="theme-color" content="#0F1410" />

View File

@@ -18,7 +18,9 @@
"@emailjs/browser": "^4.4.1",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@react-three/rapier": "^2.2.0",
"motion": "^12.28.1",
"ogl": "^1.0.11",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",

44
pnpm-lock.yaml generated
View File

@@ -17,15 +17,24 @@ importers:
'@react-three/fiber':
specifier: ^9.5.0
version: 9.5.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0)
'@react-three/rapier':
specifier: ^2.2.0
version: 2.2.0(@react-three/fiber@9.5.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0))(react@19.2.3)(three@0.182.0)
motion:
specifier: ^12.28.1
version: 12.28.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
ogl:
specifier: ^1.0.11
version: 1.0.11
react:
specifier: ^19.2.3
version: 19.2.3
react-dom:
specifier: ^19.2.3
version: 19.2.3(react@19.2.3)
react-icons:
specifier: ^5.5.0
version: 5.5.0(react@19.2.3)
react-router-dom:
specifier: ^7.12.0
version: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -144,6 +153,9 @@ packages:
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@dimforge/rapier3d-compat@0.19.2':
resolution: {integrity: sha512-AZHL1jqUF55QJkJyU1yKeh4ImX2J93bVLIezT1+o0FZqTix6O06MOaqpKoJ4MmbDCsoZmwO+qc471/SDMDm2AA==}
'@emailjs/browser@4.4.1':
resolution: {integrity: sha512-DGSlP9sPvyFba3to2A50kDtZ+pXVp/0rhmqs2LmbMS3I5J8FSOgLwzY2Xb4qfKlOVHh29EAutLYwe5yuEZmEFg==}
engines: {node: '>=14.0.0'}
@@ -364,6 +376,13 @@ packages:
react-native:
optional: true
'@react-three/rapier@2.2.0':
resolution: {integrity: sha512-mVsqbKXlGZoN+XrqdhzFZUQmy8pibEOVzl4k7LC+LHe84bQnYBSagy1Hvbda6bL1PJDdTFyiDiBk5buKFinNIQ==}
peerDependencies:
'@react-three/fiber': ^9.0.4
react: ^19
three: '>=0.159.0'
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
@@ -753,6 +772,9 @@ packages:
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
ogl@1.0.11:
resolution: {integrity: sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
@@ -779,6 +801,11 @@ packages:
peerDependencies:
react: ^19.2.3
react-icons@5.5.0:
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
peerDependencies:
react: '*'
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@@ -1119,6 +1146,8 @@ snapshots:
'@dimforge/rapier3d-compat@0.12.0': {}
'@dimforge/rapier3d-compat@0.19.2': {}
'@emailjs/browser@4.4.1': {}
'@esbuild/aix-ppc64@0.27.2':
@@ -1278,6 +1307,15 @@ snapshots:
- '@types/react'
- immer
'@react-three/rapier@2.2.0(@react-three/fiber@9.5.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0))(react@19.2.3)(three@0.182.0)':
dependencies:
'@dimforge/rapier3d-compat': 0.19.2
'@react-three/fiber': 9.5.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.182.0)
react: 19.2.3
suspend-react: 0.1.3(react@19.2.3)
three: 0.182.0
three-stdlib: 2.36.1(three@0.182.0)
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/rollup-android-arm-eabi@4.55.3':
@@ -1603,6 +1641,8 @@ snapshots:
node-releases@2.0.27: {}
ogl@1.0.11: {}
path-key@3.1.1: {}
picocolors@1.1.1: {}
@@ -1627,6 +1667,10 @@ snapshots:
react: 19.2.3
scheduler: 0.27.0
react-icons@5.5.0(react@19.2.3):
dependencies:
react: 19.2.3
react-refresh@0.18.0: {}
react-router-dom@7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#0F1410"/>
<text x="50" y="62" font-family="system-ui, sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#E1E3DF">R</text>
<text x="68" y="62" font-family="system-ui, sans-serif" font-size="36" font-weight="700" text-anchor="middle" fill="#7FD998">IT</text>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

9
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 224 KiB

View File

@@ -0,0 +1,6 @@
.gradient-blinds-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@@ -0,0 +1,389 @@
import React, { useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Triangle } from 'ogl';
import './GradientBlinds.css';
export interface GradientBlindsProps {
className?: string;
dpr?: number;
paused?: boolean;
gradientColors?: string[];
angle?: number;
noise?: number;
blindCount?: number;
blindMinWidth?: number;
mouseDampening?: number;
mirrorGradient?: boolean;
spotlightRadius?: number;
spotlightSoftness?: number;
spotlightOpacity?: number;
distortAmount?: number;
shineDirection?: 'left' | 'right';
mixBlendMode?: string;
}
const MAX_COLORS = 8;
const hexToRGB = (hex: string): [number, number, number] => {
const c = hex.replace('#', '').padEnd(6, '0');
const r = parseInt(c.slice(0, 2), 16) / 255;
const g = parseInt(c.slice(2, 4), 16) / 255;
const b = parseInt(c.slice(4, 6), 16) / 255;
return [r, g, b];
};
const prepStops = (stops?: string[]) => {
const base = (stops && stops.length ? stops : ['#FF9FFC', '#5227FF']).slice(0, MAX_COLORS);
if (base.length === 1) base.push(base[0]);
while (base.length < MAX_COLORS) base.push(base[base.length - 1]);
const arr: [number, number, number][] = [];
for (let i = 0; i < MAX_COLORS; i++) arr.push(hexToRGB(base[i]));
const count = Math.max(2, Math.min(MAX_COLORS, stops?.length ?? 2));
return { arr, count };
};
const GradientBlinds: React.FC<GradientBlindsProps> = ({
className,
dpr,
paused = false,
gradientColors,
angle = 0,
noise = 0.3,
blindCount = 16,
blindMinWidth = 60,
mouseDampening = 0.15,
mirrorGradient = false,
spotlightRadius = 0.5,
spotlightSoftness = 1,
spotlightOpacity = 1,
distortAmount = 0,
shineDirection = 'left',
mixBlendMode = 'lighten'
}) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const rafRef = useRef<number | null>(null);
const programRef = useRef<Program | null>(null);
const meshRef = useRef<Mesh<Triangle> | null>(null);
const geometryRef = useRef<Triangle | null>(null);
const rendererRef = useRef<Renderer | null>(null);
const mouseTargetRef = useRef<[number, number]>([0, 0]);
const lastTimeRef = useRef<number>(0);
const firstResizeRef = useRef<boolean>(true);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const renderer = new Renderer({
dpr: dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1),
alpha: true,
antialias: true
});
rendererRef.current = renderer;
const gl = renderer.gl;
const canvas = gl.canvas as HTMLCanvasElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
container.appendChild(canvas);
const vertex = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragment = `
#ifdef GL_ES
precision mediump float;
#endif
uniform vec3 iResolution;
uniform vec2 iMouse;
uniform float iTime;
uniform float uAngle;
uniform float uNoise;
uniform float uBlindCount;
uniform float uSpotlightRadius;
uniform float uSpotlightSoftness;
uniform float uSpotlightOpacity;
uniform float uMirror;
uniform float uDistort;
uniform float uShineFlip;
uniform vec3 uColor0;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
uniform vec3 uColor4;
uniform vec3 uColor5;
uniform vec3 uColor6;
uniform vec3 uColor7;
uniform int uColorCount;
varying vec2 vUv;
float rand(vec2 co){
return fract(sin(dot(co, vec2(12.9898,78.233))) * 43758.5453);
}
vec2 rotate2D(vec2 p, float a){
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c) * p;
}
vec3 getGradientColor(float t){
float tt = clamp(t, 0.0, 1.0);
int count = uColorCount;
if (count < 2) count = 2;
float scaled = tt * float(count - 1);
float seg = floor(scaled);
float f = fract(scaled);
if (seg < 1.0) return mix(uColor0, uColor1, f);
if (seg < 2.0 && count > 2) return mix(uColor1, uColor2, f);
if (seg < 3.0 && count > 3) return mix(uColor2, uColor3, f);
if (seg < 4.0 && count > 4) return mix(uColor3, uColor4, f);
if (seg < 5.0 && count > 5) return mix(uColor4, uColor5, f);
if (seg < 6.0 && count > 6) return mix(uColor5, uColor6, f);
if (seg < 7.0 && count > 7) return mix(uColor6, uColor7, f);
if (count > 7) return uColor7;
if (count > 6) return uColor6;
if (count > 5) return uColor5;
if (count > 4) return uColor4;
if (count > 3) return uColor3;
if (count > 2) return uColor2;
return uColor1;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv0 = fragCoord.xy / iResolution.xy;
float aspect = iResolution.x / iResolution.y;
vec2 p = uv0 * 2.0 - 1.0;
p.x *= aspect;
vec2 pr = rotate2D(p, uAngle);
pr.x /= aspect;
vec2 uv = pr * 0.5 + 0.5;
vec2 uvMod = uv;
if (uDistort > 0.0) {
float a = uvMod.y * 6.0;
float b = uvMod.x * 6.0;
float w = 0.01 * uDistort;
uvMod.x += sin(a) * w;
uvMod.y += cos(b) * w;
}
float t = uvMod.x;
if (uMirror > 0.5) {
t = 1.0 - abs(1.0 - 2.0 * fract(t));
}
vec3 base = getGradientColor(t);
vec2 offset = vec2(iMouse.x/iResolution.x, iMouse.y/iResolution.y);
float d = length(uv0 - offset);
float r = max(uSpotlightRadius, 1e-4);
float dn = d / r;
float spot = (1.0 - 2.0 * pow(dn, uSpotlightSoftness)) * uSpotlightOpacity;
vec3 cir = vec3(spot);
float stripe = fract(uvMod.x * max(uBlindCount, 1.0));
if (uShineFlip > 0.5) stripe = 1.0 - stripe;
vec3 ran = vec3(stripe);
vec3 col = cir + base - ran;
col += (rand(gl_FragCoord.xy + iTime) - 0.5) * uNoise;
fragColor = vec4(col, 1.0);
}
void main() {
vec4 color;
mainImage(color, vUv * iResolution.xy);
gl_FragColor = color;
}
`;
const { arr: colorArr, count: colorCount } = prepStops(gradientColors);
const uniforms: {
iResolution: { value: [number, number, number] };
iMouse: { value: [number, number] };
iTime: { value: number };
uAngle: { value: number };
uNoise: { value: number };
uBlindCount: { value: number };
uSpotlightRadius: { value: number };
uSpotlightSoftness: { value: number };
uSpotlightOpacity: { value: number };
uMirror: { value: number };
uDistort: { value: number };
uShineFlip: { value: number };
uColor0: { value: [number, number, number] };
uColor1: { value: [number, number, number] };
uColor2: { value: [number, number, number] };
uColor3: { value: [number, number, number] };
uColor4: { value: [number, number, number] };
uColor5: { value: [number, number, number] };
uColor6: { value: [number, number, number] };
uColor7: { value: [number, number, number] };
uColorCount: { value: number };
} = {
iResolution: {
value: [gl.drawingBufferWidth, gl.drawingBufferHeight, 1]
},
iMouse: { value: [0, 0] },
iTime: { value: 0 },
uAngle: { value: (angle * Math.PI) / 180 },
uNoise: { value: noise },
uBlindCount: { value: Math.max(1, blindCount) },
uSpotlightRadius: { value: spotlightRadius },
uSpotlightSoftness: { value: spotlightSoftness },
uSpotlightOpacity: { value: spotlightOpacity },
uMirror: { value: mirrorGradient ? 1 : 0 },
uDistort: { value: distortAmount },
uShineFlip: { value: shineDirection === 'right' ? 1 : 0 },
uColor0: { value: colorArr[0] },
uColor1: { value: colorArr[1] },
uColor2: { value: colorArr[2] },
uColor3: { value: colorArr[3] },
uColor4: { value: colorArr[4] },
uColor5: { value: colorArr[5] },
uColor6: { value: colorArr[6] },
uColor7: { value: colorArr[7] },
uColorCount: { value: colorCount }
};
const program = new Program(gl, {
vertex,
fragment,
uniforms
});
programRef.current = program;
const geometry = new Triangle(gl);
geometryRef.current = geometry;
const mesh = new Mesh(gl, { geometry, program });
meshRef.current = mesh;
const resize = () => {
const rect = container.getBoundingClientRect();
renderer.setSize(rect.width, rect.height);
uniforms.iResolution.value = [gl.drawingBufferWidth, gl.drawingBufferHeight, 1];
if (blindMinWidth && blindMinWidth > 0) {
const maxByMinWidth = Math.max(1, Math.floor(rect.width / blindMinWidth));
const effective = blindCount ? Math.min(blindCount, maxByMinWidth) : maxByMinWidth;
uniforms.uBlindCount.value = Math.max(1, effective);
} else {
uniforms.uBlindCount.value = Math.max(1, blindCount);
}
if (firstResizeRef.current) {
firstResizeRef.current = false;
const cx = gl.drawingBufferWidth / 2;
const cy = gl.drawingBufferHeight / 2;
uniforms.iMouse.value = [cx, cy];
mouseTargetRef.current = [cx, cy];
}
};
resize();
const ro = new ResizeObserver(resize);
ro.observe(container);
const onPointerMove = (e: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
const scale = (renderer as unknown as { dpr?: number }).dpr || 1;
const x = (e.clientX - rect.left) * scale;
const y = (rect.height - (e.clientY - rect.top)) * scale;
mouseTargetRef.current = [x, y];
if (mouseDampening <= 0) {
uniforms.iMouse.value = [x, y];
}
};
canvas.addEventListener('pointermove', onPointerMove);
const loop = (t: number) => {
rafRef.current = requestAnimationFrame(loop);
uniforms.iTime.value = t * 0.001;
if (mouseDampening > 0) {
if (!lastTimeRef.current) lastTimeRef.current = t;
const dt = (t - lastTimeRef.current) / 1000;
lastTimeRef.current = t;
const tau = Math.max(1e-4, mouseDampening);
let factor = 1 - Math.exp(-dt / tau);
if (factor > 1) factor = 1;
const target = mouseTargetRef.current;
const cur = uniforms.iMouse.value;
cur[0] += (target[0] - cur[0]) * factor;
cur[1] += (target[1] - cur[1]) * factor;
} else {
lastTimeRef.current = t;
}
if (!paused && programRef.current && meshRef.current) {
try {
renderer.render({ scene: meshRef.current });
} catch (e) {
console.error(e);
}
}
};
rafRef.current = requestAnimationFrame(loop);
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
canvas.removeEventListener('pointermove', onPointerMove);
ro.disconnect();
if (canvas.parentElement === container) {
container.removeChild(canvas);
}
const callIfFn = <T extends object, K extends keyof T>(obj: T | null, key: K) => {
if (obj && typeof obj[key] === 'function') {
(obj[key] as unknown as () => void).call(obj);
}
};
callIfFn(programRef.current, 'remove');
callIfFn(geometryRef.current, 'remove');
callIfFn(meshRef.current as unknown as { remove?: () => void }, 'remove');
callIfFn(rendererRef.current as unknown as { destroy?: () => void }, 'destroy');
programRef.current = null;
geometryRef.current = null;
meshRef.current = null;
rendererRef.current = null;
};
}, [
dpr,
paused,
gradientColors,
angle,
noise,
blindCount,
blindMinWidth,
mouseDampening,
mirrorGradient,
spotlightRadius,
spotlightSoftness,
spotlightOpacity,
distortAmount,
shineDirection
]);
return (
<div
ref={containerRef}
className={`gradient-blinds-container ${className}`}
style={{
...(mixBlendMode && {
mixBlendMode: mixBlendMode as React.CSSProperties['mixBlendMode']
})
}}
/>
);
};
export default React.memo(GradientBlinds);

View File

@@ -1,20 +0,0 @@
.container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.container canvas {
touch-action: none;
}
/* Fade out on smaller screens */
@media (max-width: 768px) {
.container {
opacity: 0.5;
}
}

View File

@@ -1,96 +0,0 @@
import { useRef, useMemo } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { Float, MeshDistortMaterial } from '@react-three/drei';
import * as THREE from 'three';
import styles from './Scene3D.module.css';
function FloatingShape() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
if (meshRef.current) {
meshRef.current.rotation.x = state.clock.elapsedTime * 0.1;
meshRef.current.rotation.y = state.clock.elapsedTime * 0.15;
}
});
return (
<Float
speed={2}
rotationIntensity={0.5}
floatIntensity={1}
>
<mesh ref={meshRef} scale={2.5}>
<icosahedronGeometry args={[1, 1]} />
<MeshDistortMaterial
color="#7FD998"
emissive="#004D2A"
emissiveIntensity={0.3}
roughness={0.4}
metalness={0.8}
distort={0.3}
speed={2}
/>
</mesh>
</Float>
);
}
function ParticleField() {
const count = 100;
const positions = useMemo(() => {
const pos = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * 20;
pos[i * 3 + 1] = (Math.random() - 0.5) * 20;
pos[i * 3 + 2] = (Math.random() - 0.5) * 20;
}
return pos;
}, []);
const pointsRef = useRef<THREE.Points>(null);
useFrame((state) => {
if (pointsRef.current) {
pointsRef.current.rotation.y = state.clock.elapsedTime * 0.02;
}
});
return (
<points ref={pointsRef}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
/>
</bufferGeometry>
<pointsMaterial
size={0.05}
color="#7FD998"
transparent
opacity={0.6}
sizeAttenuation
/>
</points>
);
}
export function Scene3D() {
return (
<div className={styles.container}>
<Canvas
camera={{ position: [0, 0, 8], fov: 45 }}
dpr={[1, 2]}
gl={{ antialias: true, alpha: true }}
>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} intensity={1} />
<pointLight position={[-10, -10, -5]} intensity={0.5} color="#7FD998" />
<FloatingShape />
<ParticleField />
</Canvas>
</div>
);
}

View File

@@ -1 +1 @@
export { Scene3D } from './Scene3D';
export { default as GradientBlinds } from "./GradientBlinds";

View File

@@ -16,23 +16,13 @@
.brand {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-sm);
}
.logo {
display: flex;
align-items: center;
gap: 0.25em;
font-size: 1.25rem;
font-weight: 700;
}
.logoText {
color: var(--md-sys-color-on-surface);
}
.logoAccent {
color: var(--md-sys-color-primary);
.logoImage {
height: 1.75rem;
width: auto;
}
.copyright {

View File

@@ -9,10 +9,7 @@ export function Footer() {
<footer className={styles.footer}>
<div className={`${styles.content} container`}>
<div className={styles.brand}>
<span className={styles.logo}>
<span className={styles.logoText}>Ragusa</span>
<span className={styles.logoAccent}>IT</span>
</span>
<img src="/logo.svg" alt="RagusaIT" className={styles.logoImage} />
<p className={styles.copyright}>
{t.footer.copyright.replace('{year}', String(currentYear))}
</p>
@@ -40,10 +37,7 @@ export function Footer() {
<div className={styles.credit}>
<p>
{t.footer.madeWith}{' '}
<span className={styles.heart}>React</span>{' '}
{t.footer.and}{' '}
<span className={styles.heart}>TypeScript</span>
{t.footer.madeIn} <span className={styles.heart}>{t.footer.love}</span>
</p>
</div>
</div>

View File

@@ -6,6 +6,9 @@
z-index: 100;
padding: var(--space-md) 0;
transition: background-color var(--transition-normal), backdrop-filter var(--transition-normal);
/* Ensure visibility against bright backgrounds on homepage */
background-color: rgba(15, 20, 16, 0.4);
backdrop-filter: blur(8px);
}
.header.scrolled {
@@ -24,19 +27,12 @@
.logo {
display: flex;
align-items: center;
gap: 0.25em;
font-size: 1.5rem;
font-weight: 700;
text-decoration: none;
color: var(--md-sys-color-on-surface);
}
.logoText {
color: var(--md-sys-color-on-surface);
}
.logoAccent {
color: var(--md-sys-color-primary);
.logoImage {
height: 2rem;
width: auto;
}
.navLinks {

View File

@@ -67,8 +67,7 @@ export function Navbar() {
>
<nav className={`${styles.nav} container`}>
<Link to="/" className={styles.logo}>
<span className={styles.logoText}>Ragusa</span>
<span className={styles.logoAccent}>IT</span>
<img src="/logo.svg" alt="RagusaIT" className={styles.logoImage} />
</Link>
<div

View File

@@ -9,6 +9,15 @@
overflow: hidden;
}
.backgroundContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.content {
position: relative;
z-index: 1;

View File

@@ -1,11 +1,13 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'motion/react';
import { useTranslation } from '../../i18n';
import { useTypingEffect } from '../../hooks';
import { Scene3D } from '../effects';
import { Button } from '../ui';
import styles from './Hero.module.css';
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { motion } from "motion/react";
import { useTranslation } from "../../i18n";
import { useTypingEffect } from "../../hooks";
import { GradientBlinds } from "../effects";
import { Button } from "../ui";
import styles from "./Hero.module.css";
const GRADIENT_COLORS = ["#26a269", "#8ff0a4"];
export function Hero() {
const { t } = useTranslation();
@@ -16,10 +18,10 @@ export function Hero() {
setShowScrollIndicator(window.scrollY < 50);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const { text } = useTypingEffect({
words: t.hero.rotatingWords,
typingSpeed: 80,
@@ -29,14 +31,29 @@ export function Hero() {
return (
<section className={styles.hero}>
<Scene3D />
<div className={styles.backgroundContainer}>
<GradientBlinds
gradientColors={GRADIENT_COLORS}
angle={134}
noise={0.31}
blindCount={12}
blindMinWidth={50}
spotlightRadius={0.4}
spotlightSoftness={1}
spotlightOpacity={1}
mouseDampening={0.15}
distortAmount={13}
shineDirection="left"
mixBlendMode="lighten"
/>
</div>
<div className={`${styles.content} container`}>
<motion.div
className={styles.text}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<motion.p
className={styles.greeting}
@@ -46,7 +63,7 @@ export function Hero() {
>
{t.hero.greeting}
</motion.p>
<motion.h1
className={styles.title}
initial={{ opacity: 0, y: 20 }}
@@ -55,7 +72,7 @@ export function Hero() {
>
{t.hero.company}
</motion.h1>
<motion.div
className={styles.tagline}
initial={{ opacity: 0 }}
@@ -68,7 +85,7 @@ export function Hero() {
<span className={styles.cursor}>|</span>
</span>
</motion.div>
<motion.div
className={styles.cta}
initial={{ opacity: 0, y: 20 }}
@@ -88,7 +105,7 @@ export function Hero() {
</motion.div>
</motion.div>
</div>
{showScrollIndicator && (
<motion.div
className={styles.scrollIndicator}
@@ -100,7 +117,7 @@ export function Hero() {
<motion.div
className={styles.scrollMouse}
animate={{ y: [0, 8, 0] }}
transition={{ repeat: Infinity, duration: 1.5, ease: 'easeInOut' }}
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
>
<span className={styles.scrollWheel} />
</motion.div>

View File

@@ -108,8 +108,8 @@ export const de = {
// Footer
footer: {
copyright: '© {year} Ragusa IT-Consulting. Alle Rechte vorbehalten.',
madeWith: 'Entwickelt mit',
and: 'und',
madeIn: 'Entwickelt in Deutschland mit',
love: 'Liebe',
},
};

View File

@@ -110,7 +110,7 @@ export const en: Translations = {
// Footer
footer: {
copyright: '© {year} Ragusa IT-Consulting. All rights reserved.',
madeWith: 'Built with',
and: 'and',
madeIn: 'Made in Germany with',
love: 'love',
},
};