Component Boom

This commit is contained in:
David Haz
2025-07-10 15:36:38 +03:00
parent a4982577ad
commit 9b3465b04d
135 changed files with 16697 additions and 60 deletions

View File

@@ -0,0 +1,266 @@
<template>
<div ref="containerRef" :class="className" :style="style" class="w-full h-full"></div>
</template>
<script setup lang="ts">
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
}
const props = withDefaults(defineProps<AuroraProps>(), {
colorStops: () => ['#7cff67', '#171D22', '#7cff67'],
amplitude: 1.0,
blend: 0.5,
speed: 1.0,
intensity: 1.0,
className: '',
style: () => ({})
})
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;
uniform float uTime;
uniform float uAmplitude;
uniform vec3 uColorStops[3];
uniform vec2 uResolution;
uniform float uBlend;
uniform float uIntensity;
out vec4 fragColor;
vec3 permute(vec3 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
float snoise(vec2 v){
const vec4 C = vec4(
0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439
);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute(
permute(i.y + vec3(0.0, i1.y, 1.0))
+ i.x + vec3(0.0, i1.x, 1.0)
);
vec3 m = max(
0.5 - vec3(
dot(x0, x0),
dot(x12.xy, x12.xy),
dot(x12.zw, x12.zw)
),
0.0
);
m = m * m;
m = m * m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
struct ColorStop {
vec3 color;
float position;
};
#define COLOR_RAMP(colors, factor, finalColor) { \
int index = 0; \
for (int i = 0; i < 2; i++) { \
ColorStop currentColor = colors[i]; \
bool isInBetween = currentColor.position <= factor; \
index = int(mix(float(index), float(i), float(isInBetween))); \
} \
ColorStop currentColor = colors[index]; \
ColorStop nextColor = colors[index + 1]; \
float range = nextColor.position - currentColor.position; \
float lerpFactor = (factor - currentColor.position) / range; \
finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \
}
void main() {
vec2 uv = gl_FragCoord.xy / uResolution;
ColorStop colors[3];
colors[0] = ColorStop(uColorStops[0], 0.0);
colors[1] = ColorStop(uColorStops[1], 0.5);
colors[2] = ColorStop(uColorStops[2], 1.0);
vec3 rampColor;
COLOR_RAMP(colors, uv.x, rampColor);
float height = snoise(vec2(uv.x * 2.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude;
height = exp(height);
height = (uv.y * 2.0 - height + 0.2);
float intensity = 0.6 * height;
float midPoint = 0.20;
float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity);
vec3 auroraColor = rampColor;
float finalAlpha = auroraAlpha * smoothstep(0.0, 0.5, intensity) * uIntensity;
fragColor = vec4(auroraColor * finalAlpha, finalAlpha);
}
`
let renderer: Renderer | null = null
let animateId = 0
const initAurora = () => {
const container = containerRef.value
if (!container) return
renderer = new Renderer({
alpha: true,
premultipliedAlpha: 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'
// eslint-disable-next-line prefer-const
let program: Program | undefined
const resize = () => {
if (!container) return
const width = container.offsetWidth
const height = container.offsetHeight
renderer!.setSize(width, height)
if (program) {
program.uniforms.uResolution.value = [width, height]
}
}
window.addEventListener('resize', resize)
const geometry = new Triangle(gl)
if (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]
})
program = new Program(gl, {
vertex: VERT,
fragment: FRAG,
uniforms: {
uTime: { value: 0 },
uAmplitude: { value: props.amplitude },
uColorStops: { value: colorStopsArray },
uResolution: { value: [container.offsetWidth, container.offsetHeight] },
uBlend: { value: props.blend },
uIntensity: { value: props.intensity },
},
})
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'
const update = (t: number) => {
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.uColorStops.value = stops.map((hex: string) => {
const c = new Color(hex)
return [c.r, c.g, c.b]
})
renderer!.render({ scene: mesh })
}
}
animateId = requestAnimationFrame(update)
resize()
return () => {
cancelAnimationFrame(animateId)
window.removeEventListener('resize', resize)
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
}
const cleanup = () => {
if (animateId) {
cancelAnimationFrame(animateId)
}
if (renderer) {
const gl = renderer.gl
const container = containerRef.value
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
renderer = null
}
onMounted(() => {
initAurora()
})
onUnmounted(() => {
cleanup()
})
watch(
() => [props.amplitude, props.intensity],
() => {
cleanup()
initAurora()
}
)
</script>

View File

@@ -0,0 +1,204 @@
<template>
<div ref="containerRef" class="w-full h-full" />
</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'
interface Props {
color?: [number, number, number]
speed?: number
amplitude?: number
mouseReact?: boolean
}
const props = withDefaults(defineProps<Props>(), {
color: () => [1, 1, 1] as [number, number, number],
speed: 1.0,
amplitude: 0.1,
mouseReact: true
})
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
const vertexShader = `
attribute vec2 uv;
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0, 1);
}
`
const fragmentShader = `
precision highp float;
uniform float uTime;
uniform vec3 uColor;
uniform vec3 uResolution;
uniform vec2 uMouse;
uniform float uAmplitude;
uniform float uSpeed;
varying vec2 vUv;
void main() {
float mr = min(uResolution.x, uResolution.y);
vec2 uv = (vUv.xy * 2.0 - 1.0) * uResolution.xy / mr;
uv += (uMouse - vec2(0.5)) * uAmplitude;
float d = -uTime * 0.5 * uSpeed;
float a = 0.0;
for (float i = 0.0; i < 8.0; ++i) {
a += cos(i - d - a * uv.x);
d += sin(uv.y * i + a);
}
d += uTime * 0.5 * uSpeed;
vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
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
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
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 }
if (program.uniforms.uMouse.value) {
program.uniforms.uMouse.value[0] = x
program.uniforms.uMouse.value[1] = y
}
}
const update = (t: number) => {
if (!program || !renderer || !mesh) return
animationId = requestAnimationFrame(update)
program.uniforms.uTime.value = t * 0.001
renderer.render({ scene: mesh })
}
const initializeScene = () => {
if (!containerRef.value) return
cleanup()
const container = containerRef.value
renderer = new Renderer()
gl = renderer.gl
gl.clearColor(1, 1, 1, 1)
const geometry = new Triangle(gl)
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
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
)
},
uMouse: { value: new Float32Array([mousePos.value.x, mousePos.value.y]) },
uAmplitude: { value: props.amplitude },
uSpeed: { value: props.speed }
}
})
mesh = new Mesh(gl, { geometry, program })
const canvas = gl.canvas as HTMLCanvasElement
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.style.display = 'block'
container.appendChild(canvas)
window.addEventListener('resize', resize)
if (props.mouseReact) {
container.addEventListener('mousemove', handleMouseMove)
}
resize()
animationId = requestAnimationFrame(update)
}
const cleanup = () => {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
window.removeEventListener('resize', resize)
if (containerRef.value) {
containerRef.value.removeEventListener('mousemove', handleMouseMove)
const canvas = containerRef.value.querySelector('canvas')
if (canvas) {
containerRef.value.removeChild(canvas)
}
}
if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
renderer = null
gl = null
program = null
mesh = null
}
onMounted(() => {
initializeScene()
})
onUnmounted(() => {
cleanup()
})
watch(
[() => props.color, () => props.speed, () => props.amplitude, () => props.mouseReact],
() => {
initializeScene()
},
{ deep: true }
)
</script>

