CSS3动画之造物节的两种实现

458 阅读2分钟

实现效果

jq8ip-dus6y.gif

开发准备

  • 设计图:球面投影

8ce45b5e-ea1f-4501-b4de-94473fd65a40.png

  • 原理:
    • 切图、拼成圆球
    • 刚开始叠在一起,然后按角度旋转,接着沿着z轴向外推到相应的位置,就接近一个球形拉

a4e600be-a4f9-4bb4-bfb7-dbe4001f1a73.png

  • 本事例将设计图切成 20 份

方式一:自己实现

代码结构

├── audio # 背景音乐

│ └── happy.mp3

├── css # 全局 css

│ └── index.css

├── img # 图片(背景图、球形投影切片-20份)

├── index.html # 页面

├── script # js

│ └── index.js

└── start.bat # 启动 http-server,用于手机测试

html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <!-- 移动端配置 viewport -->
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
  <title>css3d造物节</title>
  <link rel="stylesheet" href="./css/index.css">
</head>

<body>
  <!-- 语音播放器 -->
  <audio id="audio">
    <source src="./audio/happy.mp3" type="audio/mpeg">
  </audio>
  <!-- 喇叭 -->
  <div id="laba">🎺</div>
  <!-- 3d容器 -->
  <div class="container">
    <!-- 3d盒子,放图片 -->
    <div id='box' class="box"></div>
  </div>
  <script src="./script/zepto.min.js"></script>
  <script src="./script/index.js"></script>
</body>

</html>

css

  • 1rem = 16px; rem基于根的font-size,html 默认 font-size: 16px;
  • 每张图:宽129px、高1170px
* {
  padding: 0;
  margin: 0;
}

body {
  background-image: url('../img/bg.jpg');
}

#laba {
  position: absolute;
  top: 1rem;
  right: 1rem;
  z-index: 999;
}

.container {
  margin: 0 auto;
  width: 8.0625rem; /* 129 */
  perspective: 25rem; /* 设置视距: 400 */
}

.container .box {
  height: 100%;
  transform-style: preserve-3d;
  perspective-origin: 50% 50%; /* 设置观察中心点。*/
}

.container .box > div {
  position: absolute;
  width: 8.0625rem; /* 129 */
  height: 73.125rem; /* 1170 */
}

js

/**
 * 根据id获取DOM节点
 * @param {*} id
 * @returns
 */
 function _$(id) {
  return document.getElementById(id);
}

/**
 * 计算半径
 * @param  length 每张图片的宽度
 * @param  count  一共多少张图
 * @return 半径长度
 */
function calculateRadius(width, count) {
  // r = 每张图片的宽度/2 / tran(每一份的度数/2); 每一份的度数 = 360/图片数量 = 360/20 = 18
  // Math.PI = 180
  // 为什么要减3,因为合成圆时,会有两个边需要合并 -2,合并后还是存在一条1px的边,因此,把这条边再减去 -1
  // return Math.round(width / (2 * Math.tan((Math.PI * 2) / count / 2))) - 3;
  return Math.round(width / (2 * Math.tan(Math.PI / count))) - 3;
}

/**
 * 创建图片并布局
 * @param {*} $el box
 * @param {*} len 图片数量
 * @param {*} r 调整角度
 */
function createDIVLayout($el, len, r) {
  // 批量添加节点
  const fragment = document.createDocumentFragment();
  const _img_url_prefix = './img/p{n}.png';

  for (let i = 1; i <= len; i++) {
    const div = document.createElement('div');
    // 辅助标记,可以删除
    div.innerHTML = i;
    // 设置 css
    div.style.background = `url(${_img_url_prefix.replace(/\{n\}/g, i)}) no-repeat`;
    div.style.WebkitTransform = `rotateY(${(360 / len) * i}deg) translateZ(${r}px)`;

    // 添加到 fragment
    fragment.appendChild(div);
  }

  // 将 fragment 批量添加到 $el上
  $el.appendChild(fragment);
}

/* 定义常量 */
const IMG_LEN = 20;
const IMG_W = 129;
const radius = calculateRadius(IMG_W, IMG_LEN);

/* 获取相关 dom节点 */
const box = _$('box');
const audio = _$('audio');

/* 创建图片并布局 */
createDIVLayout(box, IMG_LEN, radius);

/* zepto 事件 */
// 控制背景音乐播放和暂停
$('#laba').on('tap', function () {
  if (audio.paused) {
    audio.play();
    $(this).text('⏸');
  } else {
    audio.pause();
    $(this).text('🎺');
  }
});

