纯CSS吃豆人(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>
</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
.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
// 防抖函数(默认延迟时间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)
<!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');// 创建 SVGconst 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 到 1const 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 的 rotateconst 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>