feat: reworked HeroSection background
1
build/assets/index-BAFm1LaZ.css
Normal file
128
build/assets/index-VO0wVEDp.js
Normal file
9
build/assets/three-eMwtdKkp.js
Normal file
BIN
build/favicon.ico
Normal file
|
After Width: | Height: | Size: 22 KiB |
@@ -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 |
@@ -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
|
After Width: | Height: | Size: 171 KiB |
9
build/logo.svg
Normal file
|
After Width: | Height: | Size: 224 KiB |
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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
|
After Width: | Height: | Size: 22 KiB |
@@ -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
|
After Width: | Height: | Size: 171 KiB |
9
public/logo.svg
Normal file
|
After Width: | Height: | Size: 224 KiB |
6
src/components/effects/GradientBlinds.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.gradient-blinds-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
389
src/components/effects/GradientBlinds.tsx
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export { Scene3D } from './Scene3D';
|
||||
export { default as GradientBlinds } from "./GradientBlinds";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||