feat(home): integrate motion components in home page
All checks were successful
Staging Build / build (push) Successful in 2m59s

This commit is contained in:
DanielS
2026-08-06 17:21:52 +02:00
parent 21e01b1b24
commit ec264e2680
2 changed files with 259 additions and 116 deletions

249
AsciiShaderBackground.astro Normal file
View File

@@ -0,0 +1,249 @@
---
// AsciiShaderBackground.astro
interface Props {
className?: string;
}
const { className = "" } = Astro.props;
---
<div class={`ascii-shader-container ${className}`}>
<canvas id="ascii-shader-canvas"></canvas>
<div class="grid-overlay"></div>
</div>
<style>
.ascii-shader-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
overflow: hidden;
background-color: oklch(0.145 0.01 148);
pointer-events: none;
}
#ascii-shader-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
will-change: transform;
pointer-events: none;
}
.grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: 40px 40px;
background-image:
linear-gradient(to right, oklch(0.31 0.012 148 / 0.3) 1px, transparent 1px),
linear-gradient(to bottom, oklch(0.31 0.012 148 / 0.3) 1px, transparent 1px);
pointer-events: none;
}
</style>
<script>
// OKLCH Palette definition
interface OKLCHColor {
l: number;
c: number;
h: number;
}
// Palette defining color stops based on scroll progress
const PALETTE = {
yellow: { l: 0.88, c: 0.18, h: 96 },
green: { l: 0.76, c: 0.15, h: 155 },
blue: { l: 0.68, c: 0.18, h: 255 },
purple: { l: 0.64, c: 0.18, h: 302 },
pink: { l: 0.72, c: 0.20, h: 345 }
};
// Helper for shortest angle interpolation in OKLCH hue space
function interpolateHue(h1: number, h2: number, t: number): number {
let diff = (h2 - h1) % 360;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
return (h1 + diff * t + 360) % 360;
}
// Interpolate two OKLCH colors
function lerpOKLCH(c1: OKLCHColor, c2: OKLCHColor, t: number): OKLCHColor {
return {
l: c1.l + (c2.l - c1.l) * t,
c: c1.c + (c2.c - c1.c) * t,
h: interpolateHue(c1.h, c2.h, t)
};
}
// Calculate dynamic colors based on scroll progress (0.0 to 1.0)
function getScrollColors(progress: number): { primary: OKLCHColor; secondary: OKLCHColor } {
if (progress < 0.33) {
// Hero to stage 1: Full spectrum (Pink -> Yellow)
const t = progress / 0.33;
return {
primary: lerpOKLCH(PALETTE.pink, PALETTE.yellow, t),
secondary: lerpOKLCH(PALETTE.purple, PALETTE.green, t)
};
} else if (progress < 0.66) {
// Stage 1 to stage 2: Yellow/Cream to Green
const t = (progress - 0.33) / 0.33;
return {
primary: lerpOKLCH(PALETTE.yellow, PALETTE.green, t),
secondary: lerpOKLCH(PALETTE.green, PALETTE.blue, t)
};
} else {
// Stage 2 to end: Green/Blue focus
const t = (progress - 0.66) / 0.34;
return {
primary: lerpOKLCH(PALETTE.green, PALETTE.blue, t),
secondary: lerpOKLCH(PALETTE.blue, PALETTE.purple, t)
};
}
}
class AsciiShaderEngine {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private mouse = { x: 0, y: 0, targetX: 0, targetY: 0 };
private scrollProgress = 0;
private targetScrollProgress = 0;
private chars = ['/', '\\', '-', '|', '+', '='];
private fontSize = 16;
private cols = 0;
private rows = 0;
private animationFrameId = 0;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
const context = canvas.getContext('2d');
if (!context) throw new Error('Could not get 2D context');
this.ctx = context;
this.init();
}
private init(): void {
this.resize();
this.bindEvents();
this.render();
}
private resize = (): void => {
const width = window.innerWidth;
const height = window.innerHeight;
const dpr = window.devicePixelRatio || 1;
this.canvas.width = width * dpr;
this.canvas.height = height * dpr;
this.ctx.scale(dpr, dpr);
this.cols = Math.ceil(width / this.fontSize);
this.rows = Math.ceil(height / this.fontSize);
if (this.mouse.x === 0 && this.mouse.y === 0) {
this.mouse.x = width / 2;
this.mouse.y = height / 2;
this.mouse.targetX = width / 2;
this.mouse.targetY = height / 2;
}
};
private bindEvents(): void {
window.addEventListener('resize', this.resize);
window.addEventListener('pointermove', (e: PointerEvent) => {
this.mouse.targetX = e.clientX;
this.mouse.targetY = e.clientY;
});
window.addEventListener('scroll', () => {
const maxScroll = Math.max(
document.body.scrollHeight - window.innerHeight,
1
);
this.targetScrollProgress = Math.min(Math.max(window.scrollY / maxScroll, 0), 1);
}, { passive: true });
}
private render = (time: number = 0): void => {
// Dampened mouse easing (lerp factor 0.05)
this.mouse.x += (this.mouse.targetX - this.mouse.x) * 0.05;
this.mouse.y += (this.mouse.targetY - this.mouse.y) * 0.05;
// Dampened scroll progress easing
this.scrollProgress += (this.targetScrollProgress - this.scrollProgress) * 0.05;
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Get current OKLCH interpolated colors
const { primary, secondary } = getScrollColors(this.scrollProgress);
this.ctx.font = `${this.fontSize}px monospace`;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
const timeSec = time * 0.001;
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
const x = c * this.fontSize + this.fontSize / 2;
const y = r * this.fontSize + this.fontSize / 2;
// Vector field calculations based on distance to cursor & wave function
const dx = x - this.mouse.x;
const dy = y - this.mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Dynamic angle field
const angle = Math.atan2(dy, dx) + Math.sin(dist * 0.01 - timeSec) * 0.5;
const charIndex = Math.floor(Math.abs(Math.sin(angle + dist * 0.005)) * this.chars.length) % this.chars.length;
const char = this.chars[charIndex];
// Color blend based on vector field intensity
const blendFactor = Math.min(Math.max(1 - dist / 400, 0), 1);
const currentColor = lerpOKLCH(secondary, primary, blendFactor);
// Render character with OKLCH styling
this.ctx.fillStyle = `oklch(${currentColor.l.toFixed(3)} ${currentColor.c.toFixed(3)} ${currentColor.h.toFixed(1)})`;
this.ctx.fillText(char, x, y);
}
}
this.animationFrameId = requestAnimationFrame(this.render);
};
public destroy(): void {
window.removeEventListener('resize', this.resize);
cancelAnimationFrame(this.animationFrameId);
}
}
// Lifecycle handler for Astro / SPA transitions
function initShader() {
const canvas = document.getElementById('ascii-shader-canvas') as HTMLCanvasElement;
if (canvas) {
new AsciiShaderEngine(canvas);
}
}
// Initial load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initShader);
} else {
initShader();
}
// Astro page swap support
document.addEventListener('astro:page-load', initShader);
</script>

