This commit is contained in:
David Haz
2025-07-12 20:14:41 +03:00
parent af10b2cfd6
commit fea147fef2
6 changed files with 414 additions and 452 deletions

View File

@@ -1,5 +1,5 @@
import code from '@/content/TextAnimations/AsciiText/AsciiText.vue?raw'
import type { CodeObject } from '../../../types/code'
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`,
@@ -20,4 +20,4 @@ export const asciiText: CodeObject = {
import AsciiText from "./AsciiText.vue";
</script>`,
code
}
};

View File

@@ -1,5 +1,5 @@
import code from '@content/TextAnimations/ScrambleText/ScrambleText.vue?raw'
import type { CodeObject } from '../../../types/code'
import code from '@content/TextAnimations/ScrambleText/ScrambleText.vue?raw';
import type { CodeObject } from '../../../types/code';
export const scrambleTextCode: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/ScrambleText`,
@@ -25,4 +25,4 @@ export const scrambleTextCode: CodeObject = {
import ScrambleText from "./ScrambleText.vue";
</script>`,
code
}
};

View File

@@ -1,15 +1,15 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as THREE from 'three'
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
text?: string;
asciiFontSize?: number;
textFontSize?: number;
textColor?: string;
planeBaseHeight?: number;
enableWaves?: boolean;
className?: string;
}
const props = withDefaults(defineProps<AsciiTextProps>(), {
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<AsciiTextProps>(), {
planeBaseHeight: 8,
enableWaves: true,
className: ''
})
});
const vertexShader = `
varying vec2 vUv;
@@ -42,7 +42,7 @@ void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`
`;
const fragmentShader = `
varying vec2 vUv;
@@ -61,302 +61,293 @@ void main() {
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
}
return ((n - start) / (stop - start)) * (stop2 - start2) + start2;
};
const PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1
const PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
interface AsciiFilterOptions {
fontSize?: number
fontFamily?: string
charset?: string
invert?: boolean
fontSize?: number;
fontFamily?: string;
charset?: string;
invert?: boolean;
}
interface CanvasTxtOptions {
fontSize?: number
fontFamily?: string
color?: string
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
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%'
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.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.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@$"
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.context.imageSmoothingEnabled = false;
}
this.onMouseMove = this.onMouseMove.bind(this)
document.addEventListener('mousemove', this.onMouseMove)
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.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 }
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.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.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'
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 }
this.mouse = { x: e.clientX * PX_RATIO, y: e.clientY * PX_RATIO };
}
render(scene: THREE.Scene, camera: THREE.Camera) {
this.renderer.render(scene, camera)
this.renderer.render(scene, camera);
const w = this.canvas.width
const h = this.canvas.height
this.context!.clearRect(0, 0, w, h)
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.context.drawImage(this.renderer.domElement, 0, 0, w, h);
}
this.asciify(this.context!, w, h)
this.hue()
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 = ''
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]
]
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
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]
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'
str += '\n';
}
this.pre.innerHTML = str
this.pre.innerHTML = str;
}
}
get dx() {
return this.mouse.x - this.center.x
return this.mouse.x - this.center.x;
}
get dy() {
return this.mouse.y - this.center.y
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)`
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)
document.removeEventListener('mousemove', this.onMouseMove);
}
}
class CanvasTxt {
canvas: HTMLCanvasElement
context: CanvasRenderingContext2D | null
txt: string
fontSize: number
fontFamily: string
color: string
font: string
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}`
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)
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
const textWidth = Math.ceil(metrics.width) + 20;
const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;
this.canvas.width = textWidth
this.canvas.height = textHeight
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
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
const metrics = this.context.measureText(this.txt);
const yPos = 10 + metrics.actualBoundingBoxAscent;
this.context.fillText(this.txt, 10, yPos)
this.context.fillText(this.txt, 10, yPos);
}
}
get width() {
return this.canvas.width
return this.canvas.width;
}
get height() {
return this.canvas.height
return this.canvas.height;
}
get texture() {
return this.canvas
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
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
{
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.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.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);
this.camera.position.z = 30;
this.scene = new THREE.Scene()
this.scene = new THREE.Scene();
this.onMouseMove = this.onMouseMove.bind(this)
this.setMesh()
this.setRenderer()
this.onMouseMove = this.onMouseMove.bind(this);
this.setMesh();
this.setRenderer();
}
setMesh() {
@@ -364,19 +355,19 @@ class CanvAscii {
fontSize: this.textFontSize,
fontFamily: 'IBM Plex Mono',
color: this.textColor
})
this.textCanvas.resize()
this.textCanvas.render()
});
this.textCanvas.resize();
this.textCanvas.render();
this.texture = new THREE.CanvasTexture(this.textCanvas.texture)
this.texture.minFilter = THREE.NearestFilter
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
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.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);
this.material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
@@ -387,121 +378,120 @@ class CanvAscii {
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)
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.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.appendChild(this.filter.domElement);
this.setSize(this.width, this.height);
this.container.addEventListener('mousemove', this.onMouseMove)
this.container.addEventListener('touchmove', this.onMouseMove)
this.container.addEventListener('mousemove', this.onMouseMove);
this.container.addEventListener('touchmove', this.onMouseMove);
}
setSize(w: number, h: number) {
this.width = w
this.height = h
this.width = w;
this.height = h;
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.filter.setSize(w, h)
this.center = { x: w / 2, y: h / 2 }
this.filter.setSize(w, h);
this.center = { x: w / 2, y: h / 2 };
}
load() {
this.animate()
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 }
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()
this.animationFrameId = requestAnimationFrame(animateFrame);
this.render();
};
animateFrame();
}
render() {
const time = new Date().getTime() * 0.001
const time = new Date().getTime() * 0.001;
this.textCanvas.render()
this.texture.needsUpdate = true
this.textCanvas.render();
this.texture.needsUpdate = true;
(this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time);
;(this.mesh.material as THREE.ShaderMaterial).uniforms.uTime.value = Math.sin(time)
this.updateRotation()
this.filter.render(this.scene, this.camera)
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)
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)
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
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) => {
this.scene.traverse(obj => {
if (obj instanceof THREE.Mesh && obj.material && obj.geometry) {
if (Array.isArray(obj.material)) {
obj.material.forEach((mat) => mat.dispose())
obj.material.forEach(mat => mat.dispose());
} else {
obj.material.dispose()
obj.material.dispose();
}
obj.geometry.dispose()
obj.geometry.dispose();
}
})
this.scene.clear()
});
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()
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 containerRef = ref<HTMLDivElement | null>(null);
let asciiRef: CanvAscii | null = null;
const initializeAscii = () => {
if (!containerRef.value) return
if (!containerRef.value) return;
const { width, height } = containerRef.value.getBoundingClientRect()
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
const { width: w, height: h } = entry.boundingClientRect;
asciiRef = new CanvAscii(
{
@@ -515,17 +505,17 @@ const initializeAscii = () => {
containerRef.value!,
w,
h
)
asciiRef.load()
);
asciiRef.load();
observer.disconnect()
observer.disconnect();
}
},
{ threshold: 0.1 }
)
);
observer.observe(containerRef.value)
return
observer.observe(containerRef.value);
return;
}
asciiRef = new CanvAscii(
@@ -540,49 +530,49 @@ const initializeAscii = () => {
containerRef.value,
width,
height
)
asciiRef.load()
);
asciiRef.load();
const ro = new ResizeObserver((entries) => {
if (!entries[0] || !asciiRef) return
const { width: w, height: h } = entries[0].contentRect
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)
asciiRef.setSize(w, h);
}
})
ro.observe(containerRef.value)
}
});
ro.observe(containerRef.value);
};
onMounted(() => {
initializeAscii()
})
initializeAscii();
});
onUnmounted(() => {
if (asciiRef) {
asciiRef.dispose()
asciiRef.dispose();
}
})
});
watch(
() => [props.text, props.asciiFontSize, props.textFontSize, props.textColor, props.planeBaseHeight, props.enableWaves],
() => [
props.text,
props.asciiFontSize,
props.textFontSize,
props.textColor,
props.planeBaseHeight,
props.enableWaves
],
() => {
if (asciiRef) {
asciiRef.dispose()
asciiRef.dispose();
}
initializeAscii()
initializeAscii();
}
)
);
</script>
<template>
<div
ref="containerRef"
:class="[
'ascii-text-container',
className
]"
class="absolute inset-0 w-full h-full"
/>
<div ref="containerRef" :class="['ascii-text-container', className]" class="absolute inset-0 w-full h-full" />
</template>
<style scoped>

