一个文笔一般,想到哪是哪的唯心论前端小白。
前言
提前祝大家中秋节阖家欢乐呀!
在常年的 CRUD 工作过程中,想在中秋活动中凑个热闹,忽然发现自己居然没有一点点思路!!!真的是有点废了~
看到奖品里面有个小花灯,就试试能不能用 css 绘制一个孔明灯出来。
孔明灯(别称:天灯、许愿灯)是一种古老的中国手工艺品,被公认为热气球的始祖。
其起初在古代多做传递讯息之用,现代则用于祈福许愿,一般在元宵节,中秋节等重大节日施放。其起源有“莘七娘放天灯说”和“诸葛亮发明说”两种说法。
先放一张网上找来的图片:
然后是一张我做的单个孔明灯的效果:
最终效果:
好像有点掉帧,可以参考底部的码上掘金的效果。
还是原来的配方。
思路
需求分析
需求分析是参考那张网络图片进行分析的,毕竟那才是最终的效果!
- 首先要绘制一个孔明灯,可以旋转并摇摇晃晃,还要能往上飞。
- 然后要绘制成批的绘制,分出远近景出来,近大远小。
- 最后补充两句祝福的话。
其中最麻烦的应该就是绘制孔明灯了,简单分解一下孔明灯的组成部分:
- 面:有四个面,为了效果更好一点,我做的是六个面的。
- 顶:由面延伸到中间,最终合在一起,密封的,不密封就飞不起来啦!
- 绳子:底部每个角延伸一根绳子汇集在一起,用来绑火的。
- 火焰:最下面由绳子支撑的火焰。
让它动起来就是做一个动画效果,animation 足够了,增加两个效果,一个绕中心轴无缝旋转,另一个随便摇晃两下就好啦!
批量绘制就是重复上面的过程。
至于祝福的话,就不用赘述了。
思路梳理
绘制孔明灯共有六个面,肯定是绘制一个,然后旋转就好了。
两个思路:
- 面和顶,共用一个父节点。类似下面代码这样:
<div class="light">
<div class="squre">1</div>
<div class="squre">2</div>
<div class="squre">3</div>
<div class="squre">4</div>
<div class="squre">5</div>
<div class="squre">6</div>
<div class="point"></div>
<div class="point"></div>
<div class="point"></div>
<div class="point"></div>
<div class="point"></div>
<div class="point"></div>
</div>
- 面和顶以及绳子共用一个父节点,做成一个完整的面,旋转成整个灯,类似于下面代码这样:
<div class="light">
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
</div>
最终我选择的第二种方案。我一上来就是用的第一种方案去做的,前面都没有问题,六个面可以合起来称为一个六棱柱。但是顶就不行了!变成了这样:
原因是因为在旋转的时候,用到了:
transform-origin: 50% 50% var(--sin60Width);
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(0deg) rotateX(-60deg);
这样就会导致,顶部在旋转那个 60° 的时候,他的旋转轴是 50% 50% var(--sin60Width),就需要重新定位它!
开发
1. 绘制一个面
目标效果:
如上图片所示,整个灯就是六个面,逐个旋转 60度出来的。(为什么是60度?)
可以看见,每一面确实和思路分析中一致,是有三个部分组成:顶 + 面 + 绳。
核心代码如下:
html:
<div class="face">
<div class="face-top"></div>
<div class="face-squre"></div>
<div class="face-line"></div>
</div>
css:
.face {
height: var(--faceHeight);
width: var(--faceWidth);
/* background-color: pink; */
perspective: var(--faceWidth * 10);
perspective-origin: center var(--faceHeight);
transform-style: preserve-3d;
position: absolute;
top: 0;
left: var(--faceCenter);
position: fixed;
}
.face-top {
width: var(--faceWidth);
height: calc(var(--sin60Width) / cos(30deg));
background: radial-gradient(at 50% 90%, yellow 10%, #ffae00 40%, red);
transform: rotateX(-60deg);
transform-origin: 0 100%;
clip-path: polygon(50% 0%, 0% 100%, 100% 100%)
}
.face-squre {
height: var(--faceHeight);
width: var(--faceWidth);
background: radial-gradient(at 50% 70%, yellow 10%, #ff7b00 40%, red);
animation: shape 2s linear infinite;
}
.face-line {
width: var(--faceWidth);
height: calc(var(--sin60Width) / cos(30deg));
background: linear-gradient(to right, transparent 48%, black 48%, transparent 52%);
transform: rotateX(60deg);
transform-origin: 0 0;
}
关键部分分析一下:
radial-gradient,是所有的的颜色的基础,使用径向渐变来实现每个面在夜晚被等照亮的感觉。height: calc(var(--sin60Width) / cos(30deg));,这个高度的计算是,灯心到每个面的距离,数学好的可以出来讲一下。--sin60Width这个变量对应的值是calc(sin(60deg) * var(--faceWidth))。不明白的可以简单在纸上画一下:
background: linear-gradient(to right, transparent 48%, black 48%, transparent 52%);,这个渐变要达到的效果就是只有中间的一条是黑色的,其他地方是透明的,就可以使绳子的实现和顶的实现保持一致了。clip-path: polygon(50% 0%, 0% 100%, 100% 100%),可以把矩形切成 等腰三角乡。
2. 合拢六个面
合拢六个面,主要麻烦的是转的时候,旋转轴是定点到火焰这条线,所以每一面其实是浮在light这一层上的,而最底下的一面是在light的下面的感觉。
所以最后每一面是这么画的:
.light {
height: var(--lightHeight);
width: var(--lightWidth);
position: relative;
margin: 0 auto;
perspective: var(--faceWidth * 10);
perspective-origin: center var(--faceHeight);
transform-style: preserve-3d;
animation: rotateAnimationXY 10s linear infinite;
position: relative;
transform: scale(.5);
}
/* ----squre---- */
.face:nth-of-type(6n - 5) {
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(0deg);
}
.face:nth-of-type(6n - 4) {
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(60deg);
}
.face:nth-of-type(6n - 3) {
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(120deg);
}
.face:nth-of-type(6n - 2) {
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(180deg);
}
.face:nth-of-type(6n - 1) {
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(240deg);
}
.face:nth-of-type(6n) {
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(300deg);
}
里面的 transilateZ 就是让每一面都浮起来,正三角形顶点到底边的距离的长度。
3. 绘制火焰
火焰其实也是用渐变做的,为了保证旋转过程中火焰一直保持燃烧的效果,所以用6个火焰旋转生成。
核心代码:
<div class="fire">
<div class="fire-item"></div>
<div class="fire-item"></div>
<div class="fire-item"></div>
<div class="fire-item"></div>
<div class="fire-item"></div>
<div class="fire-item"></div>
</div>
.fire-item {
position: absolute;
height: 100%;
width: 100%;
background: radial-gradient(at 50% 80%, yellow 10%, #8B0000 20%, transparent 40%, transparent);
}
.fire-item:nth-of-type(6n - 5) {
transform: rotateY(0deg);
}
.fire-item:nth-of-type(6n - 4) {
transform: rotateY(60deg);
}
.fire-item:nth-of-type(6n - 3) {
transform: rotateY(120deg);
}
.fire-item:nth-of-type(6n - 2) {
transform: rotateY(180deg);
}
.fire-item:nth-of-type(6n - 1) {
transform: rotateY(240deg);
}
.fire-item:nth-of-type(6n) {
transform: rotateY(300deg);
}
每一个火焰,按照中轴转上60度最后每一面看起来就没有那么明显的锯齿感了。
4. 批量生成
批量生成的话如果还是用 html+css 做就很麻烦了,因为dom节点会很多,代码量会几何程度上升,所以就把上面的内容,尽可能的放在了 js 中。使用循环的方式来实现!
封装方式还是用我最喜欢的 class 的方式,渲染的时候就可以用 new Light() 的方式实现渲染了!
可以看见我在写这个灯的时候,已经有针对性的把所有的数字都用变量来做了,所以改造起来也很容易,代码如下:
class Light {
constructor(s) {
const w = 200,
h = 400;
this.scale = s;
this.lightWidth = w;
this.lightHeight = h;
this.faceWidth = this.lightWidth / 2;
this.faceHeight = this.lightHeight / 2;
this.faceTop = 0;
this.sin60Width = this.faceWidth * Math.sin(60 * (Math.PI / 180));
this.faceCenter = this.lightWidth / 2 - this.faceWidth / 2;
this.fireWidth = this.faceWidth * 0.4;
this.fireHeight = this.fireWidth * 2;
this.isUpdate = true;
this.isRotate = this.scale > .4 ? true : false;
this.init();
}
init() {
const wrap = this.initWrap();
const light = this.initLight();
const fire = this.initFire();
wrap.appendChild(light);
light.appendChild(fire);
const body = document.getElementsByTagName("body")[0];
body.appendChild(wrap);
}
initWrap() {
const wrap = document.createElement("div");
wrap.className = "light-wrap";
wrap.style.position = "fixed";
wrap.style.zIndex = this.lightHeight + this.lightWidth;
let top = parseInt(Math.random() * window.innerHeight);
let left = parseInt(Math.random() * window.innerWidth);
wrap.style.top = top + "px";
wrap.style.left = left + "px";
wrap.style.transform = `scale(${this.scale})`
wrap.style.height = this.lightHeight + "px";
wrap.style.width = this.lightWidth + "px";
const update = () => {
const step = 0.2;
top - step * 2 > -this.lightHeight
? (top -= step * 2)
: (top = window.innerHeight);
left - step > -this.lightWidth
? (left -= step)
: (left = window.innerHeight);
wrap.style.top = top + "px";
wrap.style.left = left + "px";
setTimeout(update, 1000 / 30);
};
if (this.isUpdate) {
update();
}
return wrap;
}
initLight() {
const light = document.createElement("div");
light.className = "light";
light.style.height = this.lightHeight + "px";
light.style.width = this.lightWidth + "px";
light.style.position = "relative";
light.style.perspective = this.faceWidth * 10 + "px";
light.style.perspectiveOrigin = `center ${this.lightHeight / 2}px`;
light.style.transformStyle = "preserve-3d";
light.style.position = "relative";
light.style.animation = `rotateAnimationX ${
parseInt(Math.random() * 10) + 5
}s linear infinite`;
for (let i = 0; i < 6; i++) {
const face = this.initFace(i * 60, i);
light.appendChild(face);
}
return light;
}
initFace(deg, _i) {
const face = document.createElement("div");
face.className = "face";
face.style.height = this.faceHeight + "px";
face.style.width = this.faceWidth + "px";
face.style.transformStyle = "preserve-3d";
face.style.position = "absolute";
face.style.top = 0;
face.style.left = this.faceCenter + "px";
face.style.transformOrigin = `50% 50% ${this.sin60Width}px`;
face.style.transform = `translateZ(${
-1 * this.sin60Width
}px) rotateY(${deg}deg)`;
const rotate = () => {
deg++;
face.style.transform = `translateZ(${
-1 * this.sin60Width
}px) rotateY(${deg}deg)`;
setTimeout(rotate, 1000 / 30);
};
this.isRotate && rotate();
const faceTop = this.initFaceTop();
const faceSqure = this.initFaceSqure(_i);
const faceLine = this.initFaceLine();
face.appendChild(faceTop);
face.appendChild(faceSqure);
face.appendChild(faceLine);
return face;
}
initFaceTop() {
const faceTop = document.createElement("div");
faceTop.className = "face-top";
faceTop.style.width = this.faceWidth + "px";
faceTop.style.height =
this.sin60Width / Math.cos(30 * (Math.PI / 180)) + "px";
faceTop.style.background = `radial-gradient(at 50% 90%, yellow 10%, #ffae00 40%, red)`;
faceTop.style.transform = `rotateX(-60deg)`;
faceTop.style.transformOrigin = `0 100%`;
faceTop.style.clipPath = `polygon(50% 0%, 0% 100%, 100% 100%)`;
return faceTop;
}
initFaceSqure(_i) {
const faceSqure = document.createElement("div");
faceSqure.className = "face-squre";
faceSqure.style.height = this.faceHeight + "px";
faceSqure.style.width = this.faceWidth + "px";
faceSqure.style.background = `radial-gradient(at 50% 70%, #0d0034 10%, #381460 40%, red)`;
faceSqure.style.animation = `shape 2s linear infinite`;
const word = this.initWord(_i);
faceSqure.appendChild(word);
return faceSqure;
}
initWord(_i) {
const word = document.createElement("div");
word.className = "word";
word.innerText = blessings[_i];
word.style.height = "100%";
word.style.width = "100%";
word.style.textAlign = "center";
word.style.writingMode = "vertical-rl";
word.style.lineHeight = this.faceWidth + "px";
word.style.transform = "scaleX(-1)";
word.style.letterSpacing = "2px";
word.style.fontFamily = "楷体";
word.style.fontWeight = "bold";
word.style.userSelect = 'none'
word.style.color = 'rgb(126 0 0)'
word.style.fontSize = parseInt(this.faceWidth / 4) + "px";
return word;
}
initFaceLine() {
const faceLine = document.createElement("div");
faceLine.className = "face-line";
faceLine.style.width = this.faceWidth + "px";
faceLine.style.height =
this.sin60Width / Math.cos(30 * (Math.PI / 180)) + "px";
faceLine.style.background = `linear-gradient(to right, transparent 48%, black 48%, transparent 52%)`;
faceLine.style.transform = `rotateX(60deg)`;
faceLine.style.transformOrigin = `0 0`;
return faceLine;
}
initFire() {
const fire = document.createElement("div");
fire.className = "fire";
fire.style.position = `absolute`;
fire.style.top =
this.faceHeight + this.faceWidth - 0.2 * this.faceWidth + "px";
fire.style.left = this.lightWidth / 2 - this.fireWidth / 2 + "px";
fire.style.width = this.fireWidth + "px";
fire.style.height = this.fireHeight + "px";
fire.style.perspective = this.faceWidth * 10 + "px";
fire.style.perspectiveOrigin = `center ${this.faceHeight}px`;
fire.style.transformStyle = `preserve-3d`;
const firEItem = this.initFireItem(30);
fire.appendChild(firEItem);
return fire;
}
initFireItem(d) {
const fireItem = document.createElement("div");
fireItem.style.position = `absolute`;
fireItem.style.height = `100%`;
fireItem.style.width = `100%`;
fireItem.style.background = `radial-gradient(at 50% 80%, yellow 10%, #8B0000 20%, transparent 40%, transparent)`;
fireItem.style.transform = `rotateY(${d}deg)`;
return fireItem;
}
}
如上所示,使用 Light 这个类,把单个的封装了一下。
最终使用的方式就是:
new Light(.3)
那个 .3 所指的是缩放程度,我把一开始静态页面的那个孔明灯的宽高作为了一个基准,按照这个值去缩放,这样可以避免字号太小导致页面乱了。
分享
纯 css 实现六边形 孔明灯。
中秋节快乐!!!(定位是依据可是范围来的,所以挤一块了,可以自己调整)
后记
有没有发现今年过的好快!!!不知不觉就中秋了 . . .
以上就是本次所有的内容了,我发现通过数学计算,理论上可以实现五边形、八角形、等等样式的孔明灯,等待去发掘。
也可以增加按钮,触发表单自己在上面写上自己的祝福语,如果有同学商用赚钱了记得带我一个呀!!!
所思即所得
- 一个开箱即用的孔明灯 demo
- 对三角形相关的数学知识进行了温习,编程的终点依旧是数学 . . .
- css3 中的 3d转换、过渡,以及渐变色的使用进行了些许深度的温故。
- 参与一次掘金发起的中秋征文活动,重在参与!
有什么新的想法可以在评论区一起探讨呀!