video2ascii 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/hooks/useVideoToAscii.ts","../src/lib/ascii-charsets.ts","../src/lib/webgl/shaders/vertex.glsl","../src/lib/webgl/shaders/fragment.glsl","../src/lib/webgl/utils.ts","../src/lib/webgl/types.ts","../src/hooks/useAsciiMouseEffect.ts","../src/hooks/useAsciiRipple.ts","../src/hooks/useAsciiAudio.ts","../src/components/VideoToAscii.tsx"],"sourcesContent":["export { VideoToAscii } from \"./components/VideoToAscii\";\nexport type { VideoToAsciiProps } from \"./lib/webgl/types\";\nexport { ASCII_CHARSETS, type CharsetKey } from \"./lib/ascii-charsets\";\n","import { useRef, useState, useCallback, useEffect, useMemo } from \"react\";\nimport { getCharArray, DEFAULT_CHARSET } from \"@/lib/ascii-charsets\";\nimport {\n VERTEX_SHADER,\n FRAGMENT_SHADER,\n compileShader,\n createProgram,\n createFullscreenQuad,\n createVideoTexture,\n createAsciiAtlas,\n calculateGridDimensions,\n CHAR_WIDTH_RATIO,\n type UseVideoToAsciiOptions,\n type AsciiContext,\n type AsciiStats,\n type UniformSetter,\n type UniformLocations,\n} from \"@/lib/webgl\";\n\nexport type { UseVideoToAsciiOptions, AsciiContext, AsciiStats };\n\nconst MAX_TRAIL_LENGTH = 24;\nconst MAX_RIPPLES = 8;\n\n// Hook Implementation\nexport function useVideoToAscii(\n options: UseVideoToAsciiOptions = {}\n): AsciiContext {\n const {\n fontSize = 10,\n colored = false,\n blend = 0,\n highlight = 0,\n charset = DEFAULT_CHARSET,\n maxWidth = 900,\n onStats,\n } = options;\n\n // DOM refs\n const containerRef = useRef<HTMLDivElement>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // WebGL refs - these hold the GPU resources\n const glRef = useRef<WebGL2RenderingContext | null>(null);\n const programRef = useRef<WebGLProgram | null>(null);\n const videoTextureRef = useRef<WebGLTexture | null>(null);\n const atlasTextureRef = useRef<WebGLTexture | null>(null);\n const animationRef = useRef<number>(0);\n\n // Feature hooks register their uniform setters here\n const uniformSettersRef = useRef<Map<string, UniformSetter>>(new Map());\n // Cached uniform locations for performance (avoid lookup every frame)\n const uniformLocationsRef = useRef<UniformLocations | null>(null);\n\n // Benchmark/stats refs\n const frameCountRef = useRef(0);\n const frameTimesRef = useRef<number[]>([]);\n const lastFpsTimeRef = useRef(performance.now());\n\n // State\n const [dimensions, setDimensions] = useState({ cols: 80, rows: 24 });\n const [stats, setStats] = useState<AsciiStats>({ fps: 0, frameTime: 0 });\n const [isReady, setIsReady] = useState(false);\n const [isPlaying, setIsPlaying] = useState(false);\n\n // Calculate grid size based on font size\n const charWidth = fontSize * CHAR_WIDTH_RATIO;\n const cols = Math.floor(maxWidth / charWidth);\n // Memoize chars array so it only recalculates when charset changes\n const chars = useMemo(() => getCharArray(charset), [charset]);\n\n // Feature hooks call this to register their uniform setter\n const registerUniformSetter = useCallback(\n (id: string, setter: UniformSetter) => {\n uniformSettersRef.current.set(id, setter);\n },\n []\n );\n\n const unregisterUniformSetter = useCallback((id: string) => {\n uniformSettersRef.current.delete(id);\n }, []);\n\n // Cache all uniform locations after program is compiled\n // This avoids expensive getUniformLocation calls every frame\n const cacheUniformLocations = useCallback(\n (gl: WebGL2RenderingContext, program: WebGLProgram): UniformLocations => {\n const get = (name: string) => gl.getUniformLocation(program, name);\n\n return {\n // Core uniforms\n u_video: get(\"u_video\"),\n u_asciiAtlas: get(\"u_asciiAtlas\"),\n u_resolution: get(\"u_resolution\"),\n u_charSize: get(\"u_charSize\"),\n u_gridSize: get(\"u_gridSize\"),\n u_numChars: get(\"u_numChars\"),\n u_colored: get(\"u_colored\"),\n u_blend: get(\"u_blend\"),\n u_highlight: get(\"u_highlight\"),\n\n // Mouse uniforms\n u_mouse: get(\"u_mouse\"),\n u_mouseRadius: get(\"u_mouseRadius\"),\n u_trailLength: get(\"u_trailLength\"),\n u_trail: Array.from({ length: MAX_TRAIL_LENGTH }, (_, i) =>\n get(`u_trail[${i}]`)\n ),\n\n // Ripple uniforms\n u_time: get(\"u_time\"),\n u_rippleEnabled: get(\"u_rippleEnabled\"),\n u_rippleSpeed: get(\"u_rippleSpeed\"),\n u_ripples: Array.from({ length: MAX_RIPPLES }, (_, i) =>\n get(`u_ripples[${i}]`)\n ),\n\n // Audio uniforms\n u_audioLevel: get(\"u_audioLevel\"),\n u_audioReactivity: get(\"u_audioReactivity\"),\n u_audioSensitivity: get(\"u_audioSensitivity\"),\n };\n },\n []\n );\n\n // Initialize WebGL\n const initWebGL = useCallback(() => {\n const canvas = canvasRef.current;\n const video = videoRef.current;\n if (!canvas || !video || !video.videoWidth) return false;\n\n // Figure out grid dimensions from video aspect ratio\n const grid = calculateGridDimensions(\n video.videoWidth,\n video.videoHeight,\n cols\n );\n setDimensions(grid);\n\n // Set canvas size\n const pixelWidth = grid.cols * charWidth;\n const pixelHeight = grid.rows * fontSize;\n canvas.width = pixelWidth;\n canvas.height = pixelHeight;\n\n // Get WebGL2 context (WebGL2 has better texture handling)\n const gl = canvas.getContext(\"webgl2\", {\n antialias: false,\n preserveDrawingBuffer: false,\n });\n if (!gl) {\n console.error(\"WebGL2 not supported\");\n return false;\n }\n glRef.current = gl;\n\n // Compile shaders (vertex positions the quad, fragment does the ASCII magic)\n const vertexShader = compileShader(gl, VERTEX_SHADER, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(\n gl,\n FRAGMENT_SHADER,\n gl.FRAGMENT_SHADER\n );\n if (!vertexShader || !fragmentShader) return false;\n\n // Link shaders into a program\n const program = createProgram(gl, vertexShader, fragmentShader);\n if (!program) return false;\n programRef.current = program;\n gl.useProgram(program);\n\n // Create a fullscreen quad (two triangles covering the canvas)\n createFullscreenQuad(gl, program);\n\n // Create textures for video frame and ASCII character atlas\n videoTextureRef.current = createVideoTexture(gl);\n atlasTextureRef.current = createAsciiAtlas(gl, chars, fontSize);\n\n // Cache all uniform locations for fast access during render\n const locations = cacheUniformLocations(gl, program);\n uniformLocationsRef.current = locations;\n\n // Tell the shader which texture units to use\n gl.uniform1i(locations.u_video, 0); // texture unit 0\n gl.uniform1i(locations.u_asciiAtlas, 1); // texture unit 1\n\n // Set static uniforms that don't change during playback\n gl.uniform2f(locations.u_resolution, pixelWidth, pixelHeight);\n gl.uniform2f(locations.u_charSize, charWidth, fontSize);\n gl.uniform2f(locations.u_gridSize, cols, grid.rows);\n gl.uniform1f(locations.u_numChars, chars.length);\n\n // Initialize feature uniforms to disabled state\n gl.uniform2f(locations.u_mouse, -1, -1);\n gl.uniform1f(locations.u_mouseRadius, 0);\n gl.uniform1i(locations.u_trailLength, 0);\n gl.uniform1f(locations.u_rippleEnabled, 0);\n gl.uniform1f(locations.u_audioLevel, 0);\n gl.uniform1f(locations.u_audioReactivity, 0);\n gl.uniform1f(locations.u_audioSensitivity, 0);\n\n gl.viewport(0, 0, pixelWidth, pixelHeight);\n\n setIsReady(true);\n return true;\n }, [cols, charWidth, fontSize, chars, cacheUniformLocations]);\n\n // Render loop - runs every frame while video is playing\n const render = useCallback(() => {\n const gl = glRef.current;\n const video = videoRef.current;\n const program = programRef.current;\n const locations = uniformLocationsRef.current;\n\n if (!gl || !video || !program || !locations || video.paused || video.ended)\n return;\n\n const frameStart = performance.now();\n\n // Upload current video frame to GPU\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, videoTextureRef.current);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);\n // Generate mipmaps for better quality when sampling large areas\n gl.generateMipmap(gl.TEXTURE_2D);\n\n // Bind the ASCII atlas texture\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, atlasTextureRef.current);\n\n // Update uniforms that can change each frame\n gl.uniform1i(locations.u_colored, colored ? 1 : 0);\n gl.uniform1f(locations.u_blend, blend / 100);\n gl.uniform1f(locations.u_highlight, highlight / 100);\n\n // Let feature hooks update their uniforms\n for (const setter of uniformSettersRef.current.values()) {\n setter(gl, program, locations);\n }\n\n // Draw the quad (shader does all the work)\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n // Track performance\n const frameEnd = performance.now();\n frameCountRef.current++;\n frameTimesRef.current.push(frameEnd - frameStart);\n if (frameTimesRef.current.length > 60) frameTimesRef.current.shift();\n\n // Update FPS counter every second\n const now = performance.now();\n if (now - lastFpsTimeRef.current >= 1000) {\n const avgFrameTime =\n frameTimesRef.current.reduce((a, b) => a + b, 0) /\n frameTimesRef.current.length;\n const newStats = { fps: frameCountRef.current, frameTime: avgFrameTime };\n setStats(newStats);\n onStats?.(newStats);\n frameCountRef.current = 0;\n lastFpsTimeRef.current = now;\n }\n\n // Schedule next frame\n animationRef.current = requestAnimationFrame(render);\n }, [colored, blend, highlight, onStats]);\n\n // Video Event Handlers\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleLoadedMetadata = () => {\n initWebGL();\n };\n\n const handlePlay = () => {\n setIsPlaying(true);\n animationRef.current = requestAnimationFrame(render);\n };\n\n const handlePause = () => {\n setIsPlaying(false);\n cancelAnimationFrame(animationRef.current);\n };\n\n const handleEnded = () => {\n setIsPlaying(false);\n cancelAnimationFrame(animationRef.current);\n };\n\n video.addEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.addEventListener(\"play\", handlePlay);\n video.addEventListener(\"pause\", handlePause);\n video.addEventListener(\"ended\", handleEnded);\n\n // If video is already loaded when we mount\n if (video.readyState >= 1) {\n handleLoadedMetadata();\n }\n\n return () => {\n video.removeEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.removeEventListener(\"play\", handlePlay);\n video.removeEventListener(\"pause\", handlePause);\n video.removeEventListener(\"ended\", handleEnded);\n cancelAnimationFrame(animationRef.current);\n };\n }, [initWebGL, render]);\n\n // Reinitialize when config changes\n useEffect(() => {\n if (videoRef.current && videoRef.current.readyState >= 1) {\n initWebGL();\n }\n }, [initWebGL]);\n\n // Cleanup WebGL resources when unmounting\n useEffect(() => {\n return () => {\n const gl = glRef.current;\n if (gl) {\n if (videoTextureRef.current) gl.deleteTexture(videoTextureRef.current);\n if (atlasTextureRef.current) gl.deleteTexture(atlasTextureRef.current);\n if (programRef.current) gl.deleteProgram(programRef.current);\n }\n cancelAnimationFrame(animationRef.current);\n };\n }, []);\n\n // Playback Controls\n const play = useCallback(() => {\n videoRef.current?.play();\n }, []);\n\n const pause = useCallback(() => {\n videoRef.current?.pause();\n }, []);\n\n const toggle = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n if (video.paused) {\n video.play();\n } else {\n video.pause();\n }\n }, []);\n\n // Spacebar to toggle play/pause\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.code === \"Space\" && e.target === document.body) {\n e.preventDefault();\n toggle();\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [toggle]);\n\n return {\n containerRef,\n videoRef,\n canvasRef,\n glRef,\n programRef,\n uniformLocationsRef,\n registerUniformSetter,\n unregisterUniformSetter,\n dimensions,\n stats,\n isReady,\n isPlaying,\n play,\n pause,\n toggle,\n };\n}\n","/**\n * ASCII Character Set Definitions\n *\n * Character sets are ordered from dark (low brightness) to light (high brightness).\n * The shader maps pixel brightness to character index, so the first character\n * represents the darkest pixels and the last represents the brightest.\n *\n * To add a new character set:\n * 1. Add an entry to ASCII_CHARSETS with a unique key\n * 2. Order characters from dark → light (spaces/dots first, dense chars last)\n * 3. The key becomes available in CharsetKey type automatically\n */\n\nexport const ASCII_CHARSETS = {\n /** Classic 10-character gradient - good balance of detail and performance */\n standard: {\n name: \"Standard\",\n chars: \" .:-=+*#%@\",\n },\n\n /** Unicode block characters - chunky retro aesthetic */\n blocks: {\n name: \"Blocks\",\n chars: \" ░▒▓█\",\n },\n\n /** Minimal 5-character set - high contrast, fast rendering */\n minimal: {\n name: \"Minimal\",\n chars: \" .oO@\",\n },\n\n /** Binary on/off - pure silhouette mode */\n binary: {\n name: \"Binary\",\n chars: \" █\",\n },\n\n /** 70-character gradient - maximum detail, best for high resolution */\n detailed: {\n name: \"Detailed\",\n chars:\n \" .'`^\\\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$\",\n },\n\n /** Dot-based - pointillist aesthetic */\n dots: {\n name: \"Dots\",\n chars: \" ·•●\",\n },\n\n /** Directional arrows - experimental */\n arrows: {\n name: \"Arrows\",\n chars: \" ←↙↓↘→↗↑↖\",\n },\n\n /** Moon phases - decorative gradient */\n emoji: {\n name: \"Emoji\",\n chars: \" ░▒▓🌑🌒🌓🌔🌕\",\n },\n} as const;\n\n/** Type-safe key for selecting character sets */\nexport type CharsetKey = keyof typeof ASCII_CHARSETS;\n\n/** Default character set used when none is specified */\nexport const DEFAULT_CHARSET: CharsetKey = \"standard\";\n\n/**\n * Get the character array for a given charset key.\n * Uses spread operator to correctly handle multi-byte unicode characters.\n */\nexport function getCharArray(charset: CharsetKey): string[] {\n return [...ASCII_CHARSETS[charset].chars];\n}\n\n/**\n * Get the display name for a charset\n */\nexport function getCharsetName(charset: CharsetKey): string {\n return ASCII_CHARSETS[charset].name;\n}\n","#version 300 es\n\n// Fullscreen quad - passes texture coords to fragment shader\n\nin vec2 a_position;\nin vec2 a_texCoord;\nout vec2 v_texCoord;\n\nvoid main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n}\n","#version 300 es\nprecision highp float;\n\n// Textures\nuniform sampler2D u_video;\nuniform sampler2D u_asciiAtlas;\n\n// Dimensions\nuniform vec2 u_resolution;\nuniform vec2 u_charSize;\nuniform vec2 u_gridSize;\nuniform float u_numChars;\n\n// Rendering options\nuniform bool u_colored;\nuniform float u_blend;\nuniform float u_highlight;\n\n// Audio\nuniform float u_audioLevel;\nuniform float u_audioReactivity;\nuniform float u_audioSensitivity;\n\n// Mouse\nuniform vec2 u_mouse;\nuniform float u_mouseRadius;\nuniform vec2 u_trail[24];\nuniform int u_trailLength;\n\n// Ripple\nuniform vec4 u_ripples[8];\nuniform float u_time;\nuniform float u_rippleEnabled;\nuniform float u_rippleSpeed;\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nvoid main() {\n // Figure out which ASCII cell this pixel is in\n vec2 cellCoord = floor(v_texCoord * u_gridSize);\n vec2 thisCell = cellCoord;\n \n // Sample video at cell center (mipmaps handle averaging)\n vec2 cellCenter = (cellCoord + 0.5) / u_gridSize;\n vec4 videoColor = texture(u_video, cellCenter);\n \n // Perceived brightness using human eye sensitivity weights\n float baseBrightness = dot(videoColor.rgb, vec3(0.299, 0.587, 0.114));\n \n // Audio reactivity - louder = brighter, silence = darker\n float minBrightness = mix(0.3, 0.0, u_audioSensitivity);\n float maxBrightness = mix(1.0, 5.0, u_audioSensitivity);\n float audioMultiplier = mix(minBrightness, maxBrightness, u_audioLevel);\n float audioModulated = baseBrightness * audioMultiplier;\n float brightness = mix(baseBrightness, audioModulated, u_audioReactivity);\n \n // Cursor glow - blocky circle effect\n float cursorGlow = 0.0;\n float cursorRadius = 5.0;\n \n vec2 mouseCell = floor(u_mouse * u_gridSize);\n float cellDist = length(thisCell - mouseCell);\n if (cellDist <= cursorRadius && u_mouse.x >= 0.0) {\n cursorGlow += 1.0 - cellDist / cursorRadius;\n }\n \n // Trail effect\n for (int i = 0; i < 12; i++) {\n if (i >= u_trailLength) break;\n vec2 trailPos = u_trail[i];\n if (trailPos.x < 0.0) continue;\n \n vec2 trailCell = floor(trailPos * u_gridSize);\n float trailDist = length(thisCell - trailCell);\n float trailRadius = cursorRadius * 0.8;\n \n if (trailDist <= trailRadius) {\n float fade = 1.0 - float(i) / float(u_trailLength);\n cursorGlow += (1.0 - trailDist / trailRadius) * 0.5 * fade;\n }\n }\n cursorGlow = min(cursorGlow, 1.0);\n \n // Ripple effect - expanding rings on click\n float rippleGlow = 0.0;\n if (u_rippleEnabled > 0.5) {\n for (int i = 0; i < 8; i++) {\n vec4 ripple = u_ripples[i];\n if (ripple.w < 0.5) continue;\n \n float age = u_time - ripple.z;\n if (age < 0.0) continue;\n \n vec2 rippleCell = floor(ripple.xy * u_gridSize);\n float cellDist = length(thisCell - rippleCell);\n float initialRadius = 5.0;\n \n float distFromEdge = max(0.0, cellDist - initialRadius);\n float rippleSpeed = u_rippleSpeed;\n float reachTime = distFromEdge / rippleSpeed;\n float timeSinceReached = age - reachTime;\n \n float fadeDuration = 0.5;\n if (timeSinceReached >= 0.0 && timeSinceReached < fadeDuration) {\n float pop = 1.0 - timeSinceReached / fadeDuration;\n pop = pop * pop;\n rippleGlow += pop * 0.3;\n }\n }\n rippleGlow = min(rippleGlow, 1.0);\n }\n \n // Map brightness to character index (0 = darkest char, numChars-1 = brightest)\n float charIndex = floor(brightness * (u_numChars - 0.001));\n \n // Find the character in the atlas (horizontal strip of pre-rendered chars)\n float atlasX = charIndex / u_numChars;\n vec2 cellPos = fract(v_texCoord * u_gridSize);\n vec2 atlasCoord = vec2(atlasX + cellPos.x / u_numChars, cellPos.y);\n vec4 charColor = texture(u_asciiAtlas, atlasCoord);\n \n // Pick the color - video colors or green terminal aesthetic\n vec3 baseColor;\n if (u_colored) {\n baseColor = videoColor.rgb;\n } else {\n baseColor = vec3(0.0, 1.0, 0.0);\n }\n \n // Background highlight behind each character\n float bgIntensity = 0.15 + u_highlight * 0.35;\n vec3 bgColor = baseColor * bgIntensity;\n vec3 textColor = baseColor * 1.2;\n vec3 finalColor = mix(bgColor, textColor, charColor.r);\n \n // Add cursor and ripple glow\n finalColor += cursorGlow * baseColor * 0.5;\n finalColor += rippleGlow * baseColor;\n \n // Blend with original video if requested\n vec3 blendedColor = mix(finalColor, videoColor.rgb, u_blend);\n \n fragColor = vec4(blendedColor, 1.0);\n}\n","// Compiles a GLSL shader from source code\nexport function compileShader(\n gl: WebGL2RenderingContext,\n source: string,\n type: number\n): WebGLShader | null {\n const shader = gl.createShader(type);\n if (!shader) {\n console.error(\"Failed to create shader\");\n return null;\n }\n\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n\n return shader;\n}\n\n// Links vertex and fragment shaders into a program\nexport function createProgram(\n gl: WebGL2RenderingContext,\n vertexShader: WebGLShader,\n fragmentShader: WebGLShader\n): WebGLProgram | null {\n const program = gl.createProgram();\n if (!program) {\n console.error(\"Failed to create program\");\n return null;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n }\n\n return program;\n}\n\n// Creates a fullscreen quad (two triangles covering the viewport)\nexport function createFullscreenQuad(\n gl: WebGL2RenderingContext,\n program: WebGLProgram\n): void {\n // Vertex positions in clip space (-1 to 1)\n const positions = new Float32Array([\n -1,\n -1, // bottom-left\n 1,\n -1, // bottom-right\n -1,\n 1, // top-left\n -1,\n 1, // top-left\n 1,\n -1, // bottom-right\n 1,\n 1, // top-right\n ]);\n\n // Texture coords (0 to 1), Y flipped because video origin is top-left\n const texCoords = new Float32Array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0]);\n\n // Position attribute\n const posBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n const posLoc = gl.getAttribLocation(program, \"a_position\");\n gl.enableVertexAttribArray(posLoc);\n gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);\n\n // Texture coordinate attribute\n const texBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);\n const texLoc = gl.getAttribLocation(program, \"a_texCoord\");\n gl.enableVertexAttribArray(texLoc);\n gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);\n}\n\n// Creates a texture for video frames with mipmapping for quality\nexport function createVideoTexture(\n gl: WebGL2RenderingContext\n): WebGLTexture | null {\n const texture = gl.createTexture();\n if (!texture) return null;\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n\n // Mipmapping gives better quality when sampling large areas\n gl.texParameteri(\n gl.TEXTURE_2D,\n gl.TEXTURE_MIN_FILTER,\n gl.LINEAR_MIPMAP_LINEAR\n );\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\n return texture;\n}\n\n// Renders ASCII characters to a horizontal strip texture\nexport function createAsciiAtlas(\n gl: WebGL2RenderingContext,\n chars: string[],\n charSize: number = 64\n): WebGLTexture | null {\n // Draw characters to an offscreen canvas\n const canvas = document.createElement(\"canvas\");\n canvas.width = charSize * chars.length;\n canvas.height = charSize;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n // Black background, white text (shader colorizes)\n ctx.fillStyle = \"#000\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = \"#fff\";\n ctx.font = `${charSize * 0.8}px monospace`;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n\n // Draw each character centered in its cell\n for (let i = 0; i < chars.length; i++) {\n const x = i * charSize + charSize / 2;\n const y = charSize / 2;\n ctx.fillText(chars[i], x, y);\n }\n\n // Upload canvas to GPU\n const texture = gl.createTexture();\n if (!texture) return null;\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\n return texture;\n}\n\n// Helper to set uniforms with cached locations\nexport type UniformSetter = {\n set1f: (name: string, value: number) => void;\n set2f: (name: string, x: number, y: number) => void;\n set1i: (name: string, value: number) => void;\n};\n\nexport function createUniformSetter(\n gl: WebGL2RenderingContext,\n program: WebGLProgram\n): UniformSetter {\n const cache = new Map<string, WebGLUniformLocation | null>();\n\n const getLocation = (name: string) => {\n if (!cache.has(name)) {\n cache.set(name, gl.getUniformLocation(program, name));\n }\n return cache.get(name)!;\n };\n\n return {\n set1f: (name, value) => gl.uniform1f(getLocation(name), value),\n set2f: (name, x, y) => gl.uniform2f(getLocation(name), x, y),\n set1i: (name, value) => gl.uniform1i(getLocation(name), value),\n };\n}\n\n// Calculates ASCII grid dimensions from video aspect ratio\nexport function calculateGridDimensions(\n videoWidth: number,\n videoHeight: number,\n cols: number\n): { cols: number; rows: number } {\n const aspectRatio = videoWidth / videoHeight;\n // Divide by 2 because chars are ~2x taller than wide\n const rows = Math.round(cols / aspectRatio / 2);\n return { cols, rows };\n}\n","import type { CharsetKey } from \"../ascii-charsets\";\n\n// Constants\nexport const CHAR_WIDTH_RATIO = 0.6;\n\n// Core Types\nexport interface AsciiStats {\n fps: number;\n frameTime: number;\n}\n\nexport interface GridDimensions {\n cols: number;\n rows: number;\n}\n\n// Function that feature hooks register to set their uniforms each frame\nexport type UniformSetter = (\n gl: WebGL2RenderingContext,\n program: WebGLProgram,\n locations: UniformLocations\n) => void;\n\n// Cached uniform locations - looked up once at init, used every frame\nexport interface UniformLocations {\n // Core\n u_video: WebGLUniformLocation | null;\n u_asciiAtlas: WebGLUniformLocation | null;\n u_resolution: WebGLUniformLocation | null;\n u_charSize: WebGLUniformLocation | null;\n u_gridSize: WebGLUniformLocation | null;\n u_numChars: WebGLUniformLocation | null;\n u_colored: WebGLUniformLocation | null;\n u_blend: WebGLUniformLocation | null;\n u_highlight: WebGLUniformLocation | null;\n\n // Mouse\n u_mouse: WebGLUniformLocation | null;\n u_mouseRadius: WebGLUniformLocation | null;\n u_trailLength: WebGLUniformLocation | null;\n u_trail: (WebGLUniformLocation | null)[];\n\n // Ripple\n u_time: WebGLUniformLocation | null;\n u_rippleEnabled: WebGLUniformLocation | null;\n u_rippleSpeed: WebGLUniformLocation | null;\n u_ripples: (WebGLUniformLocation | null)[];\n\n // Audio\n u_audioLevel: WebGLUniformLocation | null;\n u_audioReactivity: WebGLUniformLocation | null;\n u_audioSensitivity: WebGLUniformLocation | null;\n}\n\n// Hook Options\nexport interface UseVideoToAsciiOptions {\n fontSize?: number;\n colored?: boolean;\n blend?: number;\n highlight?: number;\n charset?: CharsetKey;\n maxWidth?: number;\n onStats?: (stats: AsciiStats) => void;\n}\n\nexport interface UseAsciiMouseEffectOptions {\n enabled?: boolean;\n trailLength?: number;\n}\n\nexport interface UseAsciiRippleOptions {\n enabled?: boolean;\n speed?: number;\n}\n\nexport interface UseAsciiAudioOptions {\n enabled?: boolean;\n reactivity?: number;\n sensitivity?: number;\n}\n\n// Context returned by useVideoToAscii\nexport interface AsciiContext {\n containerRef: React.RefObject<HTMLDivElement | null>;\n videoRef: React.RefObject<HTMLVideoElement | null>;\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n glRef: React.RefObject<WebGL2RenderingContext | null>;\n programRef: React.RefObject<WebGLProgram | null>;\n uniformLocationsRef: React.RefObject<UniformLocations | null>;\n registerUniformSetter: (id: string, setter: UniformSetter) => void;\n unregisterUniformSetter: (id: string) => void;\n dimensions: GridDimensions;\n stats: AsciiStats;\n isReady: boolean;\n isPlaying: boolean;\n play: () => void;\n pause: () => void;\n toggle: () => void;\n}\n\n// Event handlers returned by feature hooks\nexport interface MouseEffectHandlers {\n onMouseMove: (e: React.MouseEvent<HTMLDivElement>) => void;\n onMouseLeave: () => void;\n}\n\nexport interface RippleHandlers {\n onClick: (e: React.MouseEvent<HTMLDivElement>) => void;\n}\n\n// Component Props - extends core options with feature-specific props\nexport interface VideoToAsciiProps extends UseVideoToAsciiOptions {\n src: string;\n\n // Mouse effect\n enableMouse?: boolean;\n trailLength?: number;\n\n // Ripple effect\n enableRipple?: boolean;\n rippleSpeed?: number;\n\n // Audio\n audioReactivity?: number;\n audioSensitivity?: number;\n\n showStats?: boolean;\n className?: string;\n}\n\n// Legacy types for backwards compat\nexport interface VideoToAsciiWebGLProps extends VideoToAsciiProps {\n showBenchmark?: boolean;\n muted?: boolean;\n}\n\nexport interface BenchmarkStats extends AsciiStats {\n gpuTime: number;\n}\n\nexport interface WebGLResources {\n gl: WebGL2RenderingContext;\n program: WebGLProgram;\n videoTexture: WebGLTexture;\n atlasTexture: WebGLTexture;\n}\n\nexport interface Ripple {\n x: number;\n y: number;\n startTime: number;\n}\n","import { useCallback, useEffect, useRef } from \"react\";\nimport type {\n AsciiContext,\n UseAsciiMouseEffectOptions,\n MouseEffectHandlers,\n} from \"@/lib/webgl\";\n\nexport type { UseAsciiMouseEffectOptions, MouseEffectHandlers };\n\nconst MAX_TRAIL_LENGTH = 24;\n\ninterface MousePosition {\n x: number;\n y: number;\n}\n\n// Hook Implementation\nexport function useAsciiMouseEffect(\n ascii: AsciiContext,\n options: UseAsciiMouseEffectOptions = {}\n): MouseEffectHandlers {\n const { enabled = true, trailLength = 24 } = options;\n\n // Current mouse position in normalized coords (0-1)\n const mouseRef = useRef<MousePosition>({ x: -1, y: -1 });\n // Array of previous positions for the trail effect\n const trailRef = useRef<MousePosition[]>([]);\n // Keep options in refs so event handlers have fresh values\n const enabledRef = useRef(enabled);\n const trailLengthRef = useRef(trailLength);\n\n useEffect(() => {\n enabledRef.current = enabled;\n trailLengthRef.current = trailLength;\n }, [enabled, trailLength]);\n\n // Register uniform setter - called every frame by core hook\n useEffect(() => {\n if (!enabled) return;\n\n const uniformSetter = (\n gl: WebGL2RenderingContext,\n _program: WebGLProgram,\n locations: NonNullable<typeof ascii.uniformLocationsRef.current>\n ) => {\n // Pass current mouse position to shader\n gl.uniform2f(locations.u_mouse, mouseRef.current.x, mouseRef.current.y);\n\n // Pass trail array to shader\n const trail = trailRef.current;\n gl.uniform1i(locations.u_trailLength, trail.length);\n\n // Fill all trail uniform slots (unused ones get -1,-1)\n for (let i = 0; i < MAX_TRAIL_LENGTH; i++) {\n const loc = locations.u_trail[i];\n if (loc) {\n const pos = trail[i] || { x: -1, y: -1 };\n gl.uniform2f(loc, pos.x, pos.y);\n }\n }\n };\n\n ascii.registerUniformSetter(\"mouse\", uniformSetter);\n\n return () => {\n ascii.unregisterUniformSetter(\"mouse\");\n };\n }, [ascii, enabled]);\n\n // Called when mouse moves over the container\n const onMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n if (!enabledRef.current) return;\n\n const rect = e.currentTarget.getBoundingClientRect();\n const newPos: MousePosition = {\n // Convert pixel coords to 0-1 range\n x: (e.clientX - rect.left) / rect.width,\n y: (e.clientY - rect.top) / rect.height,\n };\n\n // Add old position to the front of the trail\n if (mouseRef.current.x >= 0) {\n trailRef.current.unshift({ ...mouseRef.current });\n // Keep trail at max length\n if (trailRef.current.length > trailLengthRef.current) {\n trailRef.current.pop();\n }\n }\n\n mouseRef.current = newPos;\n }, []);\n\n // Reset when mouse leaves\n const onMouseLeave = useCallback(() => {\n mouseRef.current = { x: -1, y: -1 };\n trailRef.current = [];\n }, []);\n\n return { onMouseMove, onMouseLeave };\n}\n","import { useCallback, useEffect, useRef } from \"react\";\nimport type {\n AsciiContext,\n UseAsciiRippleOptions,\n RippleHandlers,\n} from \"@/lib/webgl\";\n\nexport type { UseAsciiRippleOptions, RippleHandlers };\n\nconst MAX_RIPPLES = 8;\n\ninterface Ripple {\n x: number;\n y: number;\n startTime: number;\n}\n\n// Hook Implementation\nexport function useAsciiRipple(\n ascii: AsciiContext,\n options: UseAsciiRippleOptions = {}\n): RippleHandlers {\n const { enabled = false, speed = 40 } = options;\n\n // Active ripples - each has position and start time\n const ripplesRef = useRef<Ripple[]>([]);\n const enabledRef = useRef(enabled);\n const speedRef = useRef(speed);\n\n useEffect(() => {\n enabledRef.current = enabled;\n speedRef.current = speed;\n }, [enabled, speed]);\n\n // Register uniform setter - runs every frame\n useEffect(() => {\n if (!enabled) return;\n\n const uniformSetter = (\n gl: WebGL2RenderingContext,\n _program: WebGLProgram,\n locations: NonNullable<typeof ascii.uniformLocationsRef.current>\n ) => {\n const currentTime = performance.now() / 1000; // convert to seconds\n\n gl.uniform1f(locations.u_time, currentTime);\n gl.uniform1f(locations.u_rippleEnabled, 1.0);\n gl.uniform1f(locations.u_rippleSpeed, speedRef.current);\n\n // Remove old ripples that have expanded past the screen\n const maxDist = Math.sqrt(\n ascii.dimensions.cols ** 2 + ascii.dimensions.rows ** 2\n );\n const maxLifetime = maxDist / speedRef.current + 1.0;\n ripplesRef.current = ripplesRef.current.filter(\n (r) => currentTime - r.startTime < maxLifetime\n );\n\n // Pass ripple data to shader (vec4: x, y, startTime, enabled)\n for (let i = 0; i < MAX_RIPPLES; i++) {\n const loc = locations.u_ripples[i];\n if (loc) {\n const ripple = ripplesRef.current[i];\n if (ripple) {\n gl.uniform4f(loc, ripple.x, ripple.y, ripple.startTime, 1.0);\n } else {\n gl.uniform4f(loc, 0, 0, 0, 0.0); // disabled\n }\n }\n }\n };\n\n ascii.registerUniformSetter(\"ripple\", uniformSetter);\n\n return () => {\n ascii.unregisterUniformSetter(\"ripple\");\n };\n }, [ascii, enabled]);\n\n // Spawn a new ripple where the user clicks\n const onClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n if (!enabledRef.current) return;\n\n const rect = e.currentTarget.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = (e.clientY - rect.top) / rect.height;\n\n // Add new ripple at the front\n ripplesRef.current.unshift({\n x,\n y,\n startTime: performance.now() / 1000,\n });\n\n // Cap at max ripples\n if (ripplesRef.current.length > MAX_RIPPLES) {\n ripplesRef.current.pop();\n }\n }, []);\n\n return { onClick };\n}\n","import { useEffect, useRef } from \"react\";\nimport type { AsciiContext, UseAsciiAudioOptions } from \"@/lib/webgl\";\n\nexport type { UseAsciiAudioOptions };\n\n// Hook Implementation\nexport function useAsciiAudio(\n ascii: AsciiContext,\n options: UseAsciiAudioOptions = {}\n): void {\n const { enabled = false, reactivity = 50, sensitivity = 50 } = options;\n\n // Web Audio API refs - these persist across renders\n const audioContextRef = useRef<AudioContext | null>(null);\n const analyzerRef = useRef<AnalyserNode | null>(null);\n const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);\n const dataArrayRef = useRef<Uint8Array<ArrayBuffer> | null>(null);\n const volumeRef = useRef(0);\n const connectedVideoRef = useRef<HTMLVideoElement | null>(null);\n\n // Keep options in refs so the uniform setter closure always has fresh values\n const enabledRef = useRef(enabled);\n const reactivityRef = useRef(reactivity);\n const sensitivityRef = useRef(sensitivity);\n\n useEffect(() => {\n enabledRef.current = enabled;\n reactivityRef.current = reactivity;\n sensitivityRef.current = sensitivity;\n }, [enabled, reactivity, sensitivity]);\n\n // Reads frequency data from the analyzer and calculates average volume\n const updateVolume = () => {\n const analyzer = analyzerRef.current;\n const dataArray = dataArrayRef.current;\n if (!analyzer || !dataArray) return;\n\n // getByteFrequencyData fills the array with frequency values (0-255)\n analyzer.getByteFrequencyData(dataArray);\n\n // Average all frequency bins to get overall loudness\n let sum = 0;\n for (let i = 0; i < dataArray.length; i++) {\n sum += dataArray[i];\n }\n const average = sum / dataArray.length / 255; // normalize to 0-1\n\n // Smooth the volume so it doesn't jump around too much\n volumeRef.current = volumeRef.current * 0.7 + average * 0.3;\n };\n\n // Connect to video's audio stream\n useEffect(() => {\n if (!enabled) return;\n\n const video = ascii.videoRef.current;\n if (!video) return;\n\n const connectAudio = () => {\n // If we already connected this exact video element, just resume\n if (connectedVideoRef.current === video && audioContextRef.current) {\n audioContextRef.current.resume();\n return;\n }\n\n try {\n // AudioContext is the entry point to Web Audio API\n if (!audioContextRef.current) {\n audioContextRef.current = new AudioContext();\n }\n\n const ctx = audioContextRef.current;\n\n // AnalyserNode lets us extract frequency/time data from audio\n const analyzer = ctx.createAnalyser();\n analyzer.fftSize = 256; // smaller = faster, less detailed\n analyzer.smoothingTimeConstant = 0.8; // 0-1, higher = smoother\n analyzerRef.current = analyzer;\n\n // This array will hold the frequency data each frame\n dataArrayRef.current = new Uint8Array(\n analyzer.frequencyBinCount\n ) as Uint8Array<ArrayBuffer>;\n\n // createMediaElementSource connects our video element to the audio graph\n // IMPORTANT: a video can only be connected once, ever\n const source = ctx.createMediaElementSource(video);\n source.connect(analyzer);\n analyzer.connect(ctx.destination); // so we still hear the audio\n sourceRef.current = source;\n connectedVideoRef.current = video;\n\n ctx.resume();\n } catch (error) {\n console.warn(\"Failed to connect audio analyzer:\", error);\n }\n };\n\n const handlePlay = () => {\n connectAudio();\n };\n\n video.addEventListener(\"play\", handlePlay);\n\n // If video is already playing when this hook mounts\n if (!video.paused) {\n connectAudio();\n }\n\n return () => {\n video.removeEventListener(\"play\", handlePlay);\n };\n }, [ascii.videoRef, enabled]);\n\n // Register our uniform setter - this gets called every frame by the core hook\n useEffect(() => {\n if (!enabled) return;\n\n const uniformSetter = (\n gl: WebGL2RenderingContext,\n _program: WebGLProgram,\n locations: NonNullable<typeof ascii.uniformLocationsRef.current>\n ) => {\n // Update volume from audio analyzer\n updateVolume();\n\n // Pass values to the shader\n gl.uniform1f(locations.u_audioLevel, volumeRef.current);\n gl.uniform1f(locations.u_audioReactivity, reactivityRef.current / 100);\n gl.uniform1f(locations.u_audioSensitivity, sensitivityRef.current / 100);\n };\n\n ascii.registerUniformSetter(\"audio\", uniformSetter);\n\n return () => {\n ascii.unregisterUniformSetter(\"audio\");\n };\n }, [ascii, enabled]);\n\n // Cleanup audio context when component unmounts\n useEffect(() => {\n return () => {\n if (audioContextRef.current) {\n audioContextRef.current.close();\n }\n };\n }, []);\n}\n","\"use client\";\n\nimport { useVideoToAscii } from \"@/hooks/useVideoToAscii\";\nimport { useAsciiMouseEffect } from \"@/hooks/useAsciiMouseEffect\";\nimport { useAsciiRipple } from \"@/hooks/useAsciiRipple\";\nimport { useAsciiAudio } from \"@/hooks/useAsciiAudio\";\nimport { CHAR_WIDTH_RATIO, type VideoToAsciiProps } from \"@/lib/webgl\";\n\nexport type { VideoToAsciiProps };\n\n// Component Implementation\nexport function VideoToAscii({\n src,\n fontSize = 10,\n colored = false,\n blend = 0,\n highlight = 0,\n charset = \"standard\",\n maxWidth = 900,\n enableMouse = true,\n trailLength = 24,\n enableRipple = false,\n rippleSpeed = 40,\n audioReactivity = 0,\n audioSensitivity = 50,\n showStats = false,\n className = \"\",\n}: VideoToAsciiProps) {\n // Core hook handles WebGL setup and rendering\n const ascii = useVideoToAscii({\n fontSize,\n colored,\n blend,\n highlight,\n charset,\n maxWidth,\n });\n\n // Destructure to avoid linter issues with accessing refs\n const {\n containerRef,\n videoRef,\n canvasRef,\n stats,\n dimensions,\n isReady,\n isPlaying,\n } = ascii;\n\n // Feature hooks - always call them (React rules), enable/disable via options\n const mouseHandlers = useAsciiMouseEffect(ascii, {\n enabled: enableMouse,\n trailLength,\n });\n\n const rippleHandlers = useAsciiRipple(ascii, {\n enabled: enableRipple,\n speed: rippleSpeed,\n });\n\n useAsciiAudio(ascii, {\n enabled: audioReactivity > 0,\n reactivity: audioReactivity,\n sensitivity: audioSensitivity,\n });\n\n // Calculate canvas size in pixels\n const charWidth = fontSize * CHAR_WIDTH_RATIO;\n const pixelWidth = dimensions.cols * charWidth;\n const pixelHeight = dimensions.rows * fontSize;\n\n return (\n <div className={`video-to-ascii ${className}`}>\n {/* Hidden video element - feeds frames to WebGL */}\n <video\n ref={videoRef}\n src={src}\n muted={audioReactivity === 0}\n loop\n playsInline\n crossOrigin=\"anonymous\"\n style={{ display: \"none\" }}\n />\n\n {/* Interactive container */}\n <div\n ref={containerRef}\n className=\"relative cursor-pointer select-none overflow-hidden rounded\"\n style={{\n width: pixelWidth || \"100%\",\n height: pixelHeight || \"auto\",\n backgroundColor: \"#000\",\n }}\n {...(enableMouse ? mouseHandlers : {})}\n {...(enableRipple ? rippleHandlers : {})}\n >\n {/* WebGL canvas - all ASCII rendering happens here */}\n <canvas\n ref={canvasRef}\n style={{\n width: \"100%\",\n height: \"100%\",\n display: \"block\",\n }}\n />\n\n {/* Stats overlay */}\n {showStats && isReady && (\n <div className=\"absolute top-2 left-2 bg-black/70 text-green-400 px-2 py-1 text-xs font-mono rounded\">\n {stats.fps} FPS | {stats.frameTime.toFixed(2)}ms | {dimensions.cols}\n ×{dimensions.rows}\n </div>\n )}\n\n {/* Play indicator */}\n {!isPlaying && isReady && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n <div className=\"text-white text-lg\">▶ Press Space to Play</div>\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport default VideoToAscii;\n"],"mappings":"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","names":["import_react","MAX_TRAIL_LENGTH","import_react","MAX_RIPPLES","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/components/VideoToAscii.tsx","../src/hooks/useVideoToAscii.ts","../src/lib/ascii-charsets.ts","../src/lib/webgl/shaders/vertex.glsl","../src/lib/webgl/shaders/fragment.glsl","../src/lib/webgl/utils.ts","../src/lib/webgl/types.ts","../src/hooks/useAsciiMouseEffect.ts","../src/hooks/useAsciiRipple.ts","../src/hooks/useAsciiAudio.ts"],"sourcesContent":["export { Video2Ascii } from \"./components/VideoToAscii\";\nexport { default } from \"./components/VideoToAscii\";\nexport type { VideoToAsciiProps } from \"./lib/webgl/types\";\nexport { ASCII_CHARSETS, type CharsetKey } from \"./lib/ascii-charsets\";\n","\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useVideoToAscii } from \"@/hooks/useVideoToAscii\";\nimport { useAsciiMouseEffect } from \"@/hooks/useAsciiMouseEffect\";\nimport { useAsciiRipple } from \"@/hooks/useAsciiRipple\";\nimport { useAsciiAudio } from \"@/hooks/useAsciiAudio\";\nimport { type VideoToAsciiProps } from \"@/lib/webgl\";\n\nexport type { VideoToAsciiProps };\n\n// Component Implementation\nexport function Video2Ascii({\n src,\n numColumns,\n colored = true,\n blend = 0,\n highlight = 0,\n brightness = 1.0,\n charset = \"standard\",\n enableMouse = true,\n trailLength = 24,\n enableRipple = false,\n rippleSpeed = 40,\n audioEffect = 0,\n audioRange = 50,\n isPlaying = true,\n autoPlay = true,\n enableSpacebarToggle = false,\n showStats = false,\n className = \"\",\n}: VideoToAsciiProps) {\n // Core hook handles WebGL setup and rendering\n const ascii = useVideoToAscii({\n numColumns,\n colored,\n blend,\n highlight,\n brightness,\n charset,\n enableSpacebarToggle,\n });\n\n // Destructure to avoid linter issues with accessing refs\n const { containerRef, videoRef, canvasRef, stats, dimensions, isReady } =\n ascii;\n\n // Feature hooks - always call them (React rules), enable/disable via options\n const mouseHandlers = useAsciiMouseEffect(ascii, {\n enabled: enableMouse,\n trailLength,\n });\n\n const rippleHandlers = useAsciiRipple(ascii, {\n enabled: enableRipple,\n speed: rippleSpeed,\n });\n\n useAsciiAudio(ascii, {\n enabled: audioEffect > 0,\n reactivity: audioEffect,\n sensitivity: audioRange,\n });\n\n // Control video playback based on isPlaying prop\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n if (isPlaying) {\n if (autoPlay && isReady) {\n video.play().catch(() => {\n // Auto-play may be blocked by browser, that's ok\n });\n }\n } else {\n video.pause();\n }\n }, [isPlaying, autoPlay, isReady, videoRef]);\n\n return (\n <div className={`video-to-ascii ${className}`}>\n {/* Hidden video element - feeds frames to WebGL */}\n <video\n ref={videoRef}\n src={src}\n muted={audioEffect === 0}\n loop\n playsInline\n crossOrigin=\"anonymous\"\n style={{ display: \"none\" }}\n />\n\n {/* Interactive container */}\n <div\n ref={containerRef}\n className=\"relative cursor-pointer select-none overflow-hidden rounded bg-black\"\n {...(enableMouse ? mouseHandlers : {})}\n {...(enableRipple ? rippleHandlers : {})}\n >\n {/* WebGL canvas - all ASCII rendering happens here */}\n <canvas\n ref={canvasRef}\n style={{\n width: \"100%\",\n height: \"100%\",\n display: \"block\",\n }}\n />\n\n {/* Stats overlay */}\n {showStats && isReady && (\n <div className=\"absolute top-2 left-2 bg-black/70 text-green-400 px-2 py-1 text-xs font-mono rounded\">\n {stats.fps} FPS | {stats.frameTime.toFixed(2)}ms | {dimensions.cols}\n ×{dimensions.rows}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport default Video2Ascii;\n","import { useRef, useState, useCallback, useEffect, useMemo } from \"react\";\nimport { getCharArray, DEFAULT_CHARSET } from \"@/lib/ascii-charsets\";\nimport {\n VERTEX_SHADER,\n FRAGMENT_SHADER,\n compileShader,\n createProgram,\n createFullscreenQuad,\n createVideoTexture,\n createAsciiAtlas,\n calculateGridDimensions,\n CHAR_WIDTH_RATIO,\n type UseVideoToAsciiOptions,\n type AsciiContext,\n type AsciiStats,\n type UniformSetter,\n type UniformLocations,\n} from \"@/lib/webgl\";\n\nexport type { UseVideoToAsciiOptions, AsciiContext, AsciiStats };\n\nconst MAX_TRAIL_LENGTH = 24;\nconst MAX_RIPPLES = 8;\n\n// Hook Implementation\nexport function useVideoToAscii(\n options: UseVideoToAsciiOptions = {}\n): AsciiContext {\n const {\n fontSize,\n numColumns,\n colored = true,\n blend = 0,\n highlight = 0,\n brightness = 1.0,\n charset = DEFAULT_CHARSET,\n maxWidth,\n enableSpacebarToggle = false,\n onStats,\n } = options;\n\n // DOM refs\n const containerRef = useRef<HTMLDivElement>(null);\n const videoRef = useRef<HTMLVideoElement>(null);\n const canvasRef = useRef<HTMLCanvasElement>(null);\n\n // WebGL refs - these hold the GPU resources\n const glRef = useRef<WebGL2RenderingContext | null>(null);\n const programRef = useRef<WebGLProgram | null>(null);\n const videoTextureRef = useRef<WebGLTexture | null>(null);\n const atlasTextureRef = useRef<WebGLTexture | null>(null);\n const animationRef = useRef<number>(0);\n\n // Feature hooks register their uniform setters here\n const uniformSettersRef = useRef<Map<string, UniformSetter>>(new Map());\n // Cached uniform locations for performance (avoid lookup every frame)\n const uniformLocationsRef = useRef<UniformLocations | null>(null);\n\n // Benchmark/stats refs\n const frameCountRef = useRef(0);\n const frameTimesRef = useRef<number[]>([]);\n const lastFpsTimeRef = useRef(performance.now());\n\n // State\n const [dimensions, setDimensions] = useState({ cols: 80, rows: 24 });\n const [stats, setStats] = useState<AsciiStats>({ fps: 0, frameTime: 0 });\n const [isReady, setIsReady] = useState(false);\n const [isPlaying, setIsPlaying] = useState(false);\n\n // Calculate fontSize and maxWidth from numColumns if provided\n // If numColumns is provided, we'll calculate fontSize from container width\n // For now, use a default width to calculate initial fontSize\n const defaultWidth = typeof window !== \"undefined\" ? window.innerWidth : 900;\n const containerWidth = maxWidth || defaultWidth;\n const calculatedFontSize = numColumns\n ? containerWidth / (numColumns * CHAR_WIDTH_RATIO)\n : fontSize || 10;\n const calculatedMaxWidth = numColumns\n ? numColumns * calculatedFontSize * CHAR_WIDTH_RATIO\n : maxWidth || 900;\n\n // Calculate grid size - use numColumns directly if provided\n const charWidth = calculatedFontSize * CHAR_WIDTH_RATIO;\n const cols = numColumns || Math.floor(calculatedMaxWidth / charWidth);\n // Memoize chars array so it only recalculates when charset changes\n const chars = useMemo(() => getCharArray(charset), [charset]);\n\n // Feature hooks call this to register their uniform setter\n const registerUniformSetter = useCallback(\n (id: string, setter: UniformSetter) => {\n uniformSettersRef.current.set(id, setter);\n },\n []\n );\n\n const unregisterUniformSetter = useCallback((id: string) => {\n uniformSettersRef.current.delete(id);\n }, []);\n\n // Cache all uniform locations after program is compiled\n // This avoids expensive getUniformLocation calls every frame\n const cacheUniformLocations = useCallback(\n (gl: WebGL2RenderingContext, program: WebGLProgram): UniformLocations => {\n const get = (name: string) => gl.getUniformLocation(program, name);\n\n return {\n // Core uniforms\n u_video: get(\"u_video\"),\n u_asciiAtlas: get(\"u_asciiAtlas\"),\n u_resolution: get(\"u_resolution\"),\n u_charSize: get(\"u_charSize\"),\n u_gridSize: get(\"u_gridSize\"),\n u_numChars: get(\"u_numChars\"),\n u_colored: get(\"u_colored\"),\n u_blend: get(\"u_blend\"),\n u_highlight: get(\"u_highlight\"),\n u_brightness: get(\"u_brightness\"),\n\n // Mouse uniforms\n u_mouse: get(\"u_mouse\"),\n u_mouseRadius: get(\"u_mouseRadius\"),\n u_trailLength: get(\"u_trailLength\"),\n u_trail: Array.from({ length: MAX_TRAIL_LENGTH }, (_, i) =>\n get(`u_trail[${i}]`)\n ),\n\n // Ripple uniforms\n u_time: get(\"u_time\"),\n u_rippleEnabled: get(\"u_rippleEnabled\"),\n u_rippleSpeed: get(\"u_rippleSpeed\"),\n u_ripples: Array.from({ length: MAX_RIPPLES }, (_, i) =>\n get(`u_ripples[${i}]`)\n ),\n\n // Audio uniforms\n u_audioLevel: get(\"u_audioLevel\"),\n u_audioReactivity: get(\"u_audioReactivity\"),\n u_audioSensitivity: get(\"u_audioSensitivity\"),\n };\n },\n []\n );\n\n // Initialize WebGL\n const initWebGL = useCallback(() => {\n const canvas = canvasRef.current;\n const video = videoRef.current;\n const container = containerRef.current;\n if (!canvas || !video || !video.videoWidth) return false;\n\n // Recalculate fontSize from actual container width if numColumns is provided\n let finalFontSize = calculatedFontSize;\n let finalCols = cols;\n if (numColumns && container) {\n const actualWidth = container.clientWidth || defaultWidth;\n finalFontSize = actualWidth / (numColumns * CHAR_WIDTH_RATIO);\n finalCols = numColumns;\n }\n\n // Figure out grid dimensions from video aspect ratio\n const grid = calculateGridDimensions(\n video.videoWidth,\n video.videoHeight,\n finalCols\n );\n setDimensions(grid);\n\n // Set canvas size\n const finalCharWidth = finalFontSize * CHAR_WIDTH_RATIO;\n const pixelWidth = grid.cols * finalCharWidth;\n const pixelHeight = grid.rows * finalFontSize;\n canvas.width = pixelWidth;\n canvas.height = pixelHeight;\n\n // Get WebGL2 context (WebGL2 has better texture handling)\n const gl = canvas.getContext(\"webgl2\", {\n antialias: false,\n preserveDrawingBuffer: false,\n });\n if (!gl) {\n console.error(\"WebGL2 not supported\");\n return false;\n }\n glRef.current = gl;\n\n // Compile shaders (vertex positions the quad, fragment does the ASCII magic)\n const vertexShader = compileShader(gl, VERTEX_SHADER, gl.VERTEX_SHADER);\n const fragmentShader = compileShader(\n gl,\n FRAGMENT_SHADER,\n gl.FRAGMENT_SHADER\n );\n if (!vertexShader || !fragmentShader) return false;\n\n // Link shaders into a program\n const program = createProgram(gl, vertexShader, fragmentShader);\n if (!program) return false;\n programRef.current = program;\n gl.useProgram(program);\n\n // Create a fullscreen quad (two triangles covering the canvas)\n createFullscreenQuad(gl, program);\n\n // Create textures for video frame and ASCII character atlas\n videoTextureRef.current = createVideoTexture(gl);\n const finalFontSizeForAtlas =\n numColumns && container\n ? (container.clientWidth || defaultWidth) /\n (numColumns * CHAR_WIDTH_RATIO)\n : calculatedFontSize;\n atlasTextureRef.current = createAsciiAtlas(\n gl,\n chars,\n finalFontSizeForAtlas\n );\n\n // Cache all uniform locations for fast access during render\n const locations = cacheUniformLocations(gl, program);\n uniformLocationsRef.current = locations;\n\n // Tell the shader which texture units to use\n gl.uniform1i(locations.u_video, 0); // texture unit 0\n gl.uniform1i(locations.u_asciiAtlas, 1); // texture unit 1\n\n // Set static uniforms that don't change during playback\n gl.uniform2f(locations.u_resolution, pixelWidth, pixelHeight);\n gl.uniform2f(locations.u_charSize, finalCharWidth, finalFontSize);\n gl.uniform2f(locations.u_gridSize, finalCols, grid.rows);\n gl.uniform1f(locations.u_numChars, chars.length);\n gl.uniform1f(locations.u_brightness, brightness);\n\n // Initialize feature uniforms to disabled state\n gl.uniform2f(locations.u_mouse, -1, -1);\n gl.uniform1f(locations.u_mouseRadius, 0);\n gl.uniform1i(locations.u_trailLength, 0);\n gl.uniform1f(locations.u_rippleEnabled, 0);\n gl.uniform1f(locations.u_audioLevel, 0);\n gl.uniform1f(locations.u_audioReactivity, 0);\n gl.uniform1f(locations.u_audioSensitivity, 0);\n\n gl.viewport(0, 0, pixelWidth, pixelHeight);\n\n setIsReady(true);\n return true;\n }, [\n cols,\n numColumns,\n calculatedFontSize,\n chars,\n cacheUniformLocations,\n brightness,\n defaultWidth,\n ]);\n\n // Render loop - runs every frame while video is playing\n const render = useCallback(() => {\n const gl = glRef.current;\n const video = videoRef.current;\n const program = programRef.current;\n const locations = uniformLocationsRef.current;\n\n if (!gl || !video || !program || !locations || video.paused || video.ended)\n return;\n\n const frameStart = performance.now();\n\n // Upload current video frame to GPU\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, videoTextureRef.current);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);\n // Generate mipmaps for better quality when sampling large areas\n gl.generateMipmap(gl.TEXTURE_2D);\n\n // Bind the ASCII atlas texture\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, atlasTextureRef.current);\n\n // Update uniforms that can change each frame\n gl.uniform1i(locations.u_colored, colored ? 1 : 0);\n gl.uniform1f(locations.u_blend, blend / 100);\n gl.uniform1f(locations.u_highlight, highlight / 100);\n gl.uniform1f(locations.u_brightness, brightness);\n\n // Let feature hooks update their uniforms\n for (const setter of uniformSettersRef.current.values()) {\n setter(gl, program, locations);\n }\n\n // Draw the quad (shader does all the work)\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n // Track performance\n const frameEnd = performance.now();\n frameCountRef.current++;\n frameTimesRef.current.push(frameEnd - frameStart);\n if (frameTimesRef.current.length > 60) frameTimesRef.current.shift();\n\n // Update FPS counter every second\n const now = performance.now();\n if (now - lastFpsTimeRef.current >= 1000) {\n const avgFrameTime =\n frameTimesRef.current.reduce((a, b) => a + b, 0) /\n frameTimesRef.current.length;\n const newStats = { fps: frameCountRef.current, frameTime: avgFrameTime };\n setStats(newStats);\n onStats?.(newStats);\n frameCountRef.current = 0;\n lastFpsTimeRef.current = now;\n }\n\n // Schedule next frame\n animationRef.current = requestAnimationFrame(render);\n }, [colored, blend, highlight, brightness, onStats]);\n\n // Video Event Handlers\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const handleLoadedMetadata = () => {\n initWebGL();\n };\n\n const handlePlay = () => {\n setIsPlaying(true);\n animationRef.current = requestAnimationFrame(render);\n };\n\n const handlePause = () => {\n setIsPlaying(false);\n cancelAnimationFrame(animationRef.current);\n };\n\n const handleEnded = () => {\n setIsPlaying(false);\n cancelAnimationFrame(animationRef.current);\n };\n\n video.addEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.addEventListener(\"play\", handlePlay);\n video.addEventListener(\"pause\", handlePause);\n video.addEventListener(\"ended\", handleEnded);\n\n // If video is already loaded when we mount\n if (video.readyState >= 1) {\n handleLoadedMetadata();\n }\n\n return () => {\n video.removeEventListener(\"loadedmetadata\", handleLoadedMetadata);\n video.removeEventListener(\"play\", handlePlay);\n video.removeEventListener(\"pause\", handlePause);\n video.removeEventListener(\"ended\", handleEnded);\n cancelAnimationFrame(animationRef.current);\n };\n }, [initWebGL, render]);\n\n // Reinitialize when config changes (numColumns, brightness, etc.)\n useEffect(() => {\n if (videoRef.current && videoRef.current.readyState >= 1) {\n initWebGL();\n }\n }, [initWebGL]);\n\n // Handle container resize when numColumns is used\n useEffect(() => {\n if (!numColumns || !containerRef.current) return;\n\n const container = containerRef.current;\n const resizeObserver = new ResizeObserver(() => {\n // Reinitialize WebGL when container size changes\n if (videoRef.current && videoRef.current.readyState >= 1) {\n initWebGL();\n }\n });\n\n resizeObserver.observe(container);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [numColumns, initWebGL]);\n\n // Cleanup WebGL resources when unmounting\n useEffect(() => {\n return () => {\n const gl = glRef.current;\n if (gl) {\n if (videoTextureRef.current) gl.deleteTexture(videoTextureRef.current);\n if (atlasTextureRef.current) gl.deleteTexture(atlasTextureRef.current);\n if (programRef.current) gl.deleteProgram(programRef.current);\n }\n cancelAnimationFrame(animationRef.current);\n };\n }, []);\n\n // Playback Controls\n const play = useCallback(() => {\n videoRef.current?.play();\n }, []);\n\n const pause = useCallback(() => {\n videoRef.current?.pause();\n }, []);\n\n const toggle = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n if (video.paused) {\n video.play();\n } else {\n video.pause();\n }\n }, []);\n\n // Spacebar to toggle play/pause\n useEffect(() => {\n if (!enableSpacebarToggle) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.code === \"Space\" && e.target === document.body) {\n e.preventDefault();\n toggle();\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [toggle, enableSpacebarToggle]);\n\n return {\n containerRef,\n videoRef,\n canvasRef,\n glRef,\n programRef,\n uniformLocationsRef,\n registerUniformSetter,\n unregisterUniformSetter,\n dimensions,\n stats,\n isReady,\n isPlaying,\n play,\n pause,\n toggle,\n };\n}\n","/**\n * ASCII Character Set Definitions\n *\n * Character sets are ordered from dark (low brightness) to light (high brightness).\n * The shader maps pixel brightness to character index, so the first character\n * represents the darkest pixels and the last represents the brightest.\n *\n * To add a new character set:\n * 1. Add an entry to ASCII_CHARSETS with a unique key\n * 2. Order characters from dark → light (spaces/dots first, dense chars last)\n * 3. The key becomes available in CharsetKey type automatically\n */\n\nexport const ASCII_CHARSETS = {\n /** Classic 10-character gradient - good balance of detail and performance */\n standard: {\n name: \"Standard\",\n chars: \" .:-=+*#%@\",\n },\n\n /** Unicode block characters - chunky retro aesthetic */\n blocks: {\n name: \"Blocks\",\n chars: \" ░▒▓█\",\n },\n\n /** Minimal 5-character set - high contrast, fast rendering */\n minimal: {\n name: \"Minimal\",\n chars: \" .oO@\",\n },\n\n /** Binary on/off - pure silhouette mode */\n binary: {\n name: \"Binary\",\n chars: \" █\",\n },\n\n /** 70-character gradient - maximum detail, best for high resolution */\n detailed: {\n name: \"Detailed\",\n chars:\n \" .'`^\\\",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$\",\n },\n\n /** Dot-based - pointillist aesthetic */\n dots: {\n name: \"Dots\",\n chars: \" ·•●\",\n },\n\n /** Directional arrows - experimental */\n arrows: {\n name: \"Arrows\",\n chars: \" ←↙↓↘→↗↑↖\",\n },\n\n /** Moon phases - decorative gradient */\n emoji: {\n name: \"Emoji\",\n chars: \" ░▒▓🌑🌒🌓🌔🌕\",\n },\n} as const;\n\n/** Type-safe key for selecting character sets */\nexport type CharsetKey = keyof typeof ASCII_CHARSETS;\n\n/** Default character set used when none is specified */\nexport const DEFAULT_CHARSET: CharsetKey = \"standard\";\n\n/**\n * Get the character array for a given charset key.\n * Uses spread operator to correctly handle multi-byte unicode characters.\n */\nexport function getCharArray(charset: CharsetKey): string[] {\n return [...ASCII_CHARSETS[charset].chars];\n}\n\n/**\n * Get the display name for a charset\n */\nexport function getCharsetName(charset: CharsetKey): string {\n return ASCII_CHARSETS[charset].name;\n}\n","#version 300 es\n\n// Fullscreen quad - passes texture coords to fragment shader\n\nin vec2 a_position;\nin vec2 a_texCoord;\nout vec2 v_texCoord;\n\nvoid main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n}\n","#version 300 es\nprecision highp float;\n\n// Textures\nuniform sampler2D u_video;\nuniform sampler2D u_asciiAtlas;\n\n// Dimensions\nuniform vec2 u_resolution;\nuniform vec2 u_charSize;\nuniform vec2 u_gridSize;\nuniform float u_numChars;\n\n// Rendering options\nuniform bool u_colored;\nuniform float u_blend;\nuniform float u_highlight;\nuniform float u_brightness;\n\n// Audio\nuniform float u_audioLevel;\nuniform float u_audioReactivity;\nuniform float u_audioSensitivity;\n\n// Mouse\nuniform vec2 u_mouse;\nuniform float u_mouseRadius;\nuniform vec2 u_trail[24];\nuniform int u_trailLength;\n\n// Ripple\nuniform vec4 u_ripples[8];\nuniform float u_time;\nuniform float u_rippleEnabled;\nuniform float u_rippleSpeed;\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nvoid main() {\n // Figure out which ASCII cell this pixel is in\n vec2 cellCoord = floor(v_texCoord * u_gridSize);\n vec2 thisCell = cellCoord;\n \n // Sample video at cell center (mipmaps handle averaging)\n vec2 cellCenter = (cellCoord + 0.5) / u_gridSize;\n vec4 videoColor = texture(u_video, cellCenter);\n \n // Perceived brightness using human eye sensitivity weights\n float baseBrightness = dot(videoColor.rgb, vec3(0.299, 0.587, 0.114));\n \n // Audio reactivity - louder = brighter, silence = darker\n float minBrightness = mix(0.3, 0.0, u_audioSensitivity);\n float maxBrightness = mix(1.0, 5.0, u_audioSensitivity);\n float audioMultiplier = mix(minBrightness, maxBrightness, u_audioLevel);\n float audioModulated = baseBrightness * audioMultiplier;\n float brightness = mix(baseBrightness, audioModulated, u_audioReactivity);\n \n // Cursor glow - blocky circle effect\n float cursorGlow = 0.0;\n float cursorRadius = 5.0;\n \n vec2 mouseCell = floor(u_mouse * u_gridSize);\n float cellDist = length(thisCell - mouseCell);\n if (cellDist <= cursorRadius && u_mouse.x >= 0.0) {\n cursorGlow += 1.0 - cellDist / cursorRadius;\n }\n \n // Trail effect\n for (int i = 0; i < 12; i++) {\n if (i >= u_trailLength) break;\n vec2 trailPos = u_trail[i];\n if (trailPos.x < 0.0) continue;\n \n vec2 trailCell = floor(trailPos * u_gridSize);\n float trailDist = length(thisCell - trailCell);\n float trailRadius = cursorRadius * 0.8;\n \n if (trailDist <= trailRadius) {\n float fade = 1.0 - float(i) / float(u_trailLength);\n cursorGlow += (1.0 - trailDist / trailRadius) * 0.5 * fade;\n }\n }\n cursorGlow = min(cursorGlow, 1.0);\n \n // Ripple effect - expanding rings on click\n float rippleGlow = 0.0;\n if (u_rippleEnabled > 0.5) {\n for (int i = 0; i < 8; i++) {\n vec4 ripple = u_ripples[i];\n if (ripple.w < 0.5) continue;\n \n float age = u_time - ripple.z;\n if (age < 0.0) continue;\n \n vec2 rippleCell = floor(ripple.xy * u_gridSize);\n float cellDist = length(thisCell - rippleCell);\n float initialRadius = 5.0;\n \n float distFromEdge = max(0.0, cellDist - initialRadius);\n float rippleSpeed = u_rippleSpeed;\n float reachTime = distFromEdge / rippleSpeed;\n float timeSinceReached = age - reachTime;\n \n float fadeDuration = 0.5;\n if (timeSinceReached >= 0.0 && timeSinceReached < fadeDuration) {\n float pop = 1.0 - timeSinceReached / fadeDuration;\n pop = pop * pop;\n rippleGlow += pop * 0.3;\n }\n }\n rippleGlow = min(rippleGlow, 1.0);\n }\n \n // Apply brightness multiplier\n // brightness < 1.0: darkens (multiply)\n // brightness > 1.0: brightens (compress dark values toward 1.0)\n float adjustedBrightness;\n if (u_brightness <= 1.0) {\n adjustedBrightness = brightness * u_brightness;\n } else {\n // For brightness > 1.0, compress the range: dark values get pushed up\n // Formula: 1.0 - (1.0 - brightness) / u_brightness\n // This makes dark values brighter while keeping bright values near 1.0\n adjustedBrightness = 1.0 - (1.0 - brightness) / u_brightness;\n }\n adjustedBrightness = clamp(adjustedBrightness, 0.0, 1.0);\n \n // Map brightness to character index (0 = darkest char, numChars-1 = brightest)\n float charIndex = floor(adjustedBrightness * (u_numChars - 0.001));\n \n // Find the character in the atlas (horizontal strip of pre-rendered chars)\n float atlasX = charIndex / u_numChars;\n vec2 cellPos = fract(v_texCoord * u_gridSize);\n vec2 atlasCoord = vec2(atlasX + cellPos.x / u_numChars, cellPos.y);\n vec4 charColor = texture(u_asciiAtlas, atlasCoord);\n \n // Pick the color - video colors or green terminal aesthetic\n vec3 baseColor;\n if (u_colored) {\n baseColor = videoColor.rgb;\n } else {\n baseColor = vec3(0.0, 1.0, 0.0);\n }\n \n // Background highlight behind each character\n float bgIntensity = 0.15 + u_highlight * 0.35;\n vec3 bgColor = baseColor * bgIntensity;\n vec3 textColor = baseColor * 1.2;\n vec3 finalColor = mix(bgColor, textColor, charColor.r);\n \n // Add cursor and ripple glow\n finalColor += cursorGlow * baseColor * 0.5;\n finalColor += rippleGlow * baseColor;\n \n // Blend with original video if requested\n vec3 blendedColor = mix(finalColor, videoColor.rgb, u_blend);\n \n fragColor = vec4(blendedColor, 1.0);\n}\n","// Compiles a GLSL shader from source code\nexport function compileShader(\n gl: WebGL2RenderingContext,\n source: string,\n type: number\n): WebGLShader | null {\n const shader = gl.createShader(type);\n if (!shader) {\n console.error(\"Failed to create shader\");\n return null;\n }\n\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n console.error(\"Shader compile error:\", gl.getShaderInfoLog(shader));\n gl.deleteShader(shader);\n return null;\n }\n\n return shader;\n}\n\n// Links vertex and fragment shaders into a program\nexport function createProgram(\n gl: WebGL2RenderingContext,\n vertexShader: WebGLShader,\n fragmentShader: WebGLShader\n): WebGLProgram | null {\n const program = gl.createProgram();\n if (!program) {\n console.error(\"Failed to create program\");\n return null;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n console.error(\"Program link error:\", gl.getProgramInfoLog(program));\n gl.deleteProgram(program);\n return null;\n }\n\n return program;\n}\n\n// Creates a fullscreen quad (two triangles covering the viewport)\nexport function createFullscreenQuad(\n gl: WebGL2RenderingContext,\n program: WebGLProgram\n): void {\n // Vertex positions in clip space (-1 to 1)\n const positions = new Float32Array([\n -1,\n -1, // bottom-left\n 1,\n -1, // bottom-right\n -1,\n 1, // top-left\n -1,\n 1, // top-left\n 1,\n -1, // bottom-right\n 1,\n 1, // top-right\n ]);\n\n // Texture coords (0 to 1), Y flipped because video origin is top-left\n const texCoords = new Float32Array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0]);\n\n // Position attribute\n const posBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);\n const posLoc = gl.getAttribLocation(program, \"a_position\");\n gl.enableVertexAttribArray(posLoc);\n gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);\n\n // Texture coordinate attribute\n const texBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer);\n gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);\n const texLoc = gl.getAttribLocation(program, \"a_texCoord\");\n gl.enableVertexAttribArray(texLoc);\n gl.vertexAttribPointer(texLoc, 2, gl.FLOAT, false, 0, 0);\n}\n\n// Creates a texture for video frames with mipmapping for quality\nexport function createVideoTexture(\n gl: WebGL2RenderingContext\n): WebGLTexture | null {\n const texture = gl.createTexture();\n if (!texture) return null;\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n\n // Mipmapping gives better quality when sampling large areas\n gl.texParameteri(\n gl.TEXTURE_2D,\n gl.TEXTURE_MIN_FILTER,\n gl.LINEAR_MIPMAP_LINEAR\n );\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\n return texture;\n}\n\n// Renders ASCII characters to a horizontal strip texture\nexport function createAsciiAtlas(\n gl: WebGL2RenderingContext,\n chars: string[],\n charSize: number = 64\n): WebGLTexture | null {\n // Draw characters to an offscreen canvas\n const canvas = document.createElement(\"canvas\");\n canvas.width = charSize * chars.length;\n canvas.height = charSize;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n // Black background, white text (shader colorizes)\n ctx.fillStyle = \"#000\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = \"#fff\";\n ctx.font = `${charSize * 0.8}px monospace`;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n\n // Draw each character centered in its cell\n for (let i = 0; i < chars.length; i++) {\n const x = i * charSize + charSize / 2;\n const y = charSize / 2;\n ctx.fillText(chars[i], x, y);\n }\n\n // Upload canvas to GPU\n const texture = gl.createTexture();\n if (!texture) return null;\n\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\n return texture;\n}\n\n// Helper to set uniforms with cached locations\nexport type UniformSetter = {\n set1f: (name: string, value: number) => void;\n set2f: (name: string, x: number, y: number) => void;\n set1i: (name: string, value: number) => void;\n};\n\nexport function createUniformSetter(\n gl: WebGL2RenderingContext,\n program: WebGLProgram\n): UniformSetter {\n const cache = new Map<string, WebGLUniformLocation | null>();\n\n const getLocation = (name: string) => {\n if (!cache.has(name)) {\n cache.set(name, gl.getUniformLocation(program, name));\n }\n return cache.get(name)!;\n };\n\n return {\n set1f: (name, value) => gl.uniform1f(getLocation(name), value),\n set2f: (name, x, y) => gl.uniform2f(getLocation(name), x, y),\n set1i: (name, value) => gl.uniform1i(getLocation(name), value),\n };\n}\n\n// Calculates ASCII grid dimensions from video aspect ratio\nexport function calculateGridDimensions(\n videoWidth: number,\n videoHeight: number,\n cols: number\n): { cols: number; rows: number } {\n const aspectRatio = videoWidth / videoHeight;\n // Divide by 2 because chars are ~2x taller than wide\n const rows = Math.round(cols / aspectRatio / 2);\n return { cols, rows };\n}\n","import type { CharsetKey } from \"../ascii-charsets\";\n\n// Constants\nexport const CHAR_WIDTH_RATIO = 0.6;\n\n// Core Types\nexport interface AsciiStats {\n fps: number;\n frameTime: number;\n}\n\nexport interface GridDimensions {\n cols: number;\n rows: number;\n}\n\n// Function that feature hooks register to set their uniforms each frame\nexport type UniformSetter = (\n gl: WebGL2RenderingContext,\n program: WebGLProgram,\n locations: UniformLocations\n) => void;\n\n// Cached uniform locations - looked up once at init, used every frame\nexport interface UniformLocations {\n // Core\n u_video: WebGLUniformLocation | null;\n u_asciiAtlas: WebGLUniformLocation | null;\n u_resolution: WebGLUniformLocation | null;\n u_charSize: WebGLUniformLocation | null;\n u_gridSize: WebGLUniformLocation | null;\n u_numChars: WebGLUniformLocation | null;\n u_colored: WebGLUniformLocation | null;\n u_blend: WebGLUniformLocation | null;\n u_highlight: WebGLUniformLocation | null;\n u_brightness: WebGLUniformLocation | null;\n\n // Mouse\n u_mouse: WebGLUniformLocation | null;\n u_mouseRadius: WebGLUniformLocation | null;\n u_trailLength: WebGLUniformLocation | null;\n u_trail: (WebGLUniformLocation | null)[];\n\n // Ripple\n u_time: WebGLUniformLocation | null;\n u_rippleEnabled: WebGLUniformLocation | null;\n u_rippleSpeed: WebGLUniformLocation | null;\n u_ripples: (WebGLUniformLocation | null)[];\n\n // Audio\n u_audioLevel: WebGLUniformLocation | null;\n u_audioReactivity: WebGLUniformLocation | null;\n u_audioSensitivity: WebGLUniformLocation | null;\n}\n\n// Hook Options\nexport interface UseVideoToAsciiOptions {\n fontSize?: number;\n colored?: boolean;\n blend?: number;\n highlight?: number;\n brightness?: number;\n charset?: CharsetKey;\n maxWidth?: number;\n numColumns?: number;\n enableSpacebarToggle?: boolean;\n onStats?: (stats: AsciiStats) => void;\n}\n\nexport interface UseAsciiMouseEffectOptions {\n enabled?: boolean;\n trailLength?: number;\n}\n\nexport interface UseAsciiRippleOptions {\n enabled?: boolean;\n speed?: number;\n}\n\nexport interface UseAsciiAudioOptions {\n enabled?: boolean;\n reactivity?: number;\n sensitivity?: number;\n}\n\n// Context returned by useVideoToAscii\nexport interface AsciiContext {\n containerRef: React.RefObject<HTMLDivElement | null>;\n videoRef: React.RefObject<HTMLVideoElement | null>;\n canvasRef: React.RefObject<HTMLCanvasElement | null>;\n glRef: React.RefObject<WebGL2RenderingContext | null>;\n programRef: React.RefObject<WebGLProgram | null>;\n uniformLocationsRef: React.RefObject<UniformLocations | null>;\n registerUniformSetter: (id: string, setter: UniformSetter) => void;\n unregisterUniformSetter: (id: string) => void;\n dimensions: GridDimensions;\n stats: AsciiStats;\n isReady: boolean;\n isPlaying: boolean;\n play: () => void;\n pause: () => void;\n toggle: () => void;\n}\n\n// Event handlers returned by feature hooks\nexport interface MouseEffectHandlers {\n onMouseMove: (e: React.MouseEvent<HTMLDivElement>) => void;\n onMouseLeave: () => void;\n}\n\nexport interface RippleHandlers {\n onClick: (e: React.MouseEvent<HTMLDivElement>) => void;\n}\n\n// Component Props - extends core options with feature-specific props\nexport interface VideoToAsciiProps {\n src: string;\n\n // Size control\n numColumns?: number;\n\n // Rendering\n colored?: boolean;\n blend?: number;\n highlight?: number;\n brightness?: number;\n charset?: CharsetKey;\n\n // Mouse effect\n enableMouse?: boolean;\n trailLength?: number;\n\n // Ripple effect\n enableRipple?: boolean;\n rippleSpeed?: number;\n\n // Audio\n audioEffect?: number;\n audioRange?: number;\n\n // Controls\n isPlaying?: boolean;\n autoPlay?: boolean;\n enableSpacebarToggle?: boolean;\n\n showStats?: boolean;\n className?: string;\n}\n\n// Legacy types for backwards compat\nexport interface VideoToAsciiWebGLProps extends VideoToAsciiProps {\n showBenchmark?: boolean;\n muted?: boolean;\n}\n\nexport interface BenchmarkStats extends AsciiStats {\n gpuTime: number;\n}\n\nexport interface WebGLResources {\n gl: WebGL2RenderingContext;\n program: WebGLProgram;\n videoTexture: WebGLTexture;\n atlasTexture: WebGLTexture;\n}\n\nexport interface Ripple {\n x: number;\n y: number;\n startTime: number;\n}\n","import { useCallback, useEffect, useRef } from \"react\";\nimport type {\n AsciiContext,\n UseAsciiMouseEffectOptions,\n MouseEffectHandlers,\n} from \"@/lib/webgl\";\n\nexport type { UseAsciiMouseEffectOptions, MouseEffectHandlers };\n\nconst MAX_TRAIL_LENGTH = 24;\n\ninterface MousePosition {\n x: number;\n y: number;\n}\n\n// Hook Implementation\nexport function useAsciiMouseEffect(\n ascii: AsciiContext,\n options: UseAsciiMouseEffectOptions = {}\n): MouseEffectHandlers {\n const { enabled = true, trailLength = 24 } = options;\n\n // Current mouse position in normalized coords (0-1)\n const mouseRef = useRef<MousePosition>({ x: -1, y: -1 });\n // Array of previous positions for the trail effect\n const trailRef = useRef<MousePosition[]>([]);\n // Keep options in refs so event handlers have fresh values\n const enabledRef = useRef(enabled);\n const trailLengthRef = useRef(trailLength);\n\n useEffect(() => {\n enabledRef.current = enabled;\n trailLengthRef.current = trailLength;\n }, [enabled, trailLength]);\n\n // Register uniform setter - called every frame by core hook\n useEffect(() => {\n if (!enabled) return;\n\n const uniformSetter = (\n gl: WebGL2RenderingContext,\n _program: WebGLProgram,\n locations: NonNullable<typeof ascii.uniformLocationsRef.current>\n ) => {\n // Pass current mouse position to shader\n gl.uniform2f(locations.u_mouse, mouseRef.current.x, mouseRef.current.y);\n\n // Pass trail array to shader\n const trail = trailRef.current;\n gl.uniform1i(locations.u_trailLength, trail.length);\n\n // Fill all trail uniform slots (unused ones get -1,-1)\n for (let i = 0; i < MAX_TRAIL_LENGTH; i++) {\n const loc = locations.u_trail[i];\n if (loc) {\n const pos = trail[i] || { x: -1, y: -1 };\n gl.uniform2f(loc, pos.x, pos.y);\n }\n }\n };\n\n ascii.registerUniformSetter(\"mouse\", uniformSetter);\n\n return () => {\n ascii.unregisterUniformSetter(\"mouse\");\n };\n }, [ascii, enabled]);\n\n // Called when mouse moves over the container\n const onMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n if (!enabledRef.current) return;\n\n const rect = e.currentTarget.getBoundingClientRect();\n const newPos: MousePosition = {\n // Convert pixel coords to 0-1 range\n x: (e.clientX - rect.left) / rect.width,\n y: (e.clientY - rect.top) / rect.height,\n };\n\n // Add old position to the front of the trail\n if (mouseRef.current.x >= 0) {\n trailRef.current.unshift({ ...mouseRef.current });\n // Keep trail at max length\n if (trailRef.current.length > trailLengthRef.current) {\n trailRef.current.pop();\n }\n }\n\n mouseRef.current = newPos;\n }, []);\n\n // Reset when mouse leaves\n const onMouseLeave = useCallback(() => {\n mouseRef.current = { x: -1, y: -1 };\n trailRef.current = [];\n }, []);\n\n return { onMouseMove, onMouseLeave };\n}\n","import { useCallback, useEffect, useRef } from \"react\";\nimport type {\n AsciiContext,\n UseAsciiRippleOptions,\n RippleHandlers,\n} from \"@/lib/webgl\";\n\nexport type { UseAsciiRippleOptions, RippleHandlers };\n\nconst MAX_RIPPLES = 8;\n\ninterface Ripple {\n x: number;\n y: number;\n startTime: number;\n}\n\n// Hook Implementation\nexport function useAsciiRipple(\n ascii: AsciiContext,\n options: UseAsciiRippleOptions = {}\n): RippleHandlers {\n const { enabled = false, speed = 40 } = options;\n\n // Active ripples - each has position and start time\n const ripplesRef = useRef<Ripple[]>([]);\n const enabledRef = useRef(enabled);\n const speedRef = useRef(speed);\n\n useEffect(() => {\n enabledRef.current = enabled;\n speedRef.current = speed;\n }, [enabled, speed]);\n\n // Register uniform setter - runs every frame\n useEffect(() => {\n if (!enabled) return;\n\n const uniformSetter = (\n gl: WebGL2RenderingContext,\n _program: WebGLProgram,\n locations: NonNullable<typeof ascii.uniformLocationsRef.current>\n ) => {\n const currentTime = performance.now() / 1000; // convert to seconds\n\n gl.uniform1f(locations.u_time, currentTime);\n gl.uniform1f(locations.u_rippleEnabled, 1.0);\n gl.uniform1f(locations.u_rippleSpeed, speedRef.current);\n\n // Remove old ripples that have expanded past the screen\n const maxDist = Math.sqrt(\n ascii.dimensions.cols ** 2 + ascii.dimensions.rows ** 2\n );\n const maxLifetime = maxDist / speedRef.current + 1.0;\n ripplesRef.current = ripplesRef.current.filter(\n (r) => currentTime - r.startTime < maxLifetime\n );\n\n // Pass ripple data to shader (vec4: x, y, startTime, enabled)\n for (let i = 0; i < MAX_RIPPLES; i++) {\n const loc = locations.u_ripples[i];\n if (loc) {\n const ripple = ripplesRef.current[i];\n if (ripple) {\n gl.uniform4f(loc, ripple.x, ripple.y, ripple.startTime, 1.0);\n } else {\n gl.uniform4f(loc, 0, 0, 0, 0.0); // disabled\n }\n }\n }\n };\n\n ascii.registerUniformSetter(\"ripple\", uniformSetter);\n\n return () => {\n ascii.unregisterUniformSetter(\"ripple\");\n };\n }, [ascii, enabled]);\n\n // Spawn a new ripple where the user clicks\n const onClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n if (!enabledRef.current) return;\n\n const rect = e.currentTarget.getBoundingClientRect();\n const x = (e.clientX - rect.left) / rect.width;\n const y = (e.clientY - rect.top) / rect.height;\n\n // Add new ripple at the front\n ripplesRef.current.unshift({\n x,\n y,\n startTime: performance.now() / 1000,\n });\n\n // Cap at max ripples\n if (ripplesRef.current.length > MAX_RIPPLES) {\n ripplesRef.current.pop();\n }\n }, []);\n\n return { onClick };\n}\n","import { useEffect, useRef } from \"react\";\nimport type { AsciiContext, UseAsciiAudioOptions } from \"@/lib/webgl\";\n\nexport type { UseAsciiAudioOptions };\n\n// Hook Implementation\nexport function useAsciiAudio(\n ascii: AsciiContext,\n options: UseAsciiAudioOptions = {}\n): void {\n const { enabled = false, reactivity = 50, sensitivity = 50 } = options;\n\n // Web Audio API refs - these persist across renders\n const audioContextRef = useRef<AudioContext | null>(null);\n const analyzerRef = useRef<AnalyserNode | null>(null);\n const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);\n const dataArrayRef = useRef<Uint8Array<ArrayBuffer> | null>(null);\n const volumeRef = useRef(0);\n const connectedVideoRef = useRef<HTMLVideoElement | null>(null);\n\n // Keep options in refs so the uniform setter closure always has fresh values\n const enabledRef = useRef(enabled);\n const reactivityRef = useRef(reactivity);\n const sensitivityRef = useRef(sensitivity);\n\n useEffect(() => {\n enabledRef.current = enabled;\n reactivityRef.current = reactivity;\n sensitivityRef.current = sensitivity;\n }, [enabled, reactivity, sensitivity]);\n\n // Reads frequency data from the analyzer and calculates average volume\n const updateVolume = () => {\n const analyzer = analyzerRef.current;\n const dataArray = dataArrayRef.current;\n if (!analyzer || !dataArray) return;\n\n // getByteFrequencyData fills the array with frequency values (0-255)\n analyzer.getByteFrequencyData(dataArray);\n\n // Average all frequency bins to get overall loudness\n let sum = 0;\n for (let i = 0; i < dataArray.length; i++) {\n sum += dataArray[i];\n }\n const average = sum / dataArray.length / 255; // normalize to 0-1\n\n // Smooth the volume so it doesn't jump around too much\n volumeRef.current = volumeRef.current * 0.7 + average * 0.3;\n };\n\n // Connect to video's audio stream\n useEffect(() => {\n if (!enabled) return;\n\n const video = ascii.videoRef.current;\n if (!video) return;\n\n const connectAudio = () => {\n // If we already connected this exact video element, just resume\n if (connectedVideoRef.current === video && audioContextRef.current) {\n audioContextRef.current.resume();\n return;\n }\n\n try {\n // AudioContext is the entry point to Web Audio API\n if (!audioContextRef.current) {\n audioContextRef.current = new AudioContext();\n }\n\n const ctx = audioContextRef.current;\n\n // AnalyserNode lets us extract frequency/time data from audio\n const analyzer = ctx.createAnalyser();\n analyzer.fftSize = 256; // smaller = faster, less detailed\n analyzer.smoothingTimeConstant = 0.8; // 0-1, higher = smoother\n analyzerRef.current = analyzer;\n\n // This array will hold the frequency data each frame\n dataArrayRef.current = new Uint8Array(\n analyzer.frequencyBinCount\n ) as Uint8Array<ArrayBuffer>;\n\n // createMediaElementSource connects our video element to the audio graph\n // IMPORTANT: a video can only be connected once, ever\n const source = ctx.createMediaElementSource(video);\n source.connect(analyzer);\n analyzer.connect(ctx.destination); // so we still hear the audio\n sourceRef.current = source;\n connectedVideoRef.current = video;\n\n ctx.resume();\n } catch (error) {\n console.warn(\"Failed to connect audio analyzer:\", error);\n }\n };\n\n const handlePlay = () => {\n connectAudio();\n };\n\n video.addEventListener(\"play\", handlePlay);\n\n // If video is already playing when this hook mounts\n if (!video.paused) {\n connectAudio();\n }\n\n return () => {\n video.removeEventListener(\"play\", handlePlay);\n };\n }, [ascii.videoRef, enabled]);\n\n // Register our uniform setter - this gets called every frame by the core hook\n useEffect(() => {\n if (!enabled) return;\n\n const uniformSetter = (\n gl: WebGL2RenderingContext,\n _program: WebGLProgram,\n locations: NonNullable<typeof ascii.uniformLocationsRef.current>\n ) => {\n // Update volume from audio analyzer\n updateVolume();\n\n // Pass values to the shader\n gl.uniform1f(locations.u_audioLevel, volumeRef.current);\n gl.uniform1f(locations.u_audioReactivity, reactivityRef.current / 100);\n gl.uniform1f(locations.u_audioSensitivity, sensitivityRef.current / 100);\n };\n\n ascii.registerUniformSetter(\"audio\", uniformSetter);\n\n return () => {\n ascii.unregisterUniformSetter(\"audio\");\n };\n }, [ascii, enabled]);\n\n // Cleanup audio context when component unmounts\n useEffect(() => {\n return () => {\n if (audioContextRef.current) {\n audioContextRef.current.close();\n }\n };\n }, []);\n}\n"],"mappings":"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","names":["import_react","import_react","MAX_TRAIL_LENGTH","import_react","MAX_RIPPLES","import_react"]}
package/dist/index.mjs CHANGED
@@ -18,6 +18,9 @@ var __spreadValues = (a, b) => {
18
18
  };
