mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Format
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
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 (!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);
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user