View File

@@ -0,0 +1,236 @@
<template>
<canvas ref="canvasRef" class="w-full h-full block mix-blend-screen"></canvas>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
interface LightningProps {
hue?: number
xOffset?: number
speed?: number
intensity?: number
size?: number
}
const props = withDefaults(defineProps<LightningProps>(), {
hue: 230,
xOffset: 0,
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 vertexShaderSource = `
attribute vec2 aPosition;
void main() {
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`
const fragmentShaderSource = `
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uHue;
uniform float uXOffset;
uniform float uSpeed;
uniform float uIntensity;
uniform float uSize;
#define OCTAVE_COUNT 10
vec3 hsv2rgb(vec3 c) {
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0,4.0,2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return c.z * mix(vec3(1.0), rgb, c.y);
}
float hash11(float p) {
p = fract(p * .1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
mat2 rotate2d(float theta) {
float c = cos(theta);
float s = sin(theta);
return mat2(c, -s, s, c);
}
float noise(vec2 p) {
vec2 ip = floor(p);
vec2 fp = fract(p);
float a = hash12(ip);
float b = hash12(ip + vec2(1.0, 0.0));
float c = hash12(ip + vec2(0.0, 1.0));
float d = hash12(ip + vec2(1.0, 1.0));
vec2 t = smoothstep(0.0, 1.0, fp);
return mix(mix(a, b, t.x), mix(c, d, t.x), t.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
for (int i = 0; i < OCTAVE_COUNT; ++i) {
value += amplitude * noise(p);
p *= rotate2d(0.45);
p *= 2.0;
amplitude *= 0.5;
}
return value;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
vec2 uv = fragCoord / iResolution.xy;
uv = 2.0 * uv - 1.0;
uv.x *= iResolution.x / iResolution.y;
uv.x += uXOffset;
uv += 2.0 * fbm(uv * uSize + 0.8 * iTime * uSpeed) - 1.0;
float dist = abs(uv.x);
vec3 baseColor = hsv2rgb(vec3(uHue / 360.0, 0.7, 0.8));
vec3 col = baseColor * pow(mix(0.0, 0.07, hash11(iTime * uSpeed)) / dist, 1.0) * uIntensity;
col = pow(col, vec3(1.0));
fragColor = vec4(col, 1.0);
}
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.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader))
gl.deleteShader(shader)
return null
}
return shader
}
const initWebGL = () => {
const canvas = canvasRef.value
if (!canvas) return
const resizeCanvas = () => {
const rect = canvas.getBoundingClientRect()
canvas.width = rect.width
canvas.height = rect.height
canvas.style.width = rect.width + 'px'
canvas.style.height = rect.height + 'px'
}
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
gl = canvas.getContext('webgl')
if (!gl) {
console.error('WebGL not supported')
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)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program linking error:', gl.getProgramInfoLog(program))
return
}
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 aPosition = gl.getAttribLocation(program, 'aPosition')
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
startTime = performance.now()
render()
return () => {
window.removeEventListener('resize', resizeCanvas)
}
}
const render = () => {
if (!gl || !program || !canvasRef.value) return
const canvas = canvasRef.value
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'
}
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')
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)
}
onMounted(() => {
initWebGL()
})
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
})
watch(
() => [props.hue, props.xOffset, props.speed, props.intensity, props.size],
() => {}
)
</script>

View File

@@ -0,0 +1,314 @@
<template>
<div ref="containerRef" :class="className" class="relative w-full h-full"></div>
</template>
<script setup lang="ts">
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
}
const props = withDefaults(defineProps<ParticlesProps>(), {
particleCount: 200,
particleSpread: 10,
speed: 0.1,
particleColors: () => ['#ffffff'],
moveParticlesOnHover: false,
particleHoverFactor: 1,
alphaParticles: false,
particleBaseSize: 100,
sizeRandomness: 1,
cameraDistance: 20,
disableRotation: false,
className: ''
})
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
const defaultColors = ['#ffffff', '#ffffff', '#ffffff']
const hexToRgb = (hex: string): [number, number, number] => {
hex = hex.replace(/^#/, '')
if (hex.length === 3) {
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 vertex = /* glsl */ `
attribute vec3 position;
attribute vec4 random;
attribute vec3 color;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform float uTime;
uniform float uSpread;
uniform float uBaseSize;
uniform float uSizeRandomness;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vRandom = random;
vColor = color;
vec3 pos = position * uSpread;
pos.z *= 10.0;
vec4 mPos = modelMatrix * vec4(pos, 1.0);
float t = uTime;
mPos.x += sin(t * random.z + 6.28 * random.w) * mix(0.1, 1.5, random.x);
mPos.y += sin(t * random.y + 6.28 * random.x) * mix(0.1, 1.5, random.w);
mPos.z += sin(t * random.w + 6.28 * random.y) * mix(0.1, 1.5, random.z);
vec4 mvPos = viewMatrix * mPos;
gl_PointSize = (uBaseSize * (1.0 + uSizeRandomness * (random.x - 0.5))) / length(mvPos.xyz);
gl_Position = projectionMatrix * mvPos;
}
`
const fragment = /* glsl */ `
precision highp float;
uniform float uTime;
uniform float uAlphaParticles;
varying vec4 vRandom;
varying vec3 vColor;
void main() {
vec2 uv = gl_PointCoord.xy;
float d = length(uv - vec2(0.5));
if(uAlphaParticles < 0.5) {
if(d > 0.5) {
discard;
}
gl_FragColor = vec4(vColor + 0.2 * sin(uv.yxx + uTime + vRandom.y * 6.28), 1.0);
} else {
float circle = smoothstep(0.5, 0.4, d) * 0.8;
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 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
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'
camera = new Camera(gl, { fov: 15 })
camera.position.set(0, 0, props.cameraDistance)
const resize = () => {
const width = container.clientWidth
const height = container.clientHeight
renderer!.setSize(width, height)
camera!.perspective({ aspect: width / height })
gl.canvas.style.width = '100%'
gl.canvas.style.height = '100%'
gl.canvas.style.display = 'block'
}
window.addEventListener('resize', resize, false)
resize()
if (props.moveParticlesOnHover) {
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
for (let i = 0; i < count; i++) {
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)
}
const geometry = new Geometry(gl, {
position: { size: 3, data: positions },
random: { size: 4, data: randoms },
color: { size: 3, data: colors },
})
program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uSpread: { value: props.particleSpread },
uBaseSize: { value: props.particleBaseSize },
uSizeRandomness: { value: props.sizeRandomness },
uAlphaParticles: { value: props.alphaParticles ? 1 : 0 },
},
transparent: true,
depthTest: false,
})
particles = new Mesh(gl, { mode: gl.POINTS, geometry, program })
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 (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
}
if (particles) {
if (props.moveParticlesOnHover) {
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
}
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
}
}
if (renderer && camera && particles) {
renderer.render({ scene: particles, camera })
}
}
animationFrameId = requestAnimationFrame(update)
return () => {
window.removeEventListener('resize', resize)
if (props.moveParticlesOnHover) {
container.removeEventListener('mousemove', handleMouseMove)
}
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
if (container.contains(gl.canvas)) {
container.removeChild(gl.canvas)
}
}
}
const cleanup = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
if (renderer) {
const container = containerRef.value
const gl = renderer.gl
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
renderer = null
camera = null
particles = null
program = null
}
onMounted(() => {
initParticles()
})
onUnmounted(() => {
cleanup()
})
watch(
() => [props.particleCount, props.particleColors],
() => {
cleanup()
initParticles()
},
{ deep: true }
)
watch(
() => [
props.particleSpread,
props.speed,
props.particleBaseSize,
props.sizeRandomness,
props.alphaParticles,
props.moveParticlesOnHover,
props.particleHoverFactor,
props.disableRotation
],
() => {}
)
</script>