19
19
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
20
 
21
+ // src/components/VideoToAscii.tsx
22
+ import { useEffect as useEffect5 } from "react";
23
+
21
24
  // src/hooks/useVideoToAscii.ts
22
25
  import { useRef, useState, useCallback, useEffect, useMemo } from "react";
23
26
 
@@ -73,7 +76,7 @@ function getCharArray(charset) {
73
76
  var vertex_default = "#version 300 es\n\n// Fullscreen quad - passes texture coords to fragment shader\n\nin vec2 a_position;\nin vec2 a_texCoord;\nout vec2 v_texCoord;\n\nvoid main() {\n gl_Position = vec4(a_position, 0.0, 1.0);\n v_texCoord = a_texCoord;\n}\n";
74
77
 
75
78
  // src/lib/webgl/shaders/fragment.glsl
76
- var fragment_default = "#version 300 es\nprecision highp float;\n\n// Textures\nuniform sampler2D u_video;\nuniform sampler2D u_asciiAtlas;\n\n// Dimensions\nuniform vec2 u_resolution;\nuniform vec2 u_charSize;\nuniform vec2 u_gridSize;\nuniform float u_numChars;\n\n// Rendering options\nuniform bool u_colored;\nuniform float u_blend;\nuniform float u_highlight;\n\n// Audio\nuniform float u_audioLevel;\nuniform float u_audioReactivity;\nuniform float u_audioSensitivity;\n\n// Mouse\nuniform vec2 u_mouse;\nuniform float u_mouseRadius;\nuniform vec2 u_trail[24];\nuniform int u_trailLength;\n\n// Ripple\nuniform vec4 u_ripples[8];\nuniform float u_time;\nuniform float u_rippleEnabled;\nuniform float u_rippleSpeed;\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nvoid main() {\n // Figure out which ASCII cell this pixel is in\n vec2 cellCoord = floor(v_texCoord * u_gridSize);\n vec2 thisCell = cellCoord;\n \n // Sample video at cell center (mipmaps handle averaging)\n vec2 cellCenter = (cellCoord + 0.5) / u_gridSize;\n vec4 videoColor = texture(u_video, cellCenter);\n \n // Perceived brightness using human eye sensitivity weights\n float baseBrightness = dot(videoColor.rgb, vec3(0.299, 0.587, 0.114));\n \n // Audio reactivity - louder = brighter, silence = darker\n float minBrightness = mix(0.3, 0.0, u_audioSensitivity);\n float maxBrightness = mix(1.0, 5.0, u_audioSensitivity);\n float audioMultiplier = mix(minBrightness, maxBrightness, u_audioLevel);\n float audioModulated = baseBrightness * audioMultiplier;\n float brightness = mix(baseBrightness, audioModulated, u_audioReactivity);\n \n // Cursor glow - blocky circle effect\n float cursorGlow = 0.0;\n float cursorRadius = 5.0;\n \n vec2 mouseCell = floor(u_mouse * u_gridSize);\n float cellDist = length(thisCell - mouseCell);\n if (cellDist <= cursorRadius && u_mouse.x >= 0.0) {\n cursorGlow += 1.0 - cellDist / cursorRadius;\n }\n \n // Trail effect\n for (int i = 0; i < 12; i++) {\n if (i >= u_trailLength) break;\n vec2 trailPos = u_trail[i];\n if (trailPos.x < 0.0) continue;\n \n vec2 trailCell = floor(trailPos * u_gridSize);\n float trailDist = length(thisCell - trailCell);\n float trailRadius = cursorRadius * 0.8;\n \n if (trailDist <= trailRadius) {\n float fade = 1.0 - float(i) / float(u_trailLength);\n cursorGlow += (1.0 - trailDist / trailRadius) * 0.5 * fade;\n }\n }\n cursorGlow = min(cursorGlow, 1.0);\n \n // Ripple effect - expanding rings on click\n float rippleGlow = 0.0;\n if (u_rippleEnabled > 0.5) {\n for (int i = 0; i < 8; i++) {\n vec4 ripple = u_ripples[i];\n if (ripple.w < 0.5) continue;\n \n float age = u_time - ripple.z;\n if (age < 0.0) continue;\n \n vec2 rippleCell = floor(ripple.xy * u_gridSize);\n float cellDist = length(thisCell - rippleCell);\n float initialRadius = 5.0;\n \n float distFromEdge = max(0.0, cellDist - initialRadius);\n float rippleSpeed = u_rippleSpeed;\n float reachTime = distFromEdge / rippleSpeed;\n float timeSinceReached = age - reachTime;\n \n float fadeDuration = 0.5;\n if (timeSinceReached >= 0.0 && timeSinceReached < fadeDuration) {\n float pop = 1.0 - timeSinceReached / fadeDuration;\n pop = pop * pop;\n rippleGlow += pop * 0.3;\n }\n }\n rippleGlow = min(rippleGlow, 1.0);\n }\n \n // Map brightness to character index (0 = darkest char, numChars-1 = brightest)\n float charIndex = floor(brightness * (u_numChars - 0.001));\n \n // Find the character in the atlas (horizontal strip of pre-rendered chars)\n float atlasX = charIndex / u_numChars;\n vec2 cellPos = fract(v_texCoord * u_gridSize);\n vec2 atlasCoord = vec2(atlasX + cellPos.x / u_numChars, cellPos.y);\n vec4 charColor = texture(u_asciiAtlas, atlasCoord);\n \n // Pick the color - video colors or green terminal aesthetic\n vec3 baseColor;\n if (u_colored) {\n baseColor = videoColor.rgb;\n } else {\n baseColor = vec3(0.0, 1.0, 0.0);\n }\n \n // Background highlight behind each character\n float bgIntensity = 0.15 + u_highlight * 0.35;\n vec3 bgColor = baseColor * bgIntensity;\n vec3 textColor = baseColor * 1.2;\n vec3 finalColor = mix(bgColor, textColor, charColor.r);\n \n // Add cursor and ripple glow\n finalColor += cursorGlow * baseColor * 0.5;\n finalColor += rippleGlow * baseColor;\n \n // Blend with original video if requested\n vec3 blendedColor = mix(finalColor, videoColor.rgb, u_blend);\n \n fragColor = vec4(blendedColor, 1.0);\n}\n";
79
+ var fragment_default = "#version 300 es\nprecision highp float;\n\n// Textures\nuniform sampler2D u_video;\nuniform sampler2D u_asciiAtlas;\n\n// Dimensions\nuniform vec2 u_resolution;\nuniform vec2 u_charSize;\nuniform vec2 u_gridSize;\nuniform float u_numChars;\n\n// Rendering options\nuniform bool u_colored;\nuniform float u_blend;\nuniform float u_highlight;\nuniform float u_brightness;\n\n// Audio\nuniform float u_audioLevel;\nuniform float u_audioReactivity;\nuniform float u_audioSensitivity;\n\n// Mouse\nuniform vec2 u_mouse;\nuniform float u_mouseRadius;\nuniform vec2 u_trail[24];\nuniform int u_trailLength;\n\n// Ripple\nuniform vec4 u_ripples[8];\nuniform float u_time;\nuniform float u_rippleEnabled;\nuniform float u_rippleSpeed;\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nvoid main() {\n // Figure out which ASCII cell this pixel is in\n vec2 cellCoord = floor(v_texCoord * u_gridSize);\n vec2 thisCell = cellCoord;\n \n // Sample video at cell center (mipmaps handle averaging)\n vec2 cellCenter = (cellCoord + 0.5) / u_gridSize;\n vec4 videoColor = texture(u_video, cellCenter);\n \n // Perceived brightness using human eye sensitivity weights\n float baseBrightness = dot(videoColor.rgb, vec3(0.299, 0.587, 0.114));\n \n // Audio reactivity - louder = brighter, silence = darker\n float minBrightness = mix(0.3, 0.0, u_audioSensitivity);\n float maxBrightness = mix(1.0, 5.0, u_audioSensitivity);\n float audioMultiplier = mix(minBrightness, maxBrightness, u_audioLevel);\n float audioModulated = baseBrightness * audioMultiplier;\n float brightness = mix(baseBrightness, audioModulated, u_audioReactivity);\n \n // Cursor glow - blocky circle effect\n float cursorGlow = 0.0;\n float cursorRadius = 5.0;\n \n vec2 mouseCell = floor(u_mouse * u_gridSize);\n float cellDist = length(thisCell - mouseCell);\n if (cellDist <= cursorRadius && u_mouse.x >= 0.0) {\n cursorGlow += 1.0 - cellDist / cursorRadius;\n }\n \n // Trail effect\n for (int i = 0; i < 12; i++) {\n if (i >= u_trailLength) break;\n vec2 trailPos = u_trail[i];\n if (trailPos.x < 0.0) continue;\n \n vec2 trailCell = floor(trailPos * u_gridSize);\n float trailDist = length(thisCell - trailCell);\n float trailRadius = cursorRadius * 0.8;\n \n if (trailDist <= trailRadius) {\n float fade = 1.0 - float(i) / float(u_trailLength);\n cursorGlow += (1.0 - trailDist / trailRadius) * 0.5 * fade;\n }\n }\n cursorGlow = min(cursorGlow, 1.0);\n \n // Ripple effect - expanding rings on click\n float rippleGlow = 0.0;\n if (u_rippleEnabled > 0.5) {\n for (int i = 0; i < 8; i++) {\n vec4 ripple = u_ripples[i];\n if (ripple.w < 0.5) continue;\n \n float age = u_time - ripple.z;\n if (age < 0.0) continue;\n \n vec2 rippleCell = floor(ripple.xy * u_gridSize);\n float cellDist = length(thisCell - rippleCell);\n float initialRadius = 5.0;\n \n float distFromEdge = max(0.0, cellDist - initialRadius);\n float rippleSpeed = u_rippleSpeed;\n float reachTime = distFromEdge / rippleSpeed;\n float timeSinceReached = age - reachTime;\n \n float fadeDuration = 0.5;\n if (timeSinceReached >= 0.0 && timeSinceReached < fadeDuration) {\n float pop = 1.0 - timeSinceReached / fadeDuration;\n pop = pop * pop;\n rippleGlow += pop * 0.3;\n }\n }\n rippleGlow = min(rippleGlow, 1.0);\n }\n \n // Apply brightness multiplier\n // brightness < 1.0: darkens (multiply)\n // brightness > 1.0: brightens (compress dark values toward 1.0)\n float adjustedBrightness;\n if (u_brightness <= 1.0) {\n adjustedBrightness = brightness * u_brightness;\n } else {\n // For brightness > 1.0, compress the range: dark values get pushed up\n // Formula: 1.0 - (1.0 - brightness) / u_brightness\n // This makes dark values brighter while keeping bright values near 1.0\n adjustedBrightness = 1.0 - (1.0 - brightness) / u_brightness;\n }\n adjustedBrightness = clamp(adjustedBrightness, 0.0, 1.0);\n \n // Map brightness to character index (0 = darkest char, numChars-1 = brightest)\n float charIndex = floor(adjustedBrightness * (u_numChars - 0.001));\n \n // Find the character in the atlas (horizontal strip of pre-rendered chars)\n float atlasX = charIndex / u_numChars;\n vec2 cellPos = fract(v_texCoord * u_gridSize);\n vec2 atlasCoord = vec2(atlasX + cellPos.x / u_numChars, cellPos.y);\n vec4 charColor = texture(u_asciiAtlas, atlasCoord);\n \n // Pick the color - video colors or green terminal aesthetic\n vec3 baseColor;\n if (u_colored) {\n baseColor = videoColor.rgb;\n } else {\n baseColor = vec3(0.0, 1.0, 0.0);\n }\n \n // Background highlight behind each character\n float bgIntensity = 0.15 + u_highlight * 0.35;\n vec3 bgColor = baseColor * bgIntensity;\n vec3 textColor = baseColor * 1.2;\n vec3 finalColor = mix(bgColor, textColor, charColor.r);\n \n // Add cursor and ripple glow\n finalColor += cursorGlow * baseColor * 0.5;\n finalColor += rippleGlow * baseColor;\n \n // Blend with original video if requested\n vec3 blendedColor = mix(finalColor, videoColor.rgb, u_blend);\n \n fragColor = vec4(blendedColor, 1.0);\n}\n";
77
80
 
78
81
  // src/lib/webgl/utils.ts
79
82
  function compileShader(gl, source, type) {
@@ -197,12 +200,15 @@ var MAX_TRAIL_LENGTH = 24;
197
200
  var MAX_RIPPLES = 8;
198
201
  function useVideoToAscii(options = {}) {
199
202
  const {
200
- fontSize = 10,
201
- colored = false,
203
+ fontSize,
204
+ numColumns,
205
+ colored = true,
202
206
  blend = 0,
203
207
  highlight = 0,
208
+ brightness = 1,
204
209
  charset = DEFAULT_CHARSET,
205
- maxWidth = 900,
210
+ maxWidth,
211
+ enableSpacebarToggle = false,
206
212
  onStats
207
213
  } = options;
208
214
  const containerRef = useRef(null);
@@ -222,8 +228,12 @@ function useVideoToAscii(options = {}) {
222
228
  const [stats, setStats] = useState({ fps: 0, frameTime: 0 });
223
229
  const [isReady, setIsReady] = useState(false);
224
230
  const [isPlaying, setIsPlaying] = useState(false);
225
- const charWidth = fontSize * CHAR_WIDTH_RATIO;
226
- const cols = Math.floor(maxWidth / charWidth);
231
+ const defaultWidth = typeof window !== "undefined" ? window.innerWidth : 900;
232
+ const containerWidth = maxWidth || defaultWidth;
233
+ const calculatedFontSize = numColumns ? containerWidth / (numColumns * CHAR_WIDTH_RATIO) : fontSize || 10;
234
+ const calculatedMaxWidth = numColumns ? numColumns * calculatedFontSize * CHAR_WIDTH_RATIO : maxWidth || 900;
235
+ const charWidth = calculatedFontSize * CHAR_WIDTH_RATIO;
236
+ const cols = numColumns || Math.floor(calculatedMaxWidth / charWidth);
227
237
  const chars = useMemo(() => getCharArray(charset), [charset]);
228
238
  const registerUniformSetter = useCallback(
229
239
  (id, setter) => {
@@ -248,6 +258,7 @@ function useVideoToAscii(options = {}) {
248
258
  u_colored: get("u_colored"),
249
259
  u_blend: get("u_blend"),
250
260
  u_highlight: get("u_highlight"),
261
+ u_brightness: get("u_brightness"),
251
262
  // Mouse uniforms
252
263
  u_mouse: get("u_mouse"),
253
264
  u_mouseRadius: get("u_mouseRadius"),
@@ -275,15 +286,24 @@ function useVideoToAscii(options = {}) {
275
286
  const initWebGL = useCallback(() => {
276
287
  const canvas = canvasRef.current;
277
288
  const video = videoRef.current;
289
+ const container = containerRef.current;
278
290
  if (!canvas || !video || !video.videoWidth) return false;
291
+ let finalFontSize = calculatedFontSize;
292
+ let finalCols = cols;
293
+ if (numColumns && container) {
294
+ const actualWidth = container.clientWidth || defaultWidth;
295
+ finalFontSize = actualWidth / (numColumns * CHAR_WIDTH_RATIO);
296
+ finalCols = numColumns;
297
+ }
279
298
  const grid = calculateGridDimensions(
280
299
  video.videoWidth,
281
300
  video.videoHeight,
282
- cols
301
+ finalCols
283
302
  );
284
303
  setDimensions(grid);
285
- const pixelWidth = grid.cols * charWidth;
286
- const pixelHeight = grid.rows * fontSize;
304
+ const finalCharWidth = finalFontSize * CHAR_WIDTH_RATIO;
305
+ const pixelWidth = grid.cols * finalCharWidth;
306
+ const pixelHeight = grid.rows * finalFontSize;
287
307
  canvas.width = pixelWidth;
288
308
  canvas.height = pixelHeight;
289
309
  const gl = canvas.getContext("webgl2", {
@@ -308,15 +328,21 @@ function useVideoToAscii(options = {}) {
308
328
  gl.useProgram(program);
309
329
  createFullscreenQuad(gl, program);
310
330
  videoTextureRef.current = createVideoTexture(gl);
311
- atlasTextureRef.current = createAsciiAtlas(gl, chars, fontSize);
331
+ const finalFontSizeForAtlas = numColumns && container ? (container.clientWidth || defaultWidth) / (numColumns * CHAR_WIDTH_RATIO) : calculatedFontSize;
332
+ atlasTextureRef.current = createAsciiAtlas(
333
+ gl,
334
+ chars,
335
+ finalFontSizeForAtlas
336
+ );
312
337
  const locations = cacheUniformLocations(gl, program);
313
338
  uniformLocationsRef.current = locations;
314
339
  gl.uniform1i(locations.u_video, 0);
315
340
  gl.uniform1i(locations.u_asciiAtlas, 1);
316
341
  gl.uniform2f(locations.u_resolution, pixelWidth, pixelHeight);
317
- gl.uniform2f(locations.u_charSize, charWidth, fontSize);
318
- gl.uniform2f(locations.u_gridSize, cols, grid.rows);
342
+ gl.uniform2f(locations.u_charSize, finalCharWidth, finalFontSize);
343
+ gl.uniform2f(locations.u_gridSize, finalCols, grid.rows);
319
344
  gl.uniform1f(locations.u_numChars, chars.length);
345
+ gl.uniform1f(locations.u_brightness, brightness);
320
346
  gl.uniform2f(locations.u_mouse, -1, -1);
321
347
  gl.uniform1f(locations.u_mouseRadius, 0);
322
348
  gl.uniform1i(locations.u_trailLength, 0);
@@ -327,7 +353,15 @@ function useVideoToAscii(options = {}) {
327
353
  gl.viewport(0, 0, pixelWidth, pixelHeight);
328
354
  setIsReady(true);
329
355
  return true;
330
- }, [cols, charWidth, fontSize, chars, cacheUniformLocations]);
356
+ }, [
357
+ cols,
358
+ numColumns,
359
+ calculatedFontSize,
360
+ chars,
361
+ cacheUniformLocations,
362
+ brightness,
363
+ defaultWidth
364
+ ]);
331
365
  const render = useCallback(() => {
332
366
  const gl = glRef.current;
333
367
  const video = videoRef.current;
@@ -345,6 +379,7 @@ function useVideoToAscii(options = {}) {
345
379
  gl.uniform1i(locations.u_colored, colored ? 1 : 0);
346
380
  gl.uniform1f(locations.u_blend, blend / 100);
347
381
  gl.uniform1f(locations.u_highlight, highlight / 100);
382
+ gl.uniform1f(locations.u_brightness, brightness);
348
383
  for (const setter of uniformSettersRef.current.values()) {
349
384
  setter(gl, program, locations);
350
385
  }
@@ -363,7 +398,7 @@ function useVideoToAscii(options = {}) {
363
398
  lastFpsTimeRef.current = now;
364
399
  }
365
400
  animationRef.current = requestAnimationFrame(render);
366
- }, [colored, blend, highlight, onStats]);
401
+ }, [colored, blend, highlight, brightness, onStats]);
367
402
  useEffect(() => {
368
403
  const video = videoRef.current;
369
404
  if (!video) return;
@@ -402,6 +437,19 @@ function useVideoToAscii(options = {}) {
402
437
  initWebGL();
403
438
  }
404
439
  }, [initWebGL]);
440
+ useEffect(() => {
441
+ if (!numColumns || !containerRef.current) return;
442
+ const container = containerRef.current;
443
+ const resizeObserver = new ResizeObserver(() => {
444
+ if (videoRef.current && videoRef.current.readyState >= 1) {
445
+ initWebGL();
446
+ }
447
+ });
448
+ resizeObserver.observe(container);
449
+ return () => {
450
+ resizeObserver.disconnect();
451
+ };
452
+ }, [numColumns, initWebGL]);
405
453
  useEffect(() => {
406
454
  return () => {
407
455
  const gl = glRef.current;
@@ -431,6 +479,7 @@ function useVideoToAscii(options = {}) {
431
479
  }
432
480
  }, []);
433
481
  useEffect(() => {
482
+ if (!enableSpacebarToggle) return;
434
483
  const handleKeyDown = (e) => {
435
484
  if (e.code === "Space" && e.target === document.body) {
436
485
  e.preventDefault();
@@ -439,7 +488,7 @@ function useVideoToAscii(options = {}) {
439
488
  };
440
489
  window.addEventListener("keydown", handleKeyDown);
441
490
  return () => window.removeEventListener("keydown", handleKeyDown);
442
- }, [toggle]);
491
+ }, [toggle, enableSpacebarToggle]);
443
492
  return {
444
493
  containerRef,
445
494
  videoRef,
@@ -670,40 +719,36 @@ function useAsciiAudio(ascii, options = {}) {
670
719
 
671
720
  // src/components/VideoToAscii.tsx
672
721
  import { jsx, jsxs } from "react/jsx-runtime";
673
- function VideoToAscii({
722
+ function Video2Ascii({
674
723
  src,
675
- fontSize = 10,
676
- colored = false,
724
+ numColumns,
725
+ colored = true,
677
726
  blend = 0,
678
727
  highlight = 0,
728
+ brightness = 1,
679
729
  charset = "standard",
680
- maxWidth = 900,
681
730
  enableMouse = true,
682
731
  trailLength = 24,
683
732
  enableRipple = false,
684
733
  rippleSpeed = 40,
685
- audioReactivity = 0,
686
- audioSensitivity = 50,
734
+ audioEffect = 0,
735
+ audioRange = 50,
736
+ isPlaying = true,
737
+ autoPlay = true,
738
+ enableSpacebarToggle = false,
687
739
  showStats = false,
688
740
  className = ""
689
741
  }) {
690
742
  const ascii = useVideoToAscii({
691
- fontSize,
743
+ numColumns,
692
744
  colored,
693
745
  blend,
694
746
  highlight,
747
+ brightness,
695
748
  charset,
696
- maxWidth
749
+ enableSpacebarToggle
697
750
  });
698
- const {
699
- containerRef,
700
- videoRef,
701
- canvasRef,
702
- stats,
703
- dimensions,
704
- isReady,
705
- isPlaying
706
- } = ascii;
751
+ const { containerRef, videoRef, canvasRef, stats, dimensions, isReady } = ascii;
707
752
  const mouseHandlers = useAsciiMouseEffect(ascii, {
708
753
  enabled: enableMouse,
709
754
  trailLength
@@ -713,20 +758,29 @@ function VideoToAscii({
713
758
  speed: rippleSpeed
714
759
  });
715
760
  useAsciiAudio(ascii, {
716
- enabled: audioReactivity > 0,
717
- reactivity: audioReactivity,
718
- sensitivity: audioSensitivity
761
+ enabled: audioEffect > 0,
762
+ reactivity: audioEffect,
763
+ sensitivity: audioRange
719
764
  });
720
- const charWidth = fontSize * CHAR_WIDTH_RATIO;
721
- const pixelWidth = dimensions.cols * charWidth;
722
- const pixelHeight = dimensions.rows * fontSize;
765
+ useEffect5(() => {
766
+ const video = videoRef.current;
767
+ if (!video) return;
768
+ if (isPlaying) {
769
+ if (autoPlay && isReady) {
770
+ video.play().catch(() => {
771
+ });
772
+ }
773
+ } else {
774
+ video.pause();
775
+ }
776
+ }, [isPlaying, autoPlay, isReady, videoRef]);
723
777
  return /* @__PURE__ */ jsxs("div", { className: `video-to-ascii ${className}`, children: [
724
778
  /* @__PURE__ */ jsx(
725
779
  "video",
726
780
  {
727
781
  ref: videoRef,
728
782
  src,
729
- muted: audioReactivity === 0,
783
+ muted: audioEffect === 0,
730
784
  loop: true,
731
785
  playsInline: true,
732
786
  crossOrigin: "anonymous",
@@ -737,12 +791,7 @@ function VideoToAscii({
737
791
  "div",
738
792
  __spreadProps(__spreadValues(__spreadValues({
739
793
  ref: containerRef,
740
- className: "relative cursor-pointer select-none overflow-hidden rounded",
741
- style: {
742
- width: pixelWidth || "100%",
743
- height: pixelHeight || "auto",
744
- backgroundColor: "#000"
745
- }
794
+ className: "relative cursor-pointer select-none overflow-hidden rounded bg-black"
746
795
  }, enableMouse ? mouseHandlers : {}), enableRipple ? rippleHandlers : {}), {
747
796
  children: [
748
797
  /* @__PURE__ */ jsx(
@@ -764,15 +813,16 @@ function VideoToAscii({
764
813
  dimensions.cols,
765
814
  "\xD7",
766
815
  dimensions.rows
767
- ] }),
768
- !isPlaying && isReady && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/50", children: /* @__PURE__ */ jsx("div", { className: "text-white text-lg", children: "\u25B6 Press Space to Play" }) })
816
+ ] })
769
817
  ]
770
818
  })
771
819
  )
772
820
  ] });
773
821
  }
822
+ var VideoToAscii_default = Video2Ascii;
774
823
  export {
775
824
  ASCII_CHARSETS,
776
- VideoToAscii
825
+ Video2Ascii,
826
+ VideoToAscii_default as default
777
827
  };
778
828
  //# sourceMappingURL=index.mjs.map