效果
<template>
<div id="waves" />
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
const amountX = ref(50);
const amountY = ref(50);
const color = ref("#e1b284");
const topOffset = ref(350);
let count = 0;
let mouseX = 0;
let windowHalfX = null;
let camera = null;
let scene = null;
let particles = null;
let renderer = null;
const init = () => {
const SEPARATION = 100;
const SCREEN_WIDTH = window.innerWidth;
const SCREEN_HEIGHT = window.innerHeight;
const container = document.createElement("div");
windowHalfX = window.innerWidth / 2;
container.style.position = "relative";
container.style.top = `${topOffset.value}px`;
container.style.height = `${SCREEN_HEIGHT - topOffset.value}px`;
const waves = document.getElementById("waves");
waves.appendChild(container);
camera = new THREE.PerspectiveCamera(
75,
SCREEN_WIDTH / SCREEN_HEIGHT,
1,
10000,
);
camera.position.z = 1000;
scene = new THREE.Scene();
const numParticles = amountX.value * amountY.value;
const positions = new Float32Array(numParticles * 3);
const scales = new Float32Array(numParticles);
let i = 0;
let j = 0;
for (let ix = 0; ix < amountX.value; ix++) {
for (let iy = 0; iy < amountY.value; iy++) {
positions[i] = ix * SEPARATION - (amountX.value * SEPARATION) / 2;
positions[i + 1] = 0;
positions[i + 2] = iy * SEPARATION - (amountY.value * SEPARATION) / 2;
scales[j] = 1;
i += 3;
j++;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("scale", new THREE.BufferAttribute(scales, 1));
const material = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(color.value) },
},
vertexShader: `
attribute float scale;
void main() {
vec4 mvPosition = modelViewMatrix * vec4( position, 2.0 );
gl_PointSize = scale * ( 300.0 / - mvPosition.z );
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 color;
void main() {
if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;
gl_FragColor = vec4( color, 1.0 );
}
`,
});
particles = new THREE.Points(geometry, material);
scene.add(particles);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearAlpha(0);
container.appendChild(renderer.domElement);
window.addEventListener("resize", onWindowResize, { passive: false });
document.addEventListener("mousemove", onDocumentMouseMove, {
passive: false,
});
document.addEventListener("touchstart", onDocumentTouchStart, {
passive: false,
});
document.addEventListener("touchmove", onDocumentTouchMove, {
passive: false,
});
};
const render = () => {
camera.position.x += (mouseX - camera.position.x) * 0.05;
camera.position.y = 400;
camera.lookAt(scene.position);
const positions = particles.geometry.attributes.position.array;
const scales = particles.geometry.attributes.scale.array;
let i = 0;
let j = 0;
for (let ix = 0; ix < amountX.value; ix++) {
for (let iy = 0; iy < amountY.value; iy++) {
positions[i + 1] =
Math.sin((ix + count) * 0.3) * 100 + Math.sin((iy + count) * 0.5) * 100;
scales[j] =
(Math.sin((ix + count) * 0.3) + 1) * 8 +
(Math.sin((iy + count) * 0.5) + 1) * 8;
i += 3;
j++;
}
}
particles.geometry.attributes.position.needsUpdate = true;
particles.geometry.attributes.scale.needsUpdate = true;
renderer.render(scene, camera);
count += 0.1;
};
const animate = () => {
requestAnimationFrame(animate);
render();
};
const onDocumentMouseMove = (event) => {
mouseX = event.clientX - windowHalfX;
};
const onDocumentTouchStart = (event) => {
if (event.touches.length === 1) {
mouseX = event.touches[0].pageX - windowHalfX;
}
};
const onDocumentTouchMove = (event) => {
if (event.touches.length === 1) {
event.preventDefault();
mouseX = event.touches[0].pageX - windowHalfX;
}
};
const onWindowResize = () => {
windowHalfX = window.innerWidth / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
onMounted(() => {
init();
animate();
});
onUnmounted(() => {
window.removeEventListener("resize", onWindowResize);
document.removeEventListener("mousemove", onDocumentMouseMove);
document.removeEventListener("touchstart", onDocumentTouchStart);
document.removeEventListener("touchmove", onDocumentTouchMove);
});
</script>