Add prettier config, format codebase

This commit is contained in:
David Haz
2025-07-12 11:59:33 +03:00
parent ac8b2c04d8
commit f4d97ee94e
211 changed files with 10586 additions and 8810 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>