实现一个有趣的转盘轮播页面

780 阅读7分钟

说在前面

最近在在网上看到了一个有趣的PPT轮播效果,左边是一个转盘,通过转盘转动来切换轮播页面,今天我们在html中来实现一个类似的效果。

在线体验

码上掘金

目前只做了电脑全屏页面的效果,手机页面上看效果可能不太行😁)

codePen

codepen.io/yongtaozhen…

代码实现

html部分

<div
    class="img-container"
    id="nextImgContainer"
  style="display: none"
></div>
<div class="img-container" id="imgContainer"></div>
<div class="overlay"></div>
<div class="btn-container">
  <div class="controls">
    <button class="control-btn" id="prevBtn" title="上一张">
      <i>&lt;</i>
    </button>
    <label class="toggle-container" title="自动轮播">
      <input type="checkbox" class="toggle-input" id="autoToggle" checked />
      <span class="toggle-slider"></span>
    </label>
    <button class="control-btn" id="nextBtn" title="下一张">
      <i>&gt;</i>
    </button>
  </div>
</div>
<div class="container">
  <canvas id="pieChart"></canvas>
</div>
<div class="text-container">
  <div>
    <h1 class="text-container-title">.</h1>
    <p class="text-container-desc"></p>
  </div>
</div>

html部分包括图片容器、Canvas画布、控制按钮和文本信息区

  • 1、使用两个img-container分别存放当前和下一张图片
  • 2、Canvas元素作为转盘的绘制区域
  • 3、左侧控制区包含导航按钮和自动轮播开关
  • 4、文本区采用响应式设计,宽度会根据转盘大小动态调整

css部分

/* 基础样式设置 */
body {
    margin: 0;
    padding: 0;
    height: 100vh;
    width: 100vw;
    overflow: hidden;
    position: relative;
}

/* 图片容器样式 */
.img-container {
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    width: 100vw;
    overflow: hidden;
    background-size: cover;
    background-position: center;
    position: absolute;
    top: 0;
    left: 0;
}

/* Canvas容器样式 */
.container {
    width: 100%;
    height: 100%;
    position: absolute;
    left: -50vw;
    top: 0;
}

/* 文本容器样式 */
.text-container {
    position: absolute;
    height: 100%;
    top: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    z-index: 2;
    padding: 2em;
    white-space: wrap;
    box-sizing: border-box;
    overflow: hidden;
    text-align: center;
    color: aliceblue;
}

/* 半透明遮罩层 */
.overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.4);
    pointer-events: none;
    z-index: 1;
}

/* 控制按钮样式 */
.control-btn {
    background-color: rgba(255, 255, 255, 0.8);
    color: #333;
    border: none;
    border-radius: 50%;
    width: 30px;
    height: 30px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    transition: background-color 0.3s, transform 0.3s;
    font-size: 18px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}

/* 自动轮播开关样式 */
.toggle-container {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 30px;
}

.toggle-slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    transition: 0.4s;
    border-radius: 30px;
}

.toggle-slider:before {
    position: absolute;
    content: "";
    height: 22px;
    width: 22px;
    left: 4px;
    bottom: 4px;
    background-color: white;
    transition: 0.4s;
    border-radius: 50%;
}

.toggle-input:checked + .toggle-slider {
    background-color: #2196f3;
}

.toggle-input:checked + .toggle-slider:before {
    transform: translateX(30px);
}

背景图片全屏显示,转盘置于左边且只露出一半转盘,右侧显示页面的文字信息。

饼图绘制

使用canvas绘制一个圆形,将圆形分割成5个扇形区域,每个扇形区域显示图片的一部分。

1、扇形角度计算

const gapAngle = (Math.PI * 2) / (5 * 20);
const sliceAngle = (Math.PI * 2 - gapAngle * 5) / 5;
  • 将整个圆周 () 分成 100 份 (5×20),每份为gapAngle
const gapAngle = (Math.PI * 2) / (5 * 20);

  • 总圆周减去 5 个间隙的角度后,平均分配给 5 个扇形
  • 每个扇形的实际角度为 sliceAngle
const sliceAngle = (Math.PI * 2 - gapAngle * 5) / 5;

2、扇形绘制