View File

@@ -0,0 +1,232 @@
<template>
<div ref="containerRef" :class="className" :style="style" class="w-full h-full"></div>
</template>
<script setup lang="ts">
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
}
const props = withDefaults(defineProps<SilkProps>(), {
speed: 5,
scale: 1,
color: '#7B7481',
noiseIntensity: 1.5,
rotation: 0,
className: '',
style: () => ({})
})
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 vertexShader = `
attribute vec2 uv;
attribute vec3 position;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vPosition = position;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
const fragmentShader = `
precision highp float;
varying vec2 vUv;
varying vec3 vPosition;
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uScale;
uniform float uRotation;
uniform float uNoiseIntensity;
const float e = 2.71828182845904523536;
float noise(vec2 texCoord) {
float G = e;
vec2 r = (G * sin(G * texCoord));
return fract(r.x * r.y * (1.0 + texCoord.x));
}
vec2 rotateUvs(vec2 uv, float angle) {
float c = cos(angle);
float s = sin(angle);
mat2 rot = mat2(c, -s, s, c);
return rot * uv;
}
void main() {
float rnd = noise(gl_FragCoord.xy);
vec2 uv = rotateUvs(vUv * uScale, uRotation);
vec2 tex = uv * uScale;
float tOffset = uSpeed * uTime;
tex.y += 0.03 * sin(8.0 * tex.x - tOffset);
float pattern = 0.6 +
0.4 * sin(5.0 * (tex.x + tex.y +
cos(3.0 * tex.x + 5.0 * tex.y) +
0.02 * tOffset) +
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
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
const initSilk = () => {
const container = containerRef.value
if (!container) return
renderer = new Renderer({
alpha: true,
antialias: true,
})
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
const resize = () => {
if (!container || !camera) return
const width = container.offsetWidth
const height = container.offsetHeight
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)
mesh.scale.set(width2, height2, 1)
}
}
window.addEventListener('resize', resize)
const geometry = new Plane(gl, {
width: 1,
height: 1,
})
const colorRGB = hexToNormalizedRGB(props.color)
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
uSpeed: { value: props.speed },
uScale: { value: props.scale },
uNoiseIntensity: { value: props.noiseIntensity },
uColor: { value: colorRGB },
uRotation: { value: props.rotation },
uTime: { value: 0 },
},
})
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'
let lastTime = 0
const update = (t: number) => {
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 })
}
}
animateId = requestAnimationFrame(update)
resize()
return () => {
cancelAnimationFrame(animateId)
window.removeEventListener('resize', resize)
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
}
const cleanup = () => {
if (animateId) {
cancelAnimationFrame(animateId)
}
if (renderer) {
const gl = renderer.gl
const container = containerRef.value
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas)
}
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
renderer = null
mesh = null
camera = null
program = null
}
onMounted(() => {
initSilk()
})
onUnmounted(() => {
cleanup()
})
watch(
() => [props.speed, props.scale, props.color, props.noiseIntensity, props.rotation],
() => {}
)
</script>

