UNPKG

tuix

Version:

A performant TUI framework for Bun with JSX and reactive state management

585 lines (519 loc) 15.7 kB
/** * Advanced Styling Utilities - Extended styling capabilities * * Features: * - Shadow effects and depth * - Glow and highlight effects * - Pattern fills and textures * - Complex border styles * - Layer blending and composition * - Animation and transition helpers */ import { type Color, Colors } from "./color.ts" import { type Style, style } from "./style.ts" import { type GradientConfig, textGradient } from "./gradients.ts" // ============================================================================= // Types // ============================================================================= export interface ShadowConfig { readonly offset: { x: number; y: number } readonly blur: number readonly color: Color readonly opacity: number } export interface GlowConfig { readonly radius: number readonly color: Color readonly intensity: number } export interface PatternConfig { readonly type: 'dots' | 'stripes' | 'checkerboard' | 'diagonal' | 'cross' | 'wave' readonly foreground: Color readonly background: Color readonly scale: number } export interface BorderStyle { readonly type: 'solid' | 'dashed' | 'dotted' | 'double' | 'gradient' | 'pattern' readonly width: number readonly color?: Color readonly gradient?: GradientConfig readonly pattern?: PatternConfig } export interface LayerEffect { readonly type: 'overlay' | 'multiply' | 'screen' | 'color-dodge' | 'color-burn' readonly color: Color readonly opacity: number } // ============================================================================= // Shadow Effects // ============================================================================= /** * Create drop shadow effect using box-drawing characters */ export const createDropShadow = ( content: string[], config: ShadowConfig ): string[] => { const { offset, color } = config const shadowLines: string[] = [] // Create shadow by offsetting content for (let i = 0; i < content.length; i++) { const line = content[i] || '' let shadowLine = '' // Add horizontal offset if (offset.x > 0) { shadowLine = ' '.repeat(offset.x) + line.replace(/./g, '▓') } else if (offset.x < 0) { shadowLine = line.substring(-offset.x).replace(/./g, '▓') } else { shadowLine = line.replace(/./g, '▓') } shadowLines.push(shadowLine) } // Add vertical offset const result: string[] = [] if (offset.y > 0) { // Shadow appears below result.push(...content) for (let i = 0; i < offset.y; i++) { if (i < shadowLines.length) { result.push(shadowLines[i]!) } } } else if (offset.y < 0) { // Shadow appears above for (let i = -offset.y; i < shadowLines.length; i++) { result.push(shadowLines[i]!) } result.push(...content) } else { // No vertical offset result.push(...content) } return result } /** * Create inner shadow effect */ export const createInnerShadow = ( content: string[], config: ShadowConfig ): string[] => { // Inner shadow darkens edges of content return content.map((line, y) => { return line.split('').map((char, x) => { const isEdge = x === 0 || x === line.length - 1 || y === 0 || y === content.length - 1 return isEdge ? '▓' : char }).join('') }) } // ============================================================================= // Glow Effects // ============================================================================= /** * Create glow effect around content */ export const createGlow = ( content: string[], config: GlowConfig ): string[] => { const { radius, intensity } = config const glowLines: string[] = [] // Expand content area for glow const padding = Math.ceil(radius) const maxWidth = Math.max(...content.map(line => line.length)) for (let y = -padding; y < content.length + padding; y++) { let glowLine = '' for (let x = -padding; x < maxWidth + padding; x++) { // Calculate distance from nearest content let minDistance = Infinity for (let cy = 0; cy < content.length; cy++) { const line = content[cy] || '' for (let cx = 0; cx < line.length; cx++) { if (line[cx] && line[cx] !== ' ') { const distance = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) minDistance = Math.min(minDistance, distance) } } } // Add glow based on distance if (minDistance <= radius) { const glowIntensity = Math.max(0, 1 - minDistance / radius) * intensity if (glowIntensity > 0.7) { glowLine += '▓' } else if (glowIntensity > 0.4) { glowLine += '▒' } else if (glowIntensity > 0.1) { glowLine += '░' } else { glowLine += ' ' } } else { glowLine += ' ' } } glowLines.push(glowLine) } return glowLines } // ============================================================================= // Pattern Effects // ============================================================================= /** * Generate pattern based on configuration */ export const generatePattern = ( width: number, height: number, config: PatternConfig ): string[] => { const { type, scale } = config const lines: string[] = [] for (let y = 0; y < height; y++) { let line = '' for (let x = 0; x < width; x++) { let usePattern = false switch (type) { case 'dots': usePattern = (Math.floor(x / scale) + Math.floor(y / scale)) % 2 === 0 break case 'stripes': usePattern = Math.floor(y / scale) % 2 === 0 break case 'checkerboard': usePattern = (Math.floor(x / scale) + Math.floor(y / scale)) % 2 === 0 break case 'diagonal': usePattern = (x + y) % (scale * 2) < scale break case 'cross': usePattern = x % scale === 0 || y % scale === 0 break case 'wave': usePattern = Math.sin(x / scale) * Math.sin(y / scale) > 0 break default: usePattern = false } line += usePattern ? '█' : '░' } lines.push(line) } return lines } /** * Apply pattern to existing content */ export const applyPattern = ( content: string[], config: PatternConfig ): string[] => { const maxWidth = Math.max(...content.map(line => line.length)) const pattern = generatePattern(maxWidth, content.length, config) return content.map((line, y) => { const patternLine = pattern[y] || '' return line.split('').map((char, x) => { if (char === ' ') { return patternLine[x] || ' ' } return char }).join('') }) } // ============================================================================= // Advanced Border Styles // ============================================================================= /** * Create styled border characters */ export const createStyledBorder = ( width: number, height: number, borderStyle: BorderStyle ): { top: string bottom: string left: string right: string corners: { tl: string; tr: string; bl: string; br: string } } => { const { type, width: borderWidth } = borderStyle let chars = { horizontal: '─', vertical: '│', tl: '┌', tr: '┐', bl: '└', br: '┘' } // Modify based on border type switch (type) { case 'dashed': chars = { horizontal: '┄', vertical: '┆', tl: '┌', tr: '┐', bl: '└', br: '┘' } break case 'dotted': chars = { horizontal: '┈', vertical: '┊', tl: '┌', tr: '┐', bl: '└', br: '┘' } break case 'double': chars = { horizontal: '═', vertical: '║', tl: '╔', tr: '╗', bl: '╚', br: '╝' } break } // Create border lines const top = chars.horizontal.repeat(width - 2) const bottom = chars.horizontal.repeat(width - 2) const left = chars.vertical const right = chars.vertical return { top, bottom, left, right, corners: { tl: chars.tl, tr: chars.tr, bl: chars.bl, br: chars.br } } } // ============================================================================= // Layer Effects // ============================================================================= /** * Apply layer blending effect */ export const applyLayerEffect = ( baseColor: Color, effect: LayerEffect ): Color => { const [baseR, baseG, baseB] = colorToRgb(baseColor) const [effectR, effectG, effectB] = colorToRgb(effect.color) const { opacity, type } = effect let resultR: number, resultG: number, resultB: number switch (type) { case 'overlay': resultR = Math.round(baseR * (1 - opacity) + effectR * opacity) resultG = Math.round(baseG * (1 - opacity) + effectG * opacity) resultB = Math.round(baseB * (1 - opacity) + effectB * opacity) break case 'multiply': resultR = Math.round(baseR * effectR / 255 * opacity + baseR * (1 - opacity)) resultG = Math.round(baseG * effectG / 255 * opacity + baseG * (1 - opacity)) resultB = Math.round(baseB * effectB / 255 * opacity + baseB * (1 - opacity)) break case 'screen': resultR = Math.round(255 - (255 - baseR) * (255 - effectR) / 255 * opacity + baseR * (1 - opacity)) resultG = Math.round(255 - (255 - baseG) * (255 - effectG) / 255 * opacity + baseG * (1 - opacity)) resultB = Math.round(255 - (255 - baseB) * (255 - effectB) / 255 * opacity + baseB * (1 - opacity)) break case 'color-dodge': resultR = Math.min(255, Math.round(baseR / (1 - effectR / 255) * opacity + baseR * (1 - opacity))) resultG = Math.min(255, Math.round(baseG / (1 - effectG / 255) * opacity + baseG * (1 - opacity))) resultB = Math.min(255, Math.round(baseB / (1 - effectB / 255) * opacity + baseB * (1 - opacity))) break case 'color-burn': resultR = Math.max(0, Math.round(255 - (255 - baseR) / (effectR / 255) * opacity + baseR * (1 - opacity))) resultG = Math.max(0, Math.round(255 - (255 - baseG) / (effectG / 255) * opacity + baseG * (1 - opacity))) resultB = Math.max(0, Math.round(255 - (255 - baseB) / (effectB / 255) * opacity + baseB * (1 - opacity))) break default: return baseColor } return { _tag: "RGB", r: Math.max(0, Math.min(255, resultR)), g: Math.max(0, Math.min(255, resultG)), b: Math.max(0, Math.min(255, resultB)) } } // Helper function to convert color to RGB (reused from gradients.ts concept) const colorToRgb = (color: Color): [number, number, number] => { if (color._tag === "RGB") { return [color.r, color.g, color.b] } else if (color._tag === "ANSI") { const ansiToRgb: Record<number, [number, number, number]> = { 0: [0, 0, 0], 1: [128, 0, 0], 2: [0, 128, 0], 3: [128, 128, 0], 4: [0, 0, 128], 5: [128, 0, 128], 6: [0, 128, 128], 7: [192, 192, 192], 8: [128, 128, 128], 9: [255, 0, 0], 10: [0, 255, 0], 11: [255, 255, 0], 12: [0, 0, 255], 13: [255, 0, 255], 14: [0, 255, 255], 15: [255, 255, 255] } return ansiToRgb[color.code] ?? [128, 128, 128] } return [128, 128, 128] } // ============================================================================= // Animation Helpers // ============================================================================= /** * Create pulsing effect */ export const createPulse = ( baseStyle: Style, time: number, intensity: number = 0.3, speed: number = 1 ): Style => { const pulse = (Math.sin(time * speed) + 1) / 2 // 0 to 1 const factor = 1 + pulse * intensity // This is a simplified implementation - in a real system you'd apply the pulse // to brightness or opacity return baseStyle } /** * Create shake effect for error states */ export const createShake = ( baseOffset: { x: number; y: number }, time: number, amplitude: number = 2, frequency: number = 8 ): { x: number; y: number } => { const shake = Math.sin(time * frequency) * amplitude return { x: baseOffset.x + shake, y: baseOffset.y } } /** * Create bounce effect */ export const createBounce = ( time: number, height: number = 4, duration: number = 1 ): number => { const t = (time % duration) / duration const bounce = Math.abs(Math.sin(t * Math.PI)) * height return bounce } // ============================================================================= // Text Effects // ============================================================================= /** * Create typewriter effect */ export const createTypewriter = ( text: string, time: number, speed: number = 10 ): string => { const charactersToShow = Math.floor(time * speed) return text.substring(0, charactersToShow) } /** * Create wave text effect */ export const createWaveText = ( text: string, time: number, amplitude: number = 2, frequency: number = 0.5 ): Array<{ char: string; offset: number }> => { return text.split('').map((char, index) => ({ char, offset: Math.sin(time + index * frequency) * amplitude })) } /** * Create rainbow text effect */ export const createRainbowText = ( text: string, time: number = 0, speed: number = 1 ): Style[] => { const rainbowColors = [ Colors.red, Colors.yellow, Colors.green, Colors.cyan, Colors.blue, Colors.magenta ] return text.split('').map((char, index) => { const colorIndex = Math.floor((index + time * speed) % rainbowColors.length) const color = rainbowColors[colorIndex] ?? Colors.white return style().foreground(color) }) } // ============================================================================= // Composite Effects // ============================================================================= /** * Create neon sign effect */ export const createNeonEffect = ( text: string, color: Color, time: number = 0 ): { mainStyle: Style glowStyle: Style flickerIntensity: number } => { const flicker = Math.sin(time * 13) * 0.1 + 0.9 // 0.8 to 1.0 const glow = Math.sin(time * 3) * 0.2 + 0.8 // 0.6 to 1.0 return { mainStyle: style().foreground(color).bold(), glowStyle: style().foreground(color), // Would apply glow effect flickerIntensity: flicker } } /** * Create matrix-style digital rain effect */ export const createMatrixEffect = ( width: number, height: number, time: number, density: number = 0.1 ): Array<{ x: number; y: number; char: string; intensity: number }> => { const drops: Array<{ x: number; y: number; char: string; intensity: number }> = [] for (let x = 0; x < width; x++) { if (Math.random() < density) { const chars = '0123456789ABCDEF' const char = chars[Math.floor(Math.random() * chars.length)] ?? '0' const y = Math.floor((time + x) * 5) % height const intensity = Math.random() drops.push({ x, y, char, intensity }) } } return drops } /** * Create hologram effect */ export const createHologramEffect = ( time: number, scanlineSpeed: number = 2 ): { scanlinePosition: number interference: number flicker: boolean } => { const scanline = (time * scanlineSpeed) % 1 const interference = Math.sin(time * 7) * 0.1 const flicker = Math.random() < 0.05 // 5% chance to flicker return { scanlinePosition: scanline, interference, flicker } }