for (let i = 0; i < 5; i++) {
    const startAngle = -Math.PI / 2 + i * (sliceAngle + gapAngle);
    ctx.beginPath();
    ctx.arc(centerX, centerY, maxRadius, startAngle, startAngle + sliceAngle);
    ctx.arc(centerX, centerY, innerRadius, startAngle + sliceAngle, startAngle, true);
    ctx.closePath();
    
    ctx.fillStyle = pattern;
    ctx.fill();
    ctx.stroke();
}
(1)计算每个扇形的起始角度
const startAngle = -Math.PI / 2 + i * (sliceAngle + gapAngle);
  • 起始位置-Math.PI / 2 对应 Canvas 坐标系中的 12 点钟方向
  • 角度递增:每个扇形的起始角度依次增加 sliceAngle + gapAngle
(2)绘制扇形路径
ctx.beginPath();
ctx.arc(centerX, centerY, maxRadius, startAngle, startAngle + sliceAngle);
ctx.arc(centerX, centerY, innerRadius, startAngle + sliceAngle, startAngle, true);
ctx.closePath();
  • 绘制外圆弧
    • startAnglestartAngle + sliceAngle
    • 使用外圆半径 maxRadius
  • 绘制内圆弧
    • startAngle + sliceAnglestartAngle(逆时针方向)
    • 使用内圆半径 innerRadius
  • 闭合路径:将路径首尾相连,形成一个封闭的扇形区域
(3)填充描边
ctx.fillStyle = pattern;
ctx.fill();
ctx.stroke();

使用对应的背景图案(pattern)填充扇形区域。

背景图片信息获取

计算背景图片在容器中的实际显示尺寸和位置,为后续创建对齐图案提供必要的参数。

1、获取并解析背景图片位置信息

const computedStyle = getComputedStyle(body);
const backgroundPosition = computedStyle.backgroundPosition;

const [positionX, positionY] = backgroundPosition.split(" ");
const posX = positionX === "center" ? 50 : parseFloat(positionX);
const posY = positionY === "center" ? 50 : parseFloat(positionY);
  • 获取计算样式:使用getComputedStyle()获取元素的实际样式
  • 解析位置值:将background-position值拆分为 X 和 Y 分量
  • 转换为百分比:将 "center" 转换为 50%,其他值保留为浮点数

2、计算图片与窗口的宽高比

const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const imageRatio = image.width / image.height;
const windowRatio = windowWidth / windowHeight;

3、根据宽高比确定图片显示尺寸

let displayWidth, displayHeight;

if (windowRatio > imageRatio) {
    displayWidth = windowWidth;
    displayHeight = windowWidth / imageRatio;
} else {
    displayHeight = windowHeight;
    displayWidth = windowHeight * imageRatio;
}

CSSbackground-size: cover 的实现逻辑:

  • 如果窗口更宽
    • 图片宽度铺满窗口
    • 高度按比例计算
  • 如果窗口更高
    • 图片高度铺满窗口
    • 宽度按比例计算

4、计算图片在容器中的偏移量

offsetX = (windowWidth - displayWidth) * (posX / 100);
offsetY = (windowHeight - displayHeight) * (posY / 100);
  • posX/posY:背景图片的定位百分比(默认为 50%,即居中)
  • 偏移量 = (窗口尺寸 - 图片显示尺寸) * 定位百分比

5、获取 Canvas 容器的位置信息

const containerRect = container.getBoundingClientRect();
const containerOffsetX = containerRect.left;
const containerOffsetY = containerRect.top;

containerCanvas 的父元素,使用 getBoundingClientRect() 获取其相对于视口的位置

获取扇形对应的背景图片

前面我们已经获取到了背景图片的定位和偏移信息,可以通过这些信息来创建一个和背景图片贴合的canvas图案

1、获取背景图片信息并创建临时 Canvas

const { width, height, offsetX, offsetY, containerOffsetX, containerOffsetY } = getBackgroundImageInfo();

const patternCanvas = document.createElement("canvas");
patternCanvas.width = window.innerWidth;
patternCanvas.height = window.innerHeight;

const patternCtx = patternCanvas.getContext("2d");

尺寸与窗口相同,用于绘制与背景图片相同的内容。

2、在临时 Canvas 上绘制图片

(1)右半边绘制当前显示的图片
patternCtx.drawImage(
    image,
    0, 0, image.width, image.height,
    offsetX, offsetY, width, height
);
  • 源图像区域:0, 0, image.width, image.height(整个原始图片)
  • 目标区域:offsetX, offsetY, width, height(根据前面计算的显示尺寸和偏移量)

