使用css3d-engine.js编写简单的 360全景h5页面

什么是这里所说的360全景h5页面?查看下面的案例进行了解: 

开发项目:http://game.flyh5.cn/resources/game/wechat/zjh/fangtuo/index.html
案例1:http://cpic18ny1.energytrust.com.cn/
案例2:https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxc8a2ce35fa3799ea&redirect_uri=https%3A%2F%2Fapi-wx.51h5.com%2Fweb%2Foauth%2Fcallback%2Fid%2Fzhihuipingmu.html%3Fredirect%3Dhttp%253A%252F%252Fevent.ews.m.jaeapp.com%252Foneleaf%252Fwx%252F&response_type=code&scope=snsapi_base&connect_redirect=1&state=af817cf4f82372fbb17733029aebd16e#wechat_redirect
案例3:http://wap.i-h5.cn/hzx_game/dell/index.html?from=groupmessage&isappinstalled=0

我所做的项目是一个展示类的h5营销项目,没有太多的交互逻辑,主要在于那个360场景的编写,其实我也是赶鸭子上架,boos突然丢了一个案例给我研究一下,说会有一个项目进来要做成360旋转效果,我&*¥@%*@¥%@@—¥#¥。

好吧,研究的坎坷历程就略过了吧,我也是在别人的项目基础上做的文章,而且之前也没有写过3d动画,所以只拿一些我研究出来的东西说道记录一下,具体原理我也一知半解,不过使用他写个简单项目应该还是够了。

项目中主要使用的3d动画库为:css3d-engine.js。github为:https://github.com/shrekshrek/css3d-engine
还使用了一个名为jstween的动画库,可以配合css3d-engine.js使用。代码中以JT代替。github为:https://github.com/shrekshrek/jstween

项目过程中使用了PxLoader进行360场景图片等资源的预加载。github为:https://github.com/thinkpixellab/PxLoader

话不多说,让我们一步一步来构建一个简单的360场景动画吧。

C3D.Stage

三维场景,需要首先创建,其他所有显示内容都通过addchild方法放入场景即可。

const anta = new C3D.Stage({
    el: $("#anta")[0]
});

我们选用一个id为anta的一个div作为场景的承载元素。

C3D.Sprite

C3D.Sprite是一个三维显示元素基类,一般作为容器使用,用它来创建一个显示在页面上的元素的容器

const spMain = new C3D.Sprite();
spMain.position(0, 0, -750).update();
anta.addChild(spMain);

创建一个C3D.Sprite容器作为承载所有显示内容的主容器,后续所有的显示内容都添加到这个容器中。并添加这个主容器到场景中。

C3D.Plane

C3D.Plane表示为一个平面,就是一个二维平面,一般用它来创建一个显示元素,也就是场景中所有可以看到的平面都是用它来创建,比如场景中交互的按钮这些,通过创建一个C3D.Plane,并把它添加到C3D.Sprite容器中即可。

ps:刷新相应的dom内容,位置,角度,尺寸,材质等信息只有在执行了update方法后才会被作用到dom节点,具体可以参考github

创建场景背景

场景的背景也是通过一个C3D.Sprite容器来承载,并且显示的背景元素会通过C3D.Plane来创建,这里会配置一些参数,具体看代码:
 

    // 一些参数定义

    // 定义背景图片的个数
    var bg_num = 20;

    // 背景的宽高信息,也就是那张背景大图的宽高 bgInfo
    var o = {
            w: 3868,
            h: 2474
        },
        // 每一个背景元素的宽度。其实就是背景大图的宽度除以你的切好的背景图片个数
        M = o.w / 20,

        // 此处需要根据不同的场景宽度(大图的总宽度)进行调整,否则背景图片元素之间可能有间隔
        h = 605,

        //
        bgImage = [
            // 这里是场景的背景图片url链接,比如:
            'bg-image1.jpg',
            'bg-image2.jpg',
            ...
        ];


    // 创建背景容器
    var panoBg = new C3D.Sprite();

    // No need for attention
    var d = {
            lat: 0,
            lon: 0
        },
        f = {
            lon: 0,
            lat: 0
        };

    // c.lon设置场景的水平初始的位置,也就是场景生成后,默认停留到哪一个位置,这里是角度
    // 你可以根据项目自行设置场景最后停留的位置。而且在场景初始动画中,他的动画结束位置也需要
    // 和此处保持一致
    var c = {
        lon: 205,
        lat: 0,
    };

    var p = true;


    // 设置背景容器初始信息,并添加至spMain这个主容器中。
    panoBg.name("panoBg").position(0, 0, 0).update();
    spMain.addChild(panoBg);

    for (var R = 0; R < bg_num; R++) {
        // No need for attention
        var F = new C3D.Plane,
            H = -360 / bg_num * R,
            J = H / 180 * Math.PI,
            U = h;

        // 设置场景的基本显示信息,这里默认是不可见的,alpha为0
        // size:设置大小 position:设置位置 rotation:设置选择角度 visibility:设置可见性
        // material 设置材质,这里的话,直接用来简单的设置背景图片
        F.size(M, o.h).position(Math.sin(J) * U, 150, Math.cos(J) * U).rotation(0, H + 180, 0).visibility({
            alpha: 0
        }).material({
            image: bgImage.url,
            bothsides: !1
        }).update();

        panoBg.addChild(F);
    }


    // 重力感应,通过监测重力感应,做到场景随手机的摇摆进行改动。
    // 通过此重力感应更新d,f,c这几个对象的信息,这几个对象具体有什么用,
    // 我也不太清楚。但是有些不支持这个Orienter的可能就不行。
    var o2 = new Orienter();
    o2.handler = function (t) {
        d.lon = -t.lon;
        d.lat = t.lat;
        if (p) {
            f.lat = -d.lat;
            f.lon = -d.lon;
        }
    };
    o2.init();