View File

@@ -1,59 +1,59 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { gsap } from 'gsap'
import { SplitText } from 'gsap/SplitText'
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin'
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { gsap } from 'gsap';
import { SplitText } from 'gsap/SplitText';
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin';
gsap.registerPlugin(SplitText, ScrambleTextPlugin)
gsap.registerPlugin(SplitText, ScrambleTextPlugin);
interface ScrambleTextProps {
radius?: number
duration?: number
speed?: number
scrambleChars?: string
className?: string
style?: Record<string, string | number>
radius?: number;
duration?: number;
speed?: number;
scrambleChars?: string;
className?: string;
style?: Record<string, string | number>;
}
const props = withDefaults(defineProps<ScrambleTextProps>(), {
radius: 100,
duration: 1.2,
speed: 0.5,
scrambleChars: ".:",
className: "",
scrambleChars: '.:',
className: '',
style: () => ({})
})
});
const rootRef = ref<HTMLDivElement | null>(null)
const rootRef = ref<HTMLDivElement | null>(null);
let splitText: SplitText | null = null
let handleMove: ((e: PointerEvent) => void) | null = null
let splitText: SplitText | null = null;
let handleMove: ((e: PointerEvent) => void) | null = null;
const initializeScrambleText = () => {
if (!rootRef.value) return
if (!rootRef.value) return;
const pElement = rootRef.value.querySelector('p')
if (!pElement) return
const pElement = rootRef.value.querySelector('p');
if (!pElement) return;
splitText = new SplitText(pElement, {
type: 'chars',
charsClass: 'inline-block will-change-transform'
})
});
splitText.chars.forEach((el) => {
const c = el as HTMLElement
gsap.set(c, { attr: { 'data-content': c.innerHTML } })
})
splitText.chars.forEach(el => {
const c = el as HTMLElement;
gsap.set(c, { attr: { 'data-content': c.innerHTML } });
});
handleMove = (e: PointerEvent) => {
if (!splitText) return
if (!splitText) return;
splitText.chars.forEach((el) => {
const c = el as HTMLElement
const { left, top, width, height } = c.getBoundingClientRect()
const dx = e.clientX - (left + width / 2)
const dy = e.clientY - (top + height / 2)
const dist = Math.hypot(dx, dy)
splitText.chars.forEach(el => {
const c = el as HTMLElement;
const { left, top, width, height } = c.getBoundingClientRect();
const dx = e.clientX - (left + width / 2);
const dy = e.clientY - (top + height / 2);
const dist = Math.hypot(dx, dy);
if (dist < props.radius) {
gsap.to(c, {
@@ -65,48 +65,41 @@ const initializeScrambleText = () => {
speed: props.speed
},
ease: 'none'
})
}
})
});
}
});
};
rootRef.value.addEventListener('pointermove', handleMove)
}
rootRef.value.addEventListener('pointermove', handleMove);
};
const cleanup = () => {
if (rootRef.value && handleMove) {
rootRef.value.removeEventListener('pointermove', handleMove)
rootRef.value.removeEventListener('pointermove', handleMove);
}
if (splitText) {
splitText.revert()
splitText = null
splitText.revert();
splitText = null;
}
handleMove = null
}
handleMove = null;
};
onMounted(() => {
initializeScrambleText()
})
initializeScrambleText();
});
onUnmounted(() => {
cleanup()
})
cleanup();
});
watch(
[() => props.radius, () => props.duration, () => props.speed, () => props.scrambleChars],
() => {
cleanup()
initializeScrambleText()
}
)
watch([() => props.radius, () => props.duration, () => props.speed, () => props.scrambleChars], () => {
cleanup();
initializeScrambleText();
});
</script>
<template>
<div
ref="rootRef"
:class="`scramble-text ${className}`"
:style="style"
>
<div ref="rootRef" :class="`scramble-text ${className}`" :style="style">
<p>
<slot></slot>
</p>

