纯CSS吃豆人(JS仅控制进度)

一、效果展示

二、源码

html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pac-Man SVG Demo</title>
</head>
<body>
  <div class="controls">
    <label for="animation-slider">动画进度:</label>
    <input type="number" id="slider-value" value="0" />
    <br />
    <input type="range" id="animation-slider" min="0" max="0.999" step="0.001" value="0">
  </div>
  <div class="container">
    <svg viewBox="0 0 200 200" style="scale: 1">
      <circle class="body animation-control" r="50" cx="100" cy="100"></circle>
      <circle class="eye animation-control" r="10" cx="150" cy="70"></circle>
      <line class="upper-teeth animation-control" x1="100" y1="100" x2="200" y2="100"></line>
      <path class="nose animation-control" d="M 199,100 A 99,99 0 1,0 198.9999999999985,100.0000172787596"></path>
    </svg>
  </div>
</body>
</html>

css

css 复制代码
.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

svg {
  --animation-delay: 0s;
  width: 200px;
  height: 200px;
}

.body {
  fill: transparent;
  stroke: darkorange;
  stroke-width: 100;
  stroke-dasharray: 314.159; /* 2πr = 2 * π * 50 */
  stroke-dashoffset: 0;
  transform-origin: center;
  animation: pacman-mouth-open-close 1s infinite linear;
}
.upper-teeth {
  stroke: black;
  stroke-width: 2;
  transform-origin: center;
  animation: pacman-upper-body-rotation 1s infinite linear;
}
.eye {
  fill: white;
  transform-origin: center;
  animation: pacman-eye-close-polygon 1s infinite linear, 
    pacman-upper-body-rotation 1s infinite linear;
}
.nose {
  fill: none;
  stroke: black;
  stroke-width: 2;
  transform-origin: center;
  animation: pacman-nose-adjust 1s infinite linear, 
    pacman-upper-body-rotation 1s infinite linear;
}

.animation-control {
  animation-delay: var(--animation-delay);
  animation-play-state: paused;
}

/* 身体张合动画 */
@keyframes pacman-mouth-open-close {
  0% {
    stroke-dashoffset: 0;
    transform: rotate(0deg);
  }
  100% {
    stroke-dashoffset: 314.159;
    transform: rotate(180deg);
  }
}

/* 上半身旋转跟随 */
@keyframes pacman-upper-body-rotation {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(-180deg);
  }
}

/* 眼睛缩小动画 */
@keyframes pacman-eye-shrink {
  0% {
    r: 10;
  }
  73% {
    r: 10;
  }
  83% {
    r: 0;
  }
  100% {
    r: 0;
  }
}

/* 眼睛闭眼效果, 眼角方向平行于上牙膛(椭圆裁剪路径) */
@keyframes pacman-eye-close-ellipse {
  0% {
    clip-path: ellipse(50% 50% at 50% 50%);
  }
  73% {
    clip-path: ellipse(50% 50% at 50% 50%);
  }
  83% {
    clip-path: ellipse(50% 0% at 50% 50%);
  }
  100% {
    clip-path: ellipse(50% 0% at 50% 50%);
  }
}

/* 眼睛闭眼效果, 眼角方向朝向身体圆心(多边形裁剪路径) */
@keyframes pacman-eye-close-polygon {
  /*正方形边框点*/
  0% {
    clip-path: polygon(0 80%, 0 100%, 20% 100%, 40% 100%, 60% 100%, 80% 100%, 100% 100%, 100% 80%, 100% 60%, 100% 40%, 100% 20%, 100% 0, 80% 0, 60% 0, 40% 0, 20% 0, 0 0, 0 20%, 0 40%, 0 60%);
  }
  /*正方形边框点*/
  73% {
    clip-path: polygon(0 80%, 0 100%, 20% 100%, 40% 100%, 60% 100%, 80% 100%, 100% 100%, 100% 80%, 100% 60%, 100% 40%, 100% 20%, 100% 0, 80% 0, 60% 0, 40% 0, 20% 0, 0 0, 0 20%, 0 40%, 0 60%);
  }
  /*贴圆边*/
  76% {
    clip-path: polygon(0 80%, 14% 86%, 23% 93%, 40% 100%, 60% 100%, 77% 94%, 86% 86%, 94% 77%, 100% 60%, 100% 40%, 100% 20%, 86% 14%, 77% 7%, 60% 0, 40% 0, 23% 7%, 14% 14%, 6% 23%, 0 40%, 0 60%);
  }
  /*两个扇形的弧线组合*/
  79% {
    clip-path: polygon(0 80%, 13% 82%, 26% 82%, 38% 80%, 51% 76%, 62% 71%, 73% 63%, 82% 54%, 90% 44%, 96% 32%, 100% 20%, 87% 18%, 74% 18%, 62% 20%, 49% 24%, 38% 29%, 27% 37%, 18% 46%, 10% 56%, 4% 68%);
  }
  /*闭眼*/
  83% {
    clip-path: polygon(0 80%, 10% 74%, 20% 68%, 30% 62%, 40% 56%, 50% 50%, 60% 44%, 70% 38%, 80% 32%, 90% 26%, 100% 20%, 90% 26%, 80% 32%, 70% 38%, 60% 44%, 50% 50%, 40% 56%, 30% 62%, 20% 68%, 10% 74%);
  }
  /*闭眼*/
  100% {
    clip-path: polygon(0 80%, 10% 74%, 20% 68%, 30% 62%, 40% 56%, 50% 50%, 60% 44%, 70% 38%, 80% 32%, 90% 26%, 100% 20%, 90% 26%, 80% 32%, 70% 38%, 60% 44%, 50% 50%, 40% 56%, 30% 62%, 20% 68%, 10% 74%);
  }
}

