mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Merge pull request #5 from onmax/add-ascii-texts
Add AsciiText component
This commit is contained in:
@@ -19,6 +19,7 @@ export const CATEGORIES = [
|
|||||||
'Falling Text',
|
'Falling Text',
|
||||||
'Text Cursor',
|
'Text Cursor',
|
||||||
'Decrypted Text',
|
'Decrypted Text',
|
||||||
|
'Ascii Text',
|
||||||
'Scramble Text',
|
'Scramble Text',
|
||||||
'True Focus',
|
'True Focus',
|
||||||
'Scroll Float',
|
'Scroll Float',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const textAnimations = {
|
|||||||
'falling-text': () => import("../demo/TextAnimations/FallingTextDemo.vue"),
|
'falling-text': () => import("../demo/TextAnimations/FallingTextDemo.vue"),
|
||||||
'text-cursor': () => import("../demo/TextAnimations/TextCursorDemo.vue"),
|
'text-cursor': () => import("../demo/TextAnimations/TextCursorDemo.vue"),
|
||||||
'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"),
|
'decrypted-text': () => import("../demo/TextAnimations/DecryptedTextDemo.vue"),
|
||||||
|
'ascii-text': () => import("../demo/TextAnimations/AsciiTextDemo.vue"),
|
||||||
'scramble-text': () => import("../demo/TextAnimations/ScrambleTextDemo.vue"),
|
'scramble-text': () => import("../demo/TextAnimations/ScrambleTextDemo.vue"),
|
||||||
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
|
'true-focus': () => import("../demo/TextAnimations/TrueFocusDemo.vue"),
|
||||||
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
|
'scroll-float': () => import("../demo/TextAnimations/ScrollFloatDemo.vue"),
|
||||||
|
|||||||
23
src/constants/code/TextAnimations/asciiTextCode.ts
Normal file
23
src/constants/code/TextAnimations/asciiTextCode.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import code from '@/content/TextAnimations/AsciiText/AsciiText.vue?raw'
|
||||||
|
import type { CodeObject } from '../../../types/code'
|
||||||
|
|
||||||
|
export const asciiText: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/AsciiText`,
|
||||||
|
installation: `npm install three @types/three`,
|
||||||
|
usage: `<template>
|
||||||
|
<AsciiText
|
||||||
|
text="Hey!"
|
||||||
|
:ascii-font-size="8"
|
||||||
|
:text-font-size="200"
|
||||||
|
text-color="#fdf9f3"
|
||||||
|
:plane-base-height="8"
|
||||||
|
:enable-waves="true"
|
||||||
|
class-name="w-full h-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AsciiText from "./AsciiText.vue";
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
}
|
||||||
623
src/content/TextAnimations/AsciiText/AsciiText.vue
Normal file
623
src/content/TextAnimations/AsciiText/AsciiText.vue
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
interface AsciiTextProps {
|
||||||
|
text?: string
|
||||||
|
asciiFontSize?: number
|
||||||
|
textFontSize?: number
|
||||||
|
textColor?: string
|
||||||
|
planeBaseHeight?: number
|
||||||
|
enableWaves?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<AsciiTextProps>(), {
|
||||||
|
text: 'David!',
|
||||||
|
asciiFontSize: 8,
|
||||||
|
textFontSize: 200,
|
||||||
|
textColor: '#fdf9f3',
|
||||||
|
planeBaseHeight: 8,
|
||||||
|
enableWaves: true,
|
||||||
|
className: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const vertexShader = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float mouse;
|
||||||
|
uniform float uEnableWaves;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
float time = uTime * 5.;
|
||||||
|
|
||||||
|
float waveFactor = uEnableWaves;
|
||||||
|
|
||||||
|
vec3 transformed = position;
|
||||||
|
|
||||||
|
transformed.x += sin(time + position.y) * 0.5 * waveFactor;
|
||||||
|
transformed.y += cos(time + position.z) * 0.15 * waveFactor;
|
||||||
|
transformed.z += sin(time + position.x) * waveFactor;
|
||||||
|
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const fragmentShader = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
uniform float mouse;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform sampler2D uTexture;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float time = uTime;
|
||||||
|
vec2 pos = vUv;
|
||||||
|
|
||||||
|
float move = sin(time + mouse) * 0.01;
|
||||||
|
float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;
|
||||||
|
float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;
|
||||||
|
float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;
|
||||||
|
float a = texture2D(uTexture, pos).a;
|
||||||
|
gl_FragColor = vec4(r, g, b, a);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// @ts-expect-error - Adding map function to Math object
|
||||||
|
Math.map = function (n: number, start: number, stop: number, start2: number, stop2: number) {
|
||||||
|
return ((n - start) / (stop - start)) * (stop2 - start2) + start2
|
||||||
|
}
|
||||||
|
|
||||||
|
const PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1
|
||||||
|
|
||||||
|
interface AsciiFilterOptions {
|
||||||
|
fontSize?: number
|
||||||
|
fontFamily?: string
|
||||||
|
charset?: string
|
||||||
|
invert?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CanvasTxtOptions {
|
||||||
|
fontSize?: number
|
||||||
|
fontFamily?: string
|
||||||
|
color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsciiFilter {
|
||||||
|
renderer: THREE.WebGLRenderer
|
||||||
|
domElement: HTMLDivElement
|
||||||
|
pre: HTMLPreElement
|
||||||
|
canvas: HTMLCanvasElement
|
||||||
|
context: CanvasRenderingContext2D | null
|
||||||
|
deg: number
|
||||||
|
invert: boolean
|
||||||
|
fontSize: number
|
||||||
|
fontFamily: string
|
||||||
|
charset: string
|
||||||
|
width: number = 0
|
||||||
|
height: number = 0
|
||||||
|
center: { x: number; y: number } = { x: 0, y: 0 }
|
||||||
|
mouse: { x: number; y: number } = { x: 0, y: 0 }
|
||||||
|
cols: number = 0
|
||||||
|
rows: number = 0
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: THREE.WebGLRenderer,
|
||||||
|
{ fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}
|
||||||
|
) {
|
||||||
|
this.renderer = renderer
|
||||||
|
this.domElement = document.createElement('div')
|
||||||
|
this.domElement.style.position = 'absolute'
|
||||||
|
this.domElement.style.top = '0'
|
||||||
|
this.domElement.style.left = '0'
|
||||||
|
this.domElement.style.width = '100%'
|
||||||
|
this.domElement.style.height = '100%'
|
||||||
|
|
||||||
|
this.pre = document.createElement('pre')
|
||||||
|
this.domElement.appendChild(this.pre)
|
||||||
|
|
||||||
|
this.canvas = document.createElement('canvas')
|
||||||
|
this.context = this.canvas.getContext('2d')
|
||||||
|
this.domElement.appendChild(this.canvas)
|
||||||
|
|
||||||
|
this.deg = 0
|
||||||
|
this.invert = invert ?? true
|
||||||
|
this.fontSize = fontSize ?? 12
|
||||||
|
this.fontFamily = fontFamily ?? "'Courier New', monospace"
|
||||||
|
this.charset =
|
||||||
|
charset ??
|
||||||
|
" .'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"
|
||||||
|
|
||||||
|
if (this.context) {
|
||||||
|
this.context.imageSmoothingEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onMouseMove = this.onMouseMove.bind(this)
|
||||||
|
document.addEventListener('mousemove', this.onMouseMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(width: number, height: number) {
|
||||||
|
this.width = width
|
||||||
|
this.height = height
|
||||||
|
this.renderer.setSize(width, height)
|
||||||
|
this.reset()
|
||||||
|
|
||||||
|
this.center = { x: width / 2, y: height / 2 }
|
||||||
|
this.mouse = { x: this.center.x, y: this.center.y }
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.context!.font = `${this.fontSize}px ${this.fontFamily}`
|
||||||
|
const charWidth = this.context!.measureText('A').width
|
||||||
|
|
||||||
|
this.cols = Math.floor(
|
||||||
|
this.width / (this.fontSize * (charWidth / this.fontSize))
|
||||||
|
)
|
||||||
|
this.rows = Math.floor(this.height / this.fontSize)
|
||||||
|
|
||||||
|
this.canvas.width = this.cols
|
||||||
|
this.canvas.height = this.rows
|
||||||
|
this.pre.style.fontFamily = this.fontFamily
|
||||||
|
this.pre.style.fontSize = `${this.fontSize}px`
|
||||||
|
this.pre.style.margin = '0'
|
||||||
|
this.pre.style.padding = '0'
|
||||||
|
this.pre.style.lineHeight = '1em'
|
||||||
|
this.pre.style.position = 'absolute'
|
||||||
|
this.pre.style.left = '0'
|
||||||
|
this.pre.style.top = '0'
|
||||||
|
this.pre.style.zIndex = '9'
|
||||||
|
this.pre.style.backgroundImage = 'radial-gradient(circle, #ff6188 0%, #fc9867 50%, #ffd866 100%)'
|
||||||
|
this.pre.style.backgroundAttachment = 'fixed'
|
||||||
|
this.pre.style.webkitTextFillColor = 'transparent'
|
||||||
|
this.pre.style.webkitBackgroundClip = 'text'
|
||||||
|
this.pre.style.backgroundClip = 'text'
|
||||||
|
this.pre.style.mixBlendMode = 'difference'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(e: MouseEvent) {
|
||||||
|
this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO }
|
||||||
|
}
|
||||||
|
|
||||||
|
render(scene: THREE.Scene, camera: THREE.Camera) {
|
||||||
|
this.renderer.render(scene, camera)
|
||||||
|
|
||||||
|
const w = this.canvas.width
|
||||||
|
const h = this.canvas.height
|
||||||
|
this.context!.clearRect(0, 0, w, h)
|
||||||
|
if (this.context && w && h) {
|
||||||
|
this.context.drawImage(this.renderer.domElement, 0, 0, w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.asciify(this.context!, w, h)
|
||||||
|
this.hue()
|
||||||
|
}
|
||||||
|
|
||||||
|
asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
||||||
|
if (w && h) {
|
||||||
|
const imgData = ctx.getImageData(0, 0, w, h).data
|
||||||
|
let str = ''
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const i = x * 4 + y * 4 * w
|
||||||
|
const [r, g, b, a] = [
|
||||||
|
imgData[i],
|
||||||
|
imgData[i + 1],
|
||||||
|
imgData[i + 2],
|
||||||
|
imgData[i + 3]
|
||||||
|
]
|
||||||
|
|
||||||
|
if (a === 0) {
|
||||||
|
str += ' '
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255
|
||||||
|
let idx = Math.floor((1 - gray) * (this.charset.length - 1))
|
||||||
|
if (this.invert) idx = this.charset.length - idx - 1
|
||||||
|
str += this.charset[idx]
|
||||||
|
}
|
||||||
|
str += '\n'
|
||||||
|
}
|
||||||
|
this.pre.innerHTML = str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get dx() {
|
||||||
|
return this.mouse.x - this.center.x
|
||||||
|
}
|
||||||
|
|
||||||
|
get dy() {
|
||||||
|
return this.mouse.y - this.center.y
|
||||||
|
}
|
||||||
|
|
||||||
|
hue() {
|
||||||
|
const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI
|
||||||
|
this.deg += (deg - this.deg) * 0.075
|
||||||
|
this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
document.removeEventListener('mousemove', this.onMouseMove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanvasTxt {
|
||||||
|
canvas: HTMLCanvasElement
|
||||||
|
context: CanvasRenderingContext2D | null
|
||||||
|
txt: string
|
||||||
|
fontSize: number
|
||||||
|
fontFamily: string
|
||||||
|
color: string
|
||||||
|
font: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
txt: string,
|
||||||
|
{ fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}
|
||||||
|
) {
|
||||||
|
this.canvas = document.createElement('canvas')
|
||||||
|
this.context = this.canvas.getContext('2d')
|
||||||
|
this.txt = txt
|
||||||
|
this.fontSize = fontSize
|
||||||
|
this.fontFamily = fontFamily
|
||||||
|
this.color = color
|
||||||
|
this.font = `600 ${this.fontSize}px ${this.fontFamily}`
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
if (this.context) {
|
||||||
|
this.context.font = this.font
|
||||||
|
const metrics = this.context.measureText(this.txt)
|
||||||
|
|
||||||
|
const textWidth = Math.ceil(metrics.width) + 20
|
||||||
|
const textHeight =
|
||||||
|
Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20
|
||||||
|
|
||||||
|
this.canvas.width = textWidth
|
||||||
|
this.canvas.height = textHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.context) {
|
||||||
|
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||||
|
this.context.fillStyle = this.color
|
||||||
|
this.context.font = this.font
|
||||||
|
|
||||||
|
const metrics = this.context.measureText(this.txt)
|
||||||
|
const yPos = 10 + metrics.actualBoundingBoxAscent
|
||||||
|
|
||||||
|
this.context.fillText(this.txt, 10, yPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this.canvas.width
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this.canvas.height
|
||||||
|
}
|
||||||
|
|
||||||
|
get texture() {
|
||||||
|
return this.canvas
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanvAscii {
|
||||||
|
textString: string
|
||||||
|
asciiFontSize: number
|
||||||
|
textFontSize: number
|
||||||
|
textColor!: string
|
||||||
|
planeBaseHeight!: number
|
||||||
|
container!: HTMLElement
|
||||||
|
width!: number
|
||||||
|
height!: number
|
||||||
|
enableWaves!: boolean
|
||||||
|
camera!: THREE.PerspectiveCamera
|
||||||
|
scene!: THREE.Scene
|
||||||
|
mouse: { x: number; y: number } = { x: 0, y: 0 }
|
||||||
|
textCanvas!: CanvasTxt
|
||||||
|
texture!: THREE.CanvasTexture
|
||||||
|
geometry!: THREE.PlaneGeometry
|
||||||
|
material!: THREE.ShaderMaterial
|
||||||
|
mesh!: THREE.Mesh
|
||||||
|
renderer!: THREE.WebGLRenderer
|
||||||
|
filter!: AsciiFilter
|
||||||
|
center: { x: number; y: number } = { x: 0, y: 0 }
|
||||||
|
animationFrameId: number = 0
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: {
|
||||||
|
text: string
|
||||||
|
asciiFontSize: number
|
||||||
|
textFontSize: number
|
||||||
|
textColor: string
|
||||||
|
planeBaseHeight: number
|
||||||
|
enableWaves: boolean
|
||||||
|
},
|
||||||
|
containerElem: HTMLElement,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
) {
|
||||||
|
this.textString = text
|
||||||
|
this.asciiFontSize = asciiFontSize
|
||||||
|
this.textFontSize = textFontSize
|
||||||
|
this.textColor = textColor
|
||||||
|
this.planeBaseHeight = planeBaseHeight
|
||||||
|
this.container = containerElem
|
||||||
|
this.width = width
|
||||||
|
this.height = height
|
||||||
|
this.enableWaves = enableWaves
|
||||||
|
|
||||||
|
this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000)
|
||||||
|
this.camera.position.z = 30
|
||||||
|
|
||||||
|
this.scene = new THREE.Scene()
|
||||||
|
|
||||||
|
this.onMouseMove = this.onMouseMove.bind(this)
|
||||||
|
this.setMesh()
|
||||||
|
this.setRenderer()
|
||||||
|
}
|
||||||
|
|
||||||
|
setMesh() {
|
||||||
|
this.textCanvas = new CanvasTxt(this.textString, {
|
||||||
|
fontSize: this.textFontSize,
|
||||||
|
fontFamily: 'IBM Plex Mono',
|
||||||
|
color: this.textColor
|
||||||
|
})
|
||||||
|
this.textCanvas.resize()
|
||||||
|
this.textCanvas.render()
|
||||||
|
|
||||||
|
this.texture = new THREE.CanvasTexture(this.textCanvas.texture)
|
||||||
|
this.texture.minFilter = THREE.NearestFilter
|
||||||
|
|
||||||
|
const textAspect = this.textCanvas.width / this.textCanvas.height
|
||||||
|
const baseH = this.planeBaseHeight
|
||||||
|
const planeW = baseH * textAspect
|
||||||
|
const planeH = baseH
|
||||||
|
|
||||||
|
this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36)
|
||||||
|
this.material = new THREE.ShaderMaterial({
|
||||||
|
vertexShader,
|
||||||
|
fragmentShader,
|
||||||
|
transparent: true,
|
||||||
|
uniforms: {
|
||||||
|
uTime: { value: 0 },
|
||||||
|
mouse: { value: 1.0 },
|
||||||
|
uTexture: { value: this.texture },
|
||||||
|
uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mesh = new THREE.Mesh(this.geometry, this.material)
|
||||||
|
this.scene.add(this.mesh)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRenderer() {
|
||||||
|
this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true })
|
||||||
|
this.renderer.setPixelRatio(1)
|
||||||
|
this.renderer.setClearColor(0x000000, 0)
|
||||||
|
|
||||||
|
this.filter = new AsciiFilter(this.renderer, {
|
||||||
|
fontFamily: 'IBM Plex Mono',
|
||||||
|
fontSize: this.asciiFontSize,
|
||||||
|
invert: true
|
||||||
|
})
|
||||||
|
|
||||||
|
this.container.appendChild(this.filter.domElement)
|
||||||
|
this.setSize(this.width, this.height)
|
||||||
|
|
||||||
|
this.container.addEventListener('mousemove', this.onMouseMove)
|
||||||
|
this.container.addEventListener('touchmove', this.onMouseMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(w: number, h: number) {
|
||||||
|
this.width = w
|
||||||
|
this.height = h
|
||||||
|
|
||||||
|
this.camera.aspect = w / h
|
||||||
|
this.camera.updateProjectionMatrix()
|
||||||
|
|
||||||
|
this.filter.setSize(w, h)
|
||||||
|
this.center = { x: w / 2, y: h / 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(evt: MouseEvent | TouchEvent) {
|
||||||
|
const e = 'touches' in evt ? evt.touches[0] : evt
|
||||||
|
const bounds = this.container.getBoundingClientRect()
|
||||||
|
const x = e.clientX - bounds.left
|
||||||
|
const y = e.clientY - bounds.top
|
||||||
|
this.mouse = { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
animate() {
|
||||||
|
const animateFrame = () => {
|
||||||
|
this.animationFrameId = requestAnimationFrame(animateFrame)
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
animateFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const time = new Date().getTime() * 0.001
|
||||||
|
|
||||||
|
this.textCanvas.render()
|
||||||
|
this.texture.needsUpdate = true
|
||||||
|
|
||||||
|
;(this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time)
|
||||||
|
|
||||||
|
this.updateRotation()
|
||||||
|
this.filter.render(this.scene, this.camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRotation() {
|
||||||
|
// @ts-expect-error - Using custom Math.map function
|
||||||
|
const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5)
|
||||||
|
// @ts-expect-error - Using custom Math.map function
|
||||||
|
const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5)
|
||||||
|
|
||||||
|
this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05
|
||||||
|
this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.scene.traverse((obj) => {
|
||||||
|
if (obj instanceof THREE.Mesh && obj.material && obj.geometry) {
|
||||||
|
if (Array.isArray(obj.material)) {
|
||||||
|
obj.material.forEach((mat) => mat.dispose())
|
||||||
|
} else {
|
||||||
|
obj.material.dispose()
|
||||||
|
}
|
||||||
|
obj.geometry.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.scene.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
cancelAnimationFrame(this.animationFrameId)
|
||||||
|
this.filter.dispose()
|
||||||
|
this.container.removeChild(this.filter.domElement)
|
||||||
|
this.container.removeEventListener('mousemove', this.onMouseMove)
|
||||||
|
this.container.removeEventListener('touchmove', this.onMouseMove)
|
||||||
|
this.clear()
|
||||||
|
this.renderer.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
let asciiRef: CanvAscii | null = null
|
||||||
|
|
||||||
|
const initializeAscii = () => {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
const { width, height } = containerRef.value.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (width === 0 || height === 0) {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) {
|
||||||
|
const { width: w, height: h } = entry.boundingClientRect
|
||||||
|
|
||||||
|
asciiRef = new CanvAscii(
|
||||||
|
{
|
||||||
|
text: props.text,
|
||||||
|
asciiFontSize: props.asciiFontSize,
|
||||||
|
textFontSize: props.textFontSize,
|
||||||
|
textColor: props.textColor,
|
||||||
|
planeBaseHeight: props.planeBaseHeight,
|
||||||
|
enableWaves: props.enableWaves
|
||||||
|
},
|
||||||
|
containerRef.value!,
|
||||||
|
w,
|
||||||
|
h
|
||||||
|
)
|
||||||
|
asciiRef.load()
|
||||||
|
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.observe(containerRef.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
asciiRef = new CanvAscii(
|
||||||
|
{
|
||||||
|
text: props.text,
|
||||||
|
asciiFontSize: props.asciiFontSize,
|
||||||
|
textFontSize: props.textFontSize,
|
||||||
|
textColor: props.textColor,
|
||||||
|
planeBaseHeight: props.planeBaseHeight,
|
||||||
|
enableWaves: props.enableWaves
|
||||||
|
},
|
||||||
|
containerRef.value,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
)
|
||||||
|
asciiRef.load()
|
||||||
|
|
||||||
|
const ro = new ResizeObserver((entries) => {
|
||||||
|
if (!entries[0] || !asciiRef) return
|
||||||
|
const { width: w, height: h } = entries[0].contentRect
|
||||||
|
if (w > 0 && h > 0) {
|
||||||
|
asciiRef.setSize(w, h)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ro.observe(containerRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeAscii()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (asciiRef) {
|
||||||
|
asciiRef.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.text, props.asciiFontSize, props.textFontSize, props.textColor, props.planeBaseHeight, props.enableWaves],
|
||||||
|
() => {
|
||||||
|
if (asciiRef) {
|
||||||
|
asciiRef.dispose()
|
||||||
|
}
|
||||||
|
initializeAscii()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
:class="[
|
||||||
|
'ascii-text-container',
|
||||||
|
className
|
||||||
|
]"
|
||||||
|
class="absolute inset-0 w-full h-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&display=swap');
|
||||||
|
|
||||||
|
.ascii-text-container :deep(canvas) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
image-rendering: optimizeSpeed;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: -o-crisp-edges;
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
image-rendering: optimize-contrast;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-text-container :deep(pre) {
|
||||||
|
margin: 0;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1em;
|
||||||
|
text-align: left;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-image: radial-gradient(circle, #ff6188 0%, #fc9867 50%, #ffd866 100%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
z-index: 9;
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
180
src/demo/TextAnimations/AsciiTextDemo.vue
Normal file
180
src/demo/TextAnimations/AsciiTextDemo.vue
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ascii-text-demo">
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container relative min-h-[400px] max-h-[400px] overflow-hidden">
|
||||||
|
<RefreshButton @refresh="forceRerender" />
|
||||||
|
|
||||||
|
<AsciiText
|
||||||
|
:key="rerenderKey"
|
||||||
|
:text="text"
|
||||||
|
:ascii-font-size="asciiFontSize"
|
||||||
|
:text-font-size="textFontSize"
|
||||||
|
:text-color="textColor"
|
||||||
|
:plane-base-height="planeBaseHeight"
|
||||||
|
:enable-waves="enableWaves"
|
||||||
|
class-name="w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm font-medium text-gray-300">Text</label>
|
||||||
|
<input
|
||||||
|
v-model="text"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter text..."
|
||||||
|
maxlength="10"
|
||||||
|
class="px-3 py-2 bg-[#0b0b0b] border border-[#1e3721] rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#1e3721] focus:border-transparent"
|
||||||
|
@input="forceRerender"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="ASCII Font Size"
|
||||||
|
v-model="asciiFontSize"
|
||||||
|
:min="2"
|
||||||
|
:max="20"
|
||||||
|
:step="1"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Text Font Size"
|
||||||
|
v-model="textFontSize"
|
||||||
|
:min="100"
|
||||||
|
:max="400"
|
||||||
|
:step="25"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Base Height"
|
||||||
|
v-model="planeBaseHeight"
|
||||||
|
:min="4"
|
||||||
|
:max="16"
|
||||||
|
:step="1"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch
|
||||||
|
title="Enable Waves"
|
||||||
|
v-model="enableWaves"
|
||||||
|
@update:model-value="forceRerender"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<button
|
||||||
|
v-for="color in colorOptions"
|
||||||
|
:key="color.name"
|
||||||
|
class="text-xs bg-[#0b0b0b] rounded-[10px] border border-[#1e3721] hover:bg-[#1e3721] text-white h-8 px-3 transition-colors"
|
||||||
|
:class="{ 'bg-[#1e3721]': textColor === color.value }"
|
||||||
|
@click="changeColor(color.value)"
|
||||||
|
>
|
||||||
|
{{ color.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['three', '@types/three']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="asciiText" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="asciiText.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import TabbedLayout from '@/components/common/TabbedLayout.vue'
|
||||||
|
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||||
|
import PropTable from '@/components/common/PropTable.vue'
|
||||||
|
import Dependencies from '@/components/code/Dependencies.vue'
|
||||||
|
import CliInstallation from '@/components/code/CliInstallation.vue'
|
||||||
|
import CodeExample from '@/components/code/CodeExample.vue'
|
||||||
|
import Customize from '@/components/common/Customize.vue'
|
||||||
|
import PreviewSlider from '@/components/common/PreviewSlider.vue'
|
||||||
|
import PreviewSwitch from '@/components/common/PreviewSwitch.vue'
|
||||||
|
import AsciiText from '@/content/TextAnimations/AsciiText/AsciiText.vue'
|
||||||
|
import { asciiText } from '@/constants/code/TextAnimations/asciiTextCode'
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender'
|
||||||
|
|
||||||
|
const text = ref('Hey!')
|
||||||
|
const asciiFontSize = ref(8)
|
||||||
|
const textFontSize = ref(200)
|
||||||
|
const textColor = ref('#fdf9f3')
|
||||||
|
const planeBaseHeight = ref(8)
|
||||||
|
const enableWaves = ref(true)
|
||||||
|
|
||||||
|
const { rerenderKey, forceRerender } = useForceRerender()
|
||||||
|
|
||||||
|
// Color options for easy selection
|
||||||
|
const colorOptions = [
|
||||||
|
{ name: 'Cream', value: '#fdf9f3' },
|
||||||
|
{ name: 'White', value: '#ffffff' },
|
||||||
|
{ name: 'Red', value: '#ff6b6b' },
|
||||||
|
{ name: 'Blue', value: '#4ecdc4' },
|
||||||
|
{ name: 'Green', value: '#45b7d1' },
|
||||||
|
{ name: 'Purple', value: '#9b59b6' },
|
||||||
|
{ name: 'Orange', value: '#f39c12' },
|
||||||
|
{ name: 'Pink', value: '#e91e63' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const changeColor = (color: string) => {
|
||||||
|
textColor.value = color
|
||||||
|
forceRerender()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component properties documentation
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
default: '"David!"',
|
||||||
|
description: 'The text content to display as ASCII art.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'asciiFontSize',
|
||||||
|
type: 'number',
|
||||||
|
default: '8',
|
||||||
|
description: 'Font size for the ASCII characters (in pixels).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'textFontSize',
|
||||||
|
type: 'number',
|
||||||
|
default: '200',
|
||||||
|
description: 'Font size for the original text before ASCII conversion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'textColor',
|
||||||
|
type: 'string',
|
||||||
|
default: '"#fdf9f3"',
|
||||||
|
description: 'Color of the text used in the ASCII conversion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'planeBaseHeight',
|
||||||
|
type: 'number',
|
||||||
|
default: '8',
|
||||||
|
description: 'Base height of the 3D plane geometry for the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableWaves',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Whether to enable wave animation effects on the text.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'className',
|
||||||
|
type: 'string',
|
||||||
|
default: '""',
|
||||||
|
description: 'Additional CSS classes to apply to the component.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user