中秋-一起来放孔明灯呀!!!

496 阅读8分钟

一个文笔一般,想到哪是哪的唯心论前端小白。

前言

提前祝大家中秋节阖家欢乐呀!

在常年的 CRUD 工作过程中,想在中秋活动中凑个热闹,忽然发现自己居然没有一点点思路!!!真的是有点废了~

看到奖品里面有个小花灯,就试试能不能用 css 绘制一个孔明灯出来。

孔明灯(别称:天灯、许愿灯)是一种古老的中国手工艺品,被公认为热气球的始祖。

其起初在古代多做传递讯息之用,现代则用于祈福许愿,一般在元宵节,中秋节等重大节日施放。其起源有“莘七娘放天灯说”和“诸葛亮发明说”两种说法。

先放一张网上找来的图片: image.png

然后是一张我做的单个孔明灯的效果:

Animation.gif

最终效果:

Animation.gif

好像有点掉帧,可以参考底部的码上掘金的效果。

还是原来的配方。

思路

需求分析

需求分析是参考那张网络图片进行分析的,毕竟那才是最终的效果!

  1. 首先要绘制一个孔明灯,可以旋转并摇摇晃晃,还要能往上飞。
  2. 然后要绘制成批的绘制,分出远近景出来,近大远小。
  3. 最后补充两句祝福的话。

其中最麻烦的应该就是绘制孔明灯了,简单分解一下孔明灯的组成部分:

  • 面:有四个面,为了效果更好一点,我做的是六个面的。
  • 顶:由面延伸到中间,最终合在一起,密封的,不密封就飞不起来啦!
  • 绳子:底部每个角延伸一根绳子汇集在一起,用来绑火的。
  • 火焰:最下面由绳子支撑的火焰。

让它动起来就是做一个动画效果,animation 足够了,增加两个效果,一个绕中心轴无缝旋转,另一个随便摇晃两下就好啦!

批量绘制就是重复上面的过程。

至于祝福的话,就不用赘述了。

思路梳理

绘制孔明灯共有六个面,肯定是绘制一个,然后旋转就好了。

两个思路:

  1. 面和顶,共用一个父节点。类似下面代码这样:
<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>
  1. 面和顶以及绳子共用一个父节点,做成一个完整的面,旋转成整个灯,类似于下面代码这样:
<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>

最终我选择的第二种方案。我一上来就是用的第一种方案去做的,前面都没有问题,六个面可以合起来称为一个六棱柱。但是顶就不行了!变成了这样:

image.png

原因是因为在旋转的时候,用到了:

transform-origin: 50% 50% var(--sin60Width); 
transform: translateZ(calc(-1 * var(--sin60Width))) rotateY(0deg) rotateX(-60deg);

这样就会导致,顶部在旋转那个 60° 的时候,他的旋转轴是 50% 50% var(--sin60Width),就需要重新定位它!

开发

1. 绘制一个面

目标效果:

Animation_1.gif

如上图片所示,整个灯就是六个面,逐个旋转 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;
}

关键部分分析一下:

  1. radial-gradient,是所有的的颜色的基础,使用径向渐变来实现每个面在夜晚被等照亮的感觉。
  2. height: calc(var(--sin60Width) / cos(30deg));,这个高度的计算是,灯心到每个面的距离,数学好的可以出来讲一下。--sin60Width 这个变量对应的值是 calc(sin(60deg) * var(--faceWidth))。不明白的可以简单在纸上画一下:

004fed65ea1484910a23574c670593e.jpg

  1. background: linear-gradient(to right, transparent 48%, black 48%, transparent 52%);,这个渐变要达到的效果就是只有中间的一条是黑色的,其他地方是透明的,就可以使绳子的实现和顶的实现保持一致了。
  2. 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 实现六边形 孔明灯。

中秋节快乐!!!(定位是依据可是范围来的,所以挤一块了,可以自己调整)

后记

有没有发现今年过的好快!!!不知不觉就中秋了 . . .

以上就是本次所有的内容了,我发现通过数学计算,理论上可以实现五边形、八角形、等等样式的孔明灯,等待去发掘。

也可以增加按钮,触发表单自己在上面写上自己的祝福语,如果有同学商用赚钱了记得带我一个呀!!!

所思即所得

  1. 一个开箱即用的孔明灯 demo
  2. 对三角形相关的数学知识进行了温习,编程的终点依旧是数学 . . .
  3. css3 中的 3d转换、过渡,以及渐变色的使用进行了些许深度的温故。
  4. 参与一次掘金发起的中秋征文活动,重在参与!

有什么新的想法可以在评论区一起探讨呀!