View File

@@ -31,6 +31,11 @@ import {
DialogFooter,
} from '@/components/ui/dialog'
import { AsciiShaderBackground } from '@/components/AsciiShaderBackground'
import { ProcessSteps } from '@/components/ProcessSteps'
import { ScrollIndicator } from '@/components/ScrollIndicator'
import { WorkspaceZentrale } from '@/components/WorkspaceZentrale'
interface HomeClientProps {
initialUser: User | null
}
@@ -78,7 +83,8 @@ export function HomeClient({ initialUser }: HomeClientProps) {
}
return (
<div className="min-h-screen bg-slate-950 text-slate-100 overflow-hidden relative selection:bg-blue-500/30 selection:text-blue-200">
<div className="min-h-screen bg-transparent text-slate-100 overflow-hidden relative selection:bg-blue-500/30 selection:text-blue-200">
<AsciiShaderBackground />
@@ -249,123 +255,11 @@ export function HomeClient({ initialUser }: HomeClientProps) {
</DialogContent>
</Dialog>
{/* Grid of modules */}
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/50 relative">
<div className="container max-w-6xl mx-auto px-4 relative">
<div className="absolute top-1/4 right-0 w-[400px] h-[400px] bg-indigo-600/5 blur-[120px] rounded-full -z-10 pointer-events-none" />
<div className="text-center space-y-3 mb-16">
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-white">
Workspace-Zentrale
</h2>
<p className="text-slate-400 max-w-[600px] mx-auto text-sm sm:text-base">
Die zentralen Plattformen für unsere tägliche Zusammenarbeit, Dokumentation und den Support.
</p>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Card 1 */}
<a href="https://support.teamzone.softengine.de" target="_blank" rel="noopener noreferrer"><motion.div
whileHover={{ y: -4 }}
className="group relative flex flex-col p-6 rounded-2xl border border-slate-800 bg-slate-900/40 hover:bg-slate-900/80 hover:border-slate-700 transition-all duration-200 shadow-md overflow-hidden"
>
<div className="space-y-4 relative">
<div className="inline-flex p-3 bg-blue-500/10 rounded-xl text-blue-400 group-hover:bg-blue-500/20 group-hover:scale-110 transition-all duration-300">
<MessageSquare className="h-6 w-6" />
</div>
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3>
<p className="text-slate-400 text-sm leading-relaxed">
Treten Sie unserem Partner-Chat bei! tauschen Sie sich mit anderen CASPOS Partnern aus.
</p>
</div>
</motion.div></a>
{/* Card 2 */}
<a href="https://wiki.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
whileHover={{ y: -4 }}
className="group relative flex flex-col p-6 rounded-2xl border border-slate-800 bg-slate-900/40 hover:bg-slate-900/80 hover:border-slate-700 transition-all duration-200 shadow-md overflow-hidden"
>
<div className="space-y-4 relative">
<div className="inline-flex p-3 bg-indigo-500/10 rounded-xl text-indigo-400 group-hover:bg-indigo-500/20 group-hover:scale-110 transition-all duration-300">
<BookOpen className="h-6 w-6" />
</div>
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Wiki</h3>
<p className="text-slate-400 text-sm leading-relaxed">
Ihr Wissens-Portal für die CASPOS Produktfamilie.
</p>
</div>
</motion.div></a>
{/* Card 3 */}
<a href="https://support.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
whileHover={{ y: -4 }}
className="group relative flex flex-col p-6 rounded-2xl border border-slate-800 bg-slate-900/40 hover:bg-slate-900/80 hover:border-slate-700 transition-all duration-200 shadow-md overflow-hidden"
>
<div className="space-y-4 relative">
<div className="inline-flex p-3 bg-purple-500/10 rounded-xl text-purple-400 group-hover:bg-purple-500/20 group-hover:scale-110 transition-all duration-300">
<LifeBuoy className="h-6 w-6" />
</div>
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Support</h3>
<p className="text-slate-400 text-sm leading-relaxed">
Sie haben ein technisches Problem in der CASPOS? Erstellen Sie jetzt ein Ticket!
</p>
</div>
</motion.div></a>
</div>
</div>
</section>
{/* Grid of modules / Workspace-Zentrale */}
<WorkspaceZentrale />
{/* Stepper info */}
<section className="w-full py-16 md:py-24 border-t border-slate-900 bg-slate-950/30">
<div className="container max-w-5xl mx-auto px-4">
<div className="text-center space-y-3 mb-16">
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-center text-white">
Wie funktioniert die Bereitstellung?
</h2>
</div>
<div className="relative">
{/* Connection line for Desktop */}
<div className="hidden md:block absolute top-[28px] left-[15%] right-[15%] h-[1px] border-t border-dashed border-slate-800 z-0" />
<div className="grid gap-8 md:grid-cols-3 relative z-10">
{/* Step 1 */}
<div className="flex flex-col items-center text-center space-y-4 p-4 group">
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-900 border border-slate-800 text-blue-400 font-black text-lg shadow-lg group-hover:border-blue-500/40 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all duration-300">
1
</div>
<h4 className="text-lg font-bold text-slate-200 group-hover:text-white transition-colors">Kunde wählen</h4>
<p className="text-sm text-slate-400 leading-relaxed max-w-[250px]">
Gewünschten Kunden auswählen, der die Lizenz erhalten soll.
</p>
</div>
{/* Step 2 */}
<div className="flex flex-col items-center text-center space-y-4 p-4 group">
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-900 border border-slate-800 text-blue-400 font-black text-lg shadow-lg group-hover:border-blue-500/40 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all duration-300">
2
</div>
<h4 className="text-lg font-bold text-slate-200 group-hover:text-white transition-colors">Lizenz wählen</h4>
<p className="text-sm text-slate-400 leading-relaxed max-w-[250px]">
Gewünschte Lizenzen für den Kunden auswählen und den Bestellung abschließen.
</p>
</div>
{/* Step 3 */}
<div className="flex flex-col items-center text-center space-y-4 p-4 group">
<div className="flex items-center justify-center w-14 h-14 rounded-2xl bg-slate-900 border border-slate-800 text-blue-400 font-black text-lg shadow-lg group-hover:border-blue-500/40 group-hover:shadow-[0_0_15px_rgba(59,130,246,0.15)] transition-all duration-300">
3
</div>
<h4 className="text-lg font-bold text-slate-200 group-hover:text-white transition-colors">Live gehen</h4>
<p className="text-sm text-slate-400 leading-relaxed max-w-[250px]">
Nach dem Erhalt der Lizenz Live gehen.
</p>
</div>
</div>
</div>
</div>
</section>
<ProcessSteps />
{/* Footer */}