实现效果
开发准备
- 设计图:球面投影
- 原理:
- 切图、拼成圆球
- 刚开始叠在一起,然后按角度旋转,接着沿着z轴向外推到相应的位置,就接近一个球形拉
- 本事例将设计图切成 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 命令,开启服务
- 在PC浏览器输入 http://10.1.251.161:8000,验证是否能打开页面
移动端查看
- 将本地服务地址生成二维码:这里可以使用 草料二维码生成器
- 微信扫码即可
方式二:使用 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>