// 手指拖动,box 绕y旋转
let startX = 0; // 手指开始按时的X位置
let endX = 0; // 手指松开时的X位置
let x = 0; // 移动的距离
let flag = true; // 用于区分touch触发旋转还是手机自己通过陀螺仪旋转
let touching = false;

$('#box').on('touchstart', function (event) {
  event.preventDefault();
  const touch = event.targetTouches[0];
  startX = touch.pageX - x;
  touching = true;
});
$('#box').on('touchmove', function (event) {
  if (flag && touching) {
    event.preventDefault();
    const touch = event.targetTouches[0];
    endX = touch.pageX;
    x = endX - startX;
    //x移动了多少,就绕y轴旋转多少度
    box.style.transform = `rotateY(${x}deg)`;
  }
});
$('#box').on('touchend', function () {
  touching = false;
});

// 监听陀螺仪 - 旋转角度,用于手机自己绕y轴运动时,改变旋转角度
window.addEventListener('deviceorientation', function (event) {
  const gamma = event.gamma;
  if (Math.abs(gamma) > 1) {
    flag = false;
    box.style.transform = `rotateY(${gamma * 3}deg)`; //乘3,主要是想快点
  } else {
    flag = true;
  }
});

启动server

  • http-server 简单给本地文件夹开启一个server,因为手机端无法访问你PC端上的本地文件
  • 安装 http-server: npm i -g http-server
  • 在当前代码根目录下,运行 http-server -p 8000 命令,开启服务

5205e2be-f0f1-4178-8907-e996823b5843.png

移动端查看

方式二:使用 css3d-engine

css3D-engine 是一个非常优秀的 css3d 库,上手简单,主要掌握以下几个元素

  • 三维场景(Stage):我们放置的位置
  • 平面(Plane): 构建平面时使用
  • 相机(Camera): 3D场景必备,将相机理解为我们的眼睛
  • 立方体(Box): 构建立方体使用
  • 全景盒子(Skybox): 构建全景背景时使用

构成3D效果的三要素:场景、物体、视角。把握住这三要素,理解3D就容易的多:

代码结构

├── img # 图片(背景图、球形投影切片-20份)

├── index.html # 页面

├── script # js

│ └── css3d.js

└── start.bat # 启动 http-server,用于手机测试

html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
  </style>
</head>

<body>
  <div id="main" style="width:500px;height:500px;"></div>
  <script type="text/javascript" src="./js/css3d.js"></script>
  <script>
    // Constract
    const PANO_RECT = { w: 2586, h: 1170 };
    const IMG_LEN = 20;
    const _img_prefix = 'img/p{n}.png'


    // 创建场景
    const s = new C3D.Stage();

    // 设置场景大小和材料,这里是背景图片
    s.size(window.innerWidth, window.innerHeight).material({
      image: 'img/bg.jpg'
    }).update();

    document.getElementById('main').appendChild(s.el);

    const createPano = rect => {
      const _step = rect.w / IMG_LEN;
      const _radius = Math.floor(_step / 2 / Math.tan(Math.PI / IMG_LEN)) - 1;

      // 模拟创建雪碧图
      const _sp = new C3D.Sprite();
      for (let i = 1; i <= IMG_LEN; i++) {
        const _p = new C3D.Plane();
        const _r = 360 / IMG_LEN * i;
        const _a = Math.PI * 2 / IMG_LEN * i;
        const img = _img_prefix.replace(/\{n\}/g, i);

        _p.size(_step, rect.h).position(Math.sin(_a) * _radius, 0, -Math.cos(_a) * _radius).rotation(0, -_r, 0).material({
          image: img,
          repeat: 'no-repeat',
          bothsides: false,
        }).update();

        _sp.addChild(_p);
      }
      return _sp
    }

    // 构建平面
    const pano = createPano(PANO_RECT);

    pano.position(0, 0, -400).updateT();

    // 将 pano 插入到场景中
    s.addChild(pano);

    //响应屏幕调整尺寸
    function resize() {
      s.size(window.innerWidth, window.innerHeight).update();
    }

    window.onresize = () => resize();
    resize();

    //刷新场景
    requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame ||
      function (callback) {
        setTimeout(callback, 1000 / 60);
      };

    function go() {
      pano.rotate(0, 0.2, 0).updateT();
      requestAnimationFrame(go);
    }

    requestAnimationFrame(go);

  </script>
</body>

</html>