css、svg、canvas实现按路径运行动画

270 阅读2分钟

css实现

css实现靠offset-path和offset-distance属性实现按路径运动,offset-path定义运动路径,offset-distance定义当前在路径的位置

.running-container {  
  width: 300px;  
  height: 300px;  
  position: relative;  
}  
.running-path {  
  position: absolute;  
  top: 0;  
  left: 0;  
}  
.car{  
  offset-path: path("M49.5,60.5s128-77,164-15,59,101,60,134-39,80-88,82-130-12-127-57-51-90-30-117Z");  
  animation: move 10s infinite linear;  
  width: 40px;  
  height: 40px;  
  background: url(https://assets.codepen.io/10903102/car.png) no-repeat 50%;  
  background-size: contain;  
}  
@keyframes move {  
0% {  
  offset-distance: 0;  
}  
100% {  
  offset-distance: 100%;  
}  
}
<div class="running-container">
  <svg viewBox="0 0 300 300" class="running-path">
    <path d="M49.5,60.5s128-77,164-15,59,101,60,134-39,80-88,82-130-12-127-57-51-90-30-117Z" fill="transparent"
      stroke="#000000" />
  </svg>
  <div class="car"></div>
</div>

效果

svg实现

svg实现靠animateMotion,其中rotate="auto"让其自动转向,不过明显看到其效果和css比,还是有点差距的

.car{  
  transform: translateY(-10px);  
}
<svg viewBox="0 0 300 300" width="300" height="300">  
  <path fill="none" stroke="lightgrey"  
d="M49.5,60.5s128-77,164-15,59,101,60,134-39,80-88,82-130-12-127-57-51-90-30-117Z" />  
  <image class="car" href="https://assets.codepen.io/10903102/car.png" width="40">  
  <animateMotion dur="10s" repeatCount="indefinite" rotate="auto" origin="default"  
path="M49.5,60.5s128-77,164-15,59,101,60,134-39,80-88,82-130-12-127-57-51-90-30-117Z" />  
  </image>  
</svg>

效果

canvas实现

canvas实现依赖svg的getTotalLength和getPointAtLength方法,这两个方法兼容性有待考量,如果需要支持比较老的浏览器,可能需要自己实现这两个方法

getTotalLength 用于计算路径的总长度 getPointAtLength 用于计算路径某个长度的坐标值

const canvas = document.querySelector('canvas');  
const ctx = canvas.getContext('2d');  
  
// 运动路径,用于绘制cavas的路径  
const path = new Path2D('M49.5,60.5s128-77,164-15,59,101,60,134-39,80-88,82-130-12-127-57-51-90-30-117Z');  
  
// 计算路径总长度  
const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');  
svgPath.setAttribute('d', 'M49.5,60.5s128-77,164-15,59,101,60,134-39,80-88,82-130-12-127-57-51-90-30-117Z')  
const svgPathLen = svgPath.getTotalLength();  
  
// 加载运动的小车  
let image = null;  
const request = new Request('https://assets.codepen.io/10903102/car.png');  
fetch(request).then(response => {  
  if (response.ok) {  
  return response.blob()  
}  
}).then(blob => {  
  return createImageBitmap(blob);  
}).then(bitImage => {  
  image = bitImage;  
  start();  
})  
  
// 用于绘制单个帧的方法  
const draw = (l) => {  
  
  // 获得当前点、下个点和上个点的坐标  
  const point = svgPath.getPointAtLength(l);  
  const next = svgPath.getPointAtLength(l + 1);  
  const pre = svgPath.getPointAtLength(l - 1);  
  
  // 计算斜率以旋转小车  
  const dy = next.y - pre.y;  
  const dx = next.x - pre.x;  
  let angle;  
  if (dx > 0) {  
    angle = Math.atan(dy / dx)  
  } else {  
    angle = Math.PI + Math.atan(dy / dx)  
  }  
  
  
  // 清空画布  
  ctx.fillStyle = '#ffffff';  
  ctx.clearRect(0, 0, 300, 300);  
  
  // 绘制路径  
  ctx.strokeStyle = '#000000';  
  ctx.stroke(path);  
  
  //绘制小车  
  ctx.save();  
  const width = 32;  
  const height = width * image.height / image.width;  
  //小车移动到当前点,并设置旋转  
  ctx.translate(point.x, point.y);  
  ctx.rotate(angle)  
  ctx.drawImage(image, 0 - width/2, 0 - height/2, width, height);  
  ctx.restore();  
}  
  
function start() {  
  // 使用tween.js控制动画  
  const position = {i: 0};  
  const tween = new TWEEN.Tween(position).repeat(Infinity);  
  
  tween.to({i: svgPathLen}, 10000)  
  .onUpdate(() => {  
    draw(position.i)  
  })  
  tween.start();  
  animate()  
  
  function animate() {  
    requestAnimationFrame(animate)  
    TWEEN.update()  
  }  
}

效果:代码用了tween.js可能需要科学上网才能运行