Files
webshop/AsciiShaderBackground.astro
DanielS ec264e2680
All checks were successful
Staging Build / build (push) Successful in 2m59s
feat(home): integrate motion components in home page
2026-08-06 17:21:52 +02:00

250 lines
7.3 KiB
Plaintext

---
// 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>