274 lines
6.5 KiB
Vue
274 lines
6.5 KiB
Vue
|
|
<template>
|
||
|
|
<div ref="containerRef" :class="className" :style="style" class="w-full h-full"></div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { onMounted, onUnmounted, watch, type CSSProperties, useTemplateRef } 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 = useTemplateRef<HTMLDivElement>('containerRef');
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
parent = parent.parentElement;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!width || !height) {
|
||
|
|
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 });
|
||
|
|
|
||
|
|
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';
|
||
|
|
gl.canvas.style.position = 'absolute';
|
||
|
|
gl.canvas.style.top = '0';
|
||
|
|
gl.canvas.style.left = '0';
|
||
|
|
gl.canvas.style.zIndex = '1';
|
||
|
|
|
||
|
|
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>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
div {
|
||
|
|
width: 100% !important;
|
||
|
|
height: 100% !important;
|
||
|
|
min-height: 100% !important;
|
||
|
|
display: block !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(canvas) {
|
||
|
|
width: 100% !important;
|
||
|
|
height: 100% !important;
|
||
|
|
min-height: 100% !important;
|
||
|
|
display: block !important;
|
||
|
|
object-fit: cover !important;
|
||
|
|
}
|
||
|
|
</style>
|