场景的背景是以一张左右可以闭合的大图,垂直等分为一定数量的等宽小图片,比如一张大图垂直等分为20张小图,每张小图都创建一个C3D.Plane元素。

ps: 代码中基本上我知道的都注释了,不过代码中注释了 No need for attention 字样的,表示我也不是太清楚具体的作用,但这些不用了解也可以基本满足功能。就这样就可以了。

至此,360场景中的背景的实现基本上就完成了。不过现在还是不可见的状态,不过先不急,稍后会讲解。我们先来往场景中添加一些可交互的按钮吧。

增加交互按钮(图片按钮)

360场景中的交互,一般就是可点击的按钮,这是最基础的交互方式,这里也是通过创建一个C3D.Sprite主容器用来承载所有的交互区域,并且一个交互区域也是通过一个C3D.Plane来创建的,具体看代码:

    var btnDots = [
        // 通过这个配置对象,生成场景中的交互元素。ps:这一类的交互一定是类似的
        // 如果不是,可以根据这个自己重新编写其他的交互元素
        // name: 这个交互元素的名称,不要重名了 x,y:此元素在场景中的位置
        // dot为所需要的背景图片地址,w,h为此元素的宽高。
          {
              name: "dialog1",
              x: 667,
              y: 1068,
              dot: 'images/click.png',
              w: 186,
              h: 196,
          },

          {
              name: "dialog2",
              x: 1960,
              y: 1400,
              dot: 'images/click.png',
              w: 186,
              h: 196,
          },
    ]


    var panoDots = new C3D.Sprite;
    panoDots.name("panoDots").visibility({
        alpha: 0
    }).position(0, 0, 0).update();

    $.each(btnDots, function (A, B) {

        // No need for attention
        var g = B,
            Q = -360 * (g.x - 80) / o.w,
            G = 90 * (g.y - o.h / 2) / o.h,
            M = Q / 180 * Math.PI,
            Y = h - 80,

            // C3D.create 通过配置来创建指定的元素
            i = C3D.create({
                type: "sprite",
                name: g.name,
                scale: [1],
                children: [{
                    type: "plane",
                    name: "dot",
                    size: [g.w, g.h],
                    position: [0, 2, 2],
                    rotation: [G, 0, 0],
                    material: [{
                        image: g.dot,
                        size: 'cover',
                    }],
                    bothsides: !1
                },
                {
                     type: "plane",
                     name: "label",
                     size: [0, g.h],
                     rotation: [G, 0, 0],
                     origin: [-18, 33],
                     material: [{
                         image: g.label
                     }],
                     bothsides: !1
                }]
            });


        i.position(Math.sin(M) * Y, .9 * (g.y - o.h / 2), Math.cos(M) * Y).rotation(0, Q + 180 - 5, 0).updateT(),

            // 为此元素绑定触摸事件,此处可以用于点击时,执行一些操作。
            i.on("touchend", function () {
                // TODO
            }),

            // No need for attention
            i.r0 = Q,
            i.w0 = g.w,
            i.dot.alpha = .5,
            i.dot.updateV(),
            panoDots.addChild(i)
    });

    spMain.addChild(panoDots);

以上就是向场景中添加一些交互元素,目前只是实现了简单的点击交互,而且,现在场景中还无法移动,仅仅只能通过Orienter进行移动,所以我们需要给anta这个场景添加触摸事件。来支持场景随手指移动进行移动。

