{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "ASCIIText-JS-TW", "type": "registry:block", "title": "ASCIIText", "description": "Renders text with an animated ASCII background for a retro feel.", "dependencies": [ "three" ], "files": [ { "path": "public/tailwind/src/tailwind/TextAnimations/ASCIIText/ASCIIText.jsx", "content": "// Component ported and enhanced from https://codepen.io/JuanFuentes/pen/eYEeoyE\r\n\r\nimport { useRef, useEffect } from 'react';\r\nimport * as THREE from 'three';\r\n\r\nconst vertexShader = `\r\nvarying vec2 vUv;\r\nuniform float uTime;\r\nuniform float mouse;\r\nuniform float uEnableWaves;\r\n\r\nvoid main() {\r\n vUv = uv;\r\n float time = uTime * 5.;\r\n\r\n float waveFactor = uEnableWaves;\r\n\r\n vec3 transformed = position;\r\n\r\n transformed.x += sin(time + position.y) * 0.5 * waveFactor;\r\n transformed.y += cos(time + position.z) * 0.15 * waveFactor;\r\n transformed.z += sin(time + position.x) * waveFactor;\r\n\r\n gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);\r\n}\r\n`;\r\n\r\nconst fragmentShader = `\r\nvarying vec2 vUv;\r\nuniform float mouse;\r\nuniform float uTime;\r\nuniform sampler2D uTexture;\r\n\r\nvoid main() {\r\n float time = uTime;\r\n vec2 pos = vUv;\r\n \r\n float move = sin(time + mouse) * 0.01;\r\n float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;\r\n float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;\r\n float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;\r\n float a = texture2D(uTexture, pos).a;\r\n gl_FragColor = vec4(r, g, b, a);\r\n}\r\n`;\r\n\r\nMath.map = function (n, start, stop, start2, stop2) {\r\n return ((n - start) / (stop - start)) * (stop2 - start2) + start2;\r\n};\r\n\r\nconst PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;\r\n\r\nclass AsciiFilter {\r\n constructor(renderer, { fontSize, fontFamily, charset, invert } = {}) {\r\n this.renderer = renderer;\r\n this.domElement = document.createElement('div');\r\n this.domElement.style.position = 'absolute';\r\n this.domElement.style.top = '0';\r\n this.domElement.style.left = '0';\r\n this.domElement.style.width = '100%';\r\n this.domElement.style.height = '100%';\r\n\r\n this.pre = document.createElement('pre');\r\n this.domElement.appendChild(this.pre);\r\n\r\n this.canvas = document.createElement('canvas');\r\n this.context = this.canvas.getContext('2d');\r\n this.domElement.appendChild(this.canvas);\r\n\r\n this.deg = 0;\r\n this.invert = invert ?? true;\r\n this.fontSize = fontSize ?? 12;\r\n this.fontFamily = fontFamily ?? \"'Courier New', monospace\";\r\n this.charset = charset ?? ' .\\'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';\r\n\r\n this.context.webkitImageSmoothingEnabled = false;\r\n this.context.mozImageSmoothingEnabled = false;\r\n this.context.msImageSmoothingEnabled = false;\r\n this.context.imageSmoothingEnabled = false;\r\n\r\n this.onMouseMove = this.onMouseMove.bind(this);\r\n document.addEventListener('mousemove', this.onMouseMove);\r\n }\r\n\r\n setSize(width, height) {\r\n this.width = width;\r\n this.height = height;\r\n this.renderer.setSize(width, height);\r\n this.reset();\r\n\r\n this.center = { x: width / 2, y: height / 2 };\r\n this.mouse = { x: this.center.x, y: this.center.y };\r\n }\r\n\r\n reset() {\r\n this.context.font = `${this.fontSize}px ${this.fontFamily}`;\r\n const charWidth = this.context.measureText('A').width;\r\n\r\n this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));\r\n this.rows = Math.floor(this.height / this.fontSize);\r\n\r\n this.canvas.width = this.cols;\r\n this.canvas.height = this.rows;\r\n this.pre.style.fontFamily = this.fontFamily;\r\n this.pre.style.fontSize = `${this.fontSize}px`;\r\n this.pre.style.margin = '0';\r\n this.pre.style.padding = '0';\r\n this.pre.style.lineHeight = '1em';\r\n this.pre.style.position = 'absolute';\r\n this.pre.style.left = '0';\r\n this.pre.style.top = '0';\r\n this.pre.style.zIndex = '9';\r\n this.pre.style.backgroundAttachment = 'fixed';\r\n this.pre.style.mixBlendMode = 'difference';\r\n }\r\n\r\n render(scene, camera) {\r\n this.renderer.render(scene, camera);\r\n\r\n const w = this.canvas.width;\r\n const h = this.canvas.height;\r\n this.context.clearRect(0, 0, w, h);\r\n if (this.context && w && h) {\r\n this.context.drawImage(this.renderer.domElement, 0, 0, w, h);\r\n }\r\n\r\n this.asciify(this.context, w, h);\r\n this.hue();\r\n }\r\n\r\n onMouseMove(e) {\r\n this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };\r\n }\r\n\r\n get dx() {\r\n return this.mouse.x - this.center.x;\r\n }\r\n\r\n get dy() {\r\n return this.mouse.y - this.center.y;\r\n }\r\n\r\n hue() {\r\n const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;\r\n this.deg += (deg - this.deg) * 0.075;\r\n this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;\r\n }\r\n\r\n asciify(ctx, w, h) {\r\n if (w && h) {\r\n const imgData = ctx.getImageData(0, 0, w, h).data;\r\n let str = '';\r\n for (let y = 0; y < h; y++) {\r\n for (let x = 0; x < w; x++) {\r\n const i = x * 4 + y * 4 * w;\r\n const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];\r\n\r\n if (a === 0) {\r\n str += ' ';\r\n continue;\r\n }\r\n\r\n let gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;\r\n let idx = Math.floor((1 - gray) * (this.charset.length - 1));\r\n if (this.invert) idx = this.charset.length - idx - 1;\r\n str += this.charset[idx];\r\n }\r\n str += '\\n';\r\n }\r\n this.pre.innerHTML = str;\r\n }\r\n }\r\n\r\n dispose() {\r\n document.removeEventListener('mousemove', this.onMouseMove);\r\n }\r\n}\r\n\r\nclass CanvasTxt {\r\n constructor(txt, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' } = {}) {\r\n this.canvas = document.createElement('canvas');\r\n this.context = this.canvas.getContext('2d');\r\n this.txt = txt;\r\n this.fontSize = fontSize;\r\n this.fontFamily = fontFamily;\r\n this.color = color;\r\n\r\n this.font = `600 ${this.fontSize}px ${this.fontFamily}`;\r\n }\r\n\r\n resize() {\r\n this.context.font = this.font;\r\n const metrics = this.context.measureText(this.txt);\r\n\r\n const textWidth = Math.ceil(metrics.width) + 20;\r\n const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;\r\n\r\n this.canvas.width = textWidth;\r\n this.canvas.height = textHeight;\r\n }\r\n\r\n render() {\r\n this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);\r\n this.context.fillStyle = this.color;\r\n this.context.font = this.font;\r\n\r\n const metrics = this.context.measureText(this.txt);\r\n const yPos = 10 + metrics.actualBoundingBoxAscent;\r\n\r\n this.context.fillText(this.txt, 10, yPos);\r\n }\r\n\r\n get width() {\r\n return this.canvas.width;\r\n }\r\n\r\n get height() {\r\n return this.canvas.height;\r\n }\r\n\r\n get texture() {\r\n return this.canvas;\r\n }\r\n}\r\n\r\nclass CanvAscii {\r\n constructor(\r\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\r\n containerElem,\r\n width,\r\n height\r\n ) {\r\n this.textString = text;\r\n this.asciiFontSize = asciiFontSize;\r\n this.textFontSize = textFontSize;\r\n this.textColor = textColor;\r\n this.planeBaseHeight = planeBaseHeight;\r\n this.container = containerElem;\r\n this.width = width;\r\n this.height = height;\r\n this.enableWaves = enableWaves;\r\n\r\n this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);\r\n this.camera.position.z = 30;\r\n\r\n this.scene = new THREE.Scene();\r\n this.mouse = { x: 0, y: 0 };\r\n\r\n this.onMouseMove = this.onMouseMove.bind(this);\r\n\r\n this.setMesh();\r\n this.setRenderer();\r\n }\r\n\r\n setMesh() {\r\n this.textCanvas = new CanvasTxt(this.textString, {\r\n fontSize: this.textFontSize,\r\n fontFamily: 'IBM Plex Mono',\r\n color: this.textColor\r\n });\r\n this.textCanvas.resize();\r\n this.textCanvas.render();\r\n\r\n this.texture = new THREE.CanvasTexture(this.textCanvas.texture);\r\n this.texture.minFilter = THREE.NearestFilter;\r\n\r\n const textAspect = this.textCanvas.width / this.textCanvas.height;\r\n const baseH = this.planeBaseHeight;\r\n const planeW = baseH * textAspect;\r\n const planeH = baseH;\r\n\r\n this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);\r\n this.material = new THREE.ShaderMaterial({\r\n vertexShader,\r\n fragmentShader,\r\n transparent: true,\r\n uniforms: {\r\n uTime: { value: 0 },\r\n mouse: { value: 1.0 },\r\n uTexture: { value: this.texture },\r\n uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }\r\n }\r\n });\r\n\r\n this.mesh = new THREE.Mesh(this.geometry, this.material);\r\n this.scene.add(this.mesh);\r\n }\r\n\r\n setRenderer() {\r\n this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });\r\n this.renderer.setPixelRatio(1);\r\n this.renderer.setClearColor(0x000000, 0);\r\n\r\n this.filter = new AsciiFilter(this.renderer, {\r\n fontFamily: 'IBM Plex Mono',\r\n fontSize: this.asciiFontSize,\r\n invert: true\r\n });\r\n\r\n this.container.appendChild(this.filter.domElement);\r\n this.setSize(this.width, this.height);\r\n\r\n this.container.addEventListener('mousemove', this.onMouseMove);\r\n this.container.addEventListener('touchmove', this.onMouseMove);\r\n }\r\n\r\n setSize(w, h) {\r\n this.width = w;\r\n this.height = h;\r\n\r\n this.camera.aspect = w / h;\r\n this.camera.updateProjectionMatrix();\r\n\r\n this.filter.setSize(w, h);\r\n\r\n this.center = { x: w / 2, y: h / 2 };\r\n }\r\n\r\n load() {\r\n this.animate();\r\n }\r\n\r\n onMouseMove(evt) {\r\n const e = evt.touches ? evt.touches[0] : evt;\r\n const bounds = this.container.getBoundingClientRect();\r\n const x = e.clientX - bounds.left;\r\n const y = e.clientY - bounds.top;\r\n this.mouse = { x, y };\r\n }\r\n\r\n animate() {\r\n const animateFrame = () => {\r\n this.animationFrameId = requestAnimationFrame(animateFrame);\r\n this.render();\r\n };\r\n animateFrame();\r\n }\r\n\r\n render() {\r\n const time = new Date().getTime() * 0.001;\r\n\r\n this.textCanvas.render();\r\n this.texture.needsUpdate = true;\r\n\r\n this.mesh.material.uniforms.uTime.value = Math.sin(time);\r\n\r\n this.updateRotation();\r\n this.filter.render(this.scene, this.camera);\r\n }\r\n\r\n updateRotation() {\r\n const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5);\r\n const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5);\r\n\r\n this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;\r\n this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;\r\n }\r\n\r\n clear() {\r\n this.scene.traverse(obj => {\r\n if (obj.isMesh && typeof obj.material === 'object' && obj.material !== null) {\r\n Object.keys(obj.material).forEach(key => {\r\n const matProp = obj.material[key];\r\n if (matProp !== null && typeof matProp === 'object' && typeof matProp.dispose === 'function') {\r\n matProp.dispose();\r\n }\r\n });\r\n obj.material.dispose();\r\n obj.geometry.dispose();\r\n }\r\n });\r\n this.scene.clear();\r\n }\r\n\r\n dispose() {\r\n cancelAnimationFrame(this.animationFrameId);\r\n this.filter.dispose();\r\n this.container.removeChild(this.filter.domElement);\r\n this.container.removeEventListener('mousemove', this.onMouseMove);\r\n this.container.removeEventListener('touchmove', this.onMouseMove);\r\n this.clear();\r\n this.renderer.dispose();\r\n }\r\n}\r\n\r\nexport default function ASCIIText({\r\n text = 'David!',\r\n asciiFontSize = 8,\r\n textFontSize = 200,\r\n textColor = '#fdf9f3',\r\n planeBaseHeight = 8,\r\n enableWaves = true\r\n}) {\r\n const containerRef = useRef(null);\r\n const asciiRef = useRef(null);\r\n\r\n useEffect(() => {\r\n if (!containerRef.current) return;\r\n\r\n const { width, height } = containerRef.current.getBoundingClientRect();\r\n\r\n if (width === 0 || height === 0) {\r\n const observer = new IntersectionObserver(\r\n ([entry]) => {\r\n if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {\r\n const { width: w, height: h } = entry.boundingClientRect;\r\n\r\n asciiRef.current = new CanvAscii(\r\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\r\n containerRef.current,\r\n w,\r\n h\r\n );\r\n asciiRef.current.load();\r\n\r\n observer.disconnect();\r\n }\r\n },\r\n { threshold: 0.1 }\r\n );\r\n\r\n observer.observe(containerRef.current);\r\n\r\n return () => {\r\n observer.disconnect();\r\n if (asciiRef.current) {\r\n asciiRef.current.dispose();\r\n }\r\n };\r\n }\r\n\r\n asciiRef.current = new CanvAscii(\r\n { text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves },\r\n containerRef.current,\r\n width,\r\n height\r\n );\r\n asciiRef.current.load();\r\n\r\n const ro = new ResizeObserver(entries => {\r\n if (!entries[0] || !asciiRef.current) return;\r\n const { width: w, height: h } = entries[0].contentRect;\r\n if (w > 0 && h > 0) {\r\n asciiRef.current.setSize(w, h);\r\n }\r\n });\r\n ro.observe(containerRef.current);\r\n\r\n return () => {\r\n ro.disconnect();\r\n if (asciiRef.current) {\r\n asciiRef.current.dispose();\r\n }\r\n };\r\n }, [text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves]);\r\n\r\n return (\r\n