import { useMemo } from "react"; import { cn } from "../lib/utils"; const BAYER_4X4 = [ [0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5], ] as const; interface CompanyPatternIconProps { companyName: string; brandColor?: string | null; className?: string; } function hashString(value: string): number { let hash = 2166136261; for (let i = 0; i < value.length; i++) { hash ^= value.charCodeAt(i); hash = Math.imul(hash, 16777619); } return hash >>> 0; } function mulberry32(seed: number): () => number { let state = seed >>> 0; return () => { state = (state + 0x6d2b79f5) >>> 0; let t = Math.imul(state ^ (state >>> 15), 1 | state); t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function hslToRgb(h: number, s: number, l: number): [number, number, number] { const hue = ((h % 360) + 360) % 360; const sat = Math.max(0, Math.min(100, s)) / 100; const light = Math.max(0, Math.min(100, l)) / 100; const c = (1 - Math.abs(2 * light - 1)) * sat; const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)); const m = light - c / 2; let r = 0; let g = 0; let b = 0; if (hue < 60) { r = c; g = x; } else if (hue < 120) { r = x; g = c; } else if (hue < 180) { g = c; b = x; } else if (hue < 240) { g = x; b = c; } else if (hue < 300) { r = x; b = c; } else { r = c; b = x; } return [ Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), ]; } function hexToHue(hex: string): number { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const d = max - min; if (d === 0) return 0; let h = 0; if (max === r) h = ((g - b) / d) % 6; else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4; return ((h * 60) + 360) % 360; } function makeCompanyPatternDataUrl(seed: string, brandColor?: string | null, logicalSize = 22, cellSize = 2): string { if (typeof document === "undefined") return ""; const canvas = document.createElement("canvas"); canvas.width = logicalSize * cellSize; canvas.height = logicalSize * cellSize; const ctx = canvas.getContext("2d"); if (!ctx) return ""; const rand = mulberry32(hashString(seed)); const hue = brandColor ? hexToHue(brandColor) : Math.floor(rand() * 360); const [offR, offG, offB] = hslToRgb( hue, 54 + Math.floor(rand() * 14), 36 + Math.floor(rand() * 12), ); const [onR, onG, onB] = hslToRgb( hue + (rand() > 0.5 ? 10 : -10), 86 + Math.floor(rand() * 10), 82 + Math.floor(rand() * 10), ); const center = (logicalSize - 1) / 2; const half = Math.max(center, 1); const gradientAngle = rand() * Math.PI * 2; const gradientDirX = Math.cos(gradientAngle); const gradientDirY = Math.sin(gradientAngle); const maxProjection = Math.abs(gradientDirX * half) + Math.abs(gradientDirY * half); const diagonalFrequency = 0.34 + rand() * 0.12; const antiDiagonalFrequency = 0.33 + rand() * 0.12; const diagonalPhase = rand() * Math.PI * 2; const antiDiagonalPhase = rand() * Math.PI * 2; ctx.fillStyle = `rgb(${offR} ${offG} ${offB})`; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = `rgb(${onR} ${onG} ${onB})`; const dotRadius = cellSize * 0.46; for (let y = 0; y < logicalSize; y++) { const dy = y - center; for (let x = 0; x < logicalSize; x++) { const dx = x - center; // Side-to-side signal where visible gradient is produced by dither density. const projection = dx * gradientDirX + dy * gradientDirY; const gradient = (projection / maxProjection + 1) * 0.5; const diagonal = Math.sin((dx + dy) * diagonalFrequency + diagonalPhase) * 0.5 + 0.5; const antiDiagonal = Math.sin((dx - dy) * antiDiagonalFrequency + antiDiagonalPhase) * 0.5 + 0.5; const hatch = diagonal * 0.5 + antiDiagonal * 0.5; const signal = Math.max(0, Math.min(1, gradient + (hatch - 0.5) * 0.22)); // Canonical 16-level ordered dither: level 0..15 compared to Bayer 4x4 threshold index. const level = Math.max(0, Math.min(15, Math.floor(signal * 16))); const thresholdIndex = BAYER_4X4[y & 3]![x & 3]!; if (level <= thresholdIndex) continue; const cx = x * cellSize + cellSize / 2; const cy = y * cellSize + cellSize / 2; ctx.beginPath(); ctx.arc(cx, cy, dotRadius, 0, Math.PI * 2); ctx.fill(); } } return canvas.toDataURL("image/png"); } export function CompanyPatternIcon({ companyName, brandColor, className }: CompanyPatternIconProps) { const initial = companyName.trim().charAt(0).toUpperCase() || "?"; const patternDataUrl = useMemo( () => makeCompanyPatternDataUrl(companyName.trim().toLowerCase(), brandColor), [companyName, brandColor], ); return (
{patternDataUrl ? ( ) : (
)} {initial}
); }