From 3949e54cc0efc3080a76522a46670eb9a361bc99 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 18 Feb 2026 17:51:57 +0100 Subject: [PATCH] added color flow bannar --- backend/banners/generative-flow.json | 50 ++++ frontend/src/components/DynamicBanner.tsx | 5 + .../src/components/GenerativeFlowBanner.tsx | 222 ++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 backend/banners/generative-flow.json create mode 100644 frontend/src/components/GenerativeFlowBanner.tsx diff --git a/backend/banners/generative-flow.json b/backend/banners/generative-flow.json new file mode 100644 index 0000000..ece5864 --- /dev/null +++ b/backend/banners/generative-flow.json @@ -0,0 +1,50 @@ +{ + "name": "Celestial Flow", + "slug": "generative-flow", + "description": "Smooth, organic color fields that swirl and flow like liquid light, reacting to your cursor.", + "settings": { + "blobCount": 8, + "flowSpeed": 0.008, + "distortion": 1.2, + "vibrancy": 0.8, + "blur": 60 + }, + "schema": [ + { + "key": "blobCount", + "label": "Color Density", + "type": "slider", + "min": 4, + "max": 15, + "step": 1, + "description": "Number of floating color blobs." + }, + { + "key": "flowSpeed", + "label": "Movement Speed", + "type": "slider", + "min": 0.001, + "max": 0.03, + "step": 0.001, + "description": "How fast the colors swirl autonomously." + }, + { + "key": "distortion", + "label": "Warping Intensity", + "type": "slider", + "min": 0.1, + "max": 3.0, + "step": 0.1, + "description": "How much the mouse distorts the flow." + }, + { + "key": "blur", + "label": "Softness", + "type": "slider", + "min": 20, + "max": 120, + "step": 5, + "description": "The blur level for a smooth, flowy aesthetic." + } + ] +} diff --git a/frontend/src/components/DynamicBanner.tsx b/frontend/src/components/DynamicBanner.tsx index 9a4eaca..14ebd8e 100644 --- a/frontend/src/components/DynamicBanner.tsx +++ b/frontend/src/components/DynamicBanner.tsx @@ -5,6 +5,7 @@ import NebulaBanner from './NebulaBanner'; import FirefliesBanner from './FirefliesBanner'; import HextechBanner from './HextechBanner'; import HextechCorruptionBanner from './HextechCorruptionBanner'; +import GenerativeFlowBanner from './GenerativeFlowBanner'; interface Banner { id: number; @@ -101,6 +102,10 @@ const DynamicBanner: React.FC = (props) => { return ; } + if (banner.slug === 'generative-flow') { + return ; + } + // Fallback return ; }; diff --git a/frontend/src/components/GenerativeFlowBanner.tsx b/frontend/src/components/GenerativeFlowBanner.tsx new file mode 100644 index 0000000..61cf339 --- /dev/null +++ b/frontend/src/components/GenerativeFlowBanner.tsx @@ -0,0 +1,222 @@ +import React, { useRef, useEffect, useMemo } from 'react'; +import { alpha, useTheme } from '@mui/material'; + +interface GenerativeFlowProps { + username: string; + palette: string[]; + ratingsCount: number; + followersCount: number; + settings?: { + blobCount?: number; + flowSpeed?: number; + vibrancy?: number; + blur?: number; + distortion?: number; + }; +} + +const GenerativeFlowBanner: React.FC = ({ username, palette, settings }) => { + const canvasRef = useRef(null); + const theme = useTheme(); + + const blobCount = settings?.blobCount ?? 8; + const flowSpeed = settings?.flowSpeed ?? 0.008; + const vibrancy = settings?.vibrancy ?? 0.8; + const blurAmount = settings?.blur ?? 60; + const distortionFactor = settings?.distortion ?? 1.2; + + const getSeed = (str: string) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; + }; + + const seed = useMemo(() => getSeed(username), [username]); + + const brightColors = useMemo(() => { + const colors = palette.length >= 2 ? palette : [theme.palette.primary.main, theme.palette.secondary.main, theme.palette.info.main]; + const getBrightness = (hex: string) => { + const r = parseInt(hex.slice(1, 3), 16) || 0; + const g = parseInt(hex.slice(3, 5), 16) || 0; + const b = parseInt(hex.slice(5, 7), 16) || 0; + return (r * 299 + g * 587 + b * 114) / 1000; + }; + const sorted = [...colors].sort((a, b) => getBrightness(b) - getBrightness(a)); + return sorted.slice(0, 4); + }, [palette, theme]); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d', { alpha: true }); + if (!ctx) return; + + let animationFrameId: number; + let time = 0; + + interface Blob { + x: number; + y: number; + ox: number; + oy: number; + vx: number; + vy: number; + size: number; + color: string; + phase: number; + speedFactor: number; + } + + let blobs: Blob[] = []; + + const pseudoRandom = (offset: number) => { + const x = Math.sin(seed + offset) * 10000; + return x - Math.floor(x); + }; + + const initBlobs = (width: number, height: number) => { + const newBlobs = []; + for (let i = 0; i < blobCount; i++) { + const rx = pseudoRandom(i) * width; + const ry = pseudoRandom(i + 500) * height; + newBlobs.push({ + x: rx, y: ry, + ox: rx, oy: ry, + vx: 0, vy: 0, + size: (width / 4) + pseudoRandom(i + 1000) * (width / 3), + color: brightColors[i % brightColors.length], + phase: pseudoRandom(i + 1500) * Math.PI * 2, + speedFactor: 0.5 + pseudoRandom(i + 2000) + }); + } + return newBlobs; + }; + + const resize = () => { + const parent = canvas.parentElement; + if (parent) { + const rect = parent.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr; + ctx.scale(dpr, dpr); + canvas.style.width = `${rect.width}px`; + canvas.style.height = `${rect.height}px`; + blobs = initBlobs(rect.width, rect.height); + } + }; + + const resizeObserver = new ResizeObserver(() => resize()); + if (canvas.parentElement) resizeObserver.observe(canvas.parentElement); + resize(); + + let mouse = { x: -1000, y: -1000 }; + const handleMouseMove = (e: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + mouse.x = e.clientX - rect.left; + mouse.y = e.clientY - rect.top; + }; + + const parent = canvas.parentElement; + if (parent) { + parent.addEventListener('mousemove', handleMouseMove); + parent.addEventListener('mouseleave', () => { mouse.x = -1000; mouse.y = -1000; }); + } + + const draw = () => { + const width = canvas.width / (window.devicePixelRatio || 1); + const height = canvas.height / (window.devicePixelRatio || 1); + if (!width || !height) { + animationFrameId = requestAnimationFrame(draw); + return; + } + + ctx.clearRect(0, 0, width, height); + + // Background Base + const isLight = theme.palette.mode === 'light'; + const bgGrad = ctx.createRadialGradient(width/2, height/2, 0, width/2, height/2, width); + if (isLight) { + bgGrad.addColorStop(0, alpha(brightColors[0], 0.4)); + bgGrad.addColorStop(1, alpha(brightColors[0], 0.9)); + } else { + bgGrad.addColorStop(0, '#0a0a1a'); + bgGrad.addColorStop(1, '#020205'); + } + ctx.fillStyle = bgGrad; + ctx.fillRect(0, 0, width, height); + + time += flowSpeed; + + // Apply filter for "Liquid" look + ctx.filter = `blur(${blurAmount}px)`; + ctx.globalCompositeOperation = 'screen'; + + blobs.forEach((b) => { + // Harmonic autonomous drift + const driftX = Math.sin(time * b.speedFactor + b.phase) * (width / 5); + const driftY = Math.cos(time * 0.8 * b.speedFactor + b.phase) * (height / 5); + + // Mouse distortion + const dxMouse = mouse.x - b.ox; + const dyMouse = mouse.y - b.oy; + const distMouse = Math.sqrt(dxMouse * dxMouse + dyMouse * dyMouse); + const mouseForce = Math.max(0, (500 - distMouse) / 500); + + const mX = dxMouse * mouseForce * distortionFactor; + const mY = dyMouse * mouseForce * distortionFactor; + + b.x = b.ox + driftX + mX; + b.y = b.oy + driftY + mY; + + // Pulsing size + const pulse = 1 + Math.sin(time + b.phase) * 0.1; + const currentSize = b.size * pulse; + + const grad = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, currentSize); + grad.addColorStop(0, alpha(b.color, vibrancy)); + grad.addColorStop(1, alpha(b.color, 0)); + + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(b.x, b.y, currentSize, 0, Math.PI * 2); + ctx.fill(); + }); + + ctx.filter = 'none'; + ctx.globalCompositeOperation = 'source-over'; + + animationFrameId = requestAnimationFrame(draw); + }; + + draw(); + + return () => { + resizeObserver.disconnect(); + if (parent) { + parent.removeEventListener('mousemove', handleMouseMove); + } + cancelAnimationFrame(animationFrameId); + }; + }, [seed, brightColors, blobCount, flowSpeed, vibrancy, blurAmount, distortionFactor, theme]); + + return ( + + ); +}; + +export default GenerativeFlowBanner;