圣诞节到了,用js给喜欢的人写一颗圣诞树吧

1、效果预览

圣诞

2、代码
2.1、定义数组写下祝福语

分析

定义了一个名为myLabels的数组,其中包含了三个字符串元素,分别是“Merry Christmas”, “健健康康,平安喜乐”, “一定要站在你所热爱的世界里闪闪发光”。

代码

let myLabels = [
    "Merry Christmas",
    "健健康康,平安喜乐",
    "一定要站在你所热爱的世界里闪闪发光",
];
2.2、模拟雪花落下的效果

分析

创建了一个<div>元素,用于表示雪花,设置其样式为绝对定位,颜色为白色,并设置雪花字符为 “❆”。

然后获取了页面的高度和宽度,用于确定雪花的起始和结束位置。

接下来,定义了生成一片雪花的毫秒数,以及一个定时器,定期生成一片雪花。

在每个定时器回调函数中,随机生成雪花的起始和结束位置、大小、持续时间、透明度等参数。

然后,克隆出一个雪花,并根据上述参数设置其样式,并插入到页面中。

接下来设置了一个一次性定时器,用于在雪花生成后一段时间后,修改雪花的样式,使其具有下落效果。

最后,在雪花落下后一段时间,再次设置一个定时器,将雪花从页面中删除。

代码

function snow() {
    
    
    //  1、定义一片雪花模板
    var flake = document.createElement("div");
    // 雪花字符 ❄❉❅❆✻✼❇❈❊✥✺
    flake.innerHTML = "❆";
    flake.style.cssText = "position:absolute;color:#fff;";

    //获取页面的高度 相当于雪花下落结束时Y轴的位置
    var documentHieght = window.innerHeight;
    //获取页面的宽度,利用这个数来算出,雪花开始时left的值
    var documentWidth = window.innerWidth;

    //定义生成一片雪花的毫秒数
    var millisec = 100;
    //2、设置第一个定时器,周期性定时器,每隔一段时间(millisec)生成一片雪花;
    setInterval(function () {
    
    
        //页面加载之后,定时器就开始工作
        //随机生成雪花下落 开始 时left的值,相当于开始时X轴的位置
        var startLeft = Math.random() * documentWidth;

        //随机生成雪花下落 结束 时left的值,相当于结束时X轴的位置
        var endLeft = Math.random() * documentWidth;

        //随机生成雪花大小
        var flakeSize = 5 + 20 * Math.random();

        //随机生成雪花下落持续时间
        var durationTime = 4000 + 7000 * Math.random();

        //随机生成雪花下落 开始 时的透明度
        var startOpacity = 0.7 + 0.3 * Math.random();

        //随机生成雪花下落 结束 时的透明度
        var endOpacity = 0.2 + 0.2 * Math.random();

        //克隆一个雪花模板
        var cloneFlake = flake.cloneNode(true);

        //第一次修改样式,定义克隆出来的雪花的样式
        cloneFlake.style.cssText += `
                        left: ${
      
      startLeft}px;
                        opacity: ${
      
      startOpacity};
                        font-size:${
      
      flakeSize}px;
                        top:-25px;
                            transition:${
      
      durationTime}ms;
                    `;

        //拼接到页面中
        document.body.appendChild(cloneFlake);

        //设置第二个定时器,一次性定时器,
        //当第一个定时器生成雪花,并在页面上渲染出来后,修改雪花的样式,让雪花动起来;
        setTimeout(function () {
    
    
            //第二次修改样式
            cloneFlake.style.cssText += `
                                left: ${
      
      endLeft}px;
                                top:${
      
      documentHieght}px;
                                opacity:${
      
      endOpacity};
                            `;

            //4、设置第三个定时器,当雪花落下后,删除雪花。
            setTimeout(function () {
    
    
                cloneFlake.remove();
            }, durationTime);
        }, 0);
    }, millisec);
}

调用函数

snow();
2.3、设置背景粒子

分析

使用MorphSVGPlugin插件来将指定的多边形转换为路径。

首先,调用MorphSVGPlugin.convertToPath("polygon")方法,将ID为“polygon”的多边形元素转换为路径元素。

然后,定义了两个命名空间常量,xmlnsxlinkns,分别用于设置XML命名空间和Xlink命名空间。

接下来,定义了两个辅助函数selectselectAll,用于选择一个或多个元素。

