Enhance visual effects with particle canvas and floating orbs; implement typewriter and glitch animations for improved user engagement

This commit is contained in:
Fabian Schieder 2026-02-28 02:40:40 +01:00
parent 5cbdfd2070
commit 63c6859923
2 changed files with 248 additions and 8 deletions

View File

@ -75,12 +75,18 @@ $projects = [
</head> </head>
<body> <body>
<canvas id="particle-canvas"></canvas>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="orb orb-4"></div>
<div class="background-blur"></div> <div class="background-blur"></div>
<header> <header>
<div class="avatar">FS</div> <div class="avatar">FS</div>
<h1>Fabian Schieder</h1> <h1 class="glitch" data-text="Fabian Schieder">Fabian Schieder</h1>
<p class="tagline">Entwickler · Schüler · Macher</p> <p class="tagline" id="typewriter"></p>
</header> </header>
<main> <main>
@ -143,11 +149,92 @@ $projects = [
</footer> </footer>
<script> <script>
// ── Typewriter ───────────────────────────────────────────
(function () {
const el = document.getElementById('typewriter');
const words = ['Entwickler', 'Schüler', 'Macher', 'Selfhoster', 'Tüftler'];
let wi = 0, ci = 0, deleting = false;
function type() {
const word = words[wi];
const display = deleting ? word.slice(0, ci--) : word.slice(0, ci++);
el.textContent = display;
let delay = deleting ? 60 : 110;
if (!deleting && ci > word.length) { delay = 1400; deleting = true; }
if (deleting && ci < 0) { deleting = false; wi = (wi + 1) % words.length; ci = 0; delay = 300; }
setTimeout(type, delay);
}
setTimeout(type, 900);
})();
// ── Particles ────────────────────────────────────────────
(function () {
const canvas = document.getElementById('particle-canvas');
const ctx = canvas.getContext('2d');
const COLORS = ['#6366f1', '#0ea5e9', '#a855f7', '#ec4899', '#22d3ee'];
let W, H, particles = [];
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
function rand(a, b) { return Math.random() * (b - a) + a; }
function mkP() {
return { x: rand(0,W), y: rand(0,H), r: rand(0.8,2.2), dx: rand(-0.25,0.25), dy: rand(-0.35,-0.08),
alpha: rand(0.2,0.8), fade: rand(0.002,0.006), color: COLORS[Math.floor(Math.random()*COLORS.length)] };
}
function init() { resize(); particles = Array.from({length:90}, mkP); }
function draw() {
ctx.clearRect(0, 0, W, H);
for (let p of particles) {
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
ctx.fillStyle = p.color; ctx.globalAlpha = p.alpha; ctx.fill();
p.x += p.dx; p.y += p.dy; p.alpha -= p.fade;
if (p.alpha <= 0 || p.y < -5) Object.assign(p, mkP(), { y: H+5, alpha: rand(0.2,0.7) });
}
ctx.globalAlpha = 1;
requestAnimationFrame(draw);
}
window.addEventListener('resize', resize);
init(); draw();
})();
// ── 3D Tilt Cards ────────────────────────────────────────
document.querySelectorAll('.card').forEach(card => { document.querySelectorAll('.card').forEach(card => {
card.addEventListener('mousemove', function (e) {
const rect = this.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = (e.clientX - cx) / (rect.width / 2);
const dy = (e.clientY - cy) / (rect.height / 2);
const rotX = (-dy * 8).toFixed(2);
const rotY = ( dx * 8).toFixed(2);
this.style.transform = `perspective(600px) rotateX(${rotX}deg) rotateY(${rotY}deg) translateY(-3px) scale(1.02)`;
// glare
let glare = this.querySelector('.card-glare');
if (!glare) {
glare = document.createElement('div');
glare.className = 'card-glare';
this.appendChild(glare);
}
const gx = ((e.clientX - rect.left) / rect.width * 100).toFixed(1);
const gy = ((e.clientY - rect.top) / rect.height * 100).toFixed(1);
glare.style.background = `radial-gradient(circle at ${gx}% ${gy}%, rgba(255,255,255,0.12) 0%, transparent 65%)`;
});
card.addEventListener('mouseleave', function () {
this.style.transform = '';
this.querySelector('.card-glare')?.remove();
});
// Ripple
card.addEventListener('click', function (e) { card.addEventListener('click', function (e) {
const circle = document.createElement('span'); const circle = document.createElement('span');
const diameter = Math.max(this.clientWidth, this.clientHeight); const diameter = Math.max(this.clientWidth, this.clientHeight);
const rect = this.getBoundingClientRect(); const rect = this.getBoundingClientRect();
circle.classList.add('ripple'); circle.classList.add('ripple');
circle.style.width = circle.style.height = diameter + 'px'; circle.style.width = circle.style.height = diameter + 'px';
circle.style.left = (e.clientX - rect.left - diameter / 2) + 'px'; circle.style.left = (e.clientX - rect.left - diameter / 2) + 'px';

161
style.css
View File

@ -18,6 +18,7 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
overflow-x: hidden;
} }
body { body {
@ -31,19 +32,20 @@ body {
padding: 2rem 1rem 4rem; padding: 2rem 1rem 4rem;
position: relative; position: relative;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto;
} }
/* ===== BACKGROUND GLOW ===== */ /* ===== BACKGROUND GLOW ===== */
@keyframes bgShift { @keyframes bgShift {
0% { opacity: 1; transform: scale(1) translate(0, 0); } 0% { opacity: 1; transform: scale(1) translate(0, 0); }
33% { opacity: 0.85; transform: scale(1.08) translate(2%, 3%); } 33% { opacity: 0.85; transform: scale(1.15) translate(2%, 3%); }
66% { opacity: 0.9; transform: scale(0.97) translate(-3%, -2%); } 66% { opacity: 0.9; transform: scale(1.08) translate(-3%, -2%); }
100% { opacity: 1; transform: scale(1) translate(0, 0); } 100% { opacity: 1; transform: scale(1) translate(0, 0); }
} }
.background-blur { .background-blur {
position: fixed; position: fixed;
inset: 0; inset: -25%;
z-index: -1; z-index: -1;
background: background:
radial-gradient(ellipse 60% 40% at 20% 10%, rgba(99, 102, 241, 0.18), transparent), radial-gradient(ellipse 60% 40% at 20% 10%, rgba(99, 102, 241, 0.18), transparent),
@ -97,11 +99,82 @@ header {
h1 { h1 {
font-size: clamp(1.8rem, 4vw, 2.8rem); font-size: clamp(1.8rem, 4vw, 2.8rem);
font-weight: 700; font-weight: 700;
background: linear-gradient(90deg, #e8e8f0 30%, #8888a8); background: linear-gradient(90deg, #e8e8f0 0%, #6366f1 40%, #0ea5e9 60%, #e8e8f0 100%);
background-size: 200% auto;
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
letter-spacing: -0.5px; letter-spacing: -0.5px;
animation: shimmerText 5s linear infinite;
position: relative;
display: inline-block;
}
@keyframes shimmerText {
from { background-position: 0% center; }
to { background-position: 200% center; }
}
/* ===== GLITCH ===== */
.glitch {
position: relative;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
top: 0; left: 0; right: 0;
background: inherit;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
background-image: inherit;
background-size: inherit;
}
.glitch::before {
animation: glitch1 4s infinite;
clip-path: polygon(0 0, 100% 0, 100% 40%, 0 40%);
transform: translate(-2px, 0);
opacity: 0;
}
.glitch::after {
animation: glitch2 4s infinite;
clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%);
transform: translate(2px, 0);
opacity: 0;
}
@keyframes glitch1 {
0%, 90%, 100% { opacity: 0; transform: translate(0); }
92% { opacity: 0.8; transform: translate(-3px, 1px); filter: hue-rotate(90deg); }
94% { opacity: 0.6; transform: translate(3px, -1px); }
96% { opacity: 0.8; transform: translate(-2px, 2px); }
98% { opacity: 0; }
}
@keyframes glitch2 {
0%, 90%, 100% { opacity: 0; transform: translate(0); }
91% { opacity: 0.7; transform: translate(3px, -1px); filter: hue-rotate(-90deg); }
93% { opacity: 0.5; transform: translate(-3px, 1px); }
95% { opacity: 0.7; transform: translate(2px, -2px); }
97% { opacity: 0; }
}
/* ===== TYPEWRITER ===== */
#typewriter::after {
content: '|';
animation: blink 0.8s step-end infinite;
margin-left: 1px;
color: #6366f1;
-webkit-text-fill-color: #6366f1;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
} }
.tagline { .tagline {
@ -109,6 +182,18 @@ h1 {
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 1rem; font-size: 1rem;
letter-spacing: 0.5px; letter-spacing: 0.5px;
animation: fadeDown 0.9s ease 0.3s both;
min-height: 1.5em;
}
/* ===== CARD GLARE ===== */
.card-glare {
position: absolute;
inset: 0;
border-radius: var(--radius);
pointer-events: none;
z-index: 1;
transition: background 0.05s;
} }
/* ===== MAIN CONTENT ===== */ /* ===== MAIN CONTENT ===== */
@ -299,6 +384,74 @@ footer:hover {
} }
} }
/* ===== PARTICLE CANVAS ===== */
#particle-canvas {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
opacity: 0.55;
}
/* ===== FLOATING ORBS ===== */
.orb {
position: fixed;
border-radius: 50%;
pointer-events: none;
z-index: -1;
filter: blur(70px);
opacity: 0;
animation: orbFloat var(--dur, 18s) ease-in-out infinite var(--delay, 0s);
}
.orb-1 {
width: 320px; height: 320px;
background: radial-gradient(circle, rgba(99,102,241,0.25), transparent 70%);
top: -80px; left: -80px;
--dur: 20s; --delay: 0s;
}
.orb-2 {
width: 260px; height: 260px;
background: radial-gradient(circle, rgba(14,165,233,0.2), transparent 70%);
bottom: 5%; right: -60px;
--dur: 17s; --delay: -6s;
}
.orb-3 {
width: 200px; height: 200px;
background: radial-gradient(circle, rgba(168,85,247,0.18), transparent 70%);
top: 45%; left: -40px;
--dur: 23s; --delay: -11s;
}
.orb-4 {
width: 180px; height: 180px;
background: radial-gradient(circle, rgba(236,72,153,0.15), transparent 70%);
top: 20%; right: -30px;
--dur: 19s; --delay: -4s;
}
@keyframes orbFloat {
0% { opacity: 0; transform: translate(0, 0) scale(1); }
10% { opacity: 1; }
50% { opacity: 1; transform: translate(20px, 25px) scale(1.08); }
90% { opacity: 1; }
100% { opacity: 0; transform: translate(0, 0) scale(1); }
}
/* ===== GRID LINES (subtle) ===== */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: -1;
background-image:
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
background-size: 60px 60px;
pointer-events: none;
mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%);
-webkit-mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 30%, transparent 100%);
}
/* ===== RIPPLE ===== */ /* ===== RIPPLE ===== */
.card .ripple { .card .ripple {
position: absolute; position: absolute;