Vue3 实现 B站 视差 动画

粘贴代码就能用了👇🏻:

目标:制作一个多图层的响应式 banner,每层可以是图片或视频,能根据鼠标在 banner 上的横向移动控制 animationProgress,进而驱动每层的 translate/rotate/scale/blur/opacity 动画。

实现要点:

  1. 数据结构(layers.js)描述每层的资源和动画参数。
  2. 把资源(img/video)预处理成 DOM 元素并存 .el,便于后续统一操控。
  3. 每层放到一个 .layer 容器里,统一用 requestAnimationFrame 渲染变换。
  4. 鼠标事件只绑定在 banner(或 pointer 事件),并做复位动画。
  5. 响应式处理:窗口 resize 要重算尺寸。
  6. 必须在组件卸载时移除监听并取消动画,避免内存泄漏。

分步骤实现(每步:怎么想 → 怎么写 → 为什么)

步骤 0:准备(如果你还没建项目)

怎么想:先有个能跑 Vue 3 的项目(Vite 最简单)。 怎么写(命令,任选其一):

bash 复制代码
# 推荐:Vite + Vue
npm create vite@latest my-app -- --template vue
cd my-app
npm install
npm run dev

为什么:有了运行环境你才能在浏览器实时调试组件。


步骤 1:设计数据结构(layers.js

怎么想:要把每一层需要的参数(资源列表、初始缩放、偏移量、模糊/不透明等)都写成 JSON/JS 对象,方便组件读取并驱动动画。 怎么写(示例):

js 复制代码
// src/assets/layers.js
export default [
  {
    name: '背景',
    scale: { initial: 1.0, offset: 0.05 },
    translate: { initial: [0, 0], offset: [10, 0] },
    rotate: { initial: 0, offset: 2 },
    blur: { initial: 0, offset: 2, wrap: 'clamp' },
    opacity: { initial: 1, offset: -0.2, wrap: 'clamp' },
    resources: [{ src: '/images/bg.jpg' }],
  },
  {
    name: '前景',
    scale: { initial: 1.1, offset: -0.02 },
    translate: { initial: [0, 0], offset: [-15, 0] },
    resources: [{ src: '/images/fg.png' }],
  },
];

为什么:把参数和资源分离,便于调试和 A/B 调整,也利于复用/热替换。


步骤 2:静态 DOM 与样式结构(先做最简单的静态展示)

怎么想:先把 HTML/CSS 做好,确保能显示出图片/视频,再加入 JS 控制。 怎么写(template + 最简样式):

vue 复制代码
<template>
  <div class="header-banner">
    <div ref="bannerRef" class="animated-banner"></div>
  </div>
</template>

<style>
.header-banner { position: relative; min-height: 155px; height: 9.375vw; max-height: 240px; }
.animated-banner { position: absolute; inset: 0; overflow: hidden; }
.layer { position: absolute; inset: 0; display:flex; align-items:center; justify-content:center; }
img, video { max-width:100%; height:auto; }
</style>

为什么:把容器准备好,后面 JS 只需要向 .animated-banner 插入每个 .layer 的子元素即可。


步骤 3:资源预处理(图片和视频分别处理)

怎么想:图片要等 load 获取 naturalWidth/naturalHeight;视频要等 loadedmetadata 获取尺寸。把这些尺寸存到 dataset 里,方便响应式 resize。 怎么写(示例核心):

js 复制代码
// 伪代码
if (isImage) {
  const img = new Image();
  img.src = resource.src;
  img.addEventListener('load', () => {
    img.dataset.width = img.naturalWidth;
    img.dataset.height = img.naturalHeight;
    // 根据 banner 高度计算初始缩放尺寸并设置 width/height
  });
  resource.el = img;
} else { // video
  const video = document.createElement('video');
  video.src = resource.src; video.muted = true; video.loop = true; video.autoplay = true;
  video.addEventListener('loadedmetadata', () => {
    video.dataset.width = video.videoWidth;
    video.dataset.height = video.videoHeight;
  });
  resource.el = video;
}

为什么:提前知道原始像素尺寸能精确按比例缩放,避免失真或拉伸,同时视频的元数据有时是异步的,必须等待。


步骤 4:建立每个层的容器和初始状态(layerStates)

怎么想:每层需要一个 DOM 容器和一份「初始状态」来记录最开始的 scale/rotate/translate/opacity/blur。动画时基于这个初始状态叠加偏移量。 怎么写(示例):

js 复制代码
const layerContainers = layers.map(() => {
  const el = document.createElement('div');
  el.classList.add('layer');
  bannerElement.appendChild(el);
  return el;
});

const layerStates = layers.map(layer => ({
  scale: layer.scale?.initial ?? 1,
  rotate: layer.rotate?.initial ?? 0,
  translate: layer.translate?.initial ?? [0,0],
  blur: layer.blur?.initial ?? 0,
  opacity: layer.opacity?.initial ?? 1,
}));

为什么:把状态分开保存,方便"基线值 + 动态偏移"这种组合逻辑,并且更易于调试。


步骤 5:渲染/变换逻辑(把 animationProgress 映射到 transform/filter)

怎么想:根据 animationProgress(一般 -1..1,也可以不限制)计算每层最终的 translate/rotate/scale/blur/opacity,然后把它们写到 style.transform / style.filter / style.opacity。 怎么写(简化版):

js 复制代码
const applyTransforms = (index, progress) => {
  const base = layerStates[index];
  const cfg = layers[index];
  // scale
  const scale = base.scale + (cfg.scale?.offset ?? 0) * progress;
  // rotate
  const rotate = base.rotate + (cfg.rotate?.offset ?? 0) * progress;
  // translate
  const translateOffset = (cfg.translate?.offset ?? [0,0]).map(v => v * progress);
  const translate = base.translate.map((v,i) => v + translateOffset[i]);
  element.style.transform = `translate(${translate[0]}px, ${translate[1]}px) rotate(${rotate}deg) scale(${scale})`;
};

为什么:拆成很多小步(先算 scale,再算 rotate,再算 translate),逻辑清晰,方便单独调试某个属性。


步骤 6:动画循环(为什么用 requestAnimationFrame)

怎么想:用 requestAnimationFrame 逐帧更新 DOM,浏览器会把动画和渲染周期对齐,保证流畅并节省 CPU。不要直接用 setInterval。 怎么写(核心):

js 复制代码
let rafId = 0;
const frameLoop = () => {
  // 对所有层调用 applyTransforms(...)
  rafId = requestAnimationFrame(frameLoop);
};
rafId = requestAnimationFrame(frameLoop);

为什么:requestAnimationFrame 在页面不可见时会暂停,从而节省性能;并且帧率与屏幕刷新同步,动画更流畅。


怎么想:不要把 mousemove 绑到 window 上(影响全页面性能)。绑定到 banner 或 使用 pointermove,并且当鼠标离开时做一个平滑的回退动画(200ms)。 怎么写(关键逻辑):

js 复制代码
const pointerMoveHandler = (event) => {
  if (!bannerRect) return;
  // 记录起始位置 lastMouseX,在后续的移动中根据当前位置 - 起始位置 得到 progress
  animationProgress = (event.clientX - lastMouseX) / bannerWidth;
};

const pointerLeaveHandler = () => {
  // 用 requestAnimationFrame 做线性插值回到 0
};
bannerElement.addEventListener('pointermove', pointerMoveHandler);
bannerElement.addEventListener('pointerleave', pointerLeaveHandler);

为什么:把事件限制在 banner 区域能显著降低事件触发频率,pointer 系列事件也能同时兼容鼠标与触控。


步骤 8:响应式 resize 与清理(最重要的工程细节)

怎么想:当窗口大小改变,banner 的高度/宽度以及基准缩放 baseRatio 需要重新计算,图片/视频的 width/height 也要重设。同时,组件卸载时必须 removeEventListener & cancelAnimationFrame。 怎么写(示例):

js 复制代码
const resizeHandler = () => {
  const newHeight = bannerElement.clientHeight;
  const newBaseRatio = newHeight / 155;
  layers.forEach(layer => {
    layer.resources.forEach(res => {
      const el = res.el;
      const w = Number(el.dataset.width || el.width || 0);
      const h = Number(el.dataset.height || el.height || 0);
      const newW = w * newBaseRatio * (layer.scale?.initial ?? 1);
      const newH = h * newBaseRatio * (layer.scale?.initial ?? 1);
      el.style.width = `${newW}px`; el.style.height = `${newH}px`;
    });
  });
};

window.addEventListener('resize', resizeHandler);

// 清理
onBeforeUnmount(() => {
  bannerElement.removeEventListener('pointermove', pointerMoveHandler);
  bannerElement.removeEventListener('pointerleave', pointerLeaveHandler);
  window.removeEventListener('resize', resizeHandler);
  cancelAnimationFrame(rafId);
});

为什么:不清理会导致内存泄漏、在组件再次 mount 时重复绑定监听器与动画。


代码:

js 复制代码
<template>
  <div class="header-banner">
    <div ref="bannerRef" class="animated-banner"></div>
  </div>
</template>

<script setup>
import { onMounted, ref } from "vue";
import layers from "@/assets/layers.js";

const bannerRef = ref(null);

onMounted(() => {
  const bannerElement = bannerRef.value;

  let animationProgress = 0; // 原 k
  let animationFrameId = 0; // 原 w
  let lastMouseX = 0; // 原 C

  // 初始化元素
  layers.map((layer) => {
    layer.resources.map((resource, resourceKey) => {
      if (!/\.(webm|mp4)$/.test(resource.src)) {
        const imgElement = document.createElement("img");
        imgElement.src = resource.src;

        imgElement.addEventListener("load", function () {
          imgElement.dataset.height = imgElement.naturalHeight.toString();
          imgElement.dataset.width = imgElement.naturalWidth.toString();

          const baseRatio = bannerElement.clientHeight / 155;
          const scaleInitial = layer.scale?.initial ?? 1;
          const scaledHeight =
            Number(imgElement.dataset.height) * baseRatio * scaleInitial;
          const scaledWidth =
            Number(imgElement.dataset.width) * baseRatio * scaleInitial;

          imgElement.height = scaledHeight;
          imgElement.width = scaledWidth;
          imgElement.style.height = scaledHeight + "px";
          imgElement.style.width = scaledWidth + "px";
        });

        layer.resources[resourceKey].el = imgElement;
      } else {
        const videoElement = document.createElement("video");
        videoElement.muted = true;
        videoElement.loop = true;
        videoElement.autoplay = true;
        videoElement.playsInline = true;
        videoElement.src = resource.src;
        videoElement.style.objectFit = "cover";
        layer.resources[resourceKey].el = videoElement;
      }
    });
  });

  const bannerHeight = bannerElement.clientHeight;
  const bannerWidth = bannerElement.clientWidth;
  const baseRatio = bannerHeight / 155;

  // 每个图层容器
  const layerContainerElements = layers.map(() => {
    const divElement = document.createElement("div");
    divElement.classList.add("layer");
    bannerElement.appendChild(divElement);
    return divElement;
  });

  // 每个图层初始状态
  const layerStates = layers.map((layer) => {
    return {
      scale: 1,
      rotate: layer.rotate?.initial || 0,
      translate: layer.translate?.initial || [0, 0],
      blur: layer.blur?.initial || 0,
      opacity: layer.opacity?.initial ?? 1,
    };
  });

  // 动画更新函数
  const animationFrameFn = () => {
    try {
      layerContainerElements.map((layerContainer, index) => {
        const currentLayer = layers[index];
        const resourceElement = layerContainer.firstChild;

        const transformState = {
          scale: layerStates[index].scale,
          rotate: layerStates[index].rotate,
          translate: layerStates[index].translate,
        };

        // scale
        if (currentLayer.scale) {
          const offset = currentLayer.scale.offset || 0;
          const delta = offset * animationProgress;
          transformState.scale = layerStates[index].scale + delta;
        }

        // rotate
        if (currentLayer.rotate) {
          const offset = currentLayer.rotate.offset || 0;
          const delta = offset * animationProgress;
          transformState.rotate = layerStates[index].rotate + delta;
        }

        // translate
        if (currentLayer.translate) {
          const offset = currentLayer.translate.offset || [0, 0];
          const delta = offset.map((val) => animationProgress * val);
          const newTranslate = layerStates[index].translate.map(
            (val, subIndex) => {
              return (
                (val + delta[subIndex]) *
                baseRatio *
                (currentLayer.scale?.initial || 1)
              );
            }
          );
          transformState.translate = newTranslate;
        }

        resourceElement.style.transform = `translate(${transformState.translate[0]}px, ${transformState.translate[1]}px) rotate(${transformState.rotate}deg) scale(${transformState.scale})`;

        // blur
        if (currentLayer.blur) {
          const offset = currentLayer.blur.offset || 0;
          const delta = offset * animationProgress;
          let blurValue = 0;

          if (!currentLayer.blur.wrap || currentLayer.blur.wrap === "clamp") {
            blurValue = Math.max(0, layerStates[index].blur + delta);
          } else if (currentLayer.blur.wrap === "alternate") {
            blurValue = Math.abs(layerStates[index].blur + delta);
          }

          resourceElement.style.filter =
            blurValue < 1e-4 ? "" : `blur(${blurValue}px)`;
        }

        // opacity
        if (currentLayer.opacity) {
          const offset = currentLayer.opacity.offset || 0;
          const delta = offset * animationProgress;
          const baseOpacity = layerStates[index].opacity;

          if (
            !currentLayer.opacity.wrap ||
            currentLayer.opacity.wrap === "clamp"
          ) {
            resourceElement.style.opacity = Math.max(
              0,
              Math.min(1, baseOpacity + delta)
            ).toString();
          } else if (currentLayer.opacity.wrap === "alternate") {
            const total = baseOpacity + delta;
            let finalOpacity = Math.abs(total % 1);
            if (Math.abs(total % 2) >= 1) finalOpacity = 1 - finalOpacity;
            resourceElement.style.opacity = finalOpacity.toString();
          }
        }
      });
    } catch (err) {
      console.log("animation error", err);
    }
  };

  // 初始化每个 layer 的 DOM
  layers.map((layer, index) => {
    const firstResourceElement = layer.resources[0].el;
    layerContainerElements[index].appendChild(firstResourceElement);
    requestAnimationFrame(animationFrameFn);
  });

  // 鼠标离开后复位动画
  const resetAnimation = () => {
    const startTime = performance.now();
    const duration = 200;
    const startProgress = animationProgress;

    cancelAnimationFrame(animationFrameId);

    const step = (now) => {
      if (now - startTime < duration) {
        animationProgress =
          startProgress * (1 - (now - startTime) / duration);
        animationFrameFn();
        requestAnimationFrame(step);
      } else {
        animationProgress = 0;
        animationFrameFn();
      }
    };
    animationFrameId = requestAnimationFrame(step);
  };

  const mouseActiveState = { value: false };

  // 鼠标事件
  const mouseLeaveFn = () => {
    mouseActiveState.value = false;
    resetAnimation();
  };

  const mouseMoveFn = (event) => {
    if (
      document.documentElement.scrollTop + event.clientY <
      bannerHeight
    ) {
      if (!mouseActiveState.value) {
        mouseActiveState.value = true;
        lastMouseX = event.clientX;
      }
      animationProgress = (event.clientX - lastMouseX) / bannerWidth;
      cancelAnimationFrame(animationFrameId);
      animationFrameId = requestAnimationFrame(animationFrameFn);
    } else if (mouseActiveState.value) {
      mouseActiveState.value = false;
      resetAnimation();
    }
  };

  // 窗口缩放时重新计算尺寸
  const resizeFn = () => {
    const newHeight = bannerElement.clientHeight;
    const newWidth = bannerElement.clientWidth;
    const newRatio = newHeight / 155;

    layers.forEach((layer) => {
      layer.resources.forEach((resource) => {
        const resourceElement = resource.el;
        const newWidthScaled =
          Number(resourceElement.dataset.width) *
          newRatio *
          (layer.scale?.initial || 1);
        const newHeightScaled =
          Number(resourceElement.dataset.height) *
          newRatio *
          (layer.scale?.initial || 1);

        resourceElement.height = newHeightScaled;
        resourceElement.width = newWidthScaled;
        resourceElement.style.height = `${newHeightScaled}px`;
        resourceElement.style.width = `${newWidthScaled}px`;
      });
    });

    cancelAnimationFrame(animationFrameId);
    animationFrameId = requestAnimationFrame(animationFrameFn);
  };

  document.addEventListener("mouseleave", mouseLeaveFn);
  window.addEventListener("mousemove", mouseMoveFn);
  window.addEventListener("resize", resizeFn);
});
</script>

<style>
body {
  margin: 0;
  padding: 0;
}

.header-banner {
  position: relative;
  z-index: 0;
  min-height: 155px;
  height: 9.375vw;
  max-height: 240px;
  background-color: #e3e5e7;
}

.animated-banner {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  overflow: hidden;
}

.layer {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

img {
  width: auto;
  height: auto;
}
</style>

拿B站的数据和素材,如下:

js 复制代码
const layers = [
  {
    resources: [
      {
        src: "./static_13/90240f707cb4a015bbf8bbd13e018b3f664087ce.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
      offset: 0.02,
    },
    rotate: {},
    translate: {
      offset: [40, 10],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 0,
    name: "21天空",
  },
  {
    resources: [
      {
        src: "./static_13/cd4194a7be89655450147d2384a162b966cf2ec2.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [5, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 1,
    name: "20远景色",
  },
  {
    resources: [
      {
        src: "./static_13/ab2379f7c80b225020ee42289db11b84b57c2766.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
      offset: -0.02,
    },
    rotate: {},
    translate: {
      offset: [5, 10],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 2,
    name: "19月亮",
  },
  {
    resources: [
      {
        src: "./static_13/938b58321d184fde31c783f5b321621ffb0c190e.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [8, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 3,
    name: "18沙滩1",
  },
  {
    resources: [
      {
        src: "./static_13/af840eae2cc555b0757d406e252f7a7dd6542eed.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [10, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 4,
    name: "17投影桥",
  },
  {
    resources: [
      {
        src: "./static_13/1271b110ca83ef84bf8d2c664537c8644a273216.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [15, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 5,
    name: "16投影碎石",
  },
  {
    resources: [
      {
        src: "./static_13/c8b49dc1aa86b2573ce2736de6692acfe0182481.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [10, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 6,
    name: "15星星投影+反光",
  },
  {
    resources: [
      {
        src: "./static_13/0881f755a4857a3b9bc12c3bbe41c00382ac6fc6.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [30, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 7,
    name: "14投影33+狗",
  },
  {
    resources: [
      {
        src: "./static_13/065fff3eb5ce38fd2d3a658ff825d5aa3a744e73.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [9, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 8,
    name: "13沙滩2",
  },
  {
    resources: [
      {
        src: "./static_13/7e61b8ea5efd98ade40b7ee386dd1bbc2b5898f5.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
      offset: 0.01,
    },
    rotate: {},
    translate: {
      offset: [6, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 9,
    name: "12海水",
  },
  {
    resources: [
      {
        src: "./static_13/27724404973bbe4573bfabab6fed6767d6b06815.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
      offset: 0.02,
    },
    rotate: {},
    translate: {
      offset: [15, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 10,
    name: "11海浪",
  },
  {
    resources: [
      {
        src: "./static_13/29d11ebfdd1b9d0840cc528c95ede27fe3d0a8e2.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [10, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 11,
    name: "10左侧垃圾",
  },
  {
    resources: [
      {
        src: "./static_13/3af7aa17868b8e5bc26950ac4a399bec62b83e50.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {},
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 12,
    name: "09月光",
  },
  {
    resources: [
      {
        src: "./static_13/27ec840f903725d7ad7ad8356d37ead40ece4b31.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
      offset: 0.01,
    },
    rotate: {},
    translate: {},
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 13,
    name: "08桥",
  },
  {
    resources: [
      {
        src: "./static_13/230bdd9372f4d1362d0d9cd75d3b233b2db29d43.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [15, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 14,
    name: "07沙滩上的石头",
  },
  {
    resources: [
      {
        src: "./static_13/56f088b30dadc2c99fed255fcf4cc34c4f37f313.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [30, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 15,
    name: "0633与狗",
  },
  {
    resources: [
      {
        src: "./static_13/c5a4a63c098b81d89c73d359de35fcea9bb1c7a3.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [10, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 21,
    name: "00沙滩碎星星",
  },
  {
    resources: [
      {
        src: "./static_13/d195a834c55a24c1f599e7c15e3b59e5795c5c0f.webm",
        id: 0,
      },
    ],
    scale: {
      initial: 0.5,
    },
    rotate: {},
    translate: {
      initial: [-205, 65],
      offset: [10, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 23,
    name: "动态小星星",
  },
  {
    resources: [
      {
        src: "./static_13/59b1c59d919469fc0a4632acc5a6eecd9260d099.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.45,
    },
    rotate: {},
    translate: {
      offset: [35, 10],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 17,
    name: "0422",
  },
  {
    resources: [
      {
        src: "./static_13/7b7dd8a92bf8036be6502898e9804c65ee0f6284.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.45,
    },
    rotate: {},
    translate: {
      initial: [-5, -5],
      offset: [38, 13],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 18,
    name: "0322手里星星",
  },
  {
    resources: [
      {
        src: "./static_13/aabd831214cdae044414980e3c06787c0f3073ff.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [100, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 19,
    name: "02最前景石头",
  },
  {
    resources: [
      {
        src: "./static_13/b5b5336124932336e39a99d64014fc0154d53912.webm",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      initial: [-1140, 130],
      offset: [130, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 22,
    name: "05前景星星",
  },
  {
    resources: [
      {
        src: "./static_13/928547de5ced33ce2b28b7924e1a470a110eed26.png",
        id: 0,
      },
    ],
    scale: {
      initial: 0.47,
    },
    rotate: {},
    translate: {
      offset: [-5, 0],
    },
    blur: {},
    opacity: {
      wrap: "clamp",
    },
    id: 20,
    name: "01流星",
  },
];

export default layers;
相关推荐
KenXu3 小时前
F2C Prompt to Design、AI 驱动的设计革命
前端
小鱼儿亮亮3 小时前
canvas中画线条,线条效果比预期宽1像素且模糊问题分析及解决方案
前端·react.js
@大迁世界3 小时前
用 popover=“hint“ 打造友好的 HTML 提示:一招让界面更“懂人”
开发语言·前端·javascript·css·html
伍哥的传说3 小时前
Tailwind CSS v4 终极指南:体验 Rust 驱动的闪电般性能与现代化 CSS 工作流
前端·css·rust·tailwindcss·tailwind css v4·lightning css·utility-first
小鱼儿亮亮3 小时前
使用Redux的combineReducers对数据拆分
前端·react.js
定栓3 小时前
Typescript入门-类型断言讲解
前端·javascript·typescript
码间舞3 小时前
你不知道的pnpm!如果我的电脑上安装了nvm,切换node版本后,那么pnpm还会共享一个磁盘的npm包吗?
前端·代码规范·前端工程化
用户1512905452203 小时前
itoa函数
前端
xiaominlaopodaren3 小时前
“UI里就可以请求数据库”,让你陌生的 React 特性
前端·javascript·react.js