然后,通过select函数选择了一些DOM元素,例如.pContainer.mainSVG#star.sparkle#tree等。

接下来,定义了一些变量,showParticletrue表示显示粒子,particleColorArray是一个颜色数组,particleTypeArray是一个粒子类型数组,用于指定粒子的形状。

然后,定义了一个粒子池数组particlePool、一个粒子计数器particleCount以及一个粒子数量numParticles

代码

MorphSVGPlugin.convertToPath("polygon");
    var xmlns = "http://www.w3.org/2000/svg",
      xlinkns = "http://www.w3.org/1999/xlink",
      select = function (s) {
    
    
        return document.querySelector(s);
      },
      selectAll = function (s) {
    
    
        return document.querySelectorAll(s);
      },
      pContainer = select(".pContainer"),
      mainSVG = select(".mainSVG"),
      star = select("#star"),
      sparkle = select(".sparkle"),
      tree = select("#tree"),
      showParticle = true,
      particleColorArray = [
        "#E8F6F8",
        "#ACE8F8",
        "#F6FBFE",
        "#A2CBDC",
        "#B74551",
        "#5DBA72",
        "#910B28",
        "#910B28",
        "#446D39",
      ],
      particleTypeArray = ["#star", "#circ", "#cross", "#heart"],
      // particleTypeArray = ['#star'],
      particlePool = [],
      particleCount = 0,
      numParticles = 201;
2.4、操作动画效果

分析

使用GSAP动画库(GreenSock Animation Platform)来操作SVG元素的动画效果。

首先,通过gsap.set设置了SVG的可见性为“visible”,表示将SVG元素设置为可见状态。

接下来,使用gsap.set设置了sparkle元素的transformOrigin为"50% 50%",并将其y坐标设置为-100,这将影响后续的动画效果。

然后定义了一个函数getSVGPoints,用于获取SVG路径中的点坐标。该函数接受一个路径元素作为参数,通过MotionPathPlugin.getRawPath方法获取路径的原始数据,并将其转换为点坐标的数组。

接着使用getSVGPoints函数分别获取了.treePath.treeBottomPath的点坐标,并存储在treePathtreeBottomPath变量中。

最后,创建了一个mainTl的GSAP时间轴和一个starTl的动画时间轴,用于控制整体的动画效果。

代码

// gsap动画库
gsap.set("svg", {
    
    
    visibility: "visible",
});

gsap.set(sparkle, {
    
    
    transformOrigin: "50% 50%",
    y: -100,
});

let getSVGPoints = (path) => {
    
    
    let arr = [];
    var rawPath = MotionPathPlugin.getRawPath(path)[0];
    rawPath.forEach((el, value) => {
    
    
        let obj = {
    
    };
        obj.x = rawPath[value * 2];
        obj.y = rawPath[value * 2 + 1];
        if (value % 2) {
    
    
            arr.push(obj);
        }
        //console.log(value)
    });

    return arr;
};
let treePath = getSVGPoints(".treePath");

var treeBottomPath = getSVGPoints(".treeBottomPath");

//console.log(starPath.length)
var mainTl = gsap.timeline({
    
     delay: 0, repeat: 0 }),
    starTl;
2.5、定义闪烁效果

分析

定义一个名为flicker的函数,用于实现一个闪烁效果。

该函数接受一个参数p,表示要应用闪烁效果的对象。

首先,通过gsap.killTweensOf方法停止对象p的所有关于透明度的动画效果。

然后,使用gsap.fromTo方法设置对象p的透明度动画效果。

初始状态下,设置透明度为1,即完全可见。

然后,设置动画的持续时间为0.07秒,并将透明度设置为一个随机值,范围在0到1之间。

最后,设置动画重复播放,使得闪烁效果无限循环。

代码

function flicker(p) {
    
    
      //console.log("flivker")
      gsap.killTweensOf(p, {
    
     opacity: true });
      gsap.fromTo(
        p,
        {
    
    
          opacity: 1,
        },
        {
    
    
          duration: 0.07,
          opacity: Math.random(),
          repeat: -1,
        }
      );
    }
2.6、定义粒子对象

分析

定义了一个名为createParticles的函数,用于创建和初始化粒子(particles)。

首先,注释掉了变量step的初始化代码和打印starPath.length的语句。

