diff --git a/1000X1000-AnomalyCore4.jpg b/1000X1000-AnomalyCore4.jpg new file mode 100644 index 0000000..6778cb9 Binary files /dev/null and b/1000X1000-AnomalyCore4.jpg differ diff --git a/backend/banners/hextech-corruption.json b/backend/banners/hextech-corruption.json new file mode 100644 index 0000000..ac2dfa9 --- /dev/null +++ b/backend/banners/hextech-corruption.json @@ -0,0 +1,50 @@ +{ + "name": "Hextech Corruption", + "slug": "hextech-corruption", + "description": "An organic, cellular anomaly pattern inspired by the Hextech corruption in Arcane. Features glowing iridescent cores and a warping void shell.", + "settings": { + "cellCount": 40, + "shimmerSpeed": 0.015, + "glowIntensity": 0.9, + "instability": 0.6, + "shellDarkness": 0.85 + }, + "schema": [ + { + "key": "cellCount", + "label": "Anomaly Density", + "type": "slider", + "min": 10, + "max": 100, + "step": 5, + "description": "Number of organic cells in the corruption pattern." + }, + { + "key": "shimmerSpeed", + "label": "Shimmer Pulse", + "type": "slider", + "min": 0.005, + "max": 0.05, + "step": 0.005, + "description": "How fast the internal cores pulse and shimmer." + }, + { + "key": "instability", + "label": "Core Instability", + "type": "slider", + "min": 0.1, + "max": 2.0, + "step": 0.1, + "description": "How much the cells warp and distort over time." + }, + { + "key": "shellDarkness", + "label": "Shell Contrast", + "type": "slider", + "min": 0.5, + "max": 1.0, + "step": 0.05, + "description": "How dark the outer 'void' shell appears against the glowing cores." + } + ] +} diff --git a/frontend/src/components/DynamicBanner.tsx b/frontend/src/components/DynamicBanner.tsx index f8e6451..4f5ad31 100644 --- a/frontend/src/components/DynamicBanner.tsx +++ b/frontend/src/components/DynamicBanner.tsx @@ -4,6 +4,7 @@ import GenerativeBanner from './GenerativeBanner'; import NebulaBanner from './NebulaBanner'; import FirefliesBanner from './FirefliesBanner'; import HextechBanner from './HextechBanner'; +import HextechCorruptionBanner from './HextechCorruptionBanner'; interface Banner { id: number; @@ -94,6 +95,10 @@ const DynamicBanner: React.FC = (props) => { return ; } + if (banner.slug === 'hextech-corruption') { + return ; + } + // Fallback return ; }; diff --git a/frontend/src/components/HextechCorruptionBanner.tsx b/frontend/src/components/HextechCorruptionBanner.tsx new file mode 100644 index 0000000..1dce03f --- /dev/null +++ b/frontend/src/components/HextechCorruptionBanner.tsx @@ -0,0 +1,258 @@ +import React, { useRef, useEffect, useMemo } from 'react'; +import { alpha, useTheme } from '@mui/material'; + +interface HextechCorruptionBannerProps { + username: string; + palette: string[]; + ratingsCount: number; + followersCount: number; + settings?: { + cellCount?: number; + shimmerSpeed?: number; + glowIntensity?: number; + instability?: number; + shellDarkness?: number; + }; +} + +const HextechCorruptionBanner: React.FC = ({ username, palette, settings }) => { + const canvasRef = useRef(null); + const theme = useTheme(); + + const cellCount = settings?.cellCount ?? 40; + const shimmerSpeed = settings?.shimmerSpeed ?? 0.015; + const glowIntensity = settings?.glowIntensity ?? 0.9; + const instability = settings?.instability ?? 0.6; + const shellDarkness = settings?.shellDarkness ?? 0.85; + + 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, 3); + }, [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 ControlPoint { + x: number; + y: number; + ox: number; + oy: number; + vx: number; + vy: number; + phase: number; + color: string; + } + + let points: ControlPoint[] = []; + + const pseudoRandom = (offset: number) => { + const x = Math.sin(seed + offset) * 10000; + return x - Math.floor(x); + }; + + const initPoints = (width: number, height: number) => { + const newPoints = []; + for (let i = 0; i < cellCount; i++) { + const rx = pseudoRandom(i) * width; + const ry = pseudoRandom(i + 500) * height; + newPoints.push({ + x: rx, y: ry, + ox: rx, oy: ry, + vx: 0, vy: 0, + phase: pseudoRandom(i + 1000) * Math.PI * 2, + color: brightColors[i % brightColors.length] + }); + } + return newPoints; + }; + + 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`; + points = initPoints(rect.width, rect.height); + } + }; + + const resizeObserver = new ResizeObserver(() => resize()); + if (canvas.parentElement) resizeObserver.observe(canvas.parentElement); + resize(); + + let mouse = { x: -1000, y: -1000, down: false }; + const handleMouseMove = (e: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + mouse.x = e.clientX - rect.left; + mouse.y = e.clientY - rect.top; + }; + const handleMouseDown = () => { mouse.down = true; }; + const handleMouseUp = () => { mouse.down = false; }; + + const parent = canvas.parentElement; + if (parent) { + parent.addEventListener('mousemove', handleMouseMove); + parent.addEventListener('mousedown', handleMouseDown); + parent.addEventListener('mouseup', handleMouseUp); + parent.addEventListener('mouseleave', () => { mouse.x = -1000; mouse.y = -1000; mouse.down = false; }); + } + + 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); + time += shimmerSpeed; + + // 1. Draw Under-Glow (Inner Cores) + ctx.globalCompositeOperation = 'source-over'; + points.forEach((p) => { + // Harmonic motion + mouse interaction + const dxMouse = mouse.x - p.x; + const dyMouse = mouse.y - p.y; + const distMouse = Math.sqrt(dxMouse * dxMouse + dyMouse * dyMouse); + const mouseForce = Math.max(0, (300 - distMouse) / 300); + + const driftX = Math.sin(time + p.phase) * 20 * instability; + const driftY = Math.cos(time * 0.8 + p.phase) * 20 * instability; + + p.x = p.ox + driftX + (mouse.down ? -dxMouse * mouseForce * 0.5 : 0); + p.y = p.oy + driftY + (mouse.down ? -dyMouse * mouseForce * 0.5 : 0); + + const pulse = 0.7 + Math.sin(time * 3 + p.phase) * 0.3; + const glowSize = 60 + (mouseForce * 40); + + const grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowSize); + grad.addColorStop(0, alpha(p.color, 0.8 * glowIntensity * pulse)); + grad.addColorStop(0.5, alpha(p.color, 0.3 * glowIntensity)); + grad.addColorStop(1, alpha(p.color, 0)); + + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(p.x, p.y, glowSize, 0, Math.PI * 2); + ctx.fill(); + }); + + // 2. Draw Dark "Void" Shell (Voronoi-based cutout) + // We use a temporary canvas to draw the shell then mask it, or just use a heavy overlay + ctx.globalCompositeOperation = 'source-over'; + + // Create the "holes" effect by using a pattern of circles that are actually transparent + // Actually, to get the specific look from the image, we can use a simpler approach: + // Layer a dark color over everything, but with 'destination-out' circles at the point locations. + + const offCanvas = document.createElement('canvas'); + offCanvas.width = canvas.width; + offCanvas.height = canvas.height; + const offCtx = offCanvas.getContext('2d'); + if (offCtx) { + const dpr = window.devicePixelRatio || 1; + offCtx.scale(dpr, dpr); + + // Draw full dark shell + const shellColor = theme.palette.mode === 'light' ? alpha('#1a0a2a', shellDarkness) : alpha('#05020a', shellDarkness); + offCtx.fillStyle = shellColor; + offCtx.fillRect(0, 0, width, height); + + // Cut holes + offCtx.globalCompositeOperation = 'destination-out'; + points.forEach(p => { + const holeSize = 15 + Math.sin(time + p.phase) * 5 + (instability * 10); + + // Main hole + offCtx.beginPath(); + offCtx.arc(p.x, p.y, holeSize, 0, Math.PI * 2); + offCtx.fill(); + + // Add secondary smaller holes for that "punctured" look + for(let j=0; j<3; j++) { + const angle = p.phase + (j * Math.PI * 0.6); + const hx = p.x + Math.cos(angle) * (holeSize * 1.5); + const hy = p.y + Math.sin(angle) * (holeSize * 1.5); + offCtx.beginPath(); + offCtx.arc(hx, hy, holeSize * 0.4, 0, Math.PI * 2); + offCtx.fill(); + } + }); + + ctx.drawImage(offCanvas, 0, 0, width, height); + } + + // 3. Add Iridescent Edge Highlights + ctx.globalCompositeOperation = 'screen'; + ctx.lineWidth = 2; + points.forEach(p => { + const pulse = 0.5 + Math.sin(time * 2 + p.phase) * 0.5; + ctx.strokeStyle = alpha(p.color, pulse * 0.4); + ctx.beginPath(); + ctx.arc(p.x, p.y, 20 + pulse * 5, 0, Math.PI * 2); + ctx.stroke(); + }); + + ctx.globalCompositeOperation = 'source-over'; + animationFrameId = requestAnimationFrame(draw); + }; + + draw(); + + return () => { + resizeObserver.disconnect(); + if (parent) { + parent.removeEventListener('mousemove', handleMouseMove); + parent.removeEventListener('mousedown', handleMouseDown); + parent.removeEventListener('mouseup', handleMouseUp); + } + cancelAnimationFrame(animationFrameId); + }; + }, [seed, brightColors, cellCount, shimmerSpeed, glowIntensity, instability, shellDarkness, theme]); + + return ( + + ); +}; + +export default HextechCorruptionBanner;