Metamask 小狐狸动画(react实现)

前言

在这里插入图片描述

知道web3的应该对metamask不陌生,进入网站首页,一只小狐狸头像映入眼帘,鼠标跟随的动画呈3D立体旋转,引人入胜。不禁对它是怎么做出来的产生了兴趣。

原理

在这里插入图片描述

其实就是简单的svg啊!里面的 polygon 是什么东东?

polygon 元素定义了一个由一组首尾相连的直线线段构成的闭合多边形形状。最后一点连接到第一点。欲了解开放形状

<svg>
    <!-- 具有默认填充的多边形示例 -->
    <polygon points="0,100 50,25 50,75 100,0" />
    <!-- 具有描边但无填充的相同的多边形形状示例 -->
    <polygon points="100,100 150,25 150,75 200,0" fill="none" stroke="black" />
</svg>

在这里插入图片描述

那么小狐狸的svg是如何绘制的,应该也好理解了:就是有很多个多边形拼接起来的。那么如何组织这些多边形这就是小狐狸的巧妙之处了,请参见下面json。

{"positions":[[111.0246,52.6046,46.2259],[114.025,87.6733,58.9818],[66.192,80.898,55.3943],[72.1133,35.4918,30.8714],[97.8045,116.561,73.9788],[16.7623,58.0109,58.0782],[52.6089,30.3641,42.5561],[106.8814,31.9455,46.9133],[113.4846,38.6049,49.1215],[108.6633,43.2332,46.3154],[101.2166,15.9822,46.3082],[16.6605,-16.2883,93.6187],[40.775,-10.2288,85.2764],[23.9269,-2.5103,86.7365],[11.1691,-7.0037,99.3776],[9.5692,-34.3939,141.672],[12.596,7.1655,88.741],[61.1809,8.8142,76.9968],[39.7195,-28.9271,88.9638],[13.7962,-68.5757,132.057],[15.2674,-62.32,129.688],[14.8446,-52.6096,140.113],[12.8917,-49.7716,144.741],[35.6042,-71.758,81.0639],[47.4625,-68.6061,63.3697],[38.2486,-64.7302,38.9099],[-12.8917,-49.7716,144.741],[-13.7962,-68.5757,132.057],[17.8021,-71.758,81.0639],[19.1243,-69.0168,49.4201],[38.2486,-66.2756,17.7762],[12.8928,-36.7035,141.672],[109.284,-93.5899,27.8243],[122.118,-36.8894,35.025],[67.7668,-30.197,78.4178],[33.1807,101.852,25.3186],[9.4063,-35.5898,150.722],[-9.5692,-34.3939,141.672],[-9.4063,-35.5898,150.722],[11.4565,-37.8994,150.722],[-12.596,7.1655,88.741],[-11.1691,-7.0037,99.3776],[70.2365,62.8362,-3.9475],[47.2634,54.294,-27.4148],[28.7302,91.7311,-24.9726],[69.1676,6.5862,-12.7757],[28.7302,49.1003,-48.3596],[31.903,5.692,-47.822],[35.0758,-34.4329,-16.2809],[115.2841,48.6815,48.6841],[110.8428,28.4821,49.1762],[-19.1243,-69.0168,49.4201],[-38.2486,-66.2756,17.7762],[-111.0246,52.6046,46.2259],[-72.1133,35.4918,30.8714],[-66.192,80.898,55.3943],[-114.025,87.6733,58.9818],[-97.8045,116.561,73.9788],[-52.6089,30.3641,42.5561],[-16.7623,58.0109,58.0782],[-106.8814,31.9455,46.9133],[-108.6633,43.2332,46.3154],[-113.4846,38.6049,49.1215],[-101.2166,15.9822,46.3082],[-16.6605,-16.2883,93.6187],[-23.9269,-2.5103,86.7365],[-40.775,-10.2288,85.2764],[-61.1809,8.8142,76.9968],[-39.7195,-28.9271,88.9638],[-14.8446,-52.6096,140.113],[-15.2674,-62.32,129.688],[-47.4625,-68.6061,63.3697],[-35.6042,-71.758,81.0639],[-38.2486,-64.7302,38.9099],[-17.8021,-71.758,81.0639],[-12.8928,-36.7035,141.672],[-67.7668,-30.197,78.4178],[-122.118,-36.8894,35.025],[-109.284,-93.5899,27.8243],[-33.1807,101.852,25.3186],[-11.4565,-37.8994,150.722],[-70.2365,62.8362,-3.9475],[-28.7302,91.7311,-24.9726],[-47.2634,54.294,-27.4148],[-69.1676,6.5862,-12.7757],[-28.7302,49.1003,-48.3596],[-31.903,5.692,-47.822],[-35.0758,-34.4329,-16.2809],[-115.2841,48.6815,48.6841],[-110.8428,28.4821,49.1762]],"chunks":[{"color":[246,133,27],"faces":[[17,33,10],[17,18,34],[34,33,17],[10,6,17],[11,15,31],[31,18,11],[18,12,11],[14,16,40],[40,41,14],[59,5,35],[35,79,59],[67,63,77],[67,77,76],[76,68,67],[63,67,58],[64,68,75],[75,37,64],[68,64,66],[14,41,37],[37,15,14],[5,59,40],[40,16,5]]},{"color":[228,118,27],"faces":[[31,24,18],[6,5,16],[16,17,6],[24,32,33],[33,34,24],[5,4,35],[75,68,71],[58,67,40],[40,59,58],[71,76,77],[77,78,71]]},{"color":[118,61,22],"faces":[[0,1,2],[2,3,0],[4,5,2],[6,3,2],[2,5,6],[7,8,9],[10,3,6],[10,50,7],[7,3,10],[7,9,3],[49,0,9],[3,9,0],[53,54,55],[55,56,53],[57,56,55],[58,59,55],[55,54,58],[60,61,62],[63,58,54],[63,60,89],[60,63,54],[60,54,61],[88,61,53],[54,53,61],[2,1,4],[55,59,57]]},{"color":[22,22,22],"faces":[[36,15,37],[37,38,36],[31,39,22],[22,21,31],[31,15,36],[36,39,31],[75,69,26],[26,80,75],[75,80,38],[38,37,75],[38,80,39],[39,36,38],[39,80,26],[26,22,39]]},{"color":[215,193,179],"faces":[[21,20,24],[24,31,21],[69,71,70],[71,69,75]]},{"color":[192,173,158],"faces":[[19,20,21],[21,22,19],[20,19,23],[23,24,20],[23,25,24],[19,22,26],[26,27,19],[23,28,29],[23,29,30],[25,23,30],[29,51,52],[52,30,29],[27,26,69],[69,70,27],[70,71,72],[72,27,70],[72,71,73],[51,74,72],[52,51,72],[73,52,72],[19,27,74],[74,28,19],[51,29,28],[28,74,51],[74,27,72],[28,23,19]]},{"color":[205,97,22],"faces":[[24,34,18],[16,13,12],[12,17,16],[13,16,11],[71,68,76],[40,67,66],[66,65,40],[65,64,40]]},{"color":[35,52,71],"faces":[[11,12,13],[64,65,66]]},{"color":[228,117,31],"faces":[[14,15,11],[11,16,14],[17,12,18],[41,64,37],[67,68,66]]},{"color":[226,118,27],"faces":[[35,4,42],[4,1,42],[42,43,44],[44,35,42],[45,43,42],[42,10,45],[30,32,24],[24,25,30],[30,33,32],[33,30,10],[44,43,46],[43,45,47],[47,46,43],[48,47,45],[45,30,48],[30,45,10],[49,42,0],[8,7,42],[50,42,7],[50,10,42],[1,0,42],[42,9,8],[42,49,9],[64,41,40],[57,59,79],[79,81,57],[57,81,56],[82,79,35],[35,44,82],[81,79,82],[82,83,81],[84,63,81],[81,83,84],[44,46,85],[85,82,44],[52,73,71],[71,78,52],[52,78,77],[77,63,52],[82,85,83],[83,85,86],[86,84,83],[87,52,84],[84,86,87],[52,63,84],[88,53,81],[62,81,60],[89,60,81],[89,81,63],[56,81,53],[81,62,61],[81,61,88],[48,87,86],[86,47,48],[47,86,85],[85,46,47],[48,30,52],[52,87,48]]}]}