/* 嘴越大, 身体越小, 添加动画防止鼻子超出身体 */
@keyframes pacman-nose-adjust {
  0% {
    clip-path: polygon(50% 50%, 100% 50%, 100% 0);
  }
  70% {
    clip-path: polygon(50% 50%, 100% 50%, 100% 0);
  }
  100% {
    clip-path: polygon(50% 50%, 100% 50%, 100% 50%);
  }
}
#animation-slider {
  width: 350px;
  position: absolute;
  z-index: 1;
}

js

js 复制代码
// 防抖函数(默认延迟时间300毫秒)
function debounce(func, delay = 300) {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

const slider = document.getElementById('animation-slider');
const sliderValue = document.getElementById('slider-value');
const svg = document.querySelector("svg");
// 滑块更新CSS变量
slider.addEventListener('input', function() {
  const newDelay = -this.value;
  svg.style.setProperty('--animation-delay', `${newDelay}s`);
});
// 滑块更新显示值输入框
slider.addEventListener('input', function() {
  sliderValue.value = this.value;
});
// 显示值输入框手动修改滑块值
sliderValue.addEventListener('input', debounce(function() {
  slider.value = this.value;
  slider.dispatchEvent(new Event('input', {'bubbles': true, 'cancelable': true}));
}));

三、源码(JS)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pac-Man SVG Demo</title>
  <style>
    input[type="range"] {
      width: 100%;
    }
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
    }
  </style>