View File

@@ -57,11 +57,7 @@
@update:model-value="forceRerender"
/>
<PreviewSwitch
title="Enable Waves"
v-model="enableWaves"
@update:model-value="forceRerender"
/>
<PreviewSwitch title="Enable Waves" v-model="enableWaves" @update:model-value="forceRerender" />
<div class="flex gap-4 flex-wrap">
<button
@@ -92,28 +88,28 @@
</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'
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 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()
const { rerenderKey, forceRerender } = useForceRerender();
// Color options for easy selection
const colorOptions = [
@@ -125,12 +121,12 @@ const colorOptions = [
{ name: 'Purple', value: '#9b59b6' },
{ name: 'Orange', value: '#f39c12' },
{ name: 'Pink', value: '#e91e63' }
]
];
const changeColor = (color: string) => {
textColor.value = color
forceRerender()
}
textColor.value = color;
forceRerender();
};
// Component properties documentation
const propData = [
@@ -176,5 +172,5 @@ const propData = [
default: '""',
description: 'Additional CSS classes to apply to the component.'
}
]
];
</script>

View File

@@ -1,58 +1,58 @@
<script setup lang="ts">
import { ref } from 'vue'
import TabbedLayout from '../../components/common/TabbedLayout.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 ScrambleText from '../../content/TextAnimations/ScrambleText/ScrambleText.vue'
import { scrambleTextCode } from '@/constants/code/TextAnimations/scrambleTextCode'
import { ref } from 'vue';
import TabbedLayout from '../../components/common/TabbedLayout.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 ScrambleText from '../../content/TextAnimations/ScrambleText/ScrambleText.vue';
import { scrambleTextCode } from '@/constants/code/TextAnimations/scrambleTextCode';
const radius = ref(100)
const duration = ref(1.2)
const speed = ref(0.5)
const scrambleChars = ref(".:")
const radius = ref(100);
const duration = ref(1.2);
const speed = ref(0.5);
const scrambleChars = ref('.:');
const propData = [
{
name: "radius",
type: "number",
default: "100",
description: "The radius around the mouse pointer within which characters will scramble."
name: 'radius',
type: 'number',
default: '100',
description: 'The radius around the mouse pointer within which characters will scramble.'
},
{
name: "duration",
type: "number",
default: "1.2",
description: "The duration of the scramble effect on a character."
name: 'duration',
type: 'number',
default: '1.2',
description: 'The duration of the scramble effect on a character.'
},
{
name: "speed",
type: "number",
default: "0.5",
description: "The speed of the scramble animation."
name: 'speed',
type: 'number',
default: '0.5',
description: 'The speed of the scramble animation.'
},
{
name: "scrambleChars",
type: "string",
name: 'scrambleChars',
type: 'string',
default: "'.:'",
description: "The characters used for scrambling."
description: 'The characters used for scrambling.'
},
{
name: "className",
type: "string",
name: 'className',
type: 'string',
default: '""',
description: "Additional CSS classes for the component."
description: 'Additional CSS classes for the component.'
},
{
name: "style",
type: "Record<string, string | number>",
default: "{}",
description: "Inline styles for the component."
name: 'style',
type: 'Record<string, string | number>',
default: '{}',
description: 'Inline styles for the component.'
}
]
];
</script>
<template>
@@ -67,7 +67,8 @@ const propData = [
:speed="speed"
:scrambleChars="scrambleChars"
>
Once you hover over me, you will see the effect in action! You can customize the radius, duration, and speed of the scramble effect.
Once you hover over me, you will see the effect in action! You can customize the radius, duration, and speed
of the scramble effect.
</ScrambleText>
</div>
@@ -83,29 +84,11 @@ const propData = [
/>
</div>
<PreviewSlider
title="Radius"
v-model="radius"
:min="10"
:max="300"
:step="10"
/>
<PreviewSlider title="Radius" v-model="radius" :min="10" :max="300" :step="10" />
<PreviewSlider
title="Duration"
v-model="duration"
:min="0.1"
:max="5"
:step="0.1"
/>
<PreviewSlider title="Duration" v-model="duration" :min="0.1" :max="5" :step="0.1" />
<PreviewSlider
title="Speed"
v-model="speed"
:min="0.1"
:max="2"
:step="0.1"
/>
<PreviewSlider title="Speed" v-model="speed" :min="0.1" :max="2" :step="0.1" />
</Customize>
<PropTable :data="propData" />