You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现序列图播放完成前禁止页面滚动(仿Apple效果)

仿Apple风格Canvas滚动序列图:加载完成前禁止页面滚动的实现修改

我想要制作一个仿Apple风格的页面,通过Canvas在滚动时展示图片序列,但在序列的最后一张图加载完成前,页面不随滚动移动。以下是我目前的实现代码,该如何修改?

现有代码

HTML

<body>
    <canvas id="image-hero"></canvas>
    <h1 class="centered title" style="font-size: 4rem;">Test Text</h1>
</body>

JavaScript

const html = document.documentElement; 
const canvas = document.getElementById('image-hero');
const context = canvas.getContext('2d');

const frameCount = 148;
const currentFrame = index => ( `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, '0')}.jpg` )
   
const preloadImages = () => {
   for (let i = 1; i < frameCount; i++) {
      const img = new Image();
      img.src = currentFrame(i);
   }
};

const img = new Image()
img.src = currentFrame(1);
canvas.width = 1158;
canvas.height = 770;
img.onload = function() { context.drawImage(img, 0, 0); }

const updateImage = index => {
   img.src = currentFrame(index);
   context.drawImage(img, 0, 0);
}

window.addEventListener('scroll', () => {
   //don't scroll the page until the animation is done
   

   const scrollTop = html.scrollTop;
   const maxScrollTop = html.scrollHeight - window.innerHeight;
   const scrollFraction = scrollTop / maxScrollTop;
   const frameIndex = Math.min(
      frameCount - 1,
      Math.ceil(scrollFraction * frameCount)
   );
   
   requestAnimationFrame(() => updateImage(frameIndex + 1))
});
preloadImages()

disableScrolling();

修改方案

现有代码存在几个问题:未定义disableScrolling函数、没有跟踪图片加载进度、滚动事件未做加载前的拦截处理。以下是完整的修改实现:

1. 实现滚动禁用/启用逻辑

添加函数控制页面滚动权限,覆盖鼠标滚轮、触摸滑动、键盘按键等多种滚动触发方式:

// 禁用滚动
function disableScroll() {
  const preventDefault = (e) => e.preventDefault();
  document.addEventListener('wheel', preventDefault, { passive: false });
  document.addEventListener('touchmove', preventDefault, { passive: false });
  // 阻止方向键、空格等键盘滚动操作
  document.addEventListener('keydown', (e) => {
    const scrollKeys = [33, 34, 35, 36, 37, 38, 39, 40, 32];
    if (scrollKeys.includes(e.keyCode)) e.preventDefault();
  });
  // 隐藏滚动条(可选)
  html.style.overflow = 'hidden';
}

// 启用滚动
function enableScroll() {
  const preventDefault = (e) => e.preventDefault();
  document.removeEventListener('wheel', preventDefault);
  document.removeEventListener('touchmove', preventDefault);
  document.removeEventListener('keydown', (e) => {
    const scrollKeys = [33, 34, 35, 36, 37, 38, 39, 40, 32];
    if (scrollKeys.includes(e.keyCode)) e.preventDefault();
  });
  html.style.overflow = '';
}

2. 跟踪图片加载进度

修改preloadImages函数,通过计数器统计已加载图片数量,全部加载完成后自动启用滚动:

let loadedCount = 0;

const preloadImages = () => {
  // 遍历所有帧图片(注意修正循环范围为1到frameCount)
  for (let i = 1; i <= frameCount; i++) {
    const img = new Image();
    img.src = currentFrame(i);
    // 加载成功时更新计数器
    img.onload = () => {
      loadedCount++;
      if (loadedCount === frameCount) {
        enableScroll();
      }
    };
    // 处理加载失败的情况(可选)
    img.onerror = () => {
      loadedCount++;
      console.warn(`图片 ${i} 加载失败`);
      if (loadedCount === frameCount) {
        enableScroll();
      }
    };
  }
};

3. 调整滚动事件逻辑

在滚动事件中添加判断,仅当所有图片加载完成后才执行图片更新:

window.addEventListener('scroll', () => {
  // 未加载完成时直接返回,不处理滚动
  if (loadedCount < frameCount) return;

  const scrollTop = html.scrollTop;
  const maxScrollTop = html.scrollHeight - window.innerHeight;
  const scrollFraction = scrollTop / maxScrollTop;
  // 用Math.floor替代Math.ceil,避免滚动初期就跳过多帧
  const frameIndex = Math.min(
    frameCount - 1,
    Math.floor(scrollFraction * frameCount)
  );
  
  requestAnimationFrame(() => updateImage(frameIndex + 1));
});

4. 初始化调整

确保页面加载时先禁用滚动,再启动图片预加载:

// 初始化时禁用滚动
disableScroll();
// 开始预加载图片
preloadImages();

完整修改后的代码

HTML

<body>
    <canvas id="image-hero"></canvas>
    <h1 class="centered title" style="font-size: 4rem;">Test Text</h1>
</body>

JavaScript

const html = document.documentElement; 
const canvas = document.getElementById('image-hero');
const context = canvas.getContext('2d');

const frameCount = 148;
const currentFrame = index => ( `https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${index.toString().padStart(4, '0')}.jpg` )

let loadedCount = 0;

// 禁用滚动
function disableScroll() {
  const preventDefault = (e) => e.preventDefault();
  document.addEventListener('wheel', preventDefault, { passive: false });
  document.addEventListener('touchmove', preventDefault, { passive: false });
  document.addEventListener('keydown', (e) => {
    const scrollKeys = [33, 34, 35, 36, 37, 38, 39, 40, 32];
    if (scrollKeys.includes(e.keyCode)) e.preventDefault();
  });
  html.style.overflow = 'hidden';
}

// 启用滚动
function enableScroll() {
  const preventDefault = (e) => e.preventDefault();
  document.removeEventListener('wheel', preventDefault);
  document.removeEventListener('touchmove', preventDefault);
  document.removeEventListener('keydown', (e) => {
    const scrollKeys = [33, 34, 35, 36, 37, 38, 39, 40, 32];
    if (scrollKeys.includes(e.keyCode)) e.preventDefault();
  });
  html.style.overflow = '';
}

const preloadImages = () => {
  for (let i = 1; i <= frameCount; i++) {
    const img = new Image();
    img.src = currentFrame(i);
    img.onload = () => {
      loadedCount++;
      if (loadedCount === frameCount) {
        enableScroll();
      }
    };
    img.onerror = () => {
      loadedCount++;
      console.warn(`图片 ${i} 加载失败`);
      if (loadedCount === frameCount) {
        enableScroll();
      }
    };
  }
};

const img = new Image()
img.src = currentFrame(1);
canvas.width = 1158;
canvas.height = 770;
img.onload = function() { context.drawImage(img, 0, 0); }

const updateImage = index => {
   img.src = currentFrame(index);
   context.drawImage(img, 0, 0);
}

window.addEventListener('scroll', () => {
  if (loadedCount < frameCount) return;

  const scrollTop = html.scrollTop;
  const maxScrollTop = html.scrollHeight - window.innerHeight;
  const scrollFraction = scrollTop / maxScrollTop;
  const frameIndex = Math.min(
    frameCount - 1,
    Math.floor(scrollFraction * frameCount)
  );
  
  requestAnimationFrame(() => updateImage(frameIndex + 1));
});

// 初始化
disableScroll();
preloadImages();

内容的提问来源于stack exchange,提问作者Marvil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 11:21:03