通过监听鼠标事件 获取到 clientX和clientY

useEffect(() => {
    const handleMouseMove = (event) => {
      setMouseX(event.clientX);
      setMouseY(event.clientY);
    };
    document.addEventListener("mousemove", handleMouseMove);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
    };
}, []);

计算旋转的角度

  const angleX = initAngleX - (adjustedMouseY / window.innerHeight - 0.5) * Math.PI;
  const angleY = initAngleY + (mouseX / window.innerWidth - 0.5) * Math.PI;

旋转变换。先绕Y轴旋转,再绕X轴旋转,返回旋转后的三维坐标

const rotate = (point, angleX, angleY) => {
    // 计算旋转矩阵的乘法
    const cosX = Math.cos(angleX);
    const sinX = Math.sin(angleX);
    const cosY = Math.cos(angleY);
    const sinY = Math.sin(angleY);
    
    let [x, y, z] = point; 
    
    // 绕 Y 轴旋转
    let newX = x * cosY - z * sinY;
    let newZ = z * cosY + x * sinY;
    x = newX;
    z = newZ; 
    
    // 绕 X 轴旋转
    let newY = y * cosX - z * sinX;
    let newZ2 = z * cosX + y * sinX;
    return [x, newY, newZ2];
  };
[x, y, z] = rotate([x - centerX, y - centerY, z - centerZ], angleX, angleY);