(2)左半边绘制下一张展示图片
patternCtx.translate(patternCanvas.width / 2, patternCanvas.height / 2);
patternCtx.rotate(Math.PI);
patternCtx.translate(
  -patternCanvas.width / 2,
  -patternCanvas.height / 2
);
if (nextImage) {
  patternCtx.drawImage(
    nextImage,
    0,
    0,
    nextImage.width,
    nextImage.height,
    offsetX,
    offsetY,
    width,
    height
  );
}

因为下一张图片是需要通过旋转180度旋转到页面中显示,所以绘制的时候应该要上下颠倒(patternCtx.rotate(Math.PI))再进行绘制。

3、创建图案并设置变换

const pattern = ctx.createPattern(patternCanvas, "no-repeat");
if (pattern) {
    const patternTransform = new DOMMatrix();
    patternTransform.translate(containerOffsetX, containerOffsetY);
    pattern.setTransform(patternTransform);
}

使用DOMMatrix创建变换矩阵,应用平移变换,将图案原点移动到 Canvas 容器的左上角,这一步确保图案在 Canvas 中的位置与背景图片对齐。

自动轮播

function autoRotate(continueRotation = true, step = 1) {
  rotateAngle = 0;
  const rotationSpeed = 0.01;
  const targetAngle = Math.PI;
  let lastStep = step;

  async function rotate() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.save();
    ctx.translate(canvas.width / 2, canvas.height / 2);
    ctx.rotate(rotateAngle);
    ctx.translate(-canvas.width / 2, -canvas.height / 2);
    drawConcentricPieChart();
    ctx.restore();

    rotateAngle =
      Math.min(Math.abs(rotateAngle) + rotationSpeed, targetAngle) * step;
    if (Math.abs(rotateAngle) >= targetAngle) {
      if (lastStep < 0) await init(showList[showIndex].img, true, 1);
      changeShowItem(step);
      if (!continueRotation || !isAutoRotating) return;
      lastStep = step;
      autoRotateInterval = setTimeout(autoRotate, 3000);
      return;
    }
    requestAnimationFrame(rotate);
  }

  rotate();
}

开启轮播的时候每3秒自动切换下一个轮播页面,每次转盘旋转180度,将转盘左半边旋转到页面中来。

手动切换

let lastClickTime = 0;
const debounceTime = 3000;
function debounceClick(callback) {
  return async function () {
    const currentTime = Date.now();
    if (currentTime - lastClickTime < debounceTime) return;
    lastClickTime = currentTime;
    await callback();
  };
}

prevBtn.onclick = debounceClick(async () => {
  await init(showList[showIndex].img, true, -1);
  autoRotateInterval && clearTimeout(autoRotateInterval);
  autoRotate(isAutoRotating, -1);
});
nextBtn.onclick = debounceClick(async () => {
  await init(showList[showIndex].img, true, 1);
  autoRotateInterval && clearTimeout(autoRotateInterval);
  autoRotate(isAutoRotating, 1);
});

autoToggle.addEventListener("change", async function () {
  isAutoRotating = this.checked;

  if (isAutoRotating) {
    await init(showList[showIndex].img, true, 1);
    autoRotateInterval = setTimeout(autoRotate, 3000);
  } else {
    autoRotateInterval && clearTimeout(autoRotateInterval);
  }
});

可以选择是否自动轮播,手动切换轮播页面

页面切换过渡动画

async function transparencyAdjustment(from, to, step = 0.01) {
  return new Promise((resolve) => {
    let opacity = from;
    const targetOpacity = to;

    const fadeInOut = () => {
      imgContainer.style.opacity = opacity;
      textContainer.style.opacity = opacity;

      if (
        (step < 0 && opacity > targetOpacity) ||
        (step > 0 && opacity < targetOpacity)
      ) {
        opacity += step;
        requestAnimationFrame(fadeInOut);
      } else {
        resolve();
      }
    };

    fadeInOut();
  });
}

实现元素的透明度渐变动画:

  • 接收三个参数
    • from:起始透明度(0.0-1.0)
    • to:目标透明度(0.0-1.0)
    • step:每次变化的步长(默认 0.01)
  • 返回一个 Promise,在动画完成时 resolve

页面轮播切换时先将当前页面透明度增加淡出,再淡入显示新页面

// 图片切换前先淡出
await transparencyAdjustment(1, 0, -0.01);
// 切换图片
imgContainer.style.backgroundImage = `url(${newImageUrl})`;
// 淡入显示新图片
await transparencyAdjustment(0, 1, 0.01);

源码

gitee

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。