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 code from '@/content/TextAnimations/AsciiText/AsciiText.vue?raw';
import type { CodeObject } from '../../../types/code' import type { CodeObject } from '../../../types/code';
export const asciiText: CodeObject = { export const asciiText: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/AsciiText`, cli: `npx jsrepo add https://vue-bits.dev/ui/TextAnimations/AsciiText`,
@@ -20,4 +20,4 @@ export const asciiText: CodeObject = {
import AsciiText from "./AsciiText.vue"; import AsciiText from "./AsciiText.vue";
</script>`, </script>`,
code code
} };

View File

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

View File

@@ -1,15 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as THREE from 'three' import * as THREE from 'three';
interface AsciiTextProps { interface AsciiTextProps {
text?: string text?: string;
asciiFontSize?: number asciiFontSize?: number;
textFontSize?: number textFontSize?: number;
textColor?: string textColor?: string;
planeBaseHeight?: number planeBaseHeight?: number;
enableWaves?: boolean enableWaves?: boolean;
className?: string className?: string;
} }
const props = withDefaults(defineProps<AsciiTextProps>(), { const props = withDefaults(defineProps<AsciiTextProps>(), {
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<AsciiTextProps>(), {
planeBaseHeight: 8, planeBaseHeight: 8,
enableWaves: true, enableWaves: true,
className: '' className: ''
}) });
const vertexShader = ` const vertexShader = `
varying vec2 vUv; varying vec2 vUv;
@@ -42,7 +42,7 @@ void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0); gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
} }
` `;
const fragmentShader = ` const fragmentShader = `
varying vec2 vUv; varying vec2 vUv;
@@ -61,302 +61,293 @@ void main() {
float a = texture2D(uTexture, pos).a; float a = texture2D(uTexture, pos).a;
gl_FragColor = vec4(r, g, b, a); gl_FragColor = vec4(r, g, b, a);
} }
` `;
// @ts-expect-error - Adding map function to Math object // @ts-expect-error - Adding map function to Math object
Math.map = function (n: number, start: number, stop: number, start2: number, stop2: number) { 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 { interface AsciiFilterOptions {
fontSize?: number fontSize?: number;
fontFamily?: string fontFamily?: string;
charset?: string charset?: string;
invert?: boolean invert?: boolean;
} }
interface CanvasTxtOptions { interface CanvasTxtOptions {
fontSize?: number fontSize?: number;
fontFamily?: string fontFamily?: string;
color?: string color?: string;
} }
class AsciiFilter { class AsciiFilter {
renderer: THREE.WebGLRenderer renderer: THREE.WebGLRenderer;
domElement: HTMLDivElement domElement: HTMLDivElement;
pre: HTMLPreElement pre: HTMLPreElement;
canvas: HTMLCanvasElement canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D | null context: CanvasRenderingContext2D | null;
deg: number deg: number;
invert: boolean invert: boolean;
fontSize: number fontSize: number;
fontFamily: string fontFamily: string;
charset: string charset: string;
width: number = 0 width: number = 0;
height: number = 0 height: number = 0;
center: { x: number; y: number } = { x: 0, y: 0 } center: { x: number; y: number } = { x: 0, y: 0 };
mouse: { x: number; y: number } = { x: 0, y: 0 } mouse: { x: number; y: number } = { x: 0, y: 0 };
cols: number = 0 cols: number = 0;
rows: number = 0 rows: number = 0;
constructor( constructor(renderer: THREE.WebGLRenderer, { fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {}) {
renderer: THREE.WebGLRenderer, this.renderer = renderer;
{ fontSize, fontFamily, charset, invert }: AsciiFilterOptions = {} this.domElement = document.createElement('div');
) { this.domElement.style.position = 'absolute';
this.renderer = renderer this.domElement.style.top = '0';
this.domElement = document.createElement('div') this.domElement.style.left = '0';
this.domElement.style.position = 'absolute' this.domElement.style.width = '100%';
this.domElement.style.top = '0' this.domElement.style.height = '100%';
this.domElement.style.left = '0'
this.domElement.style.width = '100%'
this.domElement.style.height = '100%'
this.pre = document.createElement('pre') this.pre = document.createElement('pre');
this.domElement.appendChild(this.pre) this.domElement.appendChild(this.pre);
this.canvas = document.createElement('canvas') this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d') this.context = this.canvas.getContext('2d');
this.domElement.appendChild(this.canvas) this.domElement.appendChild(this.canvas);
this.deg = 0 this.deg = 0;
this.invert = invert ?? true this.invert = invert ?? true;
this.fontSize = fontSize ?? 12 this.fontSize = fontSize ?? 12;
this.fontFamily = fontFamily ?? "'Courier New', monospace" this.fontFamily = fontFamily ?? "'Courier New', monospace";
this.charset = this.charset = charset ?? ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';
charset ??
" .'`^\",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"
if (this.context) { if (this.context) {
this.context.imageSmoothingEnabled = false this.context.imageSmoothingEnabled = false;
} }
this.onMouseMove = this.onMouseMove.bind(this) this.onMouseMove = this.onMouseMove.bind(this);
document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mousemove', this.onMouseMove);
} }
setSize(width: number, height: number) { setSize(width: number, height: number) {
this.width = width this.width = width;
this.height = height this.height = height;
this.renderer.setSize(width, height) this.renderer.setSize(width, height);
this.reset() this.reset();
this.center = { x: width / 2, y: height / 2 } this.center = { x: width / 2, y: height / 2 };
this.mouse = { x: this.center.x, y: this.center.y } this.mouse = { x: this.center.x, y: this.center.y };
} }
reset() { reset() {
this.context!.font = `${this.fontSize}px ${this.fontFamily}` this.context!.font = `${this.fontSize}px ${this.fontFamily}`;
const charWidth = this.context!.measureText('A').width const charWidth = this.context!.measureText('A').width;
this.cols = Math.floor( this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));
this.width / (this.fontSize * (charWidth / this.fontSize)) this.rows = Math.floor(this.height / this.fontSize);
)
this.rows = Math.floor(this.height / this.fontSize)
this.canvas.width = this.cols this.canvas.width = this.cols;
this.canvas.height = this.rows this.canvas.height = this.rows;
this.pre.style.fontFamily = this.fontFamily this.pre.style.fontFamily = this.fontFamily;
this.pre.style.fontSize = `${this.fontSize}px` this.pre.style.fontSize = `${this.fontSize}px`;
this.pre.style.margin = '0' this.pre.style.margin = '0';
this.pre.style.padding = '0' this.pre.style.padding = '0';
this.pre.style.lineHeight = '1em' this.pre.style.lineHeight = '1em';
this.pre.style.position = 'absolute' this.pre.style.position = 'absolute';
this.pre.style.left = '0' this.pre.style.left = '0';
this.pre.style.top = '0' this.pre.style.top = '0';
this.pre.style.zIndex = '9' this.pre.style.zIndex = '9';
this.pre.style.backgroundImage = 'radial-gradient(circle, #ff6188 0%, #fc9867 50%, #ffd866 100%)' this.pre.style.backgroundImage = 'radial-gradient(circle, #ff6188 0%, #fc9867 50%, #ffd866 100%)';
this.pre.style.backgroundAttachment = 'fixed' this.pre.style.backgroundAttachment = 'fixed';
this.pre.style.webkitTextFillColor = 'transparent' this.pre.style.webkitTextFillColor = 'transparent';
this.pre.style.webkitBackgroundClip = 'text' this.pre.style.webkitBackgroundClip = 'text';
this.pre.style.backgroundClip = 'text' this.pre.style.backgroundClip = 'text';
this.pre.style.mixBlendMode = 'difference' this.pre.style.mixBlendMode = 'difference';
} }
onMouseMove(e: MouseEvent) { 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) { render(scene: THREE.Scene, camera: THREE.Camera) {
this.renderer.render(scene, camera) this.renderer.render(scene, camera);
const w = this.canvas.width const w = this.canvas.width;
const h = this.canvas.height const h = this.canvas.height;
this.context!.clearRect(0, 0, w, h) this.context!.clearRect(0, 0, w, h);
if (this.context && 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.asciify(this.context!, w, h);
this.hue() this.hue();
} }
asciify(ctx: CanvasRenderingContext2D, w: number, h: number) { asciify(ctx: CanvasRenderingContext2D, w: number, h: number) {
if (w && h) { if (w && h) {
const imgData = ctx.getImageData(0, 0, w, h).data const imgData = ctx.getImageData(0, 0, w, h).data;
let str = '' let str = '';
for (let y = 0; y < h; y++) { for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) { for (let x = 0; x < w; x++) {
const i = x * 4 + y * 4 * w const i = x * 4 + y * 4 * w;
const [r, g, b, a] = [ const [r, g, b, a] = [imgData[i], imgData[i + 1], imgData[i + 2], imgData[i + 3]];
imgData[i],
imgData[i + 1],
imgData[i + 2],
imgData[i + 3]
]
if (a === 0) { if (a === 0) {
str += ' ' str += ' ';
continue continue;
} }
const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255 const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;
let idx = Math.floor((1 - gray) * (this.charset.length - 1)) let idx = Math.floor((1 - gray) * (this.charset.length - 1));
if (this.invert) idx = this.charset.length - idx - 1 if (this.invert) idx = this.charset.length - idx - 1;
str += this.charset[idx] str += this.charset[idx];
} }
str += '\n' str += '\n';
} }
this.pre.innerHTML = str this.pre.innerHTML = str;
} }
} }
get dx() { get dx() {
return this.mouse.x - this.center.x return this.mouse.x - this.center.x;
} }
get dy() { get dy() {
return this.mouse.y - this.center.y return this.mouse.y - this.center.y;
} }
hue() { hue() {
const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI const deg = (Math.atan2(this.dy, this.dx) * 180) / Math.PI;
this.deg += (deg - this.deg) * 0.075 this.deg += (deg - this.deg) * 0.075;
this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)` this.domElement.style.filter = `hue-rotate(${this.deg.toFixed(1)}deg)`;
} }
dispose() { dispose() {
document.removeEventListener('mousemove', this.onMouseMove) document.removeEventListener('mousemove', this.onMouseMove);
} }
} }
class CanvasTxt { class CanvasTxt {
canvas: HTMLCanvasElement canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D | null context: CanvasRenderingContext2D | null;
txt: string txt: string;
fontSize: number fontSize: number;
fontFamily: string fontFamily: string;
color: string color: string;
font: string font: string;
constructor( constructor(txt: string, { fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {}) {
txt: string, this.canvas = document.createElement('canvas');
{ fontSize = 200, fontFamily = 'Arial', color = '#fdf9f3' }: CanvasTxtOptions = {} this.context = this.canvas.getContext('2d');
) { this.txt = txt;
this.canvas = document.createElement('canvas') this.fontSize = fontSize;
this.context = this.canvas.getContext('2d') this.fontFamily = fontFamily;
this.txt = txt this.color = color;
this.fontSize = fontSize this.font = `600 ${this.fontSize}px ${this.fontFamily}`;
this.fontFamily = fontFamily
this.color = color
this.font = `600 ${this.fontSize}px ${this.fontFamily}`
} }
resize() { resize() {
if (this.context) { if (this.context) {
this.context.font = this.font this.context.font = this.font;
const metrics = this.context.measureText(this.txt) const metrics = this.context.measureText(this.txt);
const textWidth = Math.ceil(metrics.width) + 20 const textWidth = Math.ceil(metrics.width) + 20;
const textHeight = const textHeight = Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;
Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20
this.canvas.width = textWidth this.canvas.width = textWidth;
this.canvas.height = textHeight this.canvas.height = textHeight;
} }
} }
render() { render() {
if (this.context) { if (this.context) {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height) this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.fillStyle = this.color this.context.fillStyle = this.color;
this.context.font = this.font this.context.font = this.font;
const metrics = this.context.measureText(this.txt) const metrics = this.context.measureText(this.txt);
const yPos = 10 + metrics.actualBoundingBoxAscent const yPos = 10 + metrics.actualBoundingBoxAscent;
this.context.fillText(this.txt, 10, yPos) this.context.fillText(this.txt, 10, yPos);
} }
} }
get width() { get width() {
return this.canvas.width return this.canvas.width;
} }
get height() { get height() {
return this.canvas.height return this.canvas.height;
} }
get texture() { get texture() {
return this.canvas return this.canvas;
} }
} }
class CanvAscii { class CanvAscii {
textString: string textString: string;
asciiFontSize: number asciiFontSize: number;
textFontSize: number textFontSize: number;
textColor!: string textColor!: string;
planeBaseHeight!: number planeBaseHeight!: number;
container!: HTMLElement container!: HTMLElement;
width!: number width!: number;
height!: number height!: number;
enableWaves!: boolean enableWaves!: boolean;
camera!: THREE.PerspectiveCamera camera!: THREE.PerspectiveCamera;
scene!: THREE.Scene scene!: THREE.Scene;
mouse: { x: number; y: number } = { x: 0, y: 0 } mouse: { x: number; y: number } = { x: 0, y: 0 };
textCanvas!: CanvasTxt textCanvas!: CanvasTxt;
texture!: THREE.CanvasTexture texture!: THREE.CanvasTexture;
geometry!: THREE.PlaneGeometry geometry!: THREE.PlaneGeometry;
material!: THREE.ShaderMaterial material!: THREE.ShaderMaterial;
mesh!: THREE.Mesh mesh!: THREE.Mesh;
renderer!: THREE.WebGLRenderer renderer!: THREE.WebGLRenderer;
filter!: AsciiFilter filter!: AsciiFilter;
center: { x: number; y: number } = { x: 0, y: 0 } center: { x: number; y: number } = { x: 0, y: 0 };
animationFrameId: number = 0 animationFrameId: number = 0;
constructor( constructor(
{ text, asciiFontSize, textFontSize, textColor, planeBaseHeight, enableWaves }: { {
text: string text,
asciiFontSize: number asciiFontSize,
textFontSize: number textFontSize,
textColor: string textColor,
planeBaseHeight: number planeBaseHeight,
enableWaves: boolean enableWaves
}: {
text: string;
asciiFontSize: number;
textFontSize: number;
textColor: string;
planeBaseHeight: number;
enableWaves: boolean;
}, },
containerElem: HTMLElement, containerElem: HTMLElement,
width: number, width: number,
height: number height: number
) { ) {
this.textString = text this.textString = text;
this.asciiFontSize = asciiFontSize this.asciiFontSize = asciiFontSize;
this.textFontSize = textFontSize this.textFontSize = textFontSize;
this.textColor = textColor this.textColor = textColor;
this.planeBaseHeight = planeBaseHeight this.planeBaseHeight = planeBaseHeight;
this.container = containerElem this.container = containerElem;
this.width = width this.width = width;
this.height = height this.height = height;
this.enableWaves = enableWaves this.enableWaves = enableWaves;
this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000) this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);
this.camera.position.z = 30 this.camera.position.z = 30;
this.scene = new THREE.Scene() this.scene = new THREE.Scene();
this.onMouseMove = this.onMouseMove.bind(this) this.onMouseMove = this.onMouseMove.bind(this);
this.setMesh() this.setMesh();
this.setRenderer() this.setRenderer();
} }
setMesh() { setMesh() {
@@ -364,19 +355,19 @@ class CanvAscii {
fontSize: this.textFontSize, fontSize: this.textFontSize,
fontFamily: 'IBM Plex Mono', fontFamily: 'IBM Plex Mono',
color: this.textColor color: this.textColor
}) });
this.textCanvas.resize() this.textCanvas.resize();
this.textCanvas.render() this.textCanvas.render();
this.texture = new THREE.CanvasTexture(this.textCanvas.texture) this.texture = new THREE.CanvasTexture(this.textCanvas.texture);
this.texture.minFilter = THREE.NearestFilter this.texture.minFilter = THREE.NearestFilter;
const textAspect = this.textCanvas.width / this.textCanvas.height const textAspect = this.textCanvas.width / this.textCanvas.height;
const baseH = this.planeBaseHeight const baseH = this.planeBaseHeight;
const planeW = baseH * textAspect const planeW = baseH * textAspect;
const planeH = baseH 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({ this.material = new THREE.ShaderMaterial({
vertexShader, vertexShader,
fragmentShader, fragmentShader,
@@ -387,121 +378,120 @@ class CanvAscii {
uTexture: { value: this.texture }, uTexture: { value: this.texture },
uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 } uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }
} }
}) });
this.mesh = new THREE.Mesh(this.geometry, this.material) this.mesh = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.mesh) this.scene.add(this.mesh);
} }
setRenderer() { setRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true }) this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
this.renderer.setPixelRatio(1) this.renderer.setPixelRatio(1);
this.renderer.setClearColor(0x000000, 0) this.renderer.setClearColor(0x000000, 0);
this.filter = new AsciiFilter(this.renderer, { this.filter = new AsciiFilter(this.renderer, {
fontFamily: 'IBM Plex Mono', fontFamily: 'IBM Plex Mono',
fontSize: this.asciiFontSize, fontSize: this.asciiFontSize,
invert: true invert: true
}) });
this.container.appendChild(this.filter.domElement) this.container.appendChild(this.filter.domElement);
this.setSize(this.width, this.height) this.setSize(this.width, this.height);
this.container.addEventListener('mousemove', this.onMouseMove) this.container.addEventListener('mousemove', this.onMouseMove);
this.container.addEventListener('touchmove', this.onMouseMove) this.container.addEventListener('touchmove', this.onMouseMove);
} }
setSize(w: number, h: number) { setSize(w: number, h: number) {
this.width = w this.width = w;
this.height = h this.height = h;
this.camera.aspect = w / h this.camera.aspect = w / h;
this.camera.updateProjectionMatrix() this.camera.updateProjectionMatrix();
this.filter.setSize(w, h) this.filter.setSize(w, h);
this.center = { x: w / 2, y: h / 2 } this.center = { x: w / 2, y: h / 2 };
} }
load() { load() {
this.animate() this.animate();
} }
onMouseMove(evt: MouseEvent | TouchEvent) { onMouseMove(evt: MouseEvent | TouchEvent) {
const e = 'touches' in evt ? evt.touches[0] : evt const e = 'touches' in evt ? evt.touches[0] : evt;
const bounds = this.container.getBoundingClientRect() const bounds = this.container.getBoundingClientRect();
const x = e.clientX - bounds.left const x = e.clientX - bounds.left;
const y = e.clientY - bounds.top const y = e.clientY - bounds.top;
this.mouse = { x, y } this.mouse = { x, y };
} }
animate() { animate() {
const animateFrame = () => { const animateFrame = () => {
this.animationFrameId = requestAnimationFrame(animateFrame) this.animationFrameId = requestAnimationFrame(animateFrame);
this.render() this.render();
} };
animateFrame() animateFrame();
} }
render() { render() {
const time = new Date().getTime() * 0.001 const time = new Date().getTime() * 0.001;
this.textCanvas.render() this.textCanvas.render();
this.texture.needsUpdate = true 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() { updateRotation() {
// @ts-expect-error - Using custom Math.map function // @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 // @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.x += (x - this.mesh.rotation.x) * 0.05;
this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05 this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;
} }
clear() { clear() {
this.scene.traverse((obj) => { this.scene.traverse(obj => {
if (obj instanceof THREE.Mesh && obj.material && obj.geometry) { if (obj instanceof THREE.Mesh && obj.material && obj.geometry) {
if (Array.isArray(obj.material)) { if (Array.isArray(obj.material)) {
obj.material.forEach((mat) => mat.dispose()) obj.material.forEach(mat => mat.dispose());
} else { } else {
obj.material.dispose() obj.material.dispose();
} }
obj.geometry.dispose() obj.geometry.dispose();
} }
}) });
this.scene.clear() this.scene.clear();
} }
dispose() { dispose() {
cancelAnimationFrame(this.animationFrameId) cancelAnimationFrame(this.animationFrameId);
this.filter.dispose() this.filter.dispose();
this.container.removeChild(this.filter.domElement) this.container.removeChild(this.filter.domElement);
this.container.removeEventListener('mousemove', this.onMouseMove) this.container.removeEventListener('mousemove', this.onMouseMove);
this.container.removeEventListener('touchmove', this.onMouseMove) this.container.removeEventListener('touchmove', this.onMouseMove);
this.clear() this.clear();
this.renderer.dispose() this.renderer.dispose();
} }
} }
const containerRef = ref<HTMLDivElement | null>(null) const containerRef = ref<HTMLDivElement | null>(null);
let asciiRef: CanvAscii | null = null let asciiRef: CanvAscii | null = null;
const initializeAscii = () => { 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) { if (width === 0 || height === 0) {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
if (entry.isIntersecting && entry.boundingClientRect.width > 0 && entry.boundingClientRect.height > 0) { 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( asciiRef = new CanvAscii(
{ {
@@ -515,17 +505,17 @@ const initializeAscii = () => {
containerRef.value!, containerRef.value!,
w, w,
h h
) );
asciiRef.load() asciiRef.load();
observer.disconnect() observer.disconnect();
} }
}, },
{ threshold: 0.1 } { threshold: 0.1 }
) );
observer.observe(containerRef.value) observer.observe(containerRef.value);
return return;
} }
asciiRef = new CanvAscii( asciiRef = new CanvAscii(
@@ -540,49 +530,49 @@ const initializeAscii = () => {
containerRef.value, containerRef.value,
width, width,
height height
) );
asciiRef.load() asciiRef.load();
const ro = new ResizeObserver((entries) => { const ro = new ResizeObserver(entries => {
if (!entries[0] || !asciiRef) return if (!entries[0] || !asciiRef) return;
const { width: w, height: h } = entries[0].contentRect const { width: w, height: h } = entries[0].contentRect;
if (w > 0 && h > 0) { if (w > 0 && h > 0) {
asciiRef.setSize(w, h) asciiRef.setSize(w, h);
}
})
ro.observe(containerRef.value)
} }
});
ro.observe(containerRef.value);
};
onMounted(() => { onMounted(() => {
initializeAscii() initializeAscii();
}) });
onUnmounted(() => { onUnmounted(() => {
if (asciiRef) { if (asciiRef) {
asciiRef.dispose() asciiRef.dispose();
} }
}) });
watch( 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) { if (asciiRef) {
asciiRef.dispose() asciiRef.dispose();
} }
initializeAscii() initializeAscii();
} }
) );
</script> </script>
<template> <template>
<div <div ref="containerRef" :class="['ascii-text-container', className]" class="absolute inset-0 w-full h-full" />
ref="containerRef"
:class="[
'ascii-text-container',
className
]"
class="absolute inset-0 w-full h-full"
/>
</template> </template>
<style scoped> <style scoped>

View File

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

View File

@@ -57,11 +57,7 @@
@update:model-value="forceRerender" @update:model-value="forceRerender"
/> />
<PreviewSwitch <PreviewSwitch title="Enable Waves" v-model="enableWaves" @update:model-value="forceRerender" />
title="Enable Waves"
v-model="enableWaves"
@update:model-value="forceRerender"
/>
<div class="flex gap-4 flex-wrap"> <div class="flex gap-4 flex-wrap">
<button <button
@@ -92,28 +88,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue';
import TabbedLayout from '@/components/common/TabbedLayout.vue' import TabbedLayout from '@/components/common/TabbedLayout.vue';
import RefreshButton from '@/components/common/RefreshButton.vue' import RefreshButton from '@/components/common/RefreshButton.vue';
import PropTable from '@/components/common/PropTable.vue' import PropTable from '@/components/common/PropTable.vue';
import Dependencies from '@/components/code/Dependencies.vue' import Dependencies from '@/components/code/Dependencies.vue';
import CliInstallation from '@/components/code/CliInstallation.vue' import CliInstallation from '@/components/code/CliInstallation.vue';
import CodeExample from '@/components/code/CodeExample.vue' import CodeExample from '@/components/code/CodeExample.vue';
import Customize from '@/components/common/Customize.vue' import Customize from '@/components/common/Customize.vue';
import PreviewSlider from '@/components/common/PreviewSlider.vue' import PreviewSlider from '@/components/common/PreviewSlider.vue';
import PreviewSwitch from '@/components/common/PreviewSwitch.vue' import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
import AsciiText from '@/content/TextAnimations/AsciiText/AsciiText.vue' import AsciiText from '@/content/TextAnimations/AsciiText/AsciiText.vue';
import { asciiText } from '@/constants/code/TextAnimations/asciiTextCode' import { asciiText } from '@/constants/code/TextAnimations/asciiTextCode';
import { useForceRerender } from '@/composables/useForceRerender' import { useForceRerender } from '@/composables/useForceRerender';
const text = ref('Hey!') const text = ref('Hey!');
const asciiFontSize = ref(8) const asciiFontSize = ref(8);
const textFontSize = ref(200) const textFontSize = ref(200);
const textColor = ref('#fdf9f3') const textColor = ref('#fdf9f3');
const planeBaseHeight = ref(8) const planeBaseHeight = ref(8);
const enableWaves = ref(true) const enableWaves = ref(true);
const { rerenderKey, forceRerender } = useForceRerender() const { rerenderKey, forceRerender } = useForceRerender();
// Color options for easy selection // Color options for easy selection
const colorOptions = [ const colorOptions = [
@@ -125,12 +121,12 @@ const colorOptions = [
{ name: 'Purple', value: '#9b59b6' }, { name: 'Purple', value: '#9b59b6' },
{ name: 'Orange', value: '#f39c12' }, { name: 'Orange', value: '#f39c12' },
{ name: 'Pink', value: '#e91e63' } { name: 'Pink', value: '#e91e63' }
] ];
const changeColor = (color: string) => { const changeColor = (color: string) => {
textColor.value = color textColor.value = color;
forceRerender() forceRerender();
} };
// Component properties documentation // Component properties documentation
const propData = [ const propData = [
@@ -176,5 +172,5 @@ const propData = [
default: '""', default: '""',
description: 'Additional CSS classes to apply to the component.' description: 'Additional CSS classes to apply to the component.'
} }
] ];
</script> </script>

View File

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