前言
知道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;