Enhance visual effects with particle canvas and floating orbs; implement typewriter and glitch animations for improved user engagement
This commit is contained in:
parent
5cbdfd2070
commit
63c6859923
95
index.php
95
index.php
@ -75,12 +75,18 @@ $projects = [
|
||||
</head>
|
||||
<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>
|
||||
|
||||
<header>
|
||||
<div class="avatar">FS</div>
|
||||
<h1>Fabian Schieder</h1>
|
||||
<p class="tagline">Entwickler · Schüler · Macher</p>
|
||||
<h1 class="glitch" data-text="Fabian Schieder">Fabian Schieder</h1>
|
||||
<p class="tagline" id="typewriter"></p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@ -143,11 +149,92 @@ $projects = [
|
||||
</footer>
|
||||
|
||||
<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 => {
|
||||
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) {
|
||||
const circle = document.createElement('span');
|
||||
const circle = document.createElement('span');
|
||||
const diameter = Math.max(this.clientWidth, this.clientHeight);
|
||||
const rect = this.getBoundingClientRect();
|
||||
const rect = this.getBoundingClientRect();
|
||||
circle.classList.add('ripple');
|
||||
circle.style.width = circle.style.height = diameter + 'px';
|
||||
circle.style.left = (e.clientX - rect.left - diameter / 2) + 'px';
|
||||
|
||||
161
style.css
161
style.css
@ -18,6 +18,7 @@
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -31,19 +32,20 @@ body {
|
||||
padding: 2rem 1rem 4rem;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ===== BACKGROUND GLOW ===== */
|
||||
@keyframes bgShift {
|
||||
0% { opacity: 1; transform: scale(1) translate(0, 0); }
|
||||
33% { opacity: 0.85; transform: scale(1.08) translate(2%, 3%); }
|
||||
66% { opacity: 0.9; transform: scale(0.97) translate(-3%, -2%); }
|
||||
33% { opacity: 0.85; transform: scale(1.15) translate(2%, 3%); }
|
||||
66% { opacity: 0.9; transform: scale(1.08) translate(-3%, -2%); }
|
||||
100% { opacity: 1; transform: scale(1) translate(0, 0); }
|
||||
}
|
||||
|
||||
.background-blur {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
inset: -25%;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(ellipse 60% 40% at 20% 10%, rgba(99, 102, 241, 0.18), transparent),
|
||||
@ -97,11 +99,82 @@ header {
|
||||
h1 {
|
||||
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
||||
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-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
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 {
|
||||
@ -109,6 +182,18 @@ h1 {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 1rem;
|
||||
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 ===== */
|
||||
@ -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 ===== */
|
||||
.card .ripple {
|
||||
position: absolute;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user