透视投影

const x_proj = x / (z / distance + 1);
const y_proj = y / (z / distance + 1);

代码实现

借鉴了 https://codepen.io/shivammathur/pen/ZVJaEy 的实现

不多废话,直接上代码:

import React, { useRef, useEffect, useState } from "react";
import foxJSON from "../mock/fox.json";
import './fox.css'

const SVG_NS = "http://www.w3.org/2000/svg";

const createNode = (type) => document.createElementNS(SVG_NS, type);
const setAttribute = (node, attr, value) =>
  node.setAttributeNS(null, attr, value);

const FoxHead = ({
  width = 0.4,
  height = 0.4,
  followMouse = true,
  followMotion = true,
  slowDrift = true,
}) => {

  const distance = 400;
  const [mouseX, setMouseX] = useState(0);
  const [mouseY, setMouseY] = useState(0);
  const svgRef = useRef(null);

  // 默认的初始旋转角度, 使狐狸的正脸对着你
  const initAngleX = 0
  const initAngleY = Math.PI  // 180度, 让狐狸转过来

  useEffect(() => {
    const handleMouseMove = (event) => {
      setMouseX(event.clientX);
      setMouseY(event.clientY);
    };
    document.addEventListener("mousemove", handleMouseMove);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
    };
  }, []);
  useEffect(() => {
    drawModel(mouseX, mouseY);
  }, [mouseX, mouseY]);
  /**
   * 旋转函数,接受一个点和两个旋转角度,返回旋转后的点
   * @param {Array} point - 待旋转的点,格式为 [x, y, z]
   * @param {number} angleX - 绕 X 轴旋转的角度
   * @param {number} angleY - 绕 Y 轴旋转的角度
   * @return {Array} 旋转后的点,格式为 [x, y, z]
   */
  const rotate = (point, angleX, angleY) => {
    // 计算旋转矩阵的乘法
    const cosX = Math.cos(angleX);
    const sinX = Math.sin(angleX);
    const cosY = Math.cos(angleY);
    const sinY = Math.sin(angleY);
    
    let [x, y, z] = point; 
    
    // 绕 Y 轴旋转
    let newX = x * cosY - z * sinY;
    let newZ = z * cosY + x * sinY;
    x = newX;
    z = newZ; 
    
    // 绕 X 轴旋转
    let newY = y * cosX - z * sinX;
    let newZ2 = z * cosX + y * sinX;
    return [x, newY, newZ2];
  };
  
  /**
   * 画出svg
   * @param {number} mouseX - 鼠标的 X 坐标
   * @param {number} mouseY - 鼠标的 Y 坐标
   */
  const drawModel = (mouseX, mouseY) => {
    const svg = svgRef.current;
    while (svg.firstChild) {
      svg.removeChild(svg.firstChild);
    }

    const g = createNode('g')
    setAttribute(g, 'transform', 'scale(1, -1)')

    // 偏移量
    const adjustedMouseX = mouseX - (window.innerHeight / 2) * 0.2;
    const adjustedMouseY = mouseY + (window.innerHeight / 2) * 0.3;

    const angleX = initAngleX - (adjustedMouseY / window.innerHeight - 0.5) * Math.PI;
    const angleY = initAngleY + (mouseX / window.innerWidth - 0.5) * Math.PI;

    const centerX = 0
    const centerY = 0
    const centerZ = 0

    const polygons = []

    foxJSON.chunks.forEach((chunk) => {
      const color = `rgb(${chunk.color.join(",")})`;
      chunk.faces.forEach((face) => {
        let avgZ = 0
       
        const points = face
          .map((index) => {
            let [x, y, z] = foxJSON.positions[index];
            [x, y, z] = rotate([x - centerX, y - centerY, z - centerZ], angleX, angleY);
            // 透视投影
            const x_proj = x / (z / distance + 1);
            const y_proj = y / (z / distance + 1);
            avgZ += z;
            return `${x_proj},${y_proj}`;
          })
          .join(" ");


        avgZ /= face.length

        polygons.push({points, color, avgZ})
      });
    });

    polygons.sort((a, b) => b.avgZ - a.avgZ)

    polygons.forEach(({points, color}) => {
      const polygon = createNode("polygon");
      setAttribute(polygon, "points", points);
      setAttribute(polygon, "fill", color);
      setAttribute(polygon, "stroke", color);
      g.appendChild(polygon);
    })
    svg.appendChild(g)
  };

  return (<div className="fox">
    <svg
      ref={svgRef}
      viewBox="-150 -150 300 300"
      width={window.innerWidth * width + 'px'}
      height={window.innerHeight * height + 'px'}
    >
      <g transform="scale(1, -1)"></g>
    </svg>
    </div>
  );
};

export default FoxHead;


效果预览

在这里插入图片描述

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值