View File

@@ -0,0 +1,290 @@
<template>
<div ref="containerRef" class="w-full h-full relative" />
</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'
interface Props {
color?: [number, number, number]
amplitude?: number
distance?: number
enableMouseInteraction?: boolean
}
const props = withDefaults(defineProps<Props>(), {
color: () => [1, 1, 1] as [number, number, number],
amplitude: 1,
distance: 0,
enableMouseInteraction: false
})
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]
const vertexShader = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`
const fragmentShader = `
precision highp float;
uniform float iTime;
uniform vec3 iResolution;
uniform vec3 uColor;
uniform float uAmplitude;
uniform float uDistance;
uniform vec2 uMouse;
#define PI 3.1415926538
const int u_line_count = 40;
const float u_line_width = 7.0;
const float u_line_blur = 10.0;
float Perlin2D(vec2 P) {
vec2 Pi = floor(P);
vec4 Pf_Pfmin1 = P.xyxy - vec4(Pi, Pi + 1.0);
vec4 Pt = vec4(Pi.xy, Pi.xy + 1.0);
Pt = Pt - floor(Pt * (1.0 / 71.0)) * 71.0;
Pt += vec2(26.0, 161.0).xyxy;
Pt *= Pt;
Pt = Pt.xzxz * Pt.yyww;
vec4 hash_x = fract(Pt * (1.0 / 951.135664));
vec4 hash_y = fract(Pt * (1.0 / 642.949883));
vec4 grad_x = hash_x - 0.49999;
vec4 grad_y = hash_y - 0.49999;
vec4 grad_results = inversesqrt(grad_x * grad_x + grad_y * grad_y)
* (grad_x * Pf_Pfmin1.xzxz + grad_y * Pf_Pfmin1.yyww);
grad_results *= 1.4142135623730950;
vec2 blend = Pf_Pfmin1.xy * Pf_Pfmin1.xy * Pf_Pfmin1.xy
* (Pf_Pfmin1.xy * (Pf_Pfmin1.xy * 6.0 - 15.0) + 10.0);
vec4 blend2 = vec4(blend, vec2(1.0 - blend));
return dot(grad_results, blend2.zxzx * blend2.wwyy);
}
float pixel(float count, vec2 resolution) {
return (1.0 / max(resolution.x, resolution.y)) * count;
}
float lineFn(vec2 st, float width, float perc, float offset, vec2 mouse, float time, float amplitude, float distance) {
float split_offset = (perc * 0.4);
float split_point = 0.1 + split_offset;
float amplitude_normal = smoothstep(split_point, 0.7, st.x);
float amplitude_strength = 0.5;
float finalAmplitude = amplitude_normal * amplitude_strength
* amplitude * (1.0 + (mouse.y - 0.5) * 0.2);
float time_scaled = time / 10.0 + (mouse.x - 0.5) * 1.0;
float blur = smoothstep(split_point, split_point + 0.05, st.x) * perc;
float xnoise = mix(
Perlin2D(vec2(time_scaled, st.x + perc) * 2.5),
Perlin2D(vec2(time_scaled, st.x + time_scaled) * 3.5) / 1.5,
st.x * 0.3
);
float y = 0.5 + (perc - 0.5) * distance + xnoise / 2.0 * finalAmplitude;
float line_start = smoothstep(
y + (width / 2.0) + (u_line_blur * pixel(1.0, iResolution.xy) * blur),
y,
st.y
);
float line_end = smoothstep(
y,
y - (width / 2.0) - (u_line_blur * pixel(1.0, iResolution.xy) * blur),
st.y
);
return clamp(
(line_start - line_end) * (1.0 - smoothstep(0.0, 1.0, pow(perc, 0.3))),
0.0,
1.0
);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float line_strength = 1.0;
for (int i = 0; i < u_line_count; i++) {
float p = float(i) / float(u_line_count);
line_strength *= (1.0 - lineFn(
uv,
u_line_width * pixel(1.0, iResolution.xy) * (1.0 - p),
p,
(PI * 1.0) * p,
uMouse,
iTime,
uAmplitude,
uDistance
));
}
float colorVal = 1.0 - line_strength;
fragColor = vec4(uColor * colorVal, colorVal);
}
void main() {
mainImage(gl_FragColor, gl_FragCoord.xy);
}
`
const resize = () => {
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 handleMouseMove = (e: MouseEvent) => {
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 handleMouseLeave = () => {
targetMouse = [0.5, 0.5]
}
const update = (t: number) => {
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]
} else {
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)
}
const initializeScene = () => {
if (!containerRef.value) return
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 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
)
},
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 })
const canvas = gl.canvas as HTMLCanvasElement
canvas.style.width = '100%'
canvas.style.height = '100%'
canvas.style.display = 'block'
container.appendChild(canvas)
window.addEventListener('resize', resize)
if (props.enableMouseInteraction) {
container.addEventListener('mousemove', handleMouseMove)
container.addEventListener('mouseleave', handleMouseLeave)
}
resize()
animationId = requestAnimationFrame(update)
}
const cleanup = () => {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
window.removeEventListener('resize', resize)
if (containerRef.value) {
containerRef.value.removeEventListener('mousemove', handleMouseMove)
containerRef.value.removeEventListener('mouseleave', handleMouseLeave)
const canvas = containerRef.value.querySelector('canvas')
if (canvas) {
containerRef.value.removeChild(canvas)
}
}
if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext()
}
renderer = null
gl = null
program = null
mesh = null
currentMouse = [0.5, 0.5]
targetMouse = [0.5, 0.5]
}
onMounted(() => {
initializeScene()
})
onUnmounted(() => {
cleanup()
})
watch(
[() => props.color, () => props.amplitude, () => props.distance, () => props.enableMouseInteraction],
() => {
initializeScene()
},
{ deep: true }
)
</script>

View File

@@ -0,0 +1,443 @@
<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',
}" />
<canvas ref="canvasRef" class="block w-full h-full" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue'
class Grad {
x: number
y: number
z: number
constructor(x: number, y: number, z: number) {
this.x = x
this.y = y
this.z = z
}
dot2(x: number, y: number): number {
return this.x * x + this.y * y
}
}
class Noise {
grad3: Grad[]
p: number[]
perm: number[]
gradP: Grad[]
constructor(seed = 0) {
this.grad3 = [
new Grad(1, 1, 0),
new Grad(-1, 1, 0),
new Grad(1, -1, 0),
new Grad(-1, -1, 0),
new Grad(1, 0, 1),
new Grad(-1, 0, 1),
new Grad(1, 0, -1),
new Grad(-1, 0, -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,
]
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
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]
}
}
fade(t: number): number {
return t * t * t * (t * (t * 6 - 15) + 10)
}
lerp(a: number, b: number, t: number): number {
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
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)
)
}
}
interface Point {
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
}
interface Config {
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
}
const props = withDefaults(defineProps<WavesProps>(), {
lineColor: 'black',
backgroundColor: 'transparent',
waveSpeedX: 0.0125,
waveSpeedY: 0.005,
waveAmpX: 32,
waveAmpY: 16,
xGap: 10,
yGap: 32,
friction: 0.925,
tension: 0.005,
maxCursorMove: 100,
style: () => ({}),
className: ''
})
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[][] = []
const mouse: Mouse = {
x: -10,
y: 0,
lx: 0,
ly: 0,
sx: 0,
sy: 0,
v: 0,
vs: 0,
a: 0,
set: false,
}
let config: Config = {
lineColor: props.lineColor,
waveSpeedX: props.waveSpeedX,
waveSpeedY: props.waveSpeedY,
waveAmpX: props.waveAmpX,
waveAmpY: props.waveAmpY,
friction: props.friction,
tension: props.tension,
maxCursorMove: props.maxCursorMove,
xGap: props.xGap,
yGap: props.yGap,
}
let frameId: number | null = null
const setSize = () => {
const container = containerRef.value
const canvas = canvasRef.value
if (!container || !canvas) return
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
}
const setLines = () => {
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
for (let i = 0; i <= totalLines; i++) {
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 },
})
}
lines.push(pts)
}
}
const movePoints = (time: number) => {
if (!noise) return
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
const dx = p.x - mouse.sx,
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
}
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 drawLines = () => {
const { width, height } = bounding
if (!ctx) return
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)
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 tick = (t: number) => {
const container = containerRef.value
if (!container) return
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`)
movePoints(t)
drawLines()
frameId = requestAnimationFrame(tick)
}
const onResize = () => {
setSize()
setLines()
}
const updateMouse = (x: number, y: number) => {
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
}
}
const onMouseMove = (e: MouseEvent) => {
updateMouse(e.clientX, e.clientY)
}
const onTouchMove = (e: TouchEvent) => {
const touch = e.touches[0]
updateMouse(touch.clientX, touch.clientY)
}
onMounted(() => {
const canvas = canvasRef.value
const container = containerRef.value
if (!canvas || !container) return
ctx = canvas.getContext('2d')
noise = new Noise(Math.random())
setSize()
setLines()
frameId = requestAnimationFrame(tick)
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)
if (frameId !== null) {
cancelAnimationFrame(frameId)
}
})
watch(
() => [
props.lineColor,
props.waveSpeedX,
props.waveSpeedY,
props.waveAmpX,
props.waveAmpY,
props.friction,
props.tension,
props.maxCursorMove,
props.xGap,
props.yGap,
],
() => {
config = {
lineColor: props.lineColor,
waveSpeedX: props.waveSpeedX,
waveSpeedY: props.waveSpeedY,
waveAmpX: props.waveAmpX,
waveAmpY: props.waveAmpY,
friction: props.friction,
tension: props.tension,
maxCursorMove: props.maxCursorMove,
xGap: props.xGap,
yGap: props.yGap,
}
}
)
</script>