</head>
<body>
  <div class="container" id="demo-container"></div>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const container = document.getElementById('demo-container');
      
      // 创建 SVG
      const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('width', '200');
      svg.setAttribute('height', '200');
      svg.setAttribute('viewBox', '0 0 200 200');
      
      // 创建圆形(身体)
      const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
      circle.setAttribute('r', '50');
      circle.setAttribute('cx', '100');
      circle.setAttribute('cy', '100');
      circle.setAttribute('fill', 'transparent');
      circle.setAttribute('stroke', 'darkorange');
      circle.setAttribute('stroke-width', '100');
      circle.style.transformOrigin = 'center';

      // 创建小圆点(眼睛)
      const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
      dot.setAttribute('r', '10');
      dot.setAttribute('cx', '150');
      dot.setAttribute('cy', '70');
      dot.setAttribute('fill', 'white');
      dot.style.transformOrigin = 'center';
      // 根据旋转值设置半径
      const dotObserver = new MutationObserver((mutations) => {
        // 获取元素的当前旋转值
        const currentRotate = parseFloat(getComputedStyle(dot).getPropertyValue('rotate')) || 0;
        // 根据旋转值设置半径(dot 的角度范围是 180 度到 360 度)
        let limitRotateA = 210;
        let limitRotateB = 240;
        if (currentRotate >= 180 && currentRotate < limitRotateA) {
          // 保持 0 不变
          dot.setAttribute('r', `0`);
        } else if (currentRotate >= limitRotateA && currentRotate <= limitRotateB) {
          // 缩放从 0 到 1
          const rValue = (currentRotate - limitRotateA) / (limitRotateB - limitRotateA);
          dot.setAttribute('r', `${rValue * 10}`);
        } else if (currentRotate >= limitRotateB && currentRotate <= 360) {
          // 保持 10 不变
          dot.setAttribute('r', `10`);
        }
      });
      dotObserver.observe(dot, { attributes: true, attributeFilter: ['style'] });
      
      // 创建线段(上牙膛)
      const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
      line.setAttribute('x1', '100');
      line.setAttribute('y1', '100');
      line.setAttribute('x2', '200');
      line.setAttribute('y2', '100');
      line.setAttribute('stroke', 'white');
      line.setAttribute('stroke-width', '2');
      line.style.transformOrigin = 'center';

      // 创建圆弧(鼻子)(头盔)
      const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      arc.setAttribute('fill', 'none');
      arc.setAttribute('stroke', '#c8ff00');
      arc.setAttribute('stroke-width', '2');
      arc.style.transformOrigin = 'center';
      const arcAngle = 360;
      // 更新圆弧的 d 属性
      function updateArc(arcX, arcY, arcRadius, arcAngle) {
        if (arcAngle === 360) {
          // 圆弧不能画整圆, 但是可以差一点点
          const arcRadians2 = (arcAngle - 1e-5) * (Math.PI / 180);
          const newarcX2 = arcX + arcRadius * Math.cos(arcRadians2);
          const newarcY2 = arcY - arcRadius * Math.sin(arcRadians2);
          const arc_dm1 = `M ${arcX + arcRadius},${arcY}`;
          const arc_da1 = `A ${arcRadius},${arcRadius} 0 ${arcAngle > 180 ? '1':'0'},0 ${newarcX2},${newarcY2}`;
          arc.setAttribute('d', `${arc_dm1} ${arc_da1}`);
          return;
        }
        const arcRadians = arcAngle * (Math.PI / 180);
        const newarcX = arcX + arcRadius * Math.cos(arcRadians);
        const newarcY = arcY - arcRadius * Math.sin(arcRadians);
        const arc_dm1 = `M ${arcX + arcRadius},${arcY}`;
        const arc_da1 = `A ${arcRadius},${arcRadius} 0 ${arcAngle > 180 ? '1':'0'},0 ${newarcX},${newarcY}`;
        arc.setAttribute('d', `${arc_dm1} ${arc_da1}`);
      }
      updateArc(100, 100, 99, arcAngle);
      // 根据旋转值设置半径
      const arcObserver = new MutationObserver((mutations) => {
        // 获取元素的当前旋转值
        const currentRotate = parseFloat(getComputedStyle(arc).getPropertyValue('rotate')) || 0;
        // 根据旋转值限制角度大小不超过身体(arc 的角度范围是 180 度到 360 度)
        updateArc(100, 100, 99, Math.min((currentRotate - 180) * 2, arcAngle));
        // updateArc(100, 100, 99, (currentRotate - 180) * 2);
      });
      arcObserver.observe(arc, { attributes: true, attributeFilter: ['style'] });
      
      // 创建滑块和角度显示
      const sliderDiv = document.createElement('div');
      const sliderText = document.createTextNode('拖动滑块控制吃豆人的张嘴角度:');
      sliderDiv.style.width = '400px';

      const slider = document.createElement('input');
      slider.type = 'range';
      slider.min = '0';
      slider.max = '360';
      slider.value = 270;
      
      const angleDisplay = document.createElement('span');
      // angleDisplay.textContent = `${360 - slider.value}度`;
      
      sliderDiv.appendChild(sliderText);
      sliderDiv.appendChild(angleDisplay);
      sliderDiv.appendChild(slider);

      // 添加所有元素到容器
      container.appendChild(svg);
      container.appendChild(sliderDiv);
      svg.appendChild(circle);
      svg.appendChild(dot);
      svg.appendChild(line);
      svg.appendChild(arc);
      
      // 更新圆形的 dash 长度和偏移量,以及 circle 和 dot 的 rotate
      const updateCircle = () => {
        const angle = slider.value;
        angleDisplay.textContent = `${360 - angle}度`;
        const circumference = 100 * Math.PI;
        const dashOffset = circumference / 360 * (360 - angle);
        
        circle.setAttribute('stroke-dasharray', circumference);
        circle.setAttribute('stroke-dashoffset', dashOffset);
        
        // 更新 circle 和 dot 的 rotate 属性
        circle.style.rotate = `${180 - angle / 2}deg`;
        dot.style.rotate = `${180 + angle / 2}deg`;
        line.style.rotate = `${180 + angle / 2}deg`;
        arc.style.rotate = `${180 + angle / 2}deg`;
      };
      
      // 初始更新
      updateCircle();
      
      // 添加滑块事件监听器
      let animationFrameId;
      slider.addEventListener('input', () => {
        if (animationFrameId) {
          cancelAnimationFrame(animationFrameId);
        }
        animationFrameId = requestAnimationFrame(updateCircle);
      });
      // slider.addEventListener('input', updateCircle);
    });
  </script>
</body>
</html>
相关推荐
艾小逗1 小时前
vue3中的effectScope有什么作用,如何使用?如何自动清理
前端·javascript·vue.js
明似水5 小时前
Flutter 弹窗队列管理:支持优先级的线程安全通用弹窗队列系统
javascript·安全·flutter
大G哥5 小时前
PHP标签+注释+html混写+变量
android·开发语言·前端·html·php
whoarethenext5 小时前
html初识
前端·html
Simaoya6 小时前
【vue】【element-plus】 el-date-picker使用cell-class-name进行标记,type=year不生效解决方法
前端·javascript·vue.js
Dnn016 小时前
vue3+element-push 实现input框粘贴图片或文本,图片上传。
前端·javascript·vue.js
Nuyoah.6 小时前
《Vue3学习手记5》
前端·javascript·学习
曹牧6 小时前
Java 调用webservice接口输出xml自动转义
java·开发语言·javascript
天天扭码7 小时前
2025年了,npm 与 pnpm我们该如何选择
前端·javascript·npm
烛阴7 小时前
10个JavaScript编程技巧,助你成为高效开发高手!
前端·javascript