这是一个很酷的金属球,点击它会产生涟漪……

1. 分享

最近看到一个很酷的金属球效果:

点击它的时候会产生涟漪,效果如下:

体验地址:gnufault.github.io/ripple-sphe...

移动端也可以,但因为部署在 Github,需要你科学上网。

2. 实现

该效果使用 three.js 实现,整体代码并不复杂,核心的 JS 代码也就 130 多行。

这是源码地址:github.com/GNUfault/ri...

javascript 复制代码
import * as THREE from "https://unpkg.com/three@0.150.0/build/three.module.js";

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);
document.body.style.margin = "0";
document.body.style.overflow = "hidden";
document.body.style.touchAction = "none";

const envMap = new THREE.CubeTextureLoader().load([
  "https://threejs.org/examples/textures/cube/Bridge2/posx.jpg",
  "https://threejs.org/examples/textures/cube/Bridge2/negx.jpg",
  "https://threejs.org/examples/textures/cube/Bridge2/posy.jpg",
  "https://threejs.org/examples/textures/cube/Bridge2/negy.jpg",
  "https://threejs.org/examples/textures/cube/Bridge2/posz.jpg",
  "https://threejs.org/examples/textures/cube/Bridge2/negz.jpg",
]);
scene.background = envMap;

const material = new THREE.MeshStandardMaterial({
  metalness: 1,
  roughness: 0,
  envMap: envMap,
});

const geometry = new THREE.SphereGeometry(1, 128, 128);
const sphere = new THREE.Mesh(geometry, material);
scene.add(sphere);

const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(5, 5, 5);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 0.5));

camera.position.z = 3;

const originalPositions = geometry.attributes.position.array.slice();

let ripples = [];
const maxRipples = 6;

function addRipple(point) {
  if (ripples.length >= maxRipples) ripples.shift();
  ripples.push({ point, start: performance.now() });
}

function updateRipples() {
  const positions = geometry.attributes.position.array;
  const now = performance.now();
  const vertex = new THREE.Vector3();
  const temp = new THREE.Vector3();

  for (let i = 0; i < positions.length; i += 3) {
    vertex.set(originalPositions[i], originalPositions[i + 1], originalPositions[i + 2]);

    let offset = 0;
    for (const ripple of ripples) {
      const age = (now - ripple.start) / 1000;
      if (age > 2.5) continue;
      const dist = vertex.distanceTo(ripple.point);

      const fadeIn = Math.min(age * 8.0, 1.0);
      const fadeOut = Math.exp(-age * 3.0);
      const wave = Math.sin(dist * 60 - age * 25) * Math.exp(-dist * 5);

      offset += wave * 0.01 * fadeIn * fadeOut;
    }

    temp.copy(vertex).normalize().multiplyScalar(offset);
    positions[i] = originalPositions[i] + temp.x;
    positions[i + 1] = originalPositions[i + 1] + temp.y;
    positions[i + 2] = originalPositions[i + 2] + temp.z;
  }

  geometry.attributes.position.needsUpdate = true;
  geometry.computeVertexNormals();
}

function animate() {
  requestAnimationFrame(animate);
  updateRipples();
  renderer.render(scene, camera);
}
animate();

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

function handleInteraction(clientX, clientY) {
  mouse.x = (clientX / innerWidth) * 2 - 1;
  mouse.y = -(clientY / innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObject(sphere);
  if (intersects.length > 0) {
    const worldPoint = intersects[0].point.clone();
    const localPoint = sphere.worldToLocal(worldPoint);
    const normalizedPoint = localPoint.normalize();
    addRipple(normalizedPoint);
  }
}

window.addEventListener("click", (event) => {
  handleInteraction(event.clientX, event.clientY);
});

window.addEventListener("touchstart", (event) => {
  event.preventDefault();
  for (let i = 0; i < event.touches.length; i++) {
    const touch = event.touches[i];
    handleInteraction(touch.clientX, touch.clientY);
  }
});

window.addEventListener("touchmove", (event) => {
  event.preventDefault();
});

window.addEventListener("resize", () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

你可以:

  • fork 一份,添加其他效果,作为自己的作品集
  • 作为一个学习 three.js 的示例代码
相关推荐
G_G#8 小时前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界8 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路9 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug9 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121389 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中9 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路9 小时前
GDAL 实现矢量合并
前端
hxjhnct9 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星9 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子9 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端