接着,定义了变量i,初始值为numParticles。变量p表示粒子的元素,particleTl是粒子的动画时间轴,step用于确定粒子在treePath路径中的位置,pos用于存储具体的粒子位置。

然后,使用while循环逐个创建粒子对象。在每次循环中,通过select方法选择一个粒子的类型,使用cloneNode(true)方法对其进行克隆,并将克隆的粒子对象添加到mainSVG中。

接下来,设置粒子的填充颜色,通过particleColorArray数组来循环设置粒子的颜色属性。

然后,为粒子设置class属性为particle,并将其添加到particlePool数组中,以便后续使用。

最后,使用gsap.set方法将粒子对象的初始位置设置为(-100, -100),并设置transformOrigin为"50% 50%"。

代码

function createParticles() {
    
    
    //var step = numParticles/starPath.length;
    //console.log(starPath.length)
    var i = numParticles,
        p,
        particleTl,
        step = numParticles / treePath.length,
        pos;
    while (--i > -1) {
    
    
        p = select(particleTypeArray[i % particleTypeArray.length]).cloneNode(
            true
        );
        mainSVG.appendChild(p);
        p.setAttribute(
            "fill",
            particleColorArray[i % particleColorArray.length]
        );
        p.setAttribute("class", "particle");
        particlePool.push(p);
        //hide them initially
        gsap.set(p, {
    
    
            x: -100,
            y: -100,
            transformOrigin: "50% 50%",
        });
    }
}
2.7、粒子对象播放

分析

定义一个名为getScale的函数,用于生成一个随机的缩放比例。该函数使用了gsap.utils.random方法,传入了最小值0.5、最大值3、步长0.001和布尔值参数true。这意味着生成的随机数将在0.5到3之间,并以0.001为步长。

接下来,定义了一个名为playParticle的函数,用于播放粒子动画。在函数内部,首先判断是否允许显示粒子,如果不允许,则直接返回。

然后,通过particlePool数组获取当前要播放的粒子对象。

接下来,使用gsap.set方法设置粒子的初始位置为.pContainer元素的x和y属性的值,并设置缩放比例为通过getScale函数生成的随机数。

然后,创建一个时间轴对象tl,并使用tl.to方法对粒子对象进行动画操作。

在动画过程中,设置了动画的持续时间为0.61到6秒之间的随机数。

使用physics2D属性实现了一个物理引擎效果,具体的速度、角度和重力值通过gsap.utils.random方法生成。

同时,设置了粒子的缩放比例和旋转角度为随机的范围值。

设置了动画的缓动函数为power1

在动画开始时调用了flicker函数,并传入了粒子对象作为参数。

在动画重复时,通过onRepeat方法设置了粒子的缩放比例为通过getScale函数生成的随机数。

代码

var getScale = gsap.utils.random(0.5, 3, 0.001, true); //  圣诞树开始绘画时小光点动画的特效(参数:最小值,最大值,延迟)

function playParticle(p) {
    
    
    if (!showParticle) {
    
    
        return;
    }
    var p = particlePool[particleCount];
    gsap.set(p, {
    
    
        x: gsap.getProperty(".pContainer", "x"),
        y: gsap.getProperty(".pContainer", "y"),
        scale: getScale(),
    });
    var tl = gsap.timeline();
    tl.to(p, {
    
    
        duration: gsap.utils.random(0.61, 6),
        physics2D: {
    
    
            velocity: gsap.utils.random(-23, 23),
            angle: gsap.utils.random(-180, 180),
            gravity: gsap.utils.random(-6, 50),
        },
        scale: 0,
        rotation: gsap.utils.random(-123, 360),
        ease: "power1",
        onStart: flicker,
        onStartParams: [p],
        //repeat:-1,
        onRepeat: (p) => {
    
    
            gsap.set(p, {
    
    
                scale: getScale(),
            });
        },
        onRepeatParams: [p],
    });

    //
    //particlePool[particleCount].play();
    particleCount++;
    //mainTl.add(tl, i / 1.3)
    particleCount = particleCount >= numParticles ? 0 : particleCount;
}
2.8、绘制星星

分析

定义一个名为drawStar的函数,用于绘制星星动画。在函数内部,首先创建了一个时间轴对象starTl,并设置了更新回调函数为playParticle