为场景增加触摸事件

    // No need for attention
    var originTouchPos = {
            x: 0,
            y: 0
        },
        oldTouchPos = {
            x: 0,
            y: 0
        },
        newTouchPos = {
            x: 0,
            y: 0
        },
        firstDir = "",
        originTime = 0,
        oldTime = 0,
        newTime = 0,
        dx = 0,
        dy = 0,
        ax = 0,
        ay = 0,
        time = 0;

    // touchstart事件处理函数
    var onTouchStart = function (t) {
        firstDir = "",
            t = t.changedTouches[0];

        originTouchPos.x = oldTouchPos.x = newTouchPos.x = t.clientX;
        originTouchPos.y = oldTouchPos.y = newTouchPos.y = t.clientY;
        originTime = oldTime = newTime = Date.now();
        dx = dy = ax = ay = 0,
            anta.on("touchmove", onTouchMove),
            anta.on("touchend", onTouchEnd)
    };
    anta.on("touchstart", onTouchStart);
    var onTouchMove = function (t) {
        anta.off("touchend", onTouchEnd);
        return t = t.changedTouches[0],
            newTouchPos.x = t.clientX,
            newTouchPos.y = t.clientY,
            newTime = Date.now(),
            checkGesture(),
            oldTouchPos.x = newTouchPos.x,
            oldTouchPos.y = newTouchPos.y,
            oldTime = newTime, !1
    };
    var onTouchEnd = function (e) {
        newTime = Date.now();
        var t = (newTime - oldTime) / 1e3;

        // 这里可以通过e.target获取到触发了此touchend事件的dom对象,
        // 这样就可以根据此对象来判断是点击了哪个场景的元素。当然,可以直接为元素绑定点击事件,在此处绑定
        // TODO

        anta.off("touchmove", onTouchMove),
        anta.off("touchend", onTouchEnd);
    }


    // No need for attention
    function checkGesture() {
        dx = fixed2(newTouchPos.x - originTouchPos.x),
            dy = fixed2(newTouchPos.y - originTouchPos.y),
            ax = fixed2(newTouchPos.x - oldTouchPos.x),
            ay = fixed2(newTouchPos.y - oldTouchPos.y),
            time = (newTime - oldTime) / 1e3,
        "" == firstDir && (Math.abs(ax) > Math.abs(ay) ? firstDir = "x" : Math.abs(ax) < Math.abs(ay) && (firstDir = "y"));

        // 此处可以调整45和-45的数值,他们用于场景的上下视角控制
        if (!p) {
            c.lon = (c.lon - .2 * ax) % 360,
                c.lat = Math.max(-45, Math.min(45, c.lat + .2 * ay))
        }


    }

    // 功能函数
    function fixed2(t) {
        return Math.floor(100 * t) / 100
    }

上述为场景增加的触摸事件,他并没有更新场景位置的功能,也就是他在手指滑动时,只是更新了一些位置信息数据,而触发场景位置更新的另有其他的函数执行。
而且touchmove事件时为了场景的移动而触发,而touchend是作为场景的点击的功能触发,所以他们的执行
都会移除后续相应的touch事件。以下是持续触发的场景位置更新操作,他是一直在执行的:

    // 执行动画,有兴趣的可以了解一下requestAnimationFrame
    requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame ||
        function (callback) {
            setTimeout(callback, 1000 / 60);
        };

    // 执行场景的位置更新操作,他根据触摸事件更新的数据来进行场景的位置更新。
    function actiondh() {
        var t = (d.lon + f.lon + c.lon) % 360,
            i = .35 * (d.lat + f.lat + c.lat);
        t - panoBg.rotationY > 180 && (panoBg.rotationY += 360),
        t - panoBg.rotationY < -180 && (panoBg.rotationY -= 360);
        var n = t - panoBg.rotationY,
            a = i - panoBg.rotationX;
        Math.abs(n) < .1 ? panoBg.rotationY = t : panoBg.rotationY += .3 * n,
            Math.abs(a) < .1 ? panoBg.rotationX = i : panoBg.rotationX += .15 * a,
            panoBg.updateT();

        // 每次更新场景位置时,都需要重新更新你添加的交互元素的位置,你添加了哪些容器
        // 每次就需要更新那些容器的位置。
        panoDots.rotationY = panoBg.rotationY,
        panoDots.rotationX = panoBg.rotationX,
        panoDots.updateT(),

        t - panoItems.rotationY > 180 && (panoItems.rotationY += 360),
        t - panoItems.rotationY < -180 && (panoItems.rotationY -= 360);
        var o = t - panoItems.rotationY,
            r = i - panoItems.rotationX;
        Math.abs(o) < .1 ? panoItems.rotationY = t : panoItems.rotationY += .25 * o, Math.abs(r) < .1 ? panoItems.rotationX = i : panoItems.rotationX += .15 * r, panoItems.updateT();


        var s12 = -150 - 20 * Math.abs(n);
        spMain.z += .1 * (s12 - spMain.z),
            spMain.updateT(),
            A = requestAnimationFrame(actiondh);
    }

