feat(home): integrate motion components in home page
All checks were successful
Staging Build / build (push) Successful in 2m59s
All checks were successful
Staging Build / build (push) Successful in 2m59s
This commit is contained in:
249
AsciiShaderBackground.astro
Normal file
249
AsciiShaderBackground.astro
Normal 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>
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user