mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-09 00:19:31 -06:00
Add prettier config, format codebase
This commit is contained in:
@@ -3,18 +3,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue'
|
||||
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl'
|
||||
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue';
|
||||
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';
|
||||
|
||||
interface AuroraProps {
|
||||
colorStops?: string[]
|
||||
amplitude?: number
|
||||
blend?: number
|
||||
time?: number
|
||||
speed?: number
|
||||
intensity?: number
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
colorStops?: string[];
|
||||
amplitude?: number;
|
||||
blend?: number;
|
||||
time?: number;
|
||||
speed?: number;
|
||||
intensity?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AuroraProps>(), {
|
||||
@@ -25,16 +25,16 @@ const props = withDefaults(defineProps<AuroraProps>(), {
|
||||
intensity: 1.0,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
|
||||
const VERT = `#version 300 es
|
||||
in vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const FRAG = `#version 300 es
|
||||
precision highp float;
|
||||
@@ -136,56 +136,56 @@ void main() {
|
||||
|
||||
fragColor = vec4(auroraColor * finalAlpha, finalAlpha);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
let animateId = 0
|
||||
let renderer: Renderer | null = null;
|
||||
let animateId = 0;
|
||||
|
||||
const initAurora = () => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
renderer = new Renderer({
|
||||
alpha: true,
|
||||
premultipliedAlpha: true,
|
||||
antialias: true,
|
||||
})
|
||||
antialias: true
|
||||
});
|
||||
|
||||
const gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.canvas.style.backgroundColor = 'transparent'
|
||||
const gl = renderer.gl;
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
gl.canvas.style.backgroundColor = 'transparent';
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let program: Program | undefined
|
||||
let program: Program | undefined;
|
||||
|
||||
const resize = () => {
|
||||
if (!container) return
|
||||
if (!container) return;
|
||||
|
||||
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth
|
||||
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight
|
||||
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth;
|
||||
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight;
|
||||
|
||||
const width = Math.max(parentWidth, 300)
|
||||
const height = Math.max(parentHeight, 300)
|
||||
const width = Math.max(parentWidth, 300);
|
||||
const height = Math.max(parentHeight, 300);
|
||||
|
||||
renderer!.setSize(width, height)
|
||||
renderer!.setSize(width, height);
|
||||
if (program) {
|
||||
program.uniforms.uResolution.value = [width, height]
|
||||
program.uniforms.uResolution.value = [width, height];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
const geometry = new Triangle(gl);
|
||||
if (geometry.attributes.uv) {
|
||||
delete geometry.attributes.uv
|
||||
delete geometry.attributes.uv;
|
||||
}
|
||||
|
||||
const colorStopsArray = props.colorStops.map((hex) => {
|
||||
const c = new Color(hex)
|
||||
return [c.r, c.g, c.b]
|
||||
})
|
||||
const colorStopsArray = props.colorStops.map(hex => {
|
||||
const c = new Color(hex);
|
||||
return [c.r, c.g, c.b];
|
||||
});
|
||||
|
||||
program = new Program(gl, {
|
||||
vertex: VERT,
|
||||
@@ -194,83 +194,88 @@ const initAurora = () => {
|
||||
uTime: { value: 0 },
|
||||
uAmplitude: { value: props.amplitude },
|
||||
uColorStops: { value: colorStopsArray },
|
||||
uResolution: { value: [Math.max(container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth, 300), Math.max(container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight, 300)] },
|
||||
uResolution: {
|
||||
value: [
|
||||
Math.max(container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth, 300),
|
||||
Math.max(container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight, 300)
|
||||
]
|
||||
},
|
||||
uBlend: { value: props.blend },
|
||||
uIntensity: { value: props.intensity },
|
||||
},
|
||||
})
|
||||
uIntensity: { value: props.intensity }
|
||||
}
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program })
|
||||
container.appendChild(gl.canvas)
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
container.appendChild(gl.canvas);
|
||||
|
||||
gl.canvas.style.width = '100%'
|
||||
gl.canvas.style.height = '100%'
|
||||
gl.canvas.style.display = 'block'
|
||||
gl.canvas.style.position = 'absolute'
|
||||
gl.canvas.style.top = '0'
|
||||
gl.canvas.style.left = '0'
|
||||
gl.canvas.style.width = '100%';
|
||||
gl.canvas.style.height = '100%';
|
||||
gl.canvas.style.display = 'block';
|
||||
gl.canvas.style.position = 'absolute';
|
||||
gl.canvas.style.top = '0';
|
||||
gl.canvas.style.left = '0';
|
||||
|
||||
const update = (t: number) => {
|
||||
animateId = requestAnimationFrame(update)
|
||||
const time = props.time ?? t * 0.01
|
||||
const speed = props.speed ?? 1.0
|
||||
animateId = requestAnimationFrame(update);
|
||||
const time = props.time ?? t * 0.01;
|
||||
const speed = props.speed ?? 1.0;
|
||||
if (program) {
|
||||
program.uniforms.uTime.value = time * speed * 0.1
|
||||
program.uniforms.uAmplitude.value = props.amplitude ?? 1.0
|
||||
program.uniforms.uBlend.value = props.blend ?? 0.5
|
||||
program.uniforms.uIntensity.value = props.intensity ?? 1.0
|
||||
const stops = props.colorStops ?? ['#27FF64', '#7cff67', '#27FF64']
|
||||
program.uniforms.uTime.value = time * speed * 0.1;
|
||||
program.uniforms.uAmplitude.value = props.amplitude ?? 1.0;
|
||||
program.uniforms.uBlend.value = props.blend ?? 0.5;
|
||||
program.uniforms.uIntensity.value = props.intensity ?? 1.0;
|
||||
const stops = props.colorStops ?? ['#27FF64', '#7cff67', '#27FF64'];
|
||||
program.uniforms.uColorStops.value = stops.map((hex: string) => {
|
||||
const c = new Color(hex)
|
||||
return [c.r, c.g, c.b]
|
||||
})
|
||||
renderer!.render({ scene: mesh })
|
||||
const c = new Color(hex);
|
||||
return [c.r, c.g, c.b];
|
||||
});
|
||||
renderer!.render({ scene: mesh });
|
||||
}
|
||||
}
|
||||
animateId = requestAnimationFrame(update)
|
||||
};
|
||||
animateId = requestAnimationFrame(update);
|
||||
|
||||
resize()
|
||||
resize();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animateId)
|
||||
window.removeEventListener('resize', resize)
|
||||
cancelAnimationFrame(animateId);
|
||||
window.removeEventListener('resize', resize);
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas)
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animateId) {
|
||||
cancelAnimationFrame(animateId)
|
||||
cancelAnimationFrame(animateId);
|
||||
}
|
||||
if (renderer) {
|
||||
const gl = renderer.gl
|
||||
const container = containerRef.value
|
||||
const gl = renderer.gl;
|
||||
const container = containerRef.value;
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas)
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
renderer = null
|
||||
}
|
||||
renderer = null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initAurora()
|
||||
})
|
||||
initAurora();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.amplitude, props.intensity],
|
||||
() => {
|
||||
cleanup()
|
||||
initAurora()
|
||||
cleanup();
|
||||
initAurora();
|
||||
}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -289,4 +294,4 @@ div {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import * as THREE from 'three'
|
||||
import { degToRad } from 'three/src/math/MathUtils.js'
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
|
||||
interface BeamsProps {
|
||||
beamWidth?: number
|
||||
beamHeight?: number
|
||||
beamNumber?: number
|
||||
lightColor?: string
|
||||
speed?: number
|
||||
noiseIntensity?: number
|
||||
scale?: number
|
||||
rotation?: number
|
||||
beamWidth?: number;
|
||||
beamHeight?: number;
|
||||
beamNumber?: number;
|
||||
lightColor?: string;
|
||||
speed?: number;
|
||||
noiseIntensity?: number;
|
||||
scale?: number;
|
||||
rotation?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BeamsProps>(), {
|
||||
@@ -27,41 +27,41 @@ const props = withDefaults(defineProps<BeamsProps>(), {
|
||||
noiseIntensity: 1.75,
|
||||
scale: 0.2,
|
||||
rotation: 0
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
|
||||
let renderer: THREE.WebGLRenderer | null = null
|
||||
let scene: THREE.Scene | null = null
|
||||
let camera: THREE.PerspectiveCamera | null = null
|
||||
let beamMesh: THREE.Mesh<THREE.BufferGeometry, THREE.ShaderMaterial> | null = null
|
||||
let directionalLight: THREE.DirectionalLight | null = null
|
||||
let ambientLight: THREE.AmbientLight | null = null
|
||||
let animationId: number | null = null
|
||||
let renderer: THREE.WebGLRenderer | null = null;
|
||||
let scene: THREE.Scene | null = null;
|
||||
let camera: THREE.PerspectiveCamera | null = null;
|
||||
let beamMesh: THREE.Mesh<THREE.BufferGeometry, THREE.ShaderMaterial> | null = null;
|
||||
let directionalLight: THREE.DirectionalLight | null = null;
|
||||
let ambientLight: THREE.AmbientLight | null = null;
|
||||
let animationId: number | null = null;
|
||||
|
||||
type UniformValue = THREE.IUniform<unknown> | unknown
|
||||
type UniformValue = THREE.IUniform<unknown> | unknown;
|
||||
|
||||
interface ExtendMaterialConfig {
|
||||
header: string
|
||||
vertexHeader?: string
|
||||
fragmentHeader?: string
|
||||
material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean }
|
||||
uniforms?: Record<string, UniformValue>
|
||||
vertex?: Record<string, string>
|
||||
fragment?: Record<string, string>
|
||||
header: string;
|
||||
vertexHeader?: string;
|
||||
fragmentHeader?: string;
|
||||
material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean };
|
||||
uniforms?: Record<string, UniformValue>;
|
||||
vertex?: Record<string, string>;
|
||||
fragment?: Record<string, string>;
|
||||
}
|
||||
|
||||
type ShaderWithDefines = THREE.ShaderLibShader & {
|
||||
defines?: Record<string, string | number | boolean>
|
||||
}
|
||||
defines?: Record<string, string | number | boolean>;
|
||||
};
|
||||
|
||||
const hexToNormalizedRGB = (hex: string): [number, number, number] => {
|
||||
const clean = hex.replace('#', '')
|
||||
const r = parseInt(clean.substring(0, 2), 16)
|
||||
const g = parseInt(clean.substring(2, 4), 16)
|
||||
const b = parseInt(clean.substring(4, 6), 16)
|
||||
return [r / 255, g / 255, b / 255]
|
||||
}
|
||||
const clean = hex.replace('#', '');
|
||||
const r = parseInt(clean.substring(0, 2), 16);
|
||||
const g = parseInt(clean.substring(2, 4), 16);
|
||||
const b = parseInt(clean.substring(4, 6), 16);
|
||||
return [r / 255, g / 255, b / 255];
|
||||
};
|
||||
|
||||
const noise = `
|
||||
float random (in vec2 st) {
|
||||
@@ -138,53 +138,47 @@ float cnoise(vec3 P){
|
||||
float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x);
|
||||
return 2.2 * n_xyz;
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
function extendMaterial<T extends THREE.Material = THREE.Material>(
|
||||
BaseMaterial: new (params?: THREE.MaterialParameters) => T,
|
||||
cfg: ExtendMaterialConfig
|
||||
): THREE.ShaderMaterial {
|
||||
const physical = THREE.ShaderLib.physical as ShaderWithDefines
|
||||
const {
|
||||
vertexShader: baseVert,
|
||||
fragmentShader: baseFrag,
|
||||
uniforms: baseUniforms,
|
||||
} = physical
|
||||
const baseDefines = physical.defines ?? {}
|
||||
const physical = THREE.ShaderLib.physical as ShaderWithDefines;
|
||||
const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical;
|
||||
const baseDefines = physical.defines ?? {};
|
||||
|
||||
const uniforms: Record<string, THREE.IUniform> =
|
||||
THREE.UniformsUtils.clone(baseUniforms)
|
||||
const uniforms: Record<string, THREE.IUniform> = THREE.UniformsUtils.clone(baseUniforms);
|
||||
|
||||
const defaults = new BaseMaterial(cfg.material || {}) as T & {
|
||||
color?: THREE.Color
|
||||
roughness?: number
|
||||
metalness?: number
|
||||
envMap?: THREE.Texture
|
||||
envMapIntensity?: number
|
||||
}
|
||||
color?: THREE.Color;
|
||||
roughness?: number;
|
||||
metalness?: number;
|
||||
envMap?: THREE.Texture;
|
||||
envMapIntensity?: number;
|
||||
};
|
||||
|
||||
if (defaults.color) uniforms.diffuse.value = defaults.color
|
||||
if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness
|
||||
if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness
|
||||
if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap
|
||||
if ('envMapIntensity' in defaults)
|
||||
uniforms.envMapIntensity.value = defaults.envMapIntensity
|
||||
if (defaults.color) uniforms.diffuse.value = defaults.color;
|
||||
if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness;
|
||||
if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness;
|
||||
if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap;
|
||||
if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity;
|
||||
|
||||
Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => {
|
||||
uniforms[key] =
|
||||
u !== null && typeof u === 'object' && 'value' in u
|
||||
? (u as THREE.IUniform<unknown>)
|
||||
: ({ value: u } as THREE.IUniform<unknown>)
|
||||
})
|
||||
: ({ value: u } as THREE.IUniform<unknown>);
|
||||
});
|
||||
|
||||
let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`
|
||||
let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`
|
||||
let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`;
|
||||
let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`;
|
||||
|
||||
for (const [inc, code] of Object.entries(cfg.vertex ?? {})) {
|
||||
vert = vert.replace(inc, `${inc}\n${code}`)
|
||||
vert = vert.replace(inc, `${inc}\n${code}`);
|
||||
}
|
||||
for (const [inc, code] of Object.entries(cfg.fragment ?? {})) {
|
||||
frag = frag.replace(inc, `${inc}\n${code}`)
|
||||
frag = frag.replace(inc, `${inc}\n${code}`);
|
||||
}
|
||||
|
||||
const mat = new THREE.ShaderMaterial({
|
||||
@@ -193,10 +187,10 @@ function extendMaterial<T extends THREE.Material = THREE.Material>(
|
||||
vertexShader: vert,
|
||||
fragmentShader: frag,
|
||||
lights: true,
|
||||
fog: !!cfg.material?.fog,
|
||||
})
|
||||
fog: !!cfg.material?.fog
|
||||
});
|
||||
|
||||
return mat
|
||||
return mat;
|
||||
}
|
||||
|
||||
function createStackedPlanesBufferGeometry(
|
||||
@@ -206,54 +200,51 @@ function createStackedPlanesBufferGeometry(
|
||||
spacing: number,
|
||||
heightSegments: number
|
||||
): THREE.BufferGeometry {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
const numVertices = n * (heightSegments + 1) * 2
|
||||
const numFaces = n * heightSegments * 2
|
||||
const positions = new Float32Array(numVertices * 3)
|
||||
const indices = new Uint32Array(numFaces * 3)
|
||||
const uvs = new Float32Array(numVertices * 2)
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
const numVertices = n * (heightSegments + 1) * 2;
|
||||
const numFaces = n * heightSegments * 2;
|
||||
const positions = new Float32Array(numVertices * 3);
|
||||
const indices = new Uint32Array(numFaces * 3);
|
||||
const uvs = new Float32Array(numVertices * 2);
|
||||
|
||||
let vertexOffset = 0
|
||||
let indexOffset = 0
|
||||
let uvOffset = 0
|
||||
const totalWidth = n * width + (n - 1) * spacing
|
||||
const xOffsetBase = -totalWidth / 2
|
||||
let vertexOffset = 0;
|
||||
let indexOffset = 0;
|
||||
let uvOffset = 0;
|
||||
const totalWidth = n * width + (n - 1) * spacing;
|
||||
const xOffsetBase = -totalWidth / 2;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const xOffset = xOffsetBase + i * (width + spacing)
|
||||
const uvXOffset = Math.random() * 300
|
||||
const uvYOffset = Math.random() * 300
|
||||
const xOffset = xOffsetBase + i * (width + spacing);
|
||||
const uvXOffset = Math.random() * 300;
|
||||
const uvYOffset = Math.random() * 300;
|
||||
|
||||
for (let j = 0; j <= heightSegments; j++) {
|
||||
const y = height * (j / heightSegments - 0.5)
|
||||
const v0 = [xOffset, y, 0]
|
||||
const v1 = [xOffset + width, y, 0]
|
||||
positions.set([...v0, ...v1], vertexOffset * 3)
|
||||
const y = height * (j / heightSegments - 0.5);
|
||||
const v0 = [xOffset, y, 0];
|
||||
const v1 = [xOffset + width, y, 0];
|
||||
positions.set([...v0, ...v1], vertexOffset * 3);
|
||||
|
||||
const uvY = j / heightSegments
|
||||
uvs.set(
|
||||
[uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset],
|
||||
uvOffset
|
||||
)
|
||||
const uvY = j / heightSegments;
|
||||
uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset);
|
||||
|
||||
if (j < heightSegments) {
|
||||
const a = vertexOffset,
|
||||
b = vertexOffset + 1,
|
||||
c = vertexOffset + 2,
|
||||
d = vertexOffset + 3
|
||||
indices.set([a, b, c, c, b, d], indexOffset)
|
||||
indexOffset += 6
|
||||
d = vertexOffset + 3;
|
||||
indices.set([a, b, c, c, b, d], indexOffset);
|
||||
indexOffset += 6;
|
||||
}
|
||||
vertexOffset += 2
|
||||
uvOffset += 4
|
||||
vertexOffset += 2;
|
||||
uvOffset += 4;
|
||||
}
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
|
||||
geometry.setIndex(new THREE.BufferAttribute(indices, 1))
|
||||
geometry.computeVertexNormals()
|
||||
return geometry
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
|
||||
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
||||
geometry.computeVertexNormals();
|
||||
return geometry;
|
||||
}
|
||||
|
||||
const beamMaterial = computed(() =>
|
||||
@@ -290,12 +281,12 @@ const beamMaterial = computed(() =>
|
||||
fragmentHeader: '',
|
||||
vertex: {
|
||||
'#include <begin_vertex>': `transformed.z += getPos(transformed.xyz);`,
|
||||
'#include <beginnormal_vertex>': `objectNormal = getNormal(position.xyz);`,
|
||||
'#include <beginnormal_vertex>': `objectNormal = getNormal(position.xyz);`
|
||||
},
|
||||
fragment: {
|
||||
'#include <dithering_fragment>': `
|
||||
float randomNoise = noise(gl_FragCoord.xy);
|
||||
gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`,
|
||||
gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;`
|
||||
},
|
||||
material: { fog: true },
|
||||
uniforms: {
|
||||
@@ -306,127 +297,120 @@ const beamMaterial = computed(() =>
|
||||
uSpeed: { shared: true, mixed: true, linked: true, value: props.speed },
|
||||
envMapIntensity: 10,
|
||||
uNoiseIntensity: props.noiseIntensity,
|
||||
uScale: props.scale,
|
||||
},
|
||||
uScale: props.scale
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const initThreeJS = () => {
|
||||
if (!containerRef.value) return
|
||||
if (!containerRef.value) return;
|
||||
|
||||
cleanup()
|
||||
cleanup();
|
||||
|
||||
const container = containerRef.value;
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setClearColor(0x000000, 1);
|
||||
|
||||
scene = new THREE.Scene();
|
||||
|
||||
camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000);
|
||||
camera.position.set(0, 0, 20);
|
||||
|
||||
const geometry = createStackedPlanesBufferGeometry(props.beamNumber, props.beamWidth, props.beamHeight, 0, 100);
|
||||
|
||||
const material = beamMaterial.value;
|
||||
beamMesh = new THREE.Mesh(geometry, material);
|
||||
|
||||
const group = new THREE.Group();
|
||||
group.rotation.z = degToRad(props.rotation);
|
||||
group.add(beamMesh);
|
||||
scene.add(group);
|
||||
|
||||
directionalLight = new THREE.DirectionalLight(new THREE.Color(props.lightColor), 1);
|
||||
directionalLight.position.set(0, 3, 10);
|
||||
const shadowCamera = directionalLight.shadow.camera as THREE.OrthographicCamera;
|
||||
shadowCamera.top = 24;
|
||||
shadowCamera.bottom = -24;
|
||||
shadowCamera.left = -24;
|
||||
shadowCamera.right = 24;
|
||||
shadowCamera.far = 64;
|
||||
directionalLight.shadow.bias = -0.004;
|
||||
scene.add(directionalLight);
|
||||
|
||||
ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||
scene.add(ambientLight);
|
||||
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
const container = containerRef.value
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.setClearColor(0x000000, 1)
|
||||
|
||||
scene = new THREE.Scene()
|
||||
|
||||
camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000)
|
||||
camera.position.set(0, 0, 20)
|
||||
|
||||
const geometry = createStackedPlanesBufferGeometry(
|
||||
props.beamNumber,
|
||||
props.beamWidth,
|
||||
props.beamHeight,
|
||||
0,
|
||||
100
|
||||
)
|
||||
|
||||
const material = beamMaterial.value
|
||||
beamMesh = new THREE.Mesh(geometry, material)
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.rotation.z = degToRad(props.rotation)
|
||||
group.add(beamMesh)
|
||||
scene.add(group)
|
||||
|
||||
directionalLight = new THREE.DirectionalLight(new THREE.Color(props.lightColor), 1)
|
||||
directionalLight.position.set(0, 3, 10)
|
||||
const shadowCamera = directionalLight.shadow.camera as THREE.OrthographicCamera
|
||||
shadowCamera.top = 24
|
||||
shadowCamera.bottom = -24
|
||||
shadowCamera.left = -24
|
||||
shadowCamera.right = 24
|
||||
shadowCamera.far = 64
|
||||
directionalLight.shadow.bias = -0.004
|
||||
scene.add(directionalLight)
|
||||
|
||||
ambientLight = new THREE.AmbientLight(0xffffff, 1)
|
||||
scene.add(ambientLight)
|
||||
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
const resize = () => {
|
||||
if (!container || !renderer || !camera) return
|
||||
|
||||
const width = container.offsetWidth
|
||||
const height = container.offsetHeight
|
||||
|
||||
renderer.setSize(width, height)
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(resize)
|
||||
resizeObserver.observe(container)
|
||||
|
||||
resize()
|
||||
|
||||
if (!container || !renderer || !camera) return;
|
||||
|
||||
const width = container.offsetWidth;
|
||||
const height = container.offsetHeight;
|
||||
|
||||
renderer.setSize(width, height);
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(resize);
|
||||
resizeObserver.observe(container);
|
||||
|
||||
resize();
|
||||
|
||||
const animate = () => {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
|
||||
if (beamMesh && beamMesh.material) {
|
||||
beamMesh.material.uniforms.time.value += 0.1 * 0.016
|
||||
beamMesh.material.uniforms.time.value += 0.1 * 0.016;
|
||||
}
|
||||
|
||||
|
||||
if (renderer && scene && camera) {
|
||||
renderer.render(scene, camera)
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(animate)
|
||||
|
||||
;(container as HTMLDivElement & { _resizeObserver?: ResizeObserver })._resizeObserver = resizeObserver
|
||||
}
|
||||
};
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
(container as HTMLDivElement & { _resizeObserver?: ResizeObserver })._resizeObserver = resizeObserver;
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
|
||||
|
||||
if (containerRef.value) {
|
||||
const container = containerRef.value as HTMLDivElement & { _resizeObserver?: ResizeObserver }
|
||||
|
||||
const container = containerRef.value as HTMLDivElement & { _resizeObserver?: ResizeObserver };
|
||||
|
||||
if (container._resizeObserver) {
|
||||
container._resizeObserver.disconnect()
|
||||
delete container._resizeObserver
|
||||
container._resizeObserver.disconnect();
|
||||
delete container._resizeObserver;
|
||||
}
|
||||
|
||||
|
||||
if (renderer && renderer.domElement.parentNode === container) {
|
||||
container.removeChild(renderer.domElement)
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (beamMesh) {
|
||||
if (beamMesh.geometry) beamMesh.geometry.dispose()
|
||||
if (beamMesh.material) beamMesh.material.dispose()
|
||||
beamMesh = null
|
||||
if (beamMesh.geometry) beamMesh.geometry.dispose();
|
||||
if (beamMesh.material) beamMesh.material.dispose();
|
||||
beamMesh = null;
|
||||
}
|
||||
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose()
|
||||
renderer = null
|
||||
renderer.dispose();
|
||||
renderer = null;
|
||||
}
|
||||
|
||||
scene = null
|
||||
camera = null
|
||||
directionalLight = null
|
||||
ambientLight = null
|
||||
}
|
||||
|
||||
scene = null;
|
||||
camera = null;
|
||||
directionalLight = null;
|
||||
ambientLight = null;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [
|
||||
@@ -437,19 +421,19 @@ watch(
|
||||
props.speed,
|
||||
props.noiseIntensity,
|
||||
props.scale,
|
||||
props.rotation,
|
||||
props.rotation
|
||||
],
|
||||
() => {
|
||||
initThreeJS()
|
||||
initThreeJS();
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initThreeJS()
|
||||
})
|
||||
initThreeJS();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,45 +7,45 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { InertiaPlugin } from 'gsap/InertiaPlugin'
|
||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
import { InertiaPlugin } from 'gsap/InertiaPlugin';
|
||||
|
||||
gsap.registerPlugin(InertiaPlugin)
|
||||
gsap.registerPlugin(InertiaPlugin);
|
||||
|
||||
const throttle = <T extends unknown[]>(func: (...args: T) => void, limit: number) => {
|
||||
let lastCall = 0
|
||||
let lastCall = 0;
|
||||
return function (this: unknown, ...args: T) {
|
||||
const now = performance.now()
|
||||
const now = performance.now();
|
||||
if (now - lastCall >= limit) {
|
||||
lastCall = now
|
||||
func.apply(this, args)
|
||||
lastCall = now;
|
||||
func.apply(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
interface Dot {
|
||||
cx: number
|
||||
cy: number
|
||||
xOffset: number
|
||||
yOffset: number
|
||||
_inertiaApplied: boolean
|
||||
cx: number;
|
||||
cy: number;
|
||||
xOffset: number;
|
||||
yOffset: number;
|
||||
_inertiaApplied: boolean;
|
||||
}
|
||||
|
||||
export interface DotGridProps {
|
||||
dotSize?: number
|
||||
gap?: number
|
||||
baseColor?: string
|
||||
activeColor?: string
|
||||
proximity?: number
|
||||
speedTrigger?: number
|
||||
shockRadius?: number
|
||||
shockStrength?: number
|
||||
maxSpeed?: number
|
||||
resistance?: number
|
||||
returnDuration?: number
|
||||
className?: string
|
||||
style?: Record<string, string | number>
|
||||
dotSize?: number;
|
||||
gap?: number;
|
||||
baseColor?: string;
|
||||
activeColor?: string;
|
||||
proximity?: number;
|
||||
speedTrigger?: number;
|
||||
shockRadius?: number;
|
||||
shockStrength?: number;
|
||||
maxSpeed?: number;
|
||||
resistance?: number;
|
||||
returnDuration?: number;
|
||||
className?: string;
|
||||
style?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DotGridProps>(), {
|
||||
@@ -62,11 +62,11 @@ const props = withDefaults(defineProps<DotGridProps>(), {
|
||||
returnDuration: 1.5,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
})
|
||||
});
|
||||
|
||||
const wrapperRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const dots = ref<Dot[]>([])
|
||||
const wrapperRef = ref<HTMLDivElement>();
|
||||
const canvasRef = ref<HTMLCanvasElement>();
|
||||
const dots = ref<Dot[]>([]);
|
||||
const pointer = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -75,146 +75,146 @@ const pointer = ref({
|
||||
speed: 0,
|
||||
lastTime: 0,
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
})
|
||||
lastY: 0
|
||||
});
|
||||
|
||||
function hexToRgb(hex: string) {
|
||||
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
|
||||
if (!m) return { r: 0, g: 0, b: 0 }
|
||||
const m = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
|
||||
if (!m) return { r: 0, g: 0, b: 0 };
|
||||
return {
|
||||
r: parseInt(m[1], 16),
|
||||
g: parseInt(m[2], 16),
|
||||
b: parseInt(m[3], 16),
|
||||
}
|
||||
b: parseInt(m[3], 16)
|
||||
};
|
||||
}
|
||||
|
||||
const baseRgb = computed(() => hexToRgb(props.baseColor))
|
||||
const activeRgb = computed(() => hexToRgb(props.activeColor))
|
||||
const baseRgb = computed(() => hexToRgb(props.baseColor));
|
||||
const activeRgb = computed(() => hexToRgb(props.activeColor));
|
||||
|
||||
const circlePath = computed(() => {
|
||||
if (typeof window === 'undefined' || !window.Path2D) return null
|
||||
if (typeof window === 'undefined' || !window.Path2D) return null;
|
||||
|
||||
const p = new Path2D()
|
||||
p.arc(0, 0, props.dotSize / 2, 0, Math.PI * 2)
|
||||
return p
|
||||
})
|
||||
const p = new Path2D();
|
||||
p.arc(0, 0, props.dotSize / 2, 0, Math.PI * 2);
|
||||
return p;
|
||||
});
|
||||
|
||||
const buildGrid = () => {
|
||||
const wrap = wrapperRef.value
|
||||
const canvas = canvasRef.value
|
||||
if (!wrap || !canvas) return
|
||||
const wrap = wrapperRef.value;
|
||||
const canvas = canvasRef.value;
|
||||
if (!wrap || !canvas) return;
|
||||
|
||||
const { width, height } = wrap.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const { width, height } = wrap.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) ctx.scale(dpr, dpr)
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) ctx.scale(dpr, dpr);
|
||||
|
||||
const cols = Math.floor((width + props.gap) / (props.dotSize + props.gap))
|
||||
const rows = Math.floor((height + props.gap) / (props.dotSize + props.gap))
|
||||
const cell = props.dotSize + props.gap
|
||||
const cols = Math.floor((width + props.gap) / (props.dotSize + props.gap));
|
||||
const rows = Math.floor((height + props.gap) / (props.dotSize + props.gap));
|
||||
const cell = props.dotSize + props.gap;
|
||||
|
||||
const gridW = cell * cols - props.gap
|
||||
const gridH = cell * rows - props.gap
|
||||
const gridW = cell * cols - props.gap;
|
||||
const gridH = cell * rows - props.gap;
|
||||
|
||||
const extraX = width - gridW
|
||||
const extraY = height - gridH
|
||||
const extraX = width - gridW;
|
||||
const extraY = height - gridH;
|
||||
|
||||
const startX = extraX / 2 + props.dotSize / 2
|
||||
const startY = extraY / 2 + props.dotSize / 2
|
||||
const startX = extraX / 2 + props.dotSize / 2;
|
||||
const startY = extraY / 2 + props.dotSize / 2;
|
||||
|
||||
const newDots: Dot[] = []
|
||||
const newDots: Dot[] = [];
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const cx = startX + x * cell
|
||||
const cy = startY + y * cell
|
||||
newDots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false })
|
||||
const cx = startX + x * cell;
|
||||
const cy = startY + y * cell;
|
||||
newDots.push({ cx, cy, xOffset: 0, yOffset: 0, _inertiaApplied: false });
|
||||
}
|
||||
}
|
||||
dots.value = newDots
|
||||
}
|
||||
dots.value = newDots;
|
||||
};
|
||||
|
||||
let rafId: number
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let rafId: number;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const draw = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const { x: px, y: py } = pointer.value
|
||||
const proxSq = props.proximity * props.proximity
|
||||
const { x: px, y: py } = pointer.value;
|
||||
const proxSq = props.proximity * props.proximity;
|
||||
|
||||
for (const dot of dots.value) {
|
||||
const ox = dot.cx + dot.xOffset
|
||||
const oy = dot.cy + dot.yOffset
|
||||
const dx = dot.cx - px
|
||||
const dy = dot.cy - py
|
||||
const dsq = dx * dx + dy * dy
|
||||
const ox = dot.cx + dot.xOffset;
|
||||
const oy = dot.cy + dot.yOffset;
|
||||
const dx = dot.cx - px;
|
||||
const dy = dot.cy - py;
|
||||
const dsq = dx * dx + dy * dy;
|
||||
|
||||
let style = props.baseColor
|
||||
let style = props.baseColor;
|
||||
if (dsq <= proxSq) {
|
||||
const dist = Math.sqrt(dsq)
|
||||
const t = 1 - dist / props.proximity
|
||||
const r = Math.round(baseRgb.value.r + (activeRgb.value.r - baseRgb.value.r) * t)
|
||||
const g = Math.round(baseRgb.value.g + (activeRgb.value.g - baseRgb.value.g) * t)
|
||||
const b = Math.round(baseRgb.value.b + (activeRgb.value.b - baseRgb.value.b) * t)
|
||||
style = `rgb(${r},${g},${b})`
|
||||
const dist = Math.sqrt(dsq);
|
||||
const t = 1 - dist / props.proximity;
|
||||
const r = Math.round(baseRgb.value.r + (activeRgb.value.r - baseRgb.value.r) * t);
|
||||
const g = Math.round(baseRgb.value.g + (activeRgb.value.g - baseRgb.value.g) * t);
|
||||
const b = Math.round(baseRgb.value.b + (activeRgb.value.b - baseRgb.value.b) * t);
|
||||
style = `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
if (circlePath.value) {
|
||||
ctx.save()
|
||||
ctx.translate(ox, oy)
|
||||
ctx.fillStyle = style
|
||||
ctx.fill(circlePath.value)
|
||||
ctx.restore()
|
||||
ctx.save();
|
||||
ctx.translate(ox, oy);
|
||||
ctx.fillStyle = style;
|
||||
ctx.fill(circlePath.value);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(draw)
|
||||
}
|
||||
rafId = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const now = performance.now()
|
||||
const pr = pointer.value
|
||||
const dt = pr.lastTime ? now - pr.lastTime : 16
|
||||
const dx = e.clientX - pr.lastX
|
||||
const dy = e.clientY - pr.lastY
|
||||
let vx = (dx / dt) * 1000
|
||||
let vy = (dy / dt) * 1000
|
||||
let speed = Math.hypot(vx, vy)
|
||||
const now = performance.now();
|
||||
const pr = pointer.value;
|
||||
const dt = pr.lastTime ? now - pr.lastTime : 16;
|
||||
const dx = e.clientX - pr.lastX;
|
||||
const dy = e.clientY - pr.lastY;
|
||||
let vx = (dx / dt) * 1000;
|
||||
let vy = (dy / dt) * 1000;
|
||||
let speed = Math.hypot(vx, vy);
|
||||
if (speed > props.maxSpeed) {
|
||||
const scale = props.maxSpeed / speed
|
||||
vx *= scale
|
||||
vy *= scale
|
||||
speed = props.maxSpeed
|
||||
const scale = props.maxSpeed / speed;
|
||||
vx *= scale;
|
||||
vy *= scale;
|
||||
speed = props.maxSpeed;
|
||||
}
|
||||
pr.lastTime = now
|
||||
pr.lastX = e.clientX
|
||||
pr.lastY = e.clientY
|
||||
pr.vx = vx
|
||||
pr.vy = vy
|
||||
pr.speed = speed
|
||||
pr.lastTime = now;
|
||||
pr.lastX = e.clientX;
|
||||
pr.lastY = e.clientY;
|
||||
pr.vx = vx;
|
||||
pr.vy = vy;
|
||||
pr.speed = speed;
|
||||
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
pr.x = e.clientX - rect.left
|
||||
pr.y = e.clientY - rect.top
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pr.x = e.clientX - rect.left;
|
||||
pr.y = e.clientY - rect.top;
|
||||
|
||||
for (const dot of dots.value) {
|
||||
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y)
|
||||
const dist = Math.hypot(dot.cx - pr.x, dot.cy - pr.y);
|
||||
if (speed > props.speedTrigger && dist < props.proximity && !dot._inertiaApplied) {
|
||||
dot._inertiaApplied = true
|
||||
gsap.killTweensOf(dot)
|
||||
const pushX = dot.cx - pr.x + vx * 0.005
|
||||
const pushY = dot.cy - pr.y + vy * 0.005
|
||||
dot._inertiaApplied = true;
|
||||
gsap.killTweensOf(dot);
|
||||
const pushX = dot.cx - pr.x + vx * 0.005;
|
||||
const pushY = dot.cy - pr.y + vy * 0.005;
|
||||
gsap.to(dot, {
|
||||
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||
onComplete: () => {
|
||||
@@ -222,29 +222,29 @@ const onMove = (e: MouseEvent) => {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
duration: props.returnDuration,
|
||||
ease: 'elastic.out(1,0.75)',
|
||||
})
|
||||
dot._inertiaApplied = false
|
||||
},
|
||||
})
|
||||
ease: 'elastic.out(1,0.75)'
|
||||
});
|
||||
dot._inertiaApplied = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onClick = (e: MouseEvent) => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const cx = e.clientX - rect.left
|
||||
const cy = e.clientY - rect.top
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const cx = e.clientX - rect.left;
|
||||
const cy = e.clientY - rect.top;
|
||||
for (const dot of dots.value) {
|
||||
const dist = Math.hypot(dot.cx - cx, dot.cy - cy)
|
||||
const dist = Math.hypot(dot.cx - cx, dot.cy - cy);
|
||||
if (dist < props.shockRadius && !dot._inertiaApplied) {
|
||||
dot._inertiaApplied = true
|
||||
gsap.killTweensOf(dot)
|
||||
const falloff = Math.max(0, 1 - dist / props.shockRadius)
|
||||
const pushX = (dot.cx - cx) * props.shockStrength * falloff
|
||||
const pushY = (dot.cy - cy) * props.shockStrength * falloff
|
||||
dot._inertiaApplied = true;
|
||||
gsap.killTweensOf(dot);
|
||||
const falloff = Math.max(0, 1 - dist / props.shockRadius);
|
||||
const pushX = (dot.cx - cx) * props.shockStrength * falloff;
|
||||
const pushY = (dot.cy - cy) * props.shockStrength * falloff;
|
||||
gsap.to(dot, {
|
||||
inertia: { xOffset: pushX, yOffset: pushY, resistance: props.resistance },
|
||||
onComplete: () => {
|
||||
@@ -252,64 +252,64 @@ const onClick = (e: MouseEvent) => {
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
duration: props.returnDuration,
|
||||
ease: 'elastic.out(1,0.75)',
|
||||
})
|
||||
dot._inertiaApplied = false
|
||||
},
|
||||
})
|
||||
ease: 'elastic.out(1,0.75)'
|
||||
});
|
||||
dot._inertiaApplied = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const throttledMove = throttle(onMove, 50)
|
||||
const throttledMove = throttle(onMove, 50);
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
await nextTick();
|
||||
|
||||
buildGrid()
|
||||
buildGrid();
|
||||
|
||||
if (circlePath.value) {
|
||||
draw()
|
||||
draw();
|
||||
}
|
||||
|
||||
if ('ResizeObserver' in window) {
|
||||
resizeObserver = new ResizeObserver(buildGrid)
|
||||
resizeObserver = new ResizeObserver(buildGrid);
|
||||
if (wrapperRef.value) {
|
||||
resizeObserver.observe(wrapperRef.value)
|
||||
resizeObserver.observe(wrapperRef.value);
|
||||
}
|
||||
} else {
|
||||
(window as Window).addEventListener('resize', buildGrid)
|
||||
(window as Window).addEventListener('resize', buildGrid);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', throttledMove, { passive: true })
|
||||
window.addEventListener('click', onClick)
|
||||
})
|
||||
window.addEventListener('mousemove', throttledMove, { passive: true });
|
||||
window.addEventListener('click', onClick);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver.disconnect();
|
||||
} else {
|
||||
window.removeEventListener('resize', buildGrid)
|
||||
window.removeEventListener('resize', buildGrid);
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', throttledMove)
|
||||
window.removeEventListener('click', onClick)
|
||||
})
|
||||
window.removeEventListener('mousemove', throttledMove);
|
||||
window.removeEventListener('click', onClick);
|
||||
});
|
||||
|
||||
watch([() => props.dotSize, () => props.gap], () => {
|
||||
buildGrid()
|
||||
})
|
||||
buildGrid();
|
||||
});
|
||||
|
||||
watch([() => props.proximity, () => props.baseColor, activeRgb, baseRgb, circlePath], () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
if (circlePath.value) {
|
||||
draw()
|
||||
draw();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
|
||||
import { gsap } from "gsap";
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
interface GridMotionProps {
|
||||
items?: string[];
|
||||
@@ -9,7 +9,7 @@ interface GridMotionProps {
|
||||
|
||||
const props = withDefaults(defineProps<GridMotionProps>(), {
|
||||
items: () => [],
|
||||
gradientColor: "black",
|
||||
gradientColor: 'black'
|
||||
});
|
||||
|
||||
const gridRef = ref<HTMLElement | null>(null);
|
||||
@@ -18,16 +18,14 @@ const mouseX = ref(window.innerWidth / 2);
|
||||
|
||||
const totalItems = 28;
|
||||
const defaultItems = Array.from({ length: totalItems }, (_, i) => `Item ${i + 1}`);
|
||||
const combinedItems = computed(() =>
|
||||
props.items.length > 0 ? props.items.slice(0, totalItems) : defaultItems,
|
||||
);
|
||||
const combinedItems = computed(() => (props.items.length > 0 ? props.items.slice(0, totalItems) : defaultItems));
|
||||
|
||||
function isImage(item: string) {
|
||||
return typeof item === "string" && item.startsWith("http");
|
||||
return typeof item === 'string' && item.startsWith('http');
|
||||
}
|
||||
|
||||
function isTag(item: string) {
|
||||
return typeof item === "string" && item.startsWith("<") && item.endsWith(">");
|
||||
return typeof item === 'string' && item.startsWith('<') && item.endsWith('>');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -44,23 +42,22 @@ onMounted(() => {
|
||||
|
||||
rowRefs.value.forEach((row, index) => {
|
||||
const direction = index % 2 === 0 ? 1 : -1;
|
||||
const moveAmount =
|
||||
((mouseX.value / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;
|
||||
const moveAmount = ((mouseX.value / window.innerWidth) * maxMoveAmount - maxMoveAmount / 2) * direction;
|
||||
|
||||
gsap.to(row, {
|
||||
x: moveAmount,
|
||||
duration: baseDuration + inertiaFactors[index % inertiaFactors.length],
|
||||
ease: "power3.out",
|
||||
overwrite: "auto",
|
||||
ease: 'power3.out',
|
||||
overwrite: 'auto'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const removeAnimation = gsap.ticker.add(updateMotion);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
removeAnimation();
|
||||
});
|
||||
});
|
||||
@@ -68,24 +65,42 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div ref="gridRef" class="w-full h-full overflow-hidden">
|
||||
<section class="relative flex justify-center items-center w-full h-screen overflow-hidden" :style="{
|
||||
background: `radial-gradient(circle, ${gradientColor} 0%, transparent 100%)`,
|
||||
}">
|
||||
<section
|
||||
class="relative flex justify-center items-center w-full h-screen overflow-hidden"
|
||||
:style="{
|
||||
background: `radial-gradient(circle, ${gradientColor} 0%, transparent 100%)`
|
||||
}"
|
||||
>
|
||||
<div class="z-[4] absolute inset-0 bg-[length:250px] pointer-events-none"></div>
|
||||
|
||||
<div
|
||||
class="z-[2] relative flex-none gap-4 grid grid-cols-1 grid-rows-4 w-[150vw] h-[150vh] rotate-[-15deg] origin-center">
|
||||
<div v-for="rowIndex in 4" :key="rowIndex" class="gap-4 grid grid-cols-7"
|
||||
:style="{ willChange: 'transform, filter' }" ref="rowRefs">
|
||||
class="z-[2] relative flex-none gap-4 grid grid-cols-1 grid-rows-4 w-[150vw] h-[150vh] rotate-[-15deg] origin-center"
|
||||
>
|
||||
<div
|
||||
v-for="rowIndex in 4"
|
||||
:key="rowIndex"
|
||||
class="gap-4 grid grid-cols-7"
|
||||
:style="{ willChange: 'transform, filter' }"
|
||||
ref="rowRefs"
|
||||
>
|
||||
<div v-for="itemIndex in 7" :key="itemIndex" class="relative">
|
||||
<div
|
||||
class="relative flex justify-center items-center bg-[#111] rounded-[10px] w-full h-full overflow-hidden text-[1.5rem] text-white">
|
||||
<div v-if="isImage(combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)])"
|
||||
class="top-0 left-0 absolute bg-cover bg-center w-full h-full" :style="{
|
||||
backgroundImage: `url(${combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)]})`,
|
||||
}"></div>
|
||||
<div v-else-if="isTag(combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)])" class="z-[2] p-4 text-center"
|
||||
v-html="combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)]"></div>
|
||||
class="relative flex justify-center items-center bg-[#111] rounded-[10px] w-full h-full overflow-hidden text-[1.5rem] text-white"
|
||||
>
|
||||
<div
|
||||
v-if="isImage(combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)])"
|
||||
class="top-0 left-0 absolute bg-cover bg-center w-full h-full"
|
||||
:style="{
|
||||
backgroundImage: `url(${combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)]})`
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-else-if="isTag(combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)])"
|
||||
class="z-[2] p-4 text-center"
|
||||
v-html="combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)]"
|
||||
></div>
|
||||
|
||||
<div v-else class="z-[1] p-4 text-center">
|
||||
{{ combinedItems[(rowIndex - 1) * 7 + (itemIndex - 1)] }}
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl'
|
||||
import type { OGLRenderingContext } from 'ogl'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';
|
||||
import type { OGLRenderingContext } from 'ogl';
|
||||
|
||||
interface Props {
|
||||
color?: [number, number, number]
|
||||
speed?: number
|
||||
amplitude?: number
|
||||
mouseReact?: boolean
|
||||
color?: [number, number, number];
|
||||
speed?: number;
|
||||
amplitude?: number;
|
||||
mouseReact?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -19,16 +19,16 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
speed: 1.0,
|
||||
amplitude: 0.1,
|
||||
mouseReact: true
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const mousePos = ref({ x: 0.5, y: 0.5 })
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
const mousePos = ref({ x: 0.5, y: 0.5 });
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
let gl: OGLRenderingContext | null = null
|
||||
let program: Program | null = null
|
||||
let mesh: Mesh | null = null
|
||||
let animationId: number | null = null
|
||||
let renderer: Renderer | null = null;
|
||||
let gl: OGLRenderingContext | null = null;
|
||||
let program: Program | null = null;
|
||||
let mesh: Mesh | null = null;
|
||||
let animationId: number | null = null;
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 uv;
|
||||
@@ -40,7 +40,7 @@ void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0, 1);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
@@ -71,57 +71,57 @@ void main() {
|
||||
col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5) * uColor;
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const resize = () => {
|
||||
if (!containerRef.value || !renderer || !program || !gl) return
|
||||
if (!containerRef.value || !renderer || !program || !gl) return;
|
||||
|
||||
const container = containerRef.value
|
||||
const scale = 1
|
||||
renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale)
|
||||
const container = containerRef.value;
|
||||
const scale = 1;
|
||||
renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale);
|
||||
|
||||
if (program) {
|
||||
program.uniforms.uResolution.value = new Color(
|
||||
gl.canvas.width,
|
||||
gl.canvas.height,
|
||||
gl.canvas.width / gl.canvas.height
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef.value || !program) return
|
||||
if (!containerRef.value || !program) return;
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = 1.0 - (e.clientY - rect.top) / rect.height
|
||||
const rect = containerRef.value.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = 1.0 - (e.clientY - rect.top) / rect.height;
|
||||
|
||||
mousePos.value = { x, y }
|
||||
mousePos.value = { x, y };
|
||||
if (program.uniforms.uMouse.value) {
|
||||
program.uniforms.uMouse.value[0] = x
|
||||
program.uniforms.uMouse.value[1] = y
|
||||
program.uniforms.uMouse.value[0] = x;
|
||||
program.uniforms.uMouse.value[1] = y;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const update = (t: number) => {
|
||||
if (!program || !renderer || !mesh) return
|
||||
if (!program || !renderer || !mesh) return;
|
||||
|
||||
animationId = requestAnimationFrame(update)
|
||||
program.uniforms.uTime.value = t * 0.001
|
||||
renderer.render({ scene: mesh })
|
||||
}
|
||||
animationId = requestAnimationFrame(update);
|
||||
program.uniforms.uTime.value = t * 0.001;
|
||||
renderer.render({ scene: mesh });
|
||||
};
|
||||
|
||||
const initializeScene = () => {
|
||||
if (!containerRef.value) return
|
||||
if (!containerRef.value) return;
|
||||
|
||||
cleanup()
|
||||
cleanup();
|
||||
|
||||
const container = containerRef.value
|
||||
renderer = new Renderer()
|
||||
gl = renderer.gl
|
||||
gl.clearColor(1, 1, 1, 1)
|
||||
const container = containerRef.value;
|
||||
renderer = new Renderer();
|
||||
gl = renderer.gl;
|
||||
gl.clearColor(1, 1, 1, 1);
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
const geometry = new Triangle(gl);
|
||||
program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
@@ -129,76 +129,72 @@ const initializeScene = () => {
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new Color(...props.color) },
|
||||
uResolution: {
|
||||
value: new Color(
|
||||
gl.canvas.width,
|
||||
gl.canvas.height,
|
||||
gl.canvas.width / gl.canvas.height
|
||||
)
|
||||
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
|
||||
},
|
||||
uMouse: { value: new Float32Array([mousePos.value.x, mousePos.value.y]) },
|
||||
uAmplitude: { value: props.amplitude },
|
||||
uSpeed: { value: props.speed }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
mesh = new Mesh(gl, { geometry, program })
|
||||
mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
const canvas = gl.canvas as HTMLCanvasElement
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.display = 'block'
|
||||
const canvas = gl.canvas as HTMLCanvasElement;
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.display = 'block';
|
||||
|
||||
container.appendChild(canvas)
|
||||
container.appendChild(canvas);
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
window.addEventListener('resize', resize);
|
||||
if (props.mouseReact) {
|
||||
container.addEventListener('mousemove', handleMouseMove)
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
|
||||
resize()
|
||||
animationId = requestAnimationFrame(update)
|
||||
}
|
||||
resize();
|
||||
animationId = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', resize)
|
||||
window.removeEventListener('resize', resize);
|
||||
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('mousemove', handleMouseMove)
|
||||
containerRef.value.removeEventListener('mousemove', handleMouseMove);
|
||||
|
||||
const canvas = containerRef.value.querySelector('canvas')
|
||||
const canvas = containerRef.value.querySelector('canvas');
|
||||
if (canvas) {
|
||||
containerRef.value.removeChild(canvas)
|
||||
containerRef.value.removeChild(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
if (gl) {
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
|
||||
renderer = null
|
||||
gl = null
|
||||
program = null
|
||||
mesh = null
|
||||
}
|
||||
renderer = null;
|
||||
gl = null;
|
||||
program = null;
|
||||
mesh = null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeScene()
|
||||
})
|
||||
initializeScene();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.color, () => props.speed, () => props.amplitude, () => props.mouseReact],
|
||||
() => {
|
||||
initializeScene()
|
||||
initializeScene();
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,79 +1,140 @@
|
||||
<template>
|
||||
<div class="relative overflow-hidden">
|
||||
<canvas ref="canvasRef" class="absolute top-0 left-0 w-full h-full" />
|
||||
<div v-if="outerVignette"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]" />
|
||||
<div v-if="centerVignette"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0.8)_0%,_rgba(0,0,0,0)_60%)]" />
|
||||
|
||||
<div
|
||||
v-if="outerVignette"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0)_60%,_rgba(0,0,0,1)_100%)]"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="centerVignette"
|
||||
class="absolute top-0 left-0 w-full h-full pointer-events-none bg-[radial-gradient(circle,_rgba(0,0,0,0.8)_0%,_rgba(0,0,0,0)_60%)]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
glitchColors?: string[]
|
||||
glitchSpeed?: number
|
||||
centerVignette?: boolean
|
||||
outerVignette?: boolean
|
||||
smooth?: boolean
|
||||
glitchColors?: string[];
|
||||
glitchSpeed?: number;
|
||||
centerVignette?: boolean;
|
||||
outerVignette?: boolean;
|
||||
smooth?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
glitchColors: () => ["#2b4539", "#61dca3", "#61b3dc"],
|
||||
glitchColors: () => ['#2b4539', '#61dca3', '#61b3dc'],
|
||||
glitchSpeed: 50,
|
||||
centerVignette: false,
|
||||
outerVignette: false,
|
||||
smooth: true
|
||||
})
|
||||
});
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const animationRef = ref<number | null>(null)
|
||||
const letters = ref<{
|
||||
char: string
|
||||
color: string
|
||||
targetColor: string
|
||||
colorProgress: number
|
||||
}[]>([])
|
||||
const grid = ref({ columns: 0, rows: 0 })
|
||||
const context = ref<CanvasRenderingContext2D | null>(null)
|
||||
const lastGlitchTime = ref(Date.now())
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const animationRef = ref<number | null>(null);
|
||||
const letters = ref<
|
||||
{
|
||||
char: string;
|
||||
color: string;
|
||||
targetColor: string;
|
||||
colorProgress: number;
|
||||
}[]
|
||||
>([]);
|
||||
const grid = ref({ columns: 0, rows: 0 });
|
||||
const context = ref<CanvasRenderingContext2D | null>(null);
|
||||
const lastGlitchTime = ref(Date.now());
|
||||
|
||||
const fontSize = 16
|
||||
const charWidth = 10
|
||||
const charHeight = 20
|
||||
const fontSize = 16;
|
||||
const charWidth = 10;
|
||||
const charHeight = 20;
|
||||
|
||||
const lettersAndSymbols = [
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
"!", "@", "#", "$", "&", "*", "(", ")", "-", "_", "+", "=", "/",
|
||||
"[", "]", "{", "}", ";", ":", "<", ">", ",", "0", "1", "2", "3",
|
||||
"4", "5", "6", "7", "8", "9"
|
||||
]
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
'!',
|
||||
'@',
|
||||
'#',
|
||||
'$',
|
||||
'&',
|
||||
'*',
|
||||
'(',
|
||||
')',
|
||||
'-',
|
||||
'_',
|
||||
'+',
|
||||
'=',
|
||||
'/',
|
||||
'[',
|
||||
']',
|
||||
'{',
|
||||
'}',
|
||||
';',
|
||||
':',
|
||||
'<',
|
||||
'>',
|
||||
',',
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9'
|
||||
];
|
||||
|
||||
const getRandomChar = () => {
|
||||
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)]
|
||||
}
|
||||
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];
|
||||
};
|
||||
|
||||
const getRandomColor = () => {
|
||||
return props.glitchColors[Math.floor(Math.random() * props.glitchColors.length)]
|
||||
}
|
||||
return props.glitchColors[Math.floor(Math.random() * props.glitchColors.length)];
|
||||
};
|
||||
|
||||
const hexToRgb = (hex: string) => {
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
|
||||
return r + r + g + g + b + b
|
||||
})
|
||||
return r + r + g + g + b + b;
|
||||
});
|
||||
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: null
|
||||
}
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
const interpolateColor = (
|
||||
start: { r: number; g: number; b: number },
|
||||
@@ -83,171 +144,167 @@ const interpolateColor = (
|
||||
const result = {
|
||||
r: Math.round(start.r + (end.r - start.r) * factor),
|
||||
g: Math.round(start.g + (end.g - start.g) * factor),
|
||||
b: Math.round(start.b + (end.b - start.b) * factor),
|
||||
}
|
||||
return `rgb(${result.r}, ${result.g}, ${result.b})`
|
||||
}
|
||||
b: Math.round(start.b + (end.b - start.b) * factor)
|
||||
};
|
||||
return `rgb(${result.r}, ${result.g}, ${result.b})`;
|
||||
};
|
||||
|
||||
const calculateGrid = (width: number, height: number) => {
|
||||
const columns = Math.ceil(width / charWidth)
|
||||
const rows = Math.ceil(height / charHeight)
|
||||
return { columns, rows }
|
||||
}
|
||||
const columns = Math.ceil(width / charWidth);
|
||||
const rows = Math.ceil(height / charHeight);
|
||||
return { columns, rows };
|
||||
};
|
||||
|
||||
const initializeLetters = (columns: number, rows: number) => {
|
||||
grid.value = { columns, rows }
|
||||
const totalLetters = columns * rows
|
||||
grid.value = { columns, rows };
|
||||
const totalLetters = columns * rows;
|
||||
letters.value = Array.from({ length: totalLetters }, () => ({
|
||||
char: getRandomChar(),
|
||||
color: getRandomColor(),
|
||||
targetColor: getRandomColor(),
|
||||
colorProgress: 1,
|
||||
}))
|
||||
}
|
||||
colorProgress: 1
|
||||
}));
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const parent = canvas.parentElement
|
||||
if (!parent) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const parent = canvas.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
const parentWidth = parent.parentElement?.offsetWidth || parent.offsetWidth || window.innerWidth
|
||||
const parentHeight = parent.parentElement?.offsetHeight || parent.offsetHeight || window.innerHeight
|
||||
|
||||
const width = Math.max(parentWidth, 300)
|
||||
const height = Math.max(parentHeight, 300)
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
const parentWidth = parent.parentElement?.offsetWidth || parent.offsetWidth || window.innerWidth;
|
||||
const parentHeight = parent.parentElement?.offsetHeight || parent.offsetHeight || window.innerHeight;
|
||||
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
const width = Math.max(parentWidth, 300);
|
||||
const height = Math.max(parentHeight, 300);
|
||||
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
if (context.value) {
|
||||
context.value.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
context.value.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
const { columns, rows } = calculateGrid(width, height)
|
||||
initializeLetters(columns, rows)
|
||||
drawLetters()
|
||||
}
|
||||
const { columns, rows } = calculateGrid(width, height);
|
||||
initializeLetters(columns, rows);
|
||||
drawLetters();
|
||||
};
|
||||
|
||||
const drawLetters = () => {
|
||||
if (!context.value || letters.value.length === 0) return
|
||||
const ctx = context.value
|
||||
const { width, height } = canvasRef.value!.getBoundingClientRect()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.font = `${fontSize}px monospace`
|
||||
ctx.textBaseline = "top"
|
||||
if (!context.value || letters.value.length === 0) return;
|
||||
const ctx = context.value;
|
||||
const { width, height } = canvasRef.value!.getBoundingClientRect();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.font = `${fontSize}px monospace`;
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
letters.value.forEach((letter, index) => {
|
||||
const x = (index % grid.value.columns) * charWidth
|
||||
const y = Math.floor(index / grid.value.columns) * charHeight
|
||||
ctx.fillStyle = letter.color
|
||||
ctx.fillText(letter.char, x, y)
|
||||
})
|
||||
}
|
||||
const x = (index % grid.value.columns) * charWidth;
|
||||
const y = Math.floor(index / grid.value.columns) * charHeight;
|
||||
ctx.fillStyle = letter.color;
|
||||
ctx.fillText(letter.char, x, y);
|
||||
});
|
||||
};
|
||||
|
||||
const updateLetters = () => {
|
||||
if (!letters.value || letters.value.length === 0) return
|
||||
if (!letters.value || letters.value.length === 0) return;
|
||||
|
||||
const updateCount = Math.max(1, Math.floor(letters.value.length * 0.05))
|
||||
const updateCount = Math.max(1, Math.floor(letters.value.length * 0.05));
|
||||
|
||||
for (let i = 0; i < updateCount; i++) {
|
||||
const index = Math.floor(Math.random() * letters.value.length)
|
||||
if (!letters.value[index]) continue
|
||||
const index = Math.floor(Math.random() * letters.value.length);
|
||||
if (!letters.value[index]) continue;
|
||||
|
||||
letters.value[index].char = getRandomChar()
|
||||
letters.value[index].targetColor = getRandomColor()
|
||||
letters.value[index].char = getRandomChar();
|
||||
letters.value[index].targetColor = getRandomColor();
|
||||
|
||||
if (!props.smooth) {
|
||||
letters.value[index].color = letters.value[index].targetColor
|
||||
letters.value[index].colorProgress = 1
|
||||
letters.value[index].color = letters.value[index].targetColor;
|
||||
letters.value[index].colorProgress = 1;
|
||||
} else {
|
||||
letters.value[index].colorProgress = 0
|
||||
letters.value[index].colorProgress = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSmoothTransitions = () => {
|
||||
let needsRedraw = false
|
||||
letters.value.forEach((letter) => {
|
||||
let needsRedraw = false;
|
||||
letters.value.forEach(letter => {
|
||||
if (letter.colorProgress < 1) {
|
||||
letter.colorProgress += 0.05
|
||||
if (letter.colorProgress > 1) letter.colorProgress = 1
|
||||
letter.colorProgress += 0.05;
|
||||
if (letter.colorProgress > 1) letter.colorProgress = 1;
|
||||
|
||||
const startRgb = hexToRgb(letter.color)
|
||||
const endRgb = hexToRgb(letter.targetColor)
|
||||
const startRgb = hexToRgb(letter.color);
|
||||
const endRgb = hexToRgb(letter.targetColor);
|
||||
if (startRgb && endRgb) {
|
||||
letter.color = interpolateColor(
|
||||
startRgb,
|
||||
endRgb,
|
||||
letter.colorProgress
|
||||
)
|
||||
needsRedraw = true
|
||||
letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
|
||||
needsRedraw = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (needsRedraw) {
|
||||
drawLetters()
|
||||
drawLetters();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
const now = Date.now()
|
||||
const now = Date.now();
|
||||
if (now - lastGlitchTime.value >= props.glitchSpeed) {
|
||||
updateLetters()
|
||||
drawLetters()
|
||||
lastGlitchTime.value = now
|
||||
updateLetters();
|
||||
drawLetters();
|
||||
lastGlitchTime.value = now;
|
||||
}
|
||||
|
||||
if (props.smooth) {
|
||||
handleSmoothTransitions()
|
||||
handleSmoothTransitions();
|
||||
}
|
||||
|
||||
animationRef.value = requestAnimationFrame(animate)
|
||||
}
|
||||
animationRef.value = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
let resizeTimeout: number
|
||||
let resizeTimeout: number;
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimeout)
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
if (animationRef.value) {
|
||||
cancelAnimationFrame(animationRef.value)
|
||||
cancelAnimationFrame(animationRef.value);
|
||||
}
|
||||
resizeCanvas()
|
||||
animate()
|
||||
}, 100)
|
||||
}
|
||||
resizeCanvas();
|
||||
animate();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
context.value = canvas.getContext("2d")
|
||||
resizeCanvas()
|
||||
animate()
|
||||
context.value = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
animate();
|
||||
|
||||
window.addEventListener("resize", handleResize)
|
||||
})
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationRef.value) {
|
||||
cancelAnimationFrame(animationRef.value)
|
||||
cancelAnimationFrame(animationRef.value);
|
||||
}
|
||||
window.removeEventListener("resize", handleResize)
|
||||
})
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
watch([() => props.glitchSpeed, () => props.smooth], () => {
|
||||
if (animationRef.value) {
|
||||
cancelAnimationFrame(animationRef.value)
|
||||
cancelAnimationFrame(animationRef.value);
|
||||
}
|
||||
animate()
|
||||
})
|
||||
animate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -266,4 +323,4 @@ div {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
interface LightningProps {
|
||||
hue?: number
|
||||
xOffset?: number
|
||||
speed?: number
|
||||
intensity?: number
|
||||
size?: number
|
||||
hue?: number;
|
||||
xOffset?: number;
|
||||
speed?: number;
|
||||
intensity?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<LightningProps>(), {
|
||||
@@ -19,20 +19,20 @@ const props = withDefaults(defineProps<LightningProps>(), {
|
||||
speed: 1,
|
||||
intensity: 1,
|
||||
size: 1
|
||||
})
|
||||
});
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
let animationId = 0
|
||||
let gl: WebGLRenderingContext | null = null
|
||||
let program: WebGLProgram | null = null
|
||||
let startTime = 0
|
||||
const canvasRef = ref<HTMLCanvasElement>();
|
||||
let animationId = 0;
|
||||
let gl: WebGLRenderingContext | null = null;
|
||||
let program: WebGLProgram | null = null;
|
||||
let startTime = 0;
|
||||
|
||||
const vertexShaderSource = `
|
||||
attribute vec2 aPosition;
|
||||
void main() {
|
||||
gl_Position = vec4(aPosition, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const fragmentShaderSource = `
|
||||
precision mediump float;
|
||||
@@ -112,155 +112,153 @@ void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
|
||||
void main() {
|
||||
mainImage(gl_FragColor, gl_FragCoord.xy);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const compileShader = (source: string, type: number): WebGLShader | null => {
|
||||
if (!gl) return null
|
||||
const shader = gl.createShader(type)
|
||||
if (!shader) return null
|
||||
gl.shaderSource(shader, source)
|
||||
gl.compileShader(shader)
|
||||
if (!gl) return null;
|
||||
const shader = gl.createShader(type);
|
||||
if (!shader) return null;
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error('Shader compile error:', gl.getShaderInfoLog(shader))
|
||||
gl.deleteShader(shader)
|
||||
return null
|
||||
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
return shader
|
||||
}
|
||||
return shader;
|
||||
};
|
||||
|
||||
const initWebGL = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
let width = rect.width
|
||||
let height = rect.height
|
||||
|
||||
let parent = canvas.parentElement
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
let width = rect.width;
|
||||
let height = rect.height;
|
||||
|
||||
let parent = canvas.parentElement;
|
||||
while (parent && (!width || !height)) {
|
||||
if (parent.offsetWidth && parent.offsetHeight) {
|
||||
width = parent.offsetWidth
|
||||
height = parent.offsetHeight
|
||||
break
|
||||
width = parent.offsetWidth;
|
||||
height = parent.offsetHeight;
|
||||
break;
|
||||
}
|
||||
parent = parent.parentElement
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
|
||||
if (!width || !height) {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
}
|
||||
|
||||
width = Math.max(width, 300)
|
||||
height = Math.max(height, 300)
|
||||
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.display = 'block'
|
||||
canvas.style.position = 'absolute'
|
||||
canvas.style.top = '0'
|
||||
canvas.style.left = '0'
|
||||
}
|
||||
|
||||
resizeCanvas()
|
||||
window.addEventListener('resize', resizeCanvas)
|
||||
width = Math.max(width, 300);
|
||||
height = Math.max(height, 300);
|
||||
|
||||
gl = canvas.getContext('webgl')
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.display = 'block';
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.style.top = '0';
|
||||
canvas.style.left = '0';
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
gl = canvas.getContext('webgl');
|
||||
if (!gl) {
|
||||
console.error('WebGL not supported')
|
||||
return
|
||||
console.error('WebGL not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER)
|
||||
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
|
||||
if (!vertexShader || !fragmentShader) return
|
||||
const vertexShader = compileShader(vertexShaderSource, gl.VERTEX_SHADER);
|
||||
const fragmentShader = compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
|
||||
if (!vertexShader || !fragmentShader) return;
|
||||
|
||||
program = gl.createProgram()
|
||||
if (!program) return
|
||||
gl.attachShader(program, vertexShader)
|
||||
gl.attachShader(program, fragmentShader)
|
||||
gl.linkProgram(program)
|
||||
program = gl.createProgram();
|
||||
if (!program) return;
|
||||
gl.attachShader(program, vertexShader);
|
||||
gl.attachShader(program, fragmentShader);
|
||||
gl.linkProgram(program);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program linking error:', gl.getProgramInfoLog(program))
|
||||
return
|
||||
console.error('Program linking error:', gl.getProgramInfoLog(program));
|
||||
return;
|
||||
}
|
||||
gl.useProgram(program)
|
||||
gl.useProgram(program);
|
||||
|
||||
const vertices = new Float32Array([
|
||||
-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,
|
||||
])
|
||||
const vertexBuffer = gl.createBuffer()
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
|
||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
|
||||
const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);
|
||||
const vertexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
||||
|
||||
const aPosition = gl.getAttribLocation(program, 'aPosition')
|
||||
gl.enableVertexAttribArray(aPosition)
|
||||
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
|
||||
const aPosition = gl.getAttribLocation(program, 'aPosition');
|
||||
gl.enableVertexAttribArray(aPosition);
|
||||
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
startTime = performance.now()
|
||||
render()
|
||||
startTime = performance.now();
|
||||
render();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas)
|
||||
}
|
||||
}
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
};
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (!gl || !program || !canvasRef.value) return
|
||||
if (!gl || !program || !canvasRef.value) return;
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const canvas = canvasRef.value;
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
if (canvas.width !== rect.width || canvas.height !== rect.height) {
|
||||
canvas.width = rect.width
|
||||
canvas.height = rect.height
|
||||
canvas.style.width = rect.width + 'px'
|
||||
canvas.style.height = rect.height + 'px'
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
canvas.style.width = rect.width + 'px';
|
||||
canvas.style.height = rect.height + 'px';
|
||||
}
|
||||
|
||||
gl.viewport(0, 0, canvas.width, canvas.height)
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const iResolutionLocation = gl.getUniformLocation(program, 'iResolution')
|
||||
const iTimeLocation = gl.getUniformLocation(program, 'iTime')
|
||||
const uHueLocation = gl.getUniformLocation(program, 'uHue')
|
||||
const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset')
|
||||
const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed')
|
||||
const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity')
|
||||
const uSizeLocation = gl.getUniformLocation(program, 'uSize')
|
||||
const iResolutionLocation = gl.getUniformLocation(program, 'iResolution');
|
||||
const iTimeLocation = gl.getUniformLocation(program, 'iTime');
|
||||
const uHueLocation = gl.getUniformLocation(program, 'uHue');
|
||||
const uXOffsetLocation = gl.getUniformLocation(program, 'uXOffset');
|
||||
const uSpeedLocation = gl.getUniformLocation(program, 'uSpeed');
|
||||
const uIntensityLocation = gl.getUniformLocation(program, 'uIntensity');
|
||||
const uSizeLocation = gl.getUniformLocation(program, 'uSize');
|
||||
|
||||
gl.uniform2f(iResolutionLocation, canvas.width, canvas.height)
|
||||
const currentTime = performance.now()
|
||||
gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0)
|
||||
gl.uniform1f(uHueLocation, props.hue)
|
||||
gl.uniform1f(uXOffsetLocation, props.xOffset)
|
||||
gl.uniform1f(uSpeedLocation, props.speed)
|
||||
gl.uniform1f(uIntensityLocation, props.intensity)
|
||||
gl.uniform1f(uSizeLocation, props.size)
|
||||
gl.uniform2f(iResolutionLocation, canvas.width, canvas.height);
|
||||
const currentTime = performance.now();
|
||||
gl.uniform1f(iTimeLocation, (currentTime - startTime) / 1000.0);
|
||||
gl.uniform1f(uHueLocation, props.hue);
|
||||
gl.uniform1f(uXOffsetLocation, props.xOffset);
|
||||
gl.uniform1f(uSpeedLocation, props.speed);
|
||||
gl.uniform1f(uIntensityLocation, props.intensity);
|
||||
gl.uniform1f(uSizeLocation, props.size);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
||||
animationId = requestAnimationFrame(render)
|
||||
}
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
animationId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initWebGL()
|
||||
})
|
||||
initWebGL();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.hue, props.xOffset, props.speed, props.intensity, props.size],
|
||||
() => {}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Renderer, Camera, Geometry, Program, Mesh } from 'ogl'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Renderer, Camera, Geometry, Program, Mesh } from 'ogl';
|
||||
|
||||
interface ParticlesProps {
|
||||
particleCount?: number
|
||||
particleSpread?: number
|
||||
speed?: number
|
||||
particleColors?: string[]
|
||||
moveParticlesOnHover?: boolean
|
||||
particleHoverFactor?: number
|
||||
alphaParticles?: boolean
|
||||
particleBaseSize?: number
|
||||
sizeRandomness?: number
|
||||
cameraDistance?: number
|
||||
disableRotation?: boolean
|
||||
className?: string
|
||||
particleCount?: number;
|
||||
particleSpread?: number;
|
||||
speed?: number;
|
||||
particleColors?: string[];
|
||||
moveParticlesOnHover?: boolean;
|
||||
particleHoverFactor?: number;
|
||||
alphaParticles?: boolean;
|
||||
particleBaseSize?: number;
|
||||
sizeRandomness?: number;
|
||||
cameraDistance?: number;
|
||||
disableRotation?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ParticlesProps>(), {
|
||||
@@ -34,32 +34,35 @@ const props = withDefaults(defineProps<ParticlesProps>(), {
|
||||
cameraDistance: 20,
|
||||
disableRotation: false,
|
||||
className: ''
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const mouseRef = ref({ x: 0, y: 0 })
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
const mouseRef = ref({ x: 0, y: 0 });
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
let camera: Camera | null = null
|
||||
let particles: Mesh | null = null
|
||||
let program: Program | null = null
|
||||
let animationFrameId: number | null = null
|
||||
let lastTime = 0
|
||||
let elapsed = 0
|
||||
let renderer: Renderer | null = null;
|
||||
let camera: Camera | null = null;
|
||||
let particles: Mesh | null = null;
|
||||
let program: Program | null = null;
|
||||
let animationFrameId: number | null = null;
|
||||
let lastTime = 0;
|
||||
let elapsed = 0;
|
||||
|
||||
const defaultColors = ['#ffffff', '#ffffff', '#ffffff']
|
||||
const defaultColors = ['#ffffff', '#ffffff', '#ffffff'];
|
||||
|
||||
const hexToRgb = (hex: string): [number, number, number] => {
|
||||
hex = hex.replace(/^#/, '')
|
||||
hex = hex.replace(/^#/, '');
|
||||
if (hex.length === 3) {
|
||||
hex = hex.split('').map((c) => c + c).join('')
|
||||
hex = hex
|
||||
.split('')
|
||||
.map(c => c + c)
|
||||
.join('');
|
||||
}
|
||||
const int = parseInt(hex, 16)
|
||||
const r = ((int >> 16) & 255) / 255
|
||||
const g = ((int >> 8) & 255) / 255
|
||||
const b = (int & 255) / 255
|
||||
return [r, g, b]
|
||||
}
|
||||
const int = parseInt(hex, 16);
|
||||
const r = ((int >> 16) & 255) / 255;
|
||||
const g = ((int >> 8) & 255) / 255;
|
||||
const b = (int & 255) / 255;
|
||||
return [r, g, b];
|
||||
};
|
||||
|
||||
const vertex = /* glsl */ `
|
||||
attribute vec3 position;
|
||||
@@ -94,7 +97,7 @@ const vertex = /* glsl */ `
|
||||
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
|
||||
gl_Position = projectionMatrix * mvPos;
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const fragment = /* glsl */ `
|
||||
precision highp float;
|
||||
@@ -118,89 +121,89 @@ const fragment = /* glsl */ `
|
||||
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), circle);
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect()
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1
|
||||
const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1)
|
||||
mouseRef.value = { x, y }
|
||||
}
|
||||
const rect = container.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
|
||||
mouseRef.value = { x, y };
|
||||
};
|
||||
|
||||
const initParticles = () => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
renderer = new Renderer({ depth: false, alpha: true })
|
||||
const gl = renderer.gl
|
||||
container.appendChild(gl.canvas)
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
renderer = new Renderer({ depth: false, alpha: true });
|
||||
const gl = renderer.gl;
|
||||
container.appendChild(gl.canvas);
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
|
||||
gl.canvas.style.width = '100%'
|
||||
gl.canvas.style.height = '100%'
|
||||
gl.canvas.style.display = 'block'
|
||||
gl.canvas.style.position = 'absolute'
|
||||
gl.canvas.style.top = '0'
|
||||
gl.canvas.style.left = '0'
|
||||
gl.canvas.style.width = '100%';
|
||||
gl.canvas.style.height = '100%';
|
||||
gl.canvas.style.display = 'block';
|
||||
gl.canvas.style.position = 'absolute';
|
||||
gl.canvas.style.top = '0';
|
||||
gl.canvas.style.left = '0';
|
||||
|
||||
camera = new Camera(gl, { fov: 15 })
|
||||
camera.position.set(0, 0, props.cameraDistance)
|
||||
camera = new Camera(gl, { fov: 15 });
|
||||
camera.position.set(0, 0, props.cameraDistance);
|
||||
|
||||
const resize = () => {
|
||||
if (!container) return
|
||||
|
||||
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth
|
||||
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight
|
||||
|
||||
const width = Math.max(parentWidth, 300)
|
||||
const height = Math.max(parentHeight, 300)
|
||||
|
||||
renderer!.setSize(width, height)
|
||||
camera!.perspective({ aspect: width / height })
|
||||
if (!container) return;
|
||||
|
||||
gl.canvas.style.width = '100%'
|
||||
gl.canvas.style.height = '100%'
|
||||
gl.canvas.style.display = 'block'
|
||||
gl.canvas.style.position = 'absolute'
|
||||
gl.canvas.style.top = '0'
|
||||
gl.canvas.style.left = '0'
|
||||
}
|
||||
window.addEventListener('resize', resize, false)
|
||||
resize()
|
||||
const parentWidth = container.parentElement?.offsetWidth || container.offsetWidth || window.innerWidth;
|
||||
const parentHeight = container.parentElement?.offsetHeight || container.offsetHeight || window.innerHeight;
|
||||
|
||||
const width = Math.max(parentWidth, 300);
|
||||
const height = Math.max(parentHeight, 300);
|
||||
|
||||
renderer!.setSize(width, height);
|
||||
camera!.perspective({ aspect: width / height });
|
||||
|
||||
gl.canvas.style.width = '100%';
|
||||
gl.canvas.style.height = '100%';
|
||||
gl.canvas.style.display = 'block';
|
||||
gl.canvas.style.position = 'absolute';
|
||||
gl.canvas.style.top = '0';
|
||||
gl.canvas.style.left = '0';
|
||||
};
|
||||
window.addEventListener('resize', resize, false);
|
||||
resize();
|
||||
|
||||
if (props.moveParticlesOnHover) {
|
||||
container.addEventListener('mousemove', handleMouseMove)
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
|
||||
const count = props.particleCount
|
||||
const positions = new Float32Array(count * 3)
|
||||
const randoms = new Float32Array(count * 4)
|
||||
const colors = new Float32Array(count * 3)
|
||||
const palette = props.particleColors && props.particleColors.length > 0 ? props.particleColors : defaultColors
|
||||
const count = props.particleCount;
|
||||
const positions = new Float32Array(count * 3);
|
||||
const randoms = new Float32Array(count * 4);
|
||||
const colors = new Float32Array(count * 3);
|
||||
const palette = props.particleColors && props.particleColors.length > 0 ? props.particleColors : defaultColors;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let x: number, y: number, z: number, len: number
|
||||
let x: number, y: number, z: number, len: number;
|
||||
do {
|
||||
x = Math.random() * 2 - 1
|
||||
y = Math.random() * 2 - 1
|
||||
z = Math.random() * 2 - 1
|
||||
len = x * x + y * y + z * z
|
||||
} while (len > 1 || len === 0)
|
||||
const r = Math.cbrt(Math.random())
|
||||
positions.set([x * r, y * r, z * r], i * 3)
|
||||
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4)
|
||||
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)])
|
||||
colors.set(col, i * 3)
|
||||
x = Math.random() * 2 - 1;
|
||||
y = Math.random() * 2 - 1;
|
||||
z = Math.random() * 2 - 1;
|
||||
len = x * x + y * y + z * z;
|
||||
} while (len > 1 || len === 0);
|
||||
const r = Math.cbrt(Math.random());
|
||||
positions.set([x * r, y * r, z * r], i * 3);
|
||||
randoms.set([Math.random(), Math.random(), Math.random(), Math.random()], i * 4);
|
||||
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);
|
||||
colors.set(col, i * 3);
|
||||
}
|
||||
|
||||
const geometry = new Geometry(gl, {
|
||||
position: { size: 3, data: positions },
|
||||
random: { size: 4, data: randoms },
|
||||
color: { size: 3, data: colors },
|
||||
})
|
||||
color: { size: 3, data: colors }
|
||||
});
|
||||
|
||||
program = new Program(gl, {
|
||||
vertex,
|
||||
@@ -210,105 +213,105 @@ const initParticles = () => {
|
||||
uSpread: { value: props.particleSpread },
|
||||
uBaseSize: { value: props.particleBaseSize },
|
||||
uSizeRandomness: { value: props.sizeRandomness },
|
||||
uAlphaParticles: { value: props.alphaParticles ? 1 : 0 },
|
||||
uAlphaParticles: { value: props.alphaParticles ? 1 : 0 }
|
||||
},
|
||||
transparent: true,
|
||||
depthTest: false,
|
||||
})
|
||||
depthTest: false
|
||||
});
|
||||
|
||||
particles = new Mesh(gl, { mode: gl.POINTS, geometry, program })
|
||||
particles = new Mesh(gl, { mode: gl.POINTS, geometry, program });
|
||||
|
||||
lastTime = performance.now()
|
||||
elapsed = 0
|
||||
lastTime = performance.now();
|
||||
elapsed = 0;
|
||||
|
||||
const update = (t: number) => {
|
||||
if (!animationFrameId) return
|
||||
animationFrameId = requestAnimationFrame(update)
|
||||
const delta = t - lastTime
|
||||
lastTime = t
|
||||
elapsed += delta * props.speed
|
||||
if (!animationFrameId) return;
|
||||
animationFrameId = requestAnimationFrame(update);
|
||||
const delta = t - lastTime;
|
||||
lastTime = t;
|
||||
elapsed += delta * props.speed;
|
||||
|
||||
if (program) {
|
||||
program.uniforms.uTime.value = elapsed * 0.001
|
||||
program.uniforms.uSpread.value = props.particleSpread
|
||||
program.uniforms.uBaseSize.value = props.particleBaseSize
|
||||
program.uniforms.uSizeRandomness.value = props.sizeRandomness
|
||||
program.uniforms.uAlphaParticles.value = props.alphaParticles ? 1 : 0
|
||||
program.uniforms.uTime.value = elapsed * 0.001;
|
||||
program.uniforms.uSpread.value = props.particleSpread;
|
||||
program.uniforms.uBaseSize.value = props.particleBaseSize;
|
||||
program.uniforms.uSizeRandomness.value = props.sizeRandomness;
|
||||
program.uniforms.uAlphaParticles.value = props.alphaParticles ? 1 : 0;
|
||||
}
|
||||
|
||||
if (particles) {
|
||||
if (props.moveParticlesOnHover) {
|
||||
particles.position.x = -mouseRef.value.x * props.particleHoverFactor
|
||||
particles.position.y = -mouseRef.value.y * props.particleHoverFactor
|
||||
particles.position.x = -mouseRef.value.x * props.particleHoverFactor;
|
||||
particles.position.y = -mouseRef.value.y * props.particleHoverFactor;
|
||||
} else {
|
||||
particles.position.x = 0
|
||||
particles.position.y = 0
|
||||
particles.position.x = 0;
|
||||
particles.position.y = 0;
|
||||
}
|
||||
|
||||
if (!props.disableRotation) {
|
||||
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1
|
||||
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15
|
||||
particles.rotation.z += 0.01 * props.speed
|
||||
particles.rotation.x = Math.sin(elapsed * 0.0002) * 0.1;
|
||||
particles.rotation.y = Math.cos(elapsed * 0.0005) * 0.15;
|
||||
particles.rotation.z += 0.01 * props.speed;
|
||||
}
|
||||
}
|
||||
|
||||
if (renderer && camera && particles) {
|
||||
renderer.render({ scene: particles, camera })
|
||||
renderer.render({ scene: particles, camera });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(update)
|
||||
animationFrameId = requestAnimationFrame(update);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize)
|
||||
window.removeEventListener('resize', resize);
|
||||
if (props.moveParticlesOnHover) {
|
||||
container.removeEventListener('mousemove', handleMouseMove)
|
||||
container.removeEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
if (container.contains(gl.canvas)) {
|
||||
container.removeChild(gl.canvas)
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = null;
|
||||
}
|
||||
if (renderer) {
|
||||
const container = containerRef.value
|
||||
const gl = renderer.gl
|
||||
const container = containerRef.value;
|
||||
const gl = renderer.gl;
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas)
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
renderer = null
|
||||
camera = null
|
||||
particles = null
|
||||
program = null
|
||||
}
|
||||
renderer = null;
|
||||
camera = null;
|
||||
particles = null;
|
||||
program = null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initParticles()
|
||||
})
|
||||
initParticles();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.particleCount, props.particleColors],
|
||||
() => {
|
||||
cleanup()
|
||||
initParticles()
|
||||
cleanup();
|
||||
initParticles();
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [
|
||||
@@ -322,7 +325,7 @@ watch(
|
||||
props.disableRotation
|
||||
],
|
||||
() => {}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue'
|
||||
import { Renderer, Program, Mesh, Plane, Camera } from 'ogl'
|
||||
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue';
|
||||
import { Renderer, Program, Mesh, Plane, Camera } from 'ogl';
|
||||
|
||||
interface SilkProps {
|
||||
speed?: number
|
||||
scale?: number
|
||||
color?: string
|
||||
noiseIntensity?: number
|
||||
rotation?: number
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
speed?: number;
|
||||
scale?: number;
|
||||
color?: string;
|
||||
noiseIntensity?: number;
|
||||
rotation?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SilkProps>(), {
|
||||
@@ -24,17 +24,17 @@ const props = withDefaults(defineProps<SilkProps>(), {
|
||||
rotation: 0,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
|
||||
const hexToNormalizedRGB = (hex: string): [number, number, number] => {
|
||||
const clean = hex.replace('#', '')
|
||||
const r = parseInt(clean.slice(0, 2), 16) / 255
|
||||
const g = parseInt(clean.slice(2, 4), 16) / 255
|
||||
const b = parseInt(clean.slice(4, 6), 16) / 255
|
||||
return [r, g, b]
|
||||
}
|
||||
const clean = hex.replace('#', '');
|
||||
const r = parseInt(clean.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(clean.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(clean.slice(4, 6), 16) / 255;
|
||||
return [r, g, b];
|
||||
};
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 uv;
|
||||
@@ -51,7 +51,7 @@ void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
@@ -99,75 +99,75 @@ void main() {
|
||||
col.a = 1.0;
|
||||
gl_FragColor = col;
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
let mesh: Mesh | null = null
|
||||
let program: Program | null = null
|
||||
let camera: Camera | null = null
|
||||
let animateId = 0
|
||||
let renderer: Renderer | null = null;
|
||||
let mesh: Mesh | null = null;
|
||||
let program: Program | null = null;
|
||||
let camera: Camera | null = null;
|
||||
let animateId = 0;
|
||||
|
||||
const initSilk = () => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
renderer = new Renderer({
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
})
|
||||
antialias: true
|
||||
});
|
||||
|
||||
const gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
gl.canvas.style.backgroundColor = 'transparent'
|
||||
const gl = renderer.gl;
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.canvas.style.backgroundColor = 'transparent';
|
||||
|
||||
camera = new Camera(gl, { fov: 75 })
|
||||
camera.position.z = 1
|
||||
camera = new Camera(gl, { fov: 75 });
|
||||
camera.position.z = 1;
|
||||
|
||||
const resize = () => {
|
||||
if (!container || !camera) return
|
||||
|
||||
let width = container.offsetWidth
|
||||
let height = container.offsetHeight
|
||||
|
||||
let parent = container.parentElement
|
||||
if (!container || !camera) return;
|
||||
|
||||
let width = container.offsetWidth;
|
||||
let height = container.offsetHeight;
|
||||
|
||||
let parent = container.parentElement;
|
||||
while (parent && (!width || !height)) {
|
||||
if (parent.offsetWidth && parent.offsetHeight) {
|
||||
width = parent.offsetWidth
|
||||
height = parent.offsetHeight
|
||||
break
|
||||
width = parent.offsetWidth;
|
||||
height = parent.offsetHeight;
|
||||
break;
|
||||
}
|
||||
parent = parent.parentElement
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
|
||||
if (!width || !height) {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
}
|
||||
|
||||
width = Math.max(width, 300)
|
||||
height = Math.max(height, 300)
|
||||
|
||||
renderer!.setSize(width, height)
|
||||
camera.perspective({ aspect: width / height })
|
||||
|
||||
width = Math.max(width, 300);
|
||||
height = Math.max(height, 300);
|
||||
|
||||
renderer!.setSize(width, height);
|
||||
camera.perspective({ aspect: width / height });
|
||||
|
||||
if (mesh) {
|
||||
const distance = camera.position.z
|
||||
const fov = camera.fov * (Math.PI / 180)
|
||||
const height2 = 2 * Math.tan(fov / 2) * distance
|
||||
const width2 = height2 * (width / height)
|
||||
const distance = camera.position.z;
|
||||
const fov = camera.fov * (Math.PI / 180);
|
||||
const height2 = 2 * Math.tan(fov / 2) * distance;
|
||||
const width2 = height2 * (width / height);
|
||||
|
||||
mesh.scale.set(width2, height2, 1)
|
||||
mesh.scale.set(width2, height2, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
const geometry = new Plane(gl, {
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
height: 1
|
||||
});
|
||||
|
||||
const colorRGB = hexToNormalizedRGB(props.color)
|
||||
const colorRGB = hexToNormalizedRGB(props.color);
|
||||
|
||||
program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
@@ -178,81 +178,81 @@ const initSilk = () => {
|
||||
uNoiseIntensity: { value: props.noiseIntensity },
|
||||
uColor: { value: colorRGB },
|
||||
uRotation: { value: props.rotation },
|
||||
uTime: { value: 0 },
|
||||
},
|
||||
})
|
||||
uTime: { value: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
mesh = new Mesh(gl, { geometry, program })
|
||||
container.appendChild(gl.canvas)
|
||||
mesh = new Mesh(gl, { geometry, program });
|
||||
container.appendChild(gl.canvas);
|
||||
|
||||
gl.canvas.style.width = '100%'
|
||||
gl.canvas.style.height = '100%'
|
||||
gl.canvas.style.display = 'block'
|
||||
gl.canvas.style.position = 'absolute'
|
||||
gl.canvas.style.top = '0'
|
||||
gl.canvas.style.left = '0'
|
||||
gl.canvas.style.zIndex = '1'
|
||||
gl.canvas.style.width = '100%';
|
||||
gl.canvas.style.height = '100%';
|
||||
gl.canvas.style.display = 'block';
|
||||
gl.canvas.style.position = 'absolute';
|
||||
gl.canvas.style.top = '0';
|
||||
gl.canvas.style.left = '0';
|
||||
gl.canvas.style.zIndex = '1';
|
||||
|
||||
let lastTime = 0
|
||||
let lastTime = 0;
|
||||
const update = (t: number) => {
|
||||
animateId = requestAnimationFrame(update)
|
||||
const deltaTime = (t - lastTime) / 1000
|
||||
lastTime = t
|
||||
animateId = requestAnimationFrame(update);
|
||||
const deltaTime = (t - lastTime) / 1000;
|
||||
lastTime = t;
|
||||
|
||||
if (program && mesh && camera) {
|
||||
program.uniforms.uTime.value += 0.1 * deltaTime
|
||||
program.uniforms.uSpeed.value = props.speed
|
||||
program.uniforms.uScale.value = props.scale
|
||||
program.uniforms.uNoiseIntensity.value = props.noiseIntensity
|
||||
program.uniforms.uColor.value = hexToNormalizedRGB(props.color)
|
||||
program.uniforms.uRotation.value = props.rotation
|
||||
renderer!.render({ scene: mesh, camera })
|
||||
program.uniforms.uTime.value += 0.1 * deltaTime;
|
||||
program.uniforms.uSpeed.value = props.speed;
|
||||
program.uniforms.uScale.value = props.scale;
|
||||
program.uniforms.uNoiseIntensity.value = props.noiseIntensity;
|
||||
program.uniforms.uColor.value = hexToNormalizedRGB(props.color);
|
||||
program.uniforms.uRotation.value = props.rotation;
|
||||
renderer!.render({ scene: mesh, camera });
|
||||
}
|
||||
}
|
||||
animateId = requestAnimationFrame(update)
|
||||
};
|
||||
animateId = requestAnimationFrame(update);
|
||||
|
||||
resize()
|
||||
resize();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animateId)
|
||||
window.removeEventListener('resize', resize)
|
||||
cancelAnimationFrame(animateId);
|
||||
window.removeEventListener('resize', resize);
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas)
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
}
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animateId) {
|
||||
cancelAnimationFrame(animateId)
|
||||
cancelAnimationFrame(animateId);
|
||||
}
|
||||
if (renderer) {
|
||||
const gl = renderer.gl
|
||||
const container = containerRef.value
|
||||
const gl = renderer.gl;
|
||||
const container = containerRef.value;
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas)
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
renderer = null
|
||||
mesh = null
|
||||
camera = null
|
||||
program = null
|
||||
}
|
||||
renderer = null;
|
||||
mesh = null;
|
||||
camera = null;
|
||||
program = null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initSilk()
|
||||
})
|
||||
initSilk();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.speed, props.scale, props.color, props.noiseIntensity, props.rotation],
|
||||
() => {}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,75 +3,75 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern
|
||||
type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern;
|
||||
|
||||
interface GridOffset {
|
||||
x: number
|
||||
y: number
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
direction?: "diagonal" | "up" | "right" | "down" | "left"
|
||||
speed?: number
|
||||
borderColor?: CanvasStrokeStyle
|
||||
squareSize?: number
|
||||
hoverFillColor?: CanvasStrokeStyle
|
||||
direction?: 'diagonal' | 'up' | 'right' | 'down' | 'left';
|
||||
speed?: number;
|
||||
borderColor?: CanvasStrokeStyle;
|
||||
squareSize?: number;
|
||||
hoverFillColor?: CanvasStrokeStyle;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
direction: "right",
|
||||
direction: 'right',
|
||||
speed: 1,
|
||||
borderColor: "#999",
|
||||
borderColor: '#999',
|
||||
squareSize: 40,
|
||||
hoverFillColor: "#222"
|
||||
})
|
||||
hoverFillColor: '#222'
|
||||
});
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const requestRef = ref<number | null>(null)
|
||||
const numSquaresX = ref<number>(0)
|
||||
const numSquaresY = ref<number>(0)
|
||||
const gridOffset = ref<GridOffset>({ x: 0, y: 0 })
|
||||
const hoveredSquareRef = ref<GridOffset | null>(null)
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const requestRef = ref<number | null>(null);
|
||||
const numSquaresX = ref<number>(0);
|
||||
const numSquaresY = ref<number>(0);
|
||||
const gridOffset = ref<GridOffset>({ x: 0, y: 0 });
|
||||
const hoveredSquareRef = ref<GridOffset | null>(null);
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
numSquaresX.value = Math.ceil(canvas.width / props.squareSize) + 1
|
||||
numSquaresY.value = Math.ceil(canvas.height / props.squareSize) + 1
|
||||
}
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
numSquaresX.value = Math.ceil(canvas.width / props.squareSize) + 1;
|
||||
numSquaresY.value = Math.ceil(canvas.height / props.squareSize) + 1;
|
||||
};
|
||||
|
||||
const drawGrid = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!ctx || !canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!ctx || !canvas) return;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize
|
||||
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize
|
||||
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize;
|
||||
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize;
|
||||
|
||||
for (let x = startX; x < canvas.width + props.squareSize; x += props.squareSize) {
|
||||
for (let y = startY; y < canvas.height + props.squareSize; y += props.squareSize) {
|
||||
const squareX = x - (gridOffset.value.x % props.squareSize)
|
||||
const squareY = y - (gridOffset.value.y % props.squareSize)
|
||||
const squareX = x - (gridOffset.value.x % props.squareSize);
|
||||
const squareY = y - (gridOffset.value.y % props.squareSize);
|
||||
|
||||
if (
|
||||
hoveredSquareRef.value &&
|
||||
Math.floor((x - startX) / props.squareSize) === hoveredSquareRef.value.x &&
|
||||
Math.floor((y - startY) / props.squareSize) === hoveredSquareRef.value.y
|
||||
) {
|
||||
ctx.fillStyle = props.hoverFillColor
|
||||
ctx.fillRect(squareX, squareY, props.squareSize, props.squareSize)
|
||||
ctx.fillStyle = props.hoverFillColor;
|
||||
ctx.fillRect(squareX, squareY, props.squareSize, props.squareSize);
|
||||
}
|
||||
|
||||
ctx.strokeStyle = props.borderColor
|
||||
ctx.strokeRect(squareX, squareY, props.squareSize, props.squareSize)
|
||||
ctx.strokeStyle = props.borderColor;
|
||||
ctx.strokeRect(squareX, squareY, props.squareSize, props.squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,116 +82,118 @@ const drawGrid = () => {
|
||||
canvas.width / 2,
|
||||
canvas.height / 2,
|
||||
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2
|
||||
)
|
||||
gradient.addColorStop(0, "rgba(0, 0, 0, 0)")
|
||||
gradient.addColorStop(1, "#0b0b0b")
|
||||
);
|
||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
||||
gradient.addColorStop(1, '#0b0b0b');
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
const updateAnimation = () => {
|
||||
const effectiveSpeed = Math.max(props.speed, 0.1)
|
||||
const effectiveSpeed = Math.max(props.speed, 0.1);
|
||||
|
||||
switch (props.direction) {
|
||||
case "right":
|
||||
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "left":
|
||||
gridOffset.value.x = (gridOffset.value.x + effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "up":
|
||||
gridOffset.value.y = (gridOffset.value.y + effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "down":
|
||||
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case "diagonal":
|
||||
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize
|
||||
break
|
||||
case 'right':
|
||||
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize;
|
||||
break;
|
||||
case 'left':
|
||||
gridOffset.value.x = (gridOffset.value.x + effectiveSpeed + props.squareSize) % props.squareSize;
|
||||
break;
|
||||
case 'up':
|
||||
gridOffset.value.y = (gridOffset.value.y + effectiveSpeed + props.squareSize) % props.squareSize;
|
||||
break;
|
||||
case 'down':
|
||||
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize;
|
||||
break;
|
||||
case 'diagonal':
|
||||
gridOffset.value.x = (gridOffset.value.x - effectiveSpeed + props.squareSize) % props.squareSize;
|
||||
gridOffset.value.y = (gridOffset.value.y - effectiveSpeed + props.squareSize) % props.squareSize;
|
||||
break;
|
||||
default:
|
||||
break
|
||||
break;
|
||||
}
|
||||
|
||||
drawGrid()
|
||||
requestRef.value = requestAnimationFrame(updateAnimation)
|
||||
}
|
||||
drawGrid();
|
||||
requestRef.value = requestAnimationFrame(updateAnimation);
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const mouseX = event.clientX - rect.left
|
||||
const mouseY = event.clientY - rect.top
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = event.clientX - rect.left;
|
||||
const mouseY = event.clientY - rect.top;
|
||||
|
||||
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize
|
||||
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize
|
||||
const startX = Math.floor(gridOffset.value.x / props.squareSize) * props.squareSize;
|
||||
const startY = Math.floor(gridOffset.value.y / props.squareSize) * props.squareSize;
|
||||
|
||||
const hoveredSquareX = Math.floor(
|
||||
(mouseX + gridOffset.value.x - startX) / props.squareSize
|
||||
)
|
||||
const hoveredSquareY = Math.floor(
|
||||
(mouseY + gridOffset.value.y - startY) / props.squareSize
|
||||
)
|
||||
const hoveredSquareX = Math.floor((mouseX + gridOffset.value.x - startX) / props.squareSize);
|
||||
const hoveredSquareY = Math.floor((mouseY + gridOffset.value.y - startY) / props.squareSize);
|
||||
|
||||
if (
|
||||
!hoveredSquareRef.value ||
|
||||
hoveredSquareRef.value.x !== hoveredSquareX ||
|
||||
hoveredSquareRef.value.y !== hoveredSquareY
|
||||
) {
|
||||
hoveredSquareRef.value = { x: hoveredSquareX, y: hoveredSquareY }
|
||||
hoveredSquareRef.value = { x: hoveredSquareX, y: hoveredSquareY };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
hoveredSquareRef.value = null
|
||||
}
|
||||
hoveredSquareRef.value = null;
|
||||
};
|
||||
|
||||
const initializeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
ctx = canvas.getContext("2d")
|
||||
resizeCanvas()
|
||||
ctx = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
|
||||
canvas.addEventListener("mousemove", handleMouseMove)
|
||||
canvas.addEventListener("mouseleave", handleMouseLeave)
|
||||
window.addEventListener("resize", resizeCanvas)
|
||||
canvas.addEventListener('mousemove', handleMouseMove);
|
||||
canvas.addEventListener('mouseleave', handleMouseLeave);
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
requestRef.value = requestAnimationFrame(updateAnimation)
|
||||
}
|
||||
requestRef.value = requestAnimationFrame(updateAnimation);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
const canvas = canvasRef.value
|
||||
const canvas = canvasRef.value;
|
||||
|
||||
if (requestRef.value) {
|
||||
cancelAnimationFrame(requestRef.value)
|
||||
requestRef.value = null
|
||||
cancelAnimationFrame(requestRef.value);
|
||||
requestRef.value = null;
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
canvas.removeEventListener("mousemove", handleMouseMove)
|
||||
canvas.removeEventListener("mouseleave", handleMouseLeave)
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('mouseleave', handleMouseLeave);
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", resizeCanvas)
|
||||
}
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeCanvas()
|
||||
})
|
||||
initializeCanvas();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.direction, () => props.speed, () => props.borderColor, () => props.hoverFillColor, () => props.squareSize],
|
||||
[
|
||||
() => props.direction,
|
||||
() => props.speed,
|
||||
() => props.borderColor,
|
||||
() => props.hoverFillColor,
|
||||
() => props.squareSize
|
||||
],
|
||||
() => {
|
||||
cleanup()
|
||||
initializeCanvas()
|
||||
cleanup();
|
||||
initializeCanvas();
|
||||
}
|
||||
)
|
||||
</script>
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Renderer, Program, Mesh, Triangle, Color } from 'ogl'
|
||||
import type { OGLRenderingContext } from 'ogl'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Renderer, Program, Mesh, Triangle, Color } from 'ogl';
|
||||
import type { OGLRenderingContext } from 'ogl';
|
||||
|
||||
interface Props {
|
||||
color?: [number, number, number]
|
||||
amplitude?: number
|
||||
distance?: number
|
||||
enableMouseInteraction?: boolean
|
||||
color?: [number, number, number];
|
||||
amplitude?: number;
|
||||
distance?: number;
|
||||
enableMouseInteraction?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -19,17 +19,17 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
amplitude: 1,
|
||||
distance: 0,
|
||||
enableMouseInteraction: false
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
let renderer: Renderer | null = null
|
||||
let gl: OGLRenderingContext | null = null
|
||||
let program: Program | null = null
|
||||
let mesh: Mesh | null = null
|
||||
let animationId: number | null = null
|
||||
let currentMouse = [0.5, 0.5]
|
||||
let targetMouse = [0.5, 0.5]
|
||||
let renderer: Renderer | null = null;
|
||||
let gl: OGLRenderingContext | null = null;
|
||||
let program: Program | null = null;
|
||||
let mesh: Mesh | null = null;
|
||||
let animationId: number | null = null;
|
||||
let currentMouse = [0.5, 0.5];
|
||||
let targetMouse = [0.5, 0.5];
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 position;
|
||||
@@ -39,7 +39,7 @@ void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
@@ -146,145 +146,141 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
void main() {
|
||||
mainImage(gl_FragColor, gl_FragCoord.xy);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const resize = () => {
|
||||
if (!containerRef.value || !renderer || !program) return
|
||||
if (!containerRef.value || !renderer || !program) return;
|
||||
|
||||
const container = containerRef.value
|
||||
const { clientWidth, clientHeight } = container
|
||||
renderer.setSize(clientWidth, clientHeight)
|
||||
program.uniforms.iResolution.value.r = clientWidth
|
||||
program.uniforms.iResolution.value.g = clientHeight
|
||||
program.uniforms.iResolution.value.b = clientWidth / clientHeight
|
||||
}
|
||||
const container = containerRef.value;
|
||||
const { clientWidth, clientHeight } = container;
|
||||
renderer.setSize(clientWidth, clientHeight);
|
||||
program.uniforms.iResolution.value.r = clientWidth;
|
||||
program.uniforms.iResolution.value.g = clientHeight;
|
||||
program.uniforms.iResolution.value.b = clientWidth / clientHeight;
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!containerRef.value) return
|
||||
if (!containerRef.value) return;
|
||||
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
const x = (e.clientX - rect.left) / rect.width
|
||||
const y = 1.0 - (e.clientY - rect.top) / rect.height
|
||||
targetMouse = [x, y]
|
||||
}
|
||||
const rect = containerRef.value.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = 1.0 - (e.clientY - rect.top) / rect.height;
|
||||
targetMouse = [x, y];
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
targetMouse = [0.5, 0.5]
|
||||
}
|
||||
targetMouse = [0.5, 0.5];
|
||||
};
|
||||
|
||||
const update = (t: number) => {
|
||||
if (!program || !renderer || !mesh) return
|
||||
if (!program || !renderer || !mesh) return;
|
||||
|
||||
if (props.enableMouseInteraction) {
|
||||
const smoothing = 0.05
|
||||
currentMouse[0] += smoothing * (targetMouse[0] - currentMouse[0])
|
||||
currentMouse[1] += smoothing * (targetMouse[1] - currentMouse[1])
|
||||
program.uniforms.uMouse.value[0] = currentMouse[0]
|
||||
program.uniforms.uMouse.value[1] = currentMouse[1]
|
||||
const smoothing = 0.05;
|
||||
currentMouse[0] += smoothing * (targetMouse[0] - currentMouse[0]);
|
||||
currentMouse[1] += smoothing * (targetMouse[1] - currentMouse[1]);
|
||||
program.uniforms.uMouse.value[0] = currentMouse[0];
|
||||
program.uniforms.uMouse.value[1] = currentMouse[1];
|
||||
} else {
|
||||
program.uniforms.uMouse.value[0] = 0.5
|
||||
program.uniforms.uMouse.value[1] = 0.5
|
||||
program.uniforms.uMouse.value[0] = 0.5;
|
||||
program.uniforms.uMouse.value[1] = 0.5;
|
||||
}
|
||||
|
||||
program.uniforms.iTime.value = t * 0.001
|
||||
renderer.render({ scene: mesh })
|
||||
animationId = requestAnimationFrame(update)
|
||||
}
|
||||
program.uniforms.iTime.value = t * 0.001;
|
||||
renderer.render({ scene: mesh });
|
||||
animationId = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
const initializeScene = () => {
|
||||
if (!containerRef.value) return
|
||||
if (!containerRef.value) return;
|
||||
|
||||
cleanup()
|
||||
cleanup();
|
||||
|
||||
const container = containerRef.value
|
||||
renderer = new Renderer({ alpha: true })
|
||||
gl = renderer.gl
|
||||
gl.clearColor(0, 0, 0, 0)
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
const container = containerRef.value;
|
||||
renderer = new Renderer({ alpha: true });
|
||||
gl = renderer.gl;
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
const geometry = new Triangle(gl)
|
||||
const geometry = new Triangle(gl);
|
||||
program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
uniforms: {
|
||||
iTime: { value: 0 },
|
||||
iResolution: {
|
||||
value: new Color(
|
||||
gl.canvas.width,
|
||||
gl.canvas.height,
|
||||
gl.canvas.width / gl.canvas.height
|
||||
)
|
||||
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
|
||||
},
|
||||
uColor: { value: new Color(...props.color) },
|
||||
uAmplitude: { value: props.amplitude },
|
||||
uDistance: { value: props.distance },
|
||||
uMouse: { value: new Float32Array([0.5, 0.5]) }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
mesh = new Mesh(gl, { geometry, program })
|
||||
mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
const canvas = gl.canvas as HTMLCanvasElement
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.display = 'block'
|
||||
const canvas = gl.canvas as HTMLCanvasElement;
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.display = 'block';
|
||||
|
||||
container.appendChild(canvas)
|
||||
container.appendChild(canvas);
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
window.addEventListener('resize', resize);
|
||||
if (props.enableMouseInteraction) {
|
||||
container.addEventListener('mousemove', handleMouseMove)
|
||||
container.addEventListener('mouseleave', handleMouseLeave)
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
container.addEventListener('mouseleave', handleMouseLeave);
|
||||
}
|
||||
|
||||
resize()
|
||||
animationId = requestAnimationFrame(update)
|
||||
}
|
||||
resize();
|
||||
animationId = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', resize)
|
||||
window.removeEventListener('resize', resize);
|
||||
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('mousemove', handleMouseMove)
|
||||
containerRef.value.removeEventListener('mouseleave', handleMouseLeave)
|
||||
containerRef.value.removeEventListener('mousemove', handleMouseMove);
|
||||
containerRef.value.removeEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
const canvas = containerRef.value.querySelector('canvas')
|
||||
const canvas = containerRef.value.querySelector('canvas');
|
||||
if (canvas) {
|
||||
containerRef.value.removeChild(canvas)
|
||||
containerRef.value.removeChild(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
if (gl) {
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
|
||||
renderer = null
|
||||
gl = null
|
||||
program = null
|
||||
mesh = null
|
||||
currentMouse = [0.5, 0.5]
|
||||
targetMouse = [0.5, 0.5]
|
||||
}
|
||||
renderer = null;
|
||||
gl = null;
|
||||
program = null;
|
||||
mesh = null;
|
||||
currentMouse = [0.5, 0.5];
|
||||
targetMouse = [0.5, 0.5];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeScene()
|
||||
})
|
||||
initializeScene();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.color, () => props.amplitude, () => props.distance, () => props.enableMouseInteraction],
|
||||
() => {
|
||||
initializeScene()
|
||||
initializeScene();
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
<template>
|
||||
<div ref="containerRef" :class="className" :style="{ backgroundColor, ...style }"
|
||||
class="absolute top-0 left-0 w-full h-full overflow-hidden">
|
||||
<div class="absolute top-0 left-0 bg-[#160000] rounded-full w-[0.5rem] h-[0.5rem]" :style="{
|
||||
transform: 'translate3d(calc(var(--x) - 50%), calc(var(--y) - 50%), 0)',
|
||||
willChange: 'transform',
|
||||
}" />
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="className"
|
||||
:style="{ backgroundColor, ...style }"
|
||||
class="absolute top-0 left-0 w-full h-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 left-0 bg-[#160000] rounded-full w-[0.5rem] h-[0.5rem]"
|
||||
:style="{
|
||||
transform: 'translate3d(calc(var(--x) - 50%), calc(var(--y) - 50%), 0)',
|
||||
willChange: 'transform'
|
||||
}"
|
||||
/>
|
||||
|
||||
<canvas ref="canvasRef" class="block w-full h-full" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue';
|
||||
|
||||
class Grad {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
|
||||
constructor(x: number, y: number, z: number) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.z = z
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
dot2(x: number, y: number): number {
|
||||
return this.x * x + this.y * y
|
||||
return this.x * x + this.y * y;
|
||||
}
|
||||
}
|
||||
|
||||
class Noise {
|
||||
grad3: Grad[]
|
||||
p: number[]
|
||||
perm: number[]
|
||||
gradP: Grad[]
|
||||
grad3: Grad[];
|
||||
p: number[];
|
||||
perm: number[];
|
||||
gradP: Grad[];
|
||||
|
||||
constructor(seed = 0) {
|
||||
this.grad3 = [
|
||||
@@ -47,122 +55,113 @@ class Noise {
|
||||
new Grad(0, 1, 1),
|
||||
new Grad(0, -1, 1),
|
||||
new Grad(0, 1, -1),
|
||||
new Grad(0, -1, -1),
|
||||
]
|
||||
new Grad(0, -1, -1)
|
||||
];
|
||||
|
||||
this.p = [
|
||||
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
|
||||
140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247,
|
||||
120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177,
|
||||
33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165,
|
||||
71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211,
|
||||
133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25,
|
||||
63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
|
||||
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217,
|
||||
226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206,
|
||||
59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248,
|
||||
152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22,
|
||||
39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218,
|
||||
246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
|
||||
81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
|
||||
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
|
||||
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
|
||||
]
|
||||
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240,
|
||||
21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88,
|
||||
237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83,
|
||||
111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216,
|
||||
80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186,
|
||||
3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58,
|
||||
17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
||||
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193,
|
||||
238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
|
||||
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128,
|
||||
195, 78, 66, 215, 61, 156, 180
|
||||
];
|
||||
|
||||
this.perm = new Array(512)
|
||||
this.gradP = new Array(512)
|
||||
this.seed(seed)
|
||||
this.perm = new Array(512);
|
||||
this.gradP = new Array(512);
|
||||
this.seed(seed);
|
||||
}
|
||||
|
||||
seed(seed: number) {
|
||||
if (seed > 0 && seed < 1) seed *= 65536
|
||||
seed = Math.floor(seed)
|
||||
if (seed < 256) seed |= seed << 8
|
||||
if (seed > 0 && seed < 1) seed *= 65536;
|
||||
seed = Math.floor(seed);
|
||||
if (seed < 256) seed |= seed << 8;
|
||||
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const v = i & 1 ? this.p[i] ^ (seed & 255) : this.p[i] ^ ((seed >> 8) & 255)
|
||||
this.perm[i] = this.perm[i + 256] = v
|
||||
this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12]
|
||||
const v = i & 1 ? this.p[i] ^ (seed & 255) : this.p[i] ^ ((seed >> 8) & 255);
|
||||
this.perm[i] = this.perm[i + 256] = v;
|
||||
this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12];
|
||||
}
|
||||
}
|
||||
|
||||
fade(t: number): number {
|
||||
return t * t * t * (t * (t * 6 - 15) + 10)
|
||||
return t * t * t * (t * (t * 6 - 15) + 10);
|
||||
}
|
||||
|
||||
lerp(a: number, b: number, t: number): number {
|
||||
return (1 - t) * a + t * b
|
||||
return (1 - t) * a + t * b;
|
||||
}
|
||||
|
||||
perlin2(x: number, y: number): number {
|
||||
let X = Math.floor(x),
|
||||
Y = Math.floor(y)
|
||||
x -= X
|
||||
y -= Y
|
||||
X &= 255
|
||||
Y &= 255
|
||||
Y = Math.floor(y);
|
||||
x -= X;
|
||||
y -= Y;
|
||||
X &= 255;
|
||||
Y &= 255;
|
||||
|
||||
const n00 = this.gradP[X + this.perm[Y]].dot2(x, y)
|
||||
const n01 = this.gradP[X + this.perm[Y + 1]].dot2(x, y - 1)
|
||||
const n10 = this.gradP[X + 1 + this.perm[Y]].dot2(x - 1, y)
|
||||
const n11 = this.gradP[X + 1 + this.perm[Y + 1]].dot2(x - 1, y - 1)
|
||||
const u = this.fade(x)
|
||||
const n00 = this.gradP[X + this.perm[Y]].dot2(x, y);
|
||||
const n01 = this.gradP[X + this.perm[Y + 1]].dot2(x, y - 1);
|
||||
const n10 = this.gradP[X + 1 + this.perm[Y]].dot2(x - 1, y);
|
||||
const n11 = this.gradP[X + 1 + this.perm[Y + 1]].dot2(x - 1, y - 1);
|
||||
const u = this.fade(x);
|
||||
|
||||
return this.lerp(
|
||||
this.lerp(n00, n10, u),
|
||||
this.lerp(n01, n11, u),
|
||||
this.fade(y)
|
||||
)
|
||||
return this.lerp(this.lerp(n00, n10, u), this.lerp(n01, n11, u), this.fade(y));
|
||||
}
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
wave: { x: number; y: number }
|
||||
cursor: { x: number; y: number; vx: number; vy: number }
|
||||
x: number;
|
||||
y: number;
|
||||
wave: { x: number; y: number };
|
||||
cursor: { x: number; y: number; vx: number; vy: number };
|
||||
}
|
||||
|
||||
interface Mouse {
|
||||
x: number
|
||||
y: number
|
||||
lx: number
|
||||
ly: number
|
||||
sx: number
|
||||
sy: number
|
||||
v: number
|
||||
vs: number
|
||||
a: number
|
||||
set: boolean
|
||||
x: number;
|
||||
y: number;
|
||||
lx: number;
|
||||
ly: number;
|
||||
sx: number;
|
||||
sy: number;
|
||||
v: number;
|
||||
vs: number;
|
||||
a: number;
|
||||
set: boolean;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
lineColor: string
|
||||
waveSpeedX: number
|
||||
waveSpeedY: number
|
||||
waveAmpX: number
|
||||
waveAmpY: number
|
||||
friction: number
|
||||
tension: number
|
||||
maxCursorMove: number
|
||||
xGap: number
|
||||
yGap: number
|
||||
lineColor: string;
|
||||
waveSpeedX: number;
|
||||
waveSpeedY: number;
|
||||
waveAmpX: number;
|
||||
waveAmpY: number;
|
||||
friction: number;
|
||||
tension: number;
|
||||
maxCursorMove: number;
|
||||
xGap: number;
|
||||
yGap: number;
|
||||
}
|
||||
|
||||
interface WavesProps {
|
||||
lineColor?: string
|
||||
backgroundColor?: string
|
||||
waveSpeedX?: number
|
||||
waveSpeedY?: number
|
||||
waveAmpX?: number
|
||||
waveAmpY?: number
|
||||
xGap?: number
|
||||
yGap?: number
|
||||
friction?: number
|
||||
tension?: number
|
||||
maxCursorMove?: number
|
||||
style?: CSSProperties
|
||||
className?: string
|
||||
lineColor?: string;
|
||||
backgroundColor?: string;
|
||||
waveSpeedX?: number;
|
||||
waveSpeedY?: number;
|
||||
waveAmpX?: number;
|
||||
waveAmpY?: number;
|
||||
xGap?: number;
|
||||
yGap?: number;
|
||||
friction?: number;
|
||||
tension?: number;
|
||||
maxCursorMove?: number;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<WavesProps>(), {
|
||||
@@ -179,15 +178,15 @@ const props = withDefaults(defineProps<WavesProps>(), {
|
||||
maxCursorMove: 100,
|
||||
style: () => ({}),
|
||||
className: ''
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
const canvasRef = ref<HTMLCanvasElement>();
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let bounding = { width: 0, height: 0, left: 0, top: 0 }
|
||||
let noise: Noise | null = null
|
||||
let lines: Point[][] = []
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let bounding = { width: 0, height: 0, left: 0, top: 0 };
|
||||
let noise: Noise | null = null;
|
||||
let lines: Point[][] = [];
|
||||
const mouse: Mouse = {
|
||||
x: -10,
|
||||
y: 0,
|
||||
@@ -198,8 +197,8 @@ const mouse: Mouse = {
|
||||
v: 0,
|
||||
vs: 0,
|
||||
a: 0,
|
||||
set: false,
|
||||
}
|
||||
set: false
|
||||
};
|
||||
let config: Config = {
|
||||
lineColor: props.lineColor,
|
||||
waveSpeedX: props.waveSpeedX,
|
||||
@@ -210,207 +209,187 @@ let config: Config = {
|
||||
tension: props.tension,
|
||||
maxCursorMove: props.maxCursorMove,
|
||||
xGap: props.xGap,
|
||||
yGap: props.yGap,
|
||||
}
|
||||
let frameId: number | null = null
|
||||
yGap: props.yGap
|
||||
};
|
||||
let frameId: number | null = null;
|
||||
|
||||
const setSize = () => {
|
||||
const container = containerRef.value
|
||||
const canvas = canvasRef.value
|
||||
if (!container || !canvas) return
|
||||
const container = containerRef.value;
|
||||
const canvas = canvasRef.value;
|
||||
if (!container || !canvas) return;
|
||||
|
||||
const rect = container.getBoundingClientRect()
|
||||
const rect = container.getBoundingClientRect();
|
||||
bounding = {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
}
|
||||
canvas.width = rect.width
|
||||
canvas.height = rect.height
|
||||
}
|
||||
top: rect.top
|
||||
};
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
};
|
||||
|
||||
const setLines = () => {
|
||||
const { width, height } = bounding
|
||||
lines = []
|
||||
const { width, height } = bounding;
|
||||
lines = [];
|
||||
const oWidth = width + 200,
|
||||
oHeight = height + 30
|
||||
const { xGap, yGap } = config
|
||||
const totalLines = Math.ceil(oWidth / xGap)
|
||||
const totalPoints = Math.ceil(oHeight / yGap)
|
||||
const xStart = (width - xGap * totalLines) / 2
|
||||
const yStart = (height - yGap * totalPoints) / 2
|
||||
oHeight = height + 30;
|
||||
const { xGap, yGap } = config;
|
||||
const totalLines = Math.ceil(oWidth / xGap);
|
||||
const totalPoints = Math.ceil(oHeight / yGap);
|
||||
const xStart = (width - xGap * totalLines) / 2;
|
||||
const yStart = (height - yGap * totalPoints) / 2;
|
||||
|
||||
for (let i = 0; i <= totalLines; i++) {
|
||||
const pts: Point[] = []
|
||||
const pts: Point[] = [];
|
||||
for (let j = 0; j <= totalPoints; j++) {
|
||||
pts.push({
|
||||
x: xStart + xGap * i,
|
||||
y: yStart + yGap * j,
|
||||
wave: { x: 0, y: 0 },
|
||||
cursor: { x: 0, y: 0, vx: 0, vy: 0 },
|
||||
})
|
||||
cursor: { x: 0, y: 0, vx: 0, vy: 0 }
|
||||
});
|
||||
}
|
||||
lines.push(pts)
|
||||
lines.push(pts);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const movePoints = (time: number) => {
|
||||
if (!noise) return
|
||||
if (!noise) return;
|
||||
|
||||
const {
|
||||
waveSpeedX,
|
||||
waveSpeedY,
|
||||
waveAmpX,
|
||||
waveAmpY,
|
||||
friction,
|
||||
tension,
|
||||
maxCursorMove,
|
||||
} = config
|
||||
const { waveSpeedX, waveSpeedY, waveAmpX, waveAmpY, friction, tension, maxCursorMove } = config;
|
||||
|
||||
lines.forEach((pts) => {
|
||||
pts.forEach((p) => {
|
||||
const move = noise!.perlin2(
|
||||
(p.x + time * waveSpeedX) * 0.002,
|
||||
(p.y + time * waveSpeedY) * 0.0015
|
||||
) * 12
|
||||
p.wave.x = Math.cos(move) * waveAmpX
|
||||
p.wave.y = Math.sin(move) * waveAmpY
|
||||
lines.forEach(pts => {
|
||||
pts.forEach(p => {
|
||||
const move = noise!.perlin2((p.x + time * waveSpeedX) * 0.002, (p.y + time * waveSpeedY) * 0.0015) * 12;
|
||||
p.wave.x = Math.cos(move) * waveAmpX;
|
||||
p.wave.y = Math.sin(move) * waveAmpY;
|
||||
|
||||
const dx = p.x - mouse.sx,
|
||||
dy = p.y - mouse.sy
|
||||
const dist = Math.hypot(dx, dy)
|
||||
const l = Math.max(175, mouse.vs)
|
||||
dy = p.y - mouse.sy;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
const l = Math.max(175, mouse.vs);
|
||||
if (dist < l) {
|
||||
const s = 1 - dist / l
|
||||
const f = Math.cos(dist * 0.001) * s
|
||||
p.cursor.vx += Math.cos(mouse.a) * f * l * mouse.vs * 0.00065
|
||||
p.cursor.vy += Math.sin(mouse.a) * f * l * mouse.vs * 0.00065
|
||||
const s = 1 - dist / l;
|
||||
const f = Math.cos(dist * 0.001) * s;
|
||||
p.cursor.vx += Math.cos(mouse.a) * f * l * mouse.vs * 0.00065;
|
||||
p.cursor.vy += Math.sin(mouse.a) * f * l * mouse.vs * 0.00065;
|
||||
}
|
||||
|
||||
p.cursor.vx += (0 - p.cursor.x) * tension
|
||||
p.cursor.vy += (0 - p.cursor.y) * tension
|
||||
p.cursor.vx *= friction
|
||||
p.cursor.vy *= friction
|
||||
p.cursor.x += p.cursor.vx * 2
|
||||
p.cursor.y += p.cursor.vy * 2
|
||||
p.cursor.x = Math.min(
|
||||
maxCursorMove,
|
||||
Math.max(-maxCursorMove, p.cursor.x)
|
||||
)
|
||||
p.cursor.y = Math.min(
|
||||
maxCursorMove,
|
||||
Math.max(-maxCursorMove, p.cursor.y)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
p.cursor.vx += (0 - p.cursor.x) * tension;
|
||||
p.cursor.vy += (0 - p.cursor.y) * tension;
|
||||
p.cursor.vx *= friction;
|
||||
p.cursor.vy *= friction;
|
||||
p.cursor.x += p.cursor.vx * 2;
|
||||
p.cursor.y += p.cursor.vy * 2;
|
||||
p.cursor.x = Math.min(maxCursorMove, Math.max(-maxCursorMove, p.cursor.x));
|
||||
p.cursor.y = Math.min(maxCursorMove, Math.max(-maxCursorMove, p.cursor.y));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const moved = (point: Point, withCursor = true): { x: number; y: number } => {
|
||||
const x = point.x + point.wave.x + (withCursor ? point.cursor.x : 0)
|
||||
const y = point.y + point.wave.y + (withCursor ? point.cursor.y : 0)
|
||||
return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 }
|
||||
}
|
||||
const x = point.x + point.wave.x + (withCursor ? point.cursor.x : 0);
|
||||
const y = point.y + point.wave.y + (withCursor ? point.cursor.y : 0);
|
||||
return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 };
|
||||
};
|
||||
|
||||
const drawLines = () => {
|
||||
const { width, height } = bounding
|
||||
if (!ctx) return
|
||||
const { width, height } = bounding;
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = config.lineColor
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = config.lineColor;
|
||||
|
||||
lines.forEach((points) => {
|
||||
let p1 = moved(points[0], false)
|
||||
ctx!.moveTo(p1.x, p1.y)
|
||||
lines.forEach(points => {
|
||||
let p1 = moved(points[0], false);
|
||||
ctx!.moveTo(p1.x, p1.y);
|
||||
points.forEach((p, idx) => {
|
||||
const isLast = idx === points.length - 1
|
||||
p1 = moved(p, !isLast)
|
||||
const p2 = moved(
|
||||
points[idx + 1] || points[points.length - 1],
|
||||
!isLast
|
||||
)
|
||||
ctx!.lineTo(p1.x, p1.y)
|
||||
if (isLast) ctx!.moveTo(p2.x, p2.y)
|
||||
})
|
||||
})
|
||||
ctx.stroke()
|
||||
}
|
||||
const isLast = idx === points.length - 1;
|
||||
p1 = moved(p, !isLast);
|
||||
const p2 = moved(points[idx + 1] || points[points.length - 1], !isLast);
|
||||
ctx!.lineTo(p1.x, p1.y);
|
||||
if (isLast) ctx!.moveTo(p2.x, p2.y);
|
||||
});
|
||||
});
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const tick = (t: number) => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
mouse.sx += (mouse.x - mouse.sx) * 0.1
|
||||
mouse.sy += (mouse.y - mouse.sy) * 0.1
|
||||
mouse.sx += (mouse.x - mouse.sx) * 0.1;
|
||||
mouse.sy += (mouse.y - mouse.sy) * 0.1;
|
||||
const dx = mouse.x - mouse.lx,
|
||||
dy = mouse.y - mouse.ly
|
||||
const d = Math.hypot(dx, dy)
|
||||
mouse.v = d
|
||||
mouse.vs += (d - mouse.vs) * 0.1
|
||||
mouse.vs = Math.min(100, mouse.vs)
|
||||
mouse.lx = mouse.x
|
||||
mouse.ly = mouse.y
|
||||
mouse.a = Math.atan2(dy, dx)
|
||||
container.style.setProperty('--x', `${mouse.sx}px`)
|
||||
container.style.setProperty('--y', `${mouse.sy}px`)
|
||||
dy = mouse.y - mouse.ly;
|
||||
const d = Math.hypot(dx, dy);
|
||||
mouse.v = d;
|
||||
mouse.vs += (d - mouse.vs) * 0.1;
|
||||
mouse.vs = Math.min(100, mouse.vs);
|
||||
mouse.lx = mouse.x;
|
||||
mouse.ly = mouse.y;
|
||||
mouse.a = Math.atan2(dy, dx);
|
||||
container.style.setProperty('--x', `${mouse.sx}px`);
|
||||
container.style.setProperty('--y', `${mouse.sy}px`);
|
||||
|
||||
movePoints(t)
|
||||
drawLines()
|
||||
frameId = requestAnimationFrame(tick)
|
||||
}
|
||||
movePoints(t);
|
||||
drawLines();
|
||||
frameId = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
setSize()
|
||||
setLines()
|
||||
}
|
||||
setSize();
|
||||
setLines();
|
||||
};
|
||||
|
||||
const updateMouse = (x: number, y: number) => {
|
||||
mouse.x = x - bounding.left
|
||||
mouse.y = y - bounding.top
|
||||
mouse.x = x - bounding.left;
|
||||
mouse.y = y - bounding.top;
|
||||
if (!mouse.set) {
|
||||
mouse.sx = mouse.x
|
||||
mouse.sy = mouse.y
|
||||
mouse.lx = mouse.x
|
||||
mouse.ly = mouse.y
|
||||
mouse.set = true
|
||||
mouse.sx = mouse.x;
|
||||
mouse.sy = mouse.y;
|
||||
mouse.lx = mouse.x;
|
||||
mouse.ly = mouse.y;
|
||||
mouse.set = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
updateMouse(e.clientX, e.clientY)
|
||||
}
|
||||
updateMouse(e.clientX, e.clientY);
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
const touch = e.touches[0]
|
||||
updateMouse(touch.clientX, touch.clientY)
|
||||
}
|
||||
const touch = e.touches[0];
|
||||
updateMouse(touch.clientX, touch.clientY);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasRef.value
|
||||
const container = containerRef.value
|
||||
if (!canvas || !container) return
|
||||
const canvas = canvasRef.value;
|
||||
const container = containerRef.value;
|
||||
if (!canvas || !container) return;
|
||||
|
||||
ctx = canvas.getContext('2d')
|
||||
noise = new Noise(Math.random())
|
||||
ctx = canvas.getContext('2d');
|
||||
noise = new Noise(Math.random());
|
||||
|
||||
setSize()
|
||||
setLines()
|
||||
frameId = requestAnimationFrame(tick)
|
||||
setSize();
|
||||
setLines();
|
||||
frameId = requestAnimationFrame(tick);
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||
})
|
||||
window.addEventListener('resize', onResize);
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('touchmove', onTouchMove)
|
||||
window.removeEventListener('resize', onResize);
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('touchmove', onTouchMove);
|
||||
if (frameId !== null) {
|
||||
cancelAnimationFrame(frameId)
|
||||
cancelAnimationFrame(frameId);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [
|
||||
@@ -423,7 +402,7 @@ watch(
|
||||
props.tension,
|
||||
props.maxCursorMove,
|
||||
props.xGap,
|
||||
props.yGap,
|
||||
props.yGap
|
||||
],
|
||||
() => {
|
||||
config = {
|
||||
@@ -436,8 +415,8 @@ watch(
|
||||
tension: props.tension,
|
||||
maxCursorMove: props.maxCursorMove,
|
||||
xGap: props.xGap,
|
||||
yGap: props.yGap,
|
||||
}
|
||||
yGap: props.yGap
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user