这样,每次通过重力感应对象Orienter或者通过触摸事件时都会更新场景的位置信息。基本至此,一个360场景h5项目的简单点击交互功能都可以实现了,还有一些有点复杂的交互判断,我自己也不太搞得清楚所以就不拿出来说了。

卷轴般的展开动画效果

基本上我们看这种类型的h5项目都有一个类似卷轴般的展开动画效果,其时机在于场景背景资源加载完毕时,就可以通过JT动画库来实现一个卷轴效果,具体代码如下:

    // loc是和上面创建背景容器时设置的c变量中的lon是一致的。
    var loc = 205;
    const stageInit = function stageInit() {

        // spMain设置主容器的缩放,z为缩放,此处是从小变大的缩放效果,持续四秒,并且每次动画执行都需要更新场景的位置信息。
        JT.fromTo(spMain, 4, {
            z: -2200
        }, {
            z: -150,
            ease: JT.Quad.Out,
            onUpdate: function () {
                this.target.updateT().updateV()
            }, onEnd: function () {
                p = false
                // 开始执行场景位置更新
                actiondh();

            }
        });
        // 设置背景容器的卷轴滚动效果。
        JT.fromTo(panoBg, 4, {
            rotationY: -720,
        }, {
            rotationY: loc,
            ease: JT.Quad.Out,
            onUpdate: function () {
                this.target.updateT().updateV()
            }, onEnd: function () {
                this.target.updateT().updateV()
            }
        });

        // 设置背景容器中的所有背景元素的滚动动画(主要效果)
        for (var A = 0, B = panoBg.children.length; B > A; A++) {
            JT.from(panoBg.children[A], 0.5, {
                x: 0,
                z: 0,
                scaleX: 0,
                scaleY: 0,
                delay: .05 * A,
                ease: JT.Quad.Out,
                onUpdate: function () {
                    this.target.updateT()
                },
                onStart: function () {
                    this.target.visibility({
                        alpha: 1
                    }).updateV()
                }

            });
        }

       // 等到卷轴滚动动画执行完后,显示交互元素
        JT.fromTo(panoDots, 0.1, {
            rotationY: -360,
            alpha: 0
        }, {
            rotationY: loc,
            alpha: 1,
            delay: 4,
            ease: JT.Quad.Out,
            onUpdate: function () {
                this.target.updateT().updateV()
            },
            onStart: function () {
                this.target.visibility({
                    alpha: 1
                }).updateV()
            }
        })


    }

以上代码就是场景的类似卷轴滚动的交互动画,而此动画的执行时机可以根据项目需要进行调用。比如通常项目会有loading页,可以等待loading页加载完后在执行此动画函数。

至此一个简单的360场景类h5交互项目就可以实现了,不过在此还是有一些需要注意的地方:

1.场景的背景图最好不要太大,因为图像越大,在执行场景动画时效果就卡,ios还好,安卓就不太理想了。
2.场景图最好提前使用PxLoader进行预加载,他的使用也很简单,看一下官方文档即可,你也可以等场景图预加载完成后在执行场景动画函数。PxLoader的github:https://github.com/thinkpixellab/PxLoader
3.注意场景背景创建中的h变量的设置,不同的总图片宽度不同,h变量就不同,需要自行测试设置,图片宽度越大
则h需要调整的越大。


总结:
最开始boos丢个案列给我时,让我以这个案例为基础改出一个新项目时,其实我是一脸懵b的,不过老板交代的,没办法,所幸也不是很复杂的项目,交互也仅仅限于场景中的点击交互,经过我一段段代码的测试,一个个参数的修改,查明他是什么意思,费了比较大的一番功夫,而且有些东西不是全都可以明白的。让我自己从头开始重新写,那可就不是看看别人写的这么简单的了,不过也不是没有收获吧,编写这种360效果的h5项目,让我多了解了一个新领域,而且,相对于功能单一,交互简单的这种,我对一个叫做krpano的软件编写出来的全景效果更感兴趣。而且这也可以作为一个技术方向,了解和学习。

ps:以上代码是在某个项目中截取出来,以作为学习交流之用,如有侵权,请联系删除。

你可能感兴趣的:(360全景)