接下来,使用to方法对.pContainer.sparkle元素进行动画操作。在动画过程中,设置了动画的持续时间为6秒,并使用motionPath属性指定了动画的路径为.treePath元素,并禁用了自动旋转效果。设置了动画的缓动函数为linear

接着,使用to方法对.pContainer.sparkle元素进行动画操作。在动画开始时,通过onStart函数将showParticle变量设置为false,表示不显示粒子。设置了动画的持续时间为1秒,并将元素的x和y属性设置为treeBottomPath[0].xtreeBottomPath[0].y

然后,使用to方法对.pContainer.sparkle元素进行动画操作。在动画开始时,通过onStart函数将showParticle变量设置为true,表示显示粒子。设置了动画的持续时间为2秒,并使用motionPath属性指定了动画的路径为.treeBottomPath元素,并禁用了自动旋转效果。设置了动画的缓动函数为linear

最后,使用from方法对.treeBottomMask元素进行动画操作。设置了动画的持续时间为2秒,并使用drawSVG属性指定了路径的起始结束百分比为0% 0%,即不显示路径,同时设置了画笔的颜色为#FFF,表示白色。设置了动画的缓动函数为linear

代码

function drawStar() {
    
    
    starTl = gsap.timeline({
    
     onUpdate: playParticle });
    starTl
        .to(".pContainer, .sparkle", {
    
    
        duration: 6,
        motionPath: {
    
    
            path: ".treePath",
            autoRotate: false,
        },
        ease: "linear",
    })
        .to(".pContainer, .sparkle", {
    
    
        duration: 1,
        onStart: function () {
    
    
            showParticle = false;
        },
        x: treeBottomPath[0].x,
        y: treeBottomPath[0].y,
    })
        .to(
        ".pContainer, .sparkle",
        {
    
    
            duration: 2,
            onStart: function () {
    
    
                showParticle = true;
            },
            motionPath: {
    
    
                path: ".treeBottomPath",
                autoRotate: false,
            },
            ease: "linear",
        },
        "-=0"
    )
    // 圣诞树中间那条横线动画   .treeBottomMask  是绑定class='treeBottomMask'这个标签
        .from(
        ".treeBottomMask",
        {
    
    
            duration: 2,
            drawSVG: "0% 0%",
            stroke: "#FFF",
            ease: "linear",
        },
        "-=2"
    );

    //gsap.staggerTo(particlePool, 2, {})
}
2.9、绘制圣诞树

分析

定义了一个名为drawMain的函数,用于绘制主要的圣诞树动画。在函数内部,首先创建了一个时间轴对象mainTl

接下来,使用from方法对.treePathMask.treePotMask元素进行动画操作。设置了动画的持续时间为6秒,并使用drawSVG属性指定了路径的起始结束百分比为0% 0%,即不显示路径,同时设置了画笔的颜色为#FFF表示白色。设置了动画的缓动函数为linear

然后,使用from方法对.treeStar元素进行动画操作。设置了动画的持续时间为3秒,并设置了元素的scaleY属性为0、scaleX属性为0.15,表示缩放比例,使用transformOrigin属性指定了缩放的基点在元素的中心。设置了动画的缓动函数为elastic(1,0.5)

接着,使用to方法对.sparkle元素进行动画操作。设置了动画的持续时间为3秒,并将元素的透明度设置为0,使用了rough缓动函数,该缓动函数会在动画期间以随机方式改变元素的透明度值。该动画将在前一个动画的结束点之后开始。

最后,使用to方法对.treeStarOutline元素进行动画操作。设置了动画的持续时间为1秒,并将元素的透明度设置为0.3,使用了rough缓动函数,该缓动函数会在动画期间以随机方式改变元素的透明度值。该动画将在前一个动画的结束点之后开始。

代码

