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