function drawMain() {
    
    

    mainTl
    // 圣诞树上半身轮廓动画
        .from([".treePathMask", ".treePotMask"], {
    
    
        duration: 6,
        drawSVG: "0% 0%",
        stroke: "#FFF",
        stagger: {
    
    
            each: 6,
        },
        duration: gsap.utils.wrap([6, 1, 2]),
        ease: "linear",
    })
    //  圣诞树头上的星星动画
        .from(
        ".treeStar",
        {
    
    
            duration: 3,
            //skewY:270,
            scaleY: 0,
            scaleX: 0.15,
            transformOrigin: "50% 50%",
            ease: "elastic(1,0.5)",
        },
        "-=4"
    )
    // 当绘画圣诞树的小光点绘制完时,让小光点消失
        .to(
        ".sparkle",
        {
    
    
            duration: 3,
            opacity: 0,
            ease: "rough({strength: 2, points: 100, template: linear, taper: both, randomize: true, clamp: false})",
        },
        "-=0"
    )
    // 给圣诞树头上的星星加个白色特效
        .to(
        ".treeStarOutline",
        {
    
    
            duration: 1,
            opacity: 0.3,
            ease: "rough({strength: 2, points: 16, template: linear, taper: none, randomize: true, clamp: false})",
        },
        "+=1"
    );
    /* .to('.whole', {
opacity: 0
}, '+=2') */
}
2.10、绘制星星背景动画

分析

定义一个名为drawStars的函数,用于绘制星星背景动画。在函数内部,首先获取了stars元素,然后创建了一个canvas元素并将其上下文赋给ctx变量。接着,设置了canvas的宽度和高度为窗口的宽度和高度,并分别赋给wh变量。

然后,定义了一些变量,包括色调色彩hue、保存星星对象的数组stars、计数器count和最大星星数量maxStars

接下来,创建了一个用于绘制星星的源图像canvas2,并获取其上下文赋给ctx2变量。设置了canvas2的宽度和高度为100,并创建了径向渐变gradient2,以半径为0的圆开始,到半径为half的圆结束。根据色调色彩hue的不同,设置了不同的颜色。然后,使用路径arc创建了一个圆,并使用fill方法填充了渐变。

接着,定义了一个random函数,用于生成指定范围内的随机数。

定义了一个maxOrbit函数,用于根据屏幕宽高计算星星的移动范围。

创建了一个Star构造函数,用于实例化星星对象。在构造函数中,设置了星星的移动半径、大小、圆心位置、初始角度、移动速度和透明度等属性,并将新创建的星星对象存入stars数组。

Star对象定义了一个draw方法,用于绘制星星。在该方法中,根据星星的移动半径和角度计算出星星在屏幕上的位置。根据随机数判断星星是否变亮或变暗,并根据透明度绘制星星。最后,更新星星的角度。

然后,使用循环创建了指定数量的星星对象。

最后,定义了一个animation函数,用于执行星星的动画效果。在该函数中,首先绘制了一个半透明的背景颜色,然后使用for循环调用每个星星对象的draw方法进行绘制。最后,通过window.requestAnimationFrame方法不断循环调用animation函数,实现动画效果。

代码

function drawStars() {
    
    
    let canvas = document.getElementById("stars"),
        ctx = canvas.getContext("2d"),
        w = (canvas.width = window.innerWidth),
        h = (canvas.height = window.innerHeight),
        hue = 37, //色调色彩
        stars = [], //保存所有星星
        count = 0, //用于计算星星
        maxStars = 1300; //星星数量

    //canvas2是用来创建星星的源图像,即母版,
    //根据星星自身属性的大小来设置
    var canvas2 = document.createElement("canvas"),
        ctx2 = canvas2.getContext("2d");
    canvas2.width = 100;
    canvas2.height = 100;
    //创建径向渐变,从坐标(half,half)半径为0的圆开始,
    //到坐标为(half,half)半径为half的圆结束
    var half = canvas2.width / 2,
        gradient2 = ctx2.createRadialGradient(half, half, 0, half, half, half);
    gradient2.addColorStop(0.025, "#CCC");
    //hsl是另一种颜色的表示方式,
    //h->hue,代表色调色彩,0为red,120为green,240为blue
    //s->saturation,代表饱和度,0%-100%
    //l->lightness,代表亮度,0%为black,100%位white
    gradient2.addColorStop(0.1, "hsl(" + hue + ", 61%, 10%)");
    gradient2.addColorStop(0.25, "hsl(" + hue + ", 64%, 2%)");
    gradient2.addColorStop(1, "transparent");

    ctx2.fillStyle = gradient2;
    ctx2.beginPath();
    ctx2.arc(half, half, half, 0, Math.PI * 2);
    ctx2.fill();

    // End cache
    function random(min, max) {
    
    
        if (arguments.length < 2) {
    
    
            max = min;
            min = 0;
        }

        if (min > max) {
    
    
            var hold = max;
            max = min;
            min = hold;
        }

        //返回min和max之间的一个随机值
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    function maxOrbit(x, y) {
    
    
        var max = Math.max(x, y),
            diameter = Math.round(Math.sqrt(max * max + max * max));
        //星星移动范围,值越大范围越小,
        return diameter / 2;
    }

    var Star = function () {
    
    
        //星星移动的半径
        this.orbitRadius = random(maxOrbit(w, h));
        //星星大小,半径越小,星星也越小,即外面的星星会比较大
        this.radius = random(60, this.orbitRadius) / 8;
        //所有星星都是以屏幕的中心为圆心
        this.orbitX = w / 2;
        this.orbitY = h / 2;
        //星星在旋转圆圈位置的角度,每次增加speed值的角度
        //利用正弦余弦算出真正的x、y位置
        this.timePassed = random(0, maxStars);
        //星星移动速度
        this.speed = random(this.orbitRadius) / 50000;
        //星星图像的透明度
        this.alpha = random(2, 10) / 10;

        count++;
        stars[count] = this;
    };

    Star.prototype.draw = function () {
    
    
        //星星围绕在以屏幕中心为圆心,半径为orbitRadius的圆旋转
        var x = Math.sin(this.timePassed) * this.orbitRadius + this.orbitX,
            y = Math.cos(this.timePassed) * this.orbitRadius + this.orbitY,
            twinkle = random(10);

        //星星每次移动会有1/10的几率变亮或者变暗
        if (twinkle === 1 && this.alpha > 0) {
    
    
            //透明度降低,变暗
            this.alpha -= 0.05;
        } else if (twinkle === 2 && this.alpha < 1) {
    
    
            //透明度升高,变亮
            this.alpha += 0.05;
        }

        ctx.globalAlpha = this.alpha;
        //使用canvas2作为源图像来创建星星,
        //位置在x - this.radius / 2, y - this.radius / 2
        //大小为 this.radius
        ctx.drawImage(
            canvas2,
            x - this.radius / 2,
            y - this.radius / 2,
            this.radius,
            this.radius
        );
        //没旋转一次,角度就会增加
        this.timePassed += this.speed;
    };

    //初始化所有星星
    for (var i = 0; i < maxStars; i++) {
    
    
        new Star();
    }

    function animation() {
    
    
        //以新图像覆盖已有图像的方式进行绘制背景颜色
        ctx.globalCompositeOperation = "source-over";
        ctx.globalAlpha = 0.9; //尾巴
        ctx.fillStyle = "hsla(" + hue + ", 64%, 6%, 2)";
        ctx.fillRect(0, 0, w, h);

        //源图像和目标图像同时显示,重叠部分叠加颜色效果
        ctx.globalCompositeOperation = "lighter";
        for (var i = 1, l = stars.length; i < l; i++) {
    
    
            stars[i].draw();
        }

        //调用该方法执行动画,并且在重绘的时候调用指定的函数来更新动画
        //回调的次数通常是每秒60次
        window.requestAnimationFrame(animation);
    }

    animation();
}
2.11、定义初始化函数并调用

分析

执行了init函数,接着依次执行了createParticlesdrawStardrawMaindrawStars等函数,并将它们添加到了mainTl时间轴中。最后,通过gsap.globalTimeline.timeScale(1.5)方法设置了全局时间轴的绘画速率。

代码

function init() {
    
    
    let element = document.getElementById("header");
    for(let i = 0; i < myLabels.length; i++) {
    
    
        let _p = document.createElement("p");
        _p.className = "header-item";
        _p.innerHTML = myLabels[i];
        element.appendChild(_p);
    }

    let labels = document.getElementsByClassName('header-item');
    for(let i = 0; i < myLabels.length; i++) {
    
    
        setTimeout(() => {
    
    
            labels[i].classList.add("show");
        }, 1000 * i);
    }      
}
init();
createParticles()
drawStar();
drawMain();
mainTl.add(starTl, 0);
gsap.globalTimeline.timeScale(1.5); //  圣诞树开始绘画时小光点动画的绘画速率,越大越快
drawStars();
3、结尾

大部分都是js代码,其实还有一些html和css需要自己研究一下,需要源码私信,祝大家平安夜圣诞夜快乐

猜你喜欢

转载自blog.csdn.net/weixin_53961667/article/details/135182124