整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何实现一个基于自然流布局的可视化拖拽搭建平台?

如何实现一个基于自然流布局的可视化拖拽搭建平台?

owCode 是高效、高性能的拖拽式低代码开发平台. 也是笔者最近一直在研究的方向, 对于可视化搭建平台的实现方案笔者之前写过很多文章, 这里带大家探索一个新方向——基于自然流布局的可视化搭建平台.

在我们之前实现的 h5-dooring 搭建平台中, 我们采用了网格布局的方式来实现拖拽生成H5页面或者Web app, 其好处就是灵活简单, 用户基本没有任何使用成本, 在前端层也能做一定的横向扩展, 但是存在几个缺陷:

  • 实现嵌套组件比较复杂
  • 没有层的概念

虽然通过改造可以实现层和嵌套的问题, 最近也在努力往这个方向实现(虽然和设计初衷相悖, dooring的初衷是抹去层和嵌套的概念, 让搭建扁平化和智能化, 所以没有采用自由布局的方案)

但是如果一定要实现嵌套和层的功能, 有没有另一种更简单的方案呢? 笔者目前想到了两种解决方案:

  • 将智能布局改为自由布局, 即可以采用类似 react-resizable 的这种方案
  • 基于自然流来实现, 也就是抹去定位的概念, 完全基于元素在文档的顺序, 层级和定位的选择权交给用户

因为第一种方案笔者在dooring的早期已经实现过一版, 最后弃用采用了网格布局, 所以说我们来探讨一下第二种方案的实现.

基于自然流布局实现拖拽生成页面

自然流布局的好处就是我们不用通过定位的方式来限定元素的位置等信息, 而是以html文档流的方式来布局元素, 并且用户可以灵活的设置元素的层级(layer)和偏移(transform), 接下来我们来看看简单的实现效果.

1. demo效果

H5建站, 页面制作

H5制作, H5编辑器

由上图的demo我们可以发现组件在画布中的布局完全是默认的文档流的方式, 所以我们有更灵活的布局实现.

2. 实现思路

具体实现思路主要分以下几个部分:

  • 组件区拖拽至画布
  • 画布区拖拽
  • 组件编辑器和更新机制

第一点和第三点我们在 H5-dooring中已经实现了, 感兴趣的可以看我之前的文章, 我们这里重点来实现画布区拖拽, 也是比较核心的环节.

2.1 H5拖放api基本介绍

拖放(Dragdrop)是 HTML5 标准的组成部分, 早已被大多数浏览器支持. 我们目前使用的拖放插件基本上基于 H5 拖放 API 来实现的, 其实实现第一点组件区拖拽至画布我们完全可以用原生来实现, 这里笔者简单来介绍以下.

首先我们来看看一个完整的拖放过程:

  1. 首先要设置一个元素可拖放(比如<img draggable="true" />)
  2. 设计拖动的时候会发生什么(需要用到ondragstart事件 和 setData(你要传递的数据))
  3. 放到何处,也就是目标容器(通常在目标容器上绑定ondragoverondrop事件)

有了以上3个步骤, 我们就能实现第一点的需求, 笔者写个简单demo来给大家参考一下:

<script type="text/javascript">
  function allowDrop(ev) {
    ev.preventDefault();
  }

  function drag(ev){
    ev.dataTransfer.setData("Text",ev.target.id);
  }

  function drop(ev){
    ev.preventDefault();
    let data=ev.dataTransfer.getData("Text");
    ev.target.appendChild(document.getElementById(data));
  }
</script>

<div id="box" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<img id="drag" src="dooring.png" draggable="true" ondragstart="drag(event)" width="336" height="69" />

也就是对应的我们的组件拖放区域, 如下图所示:

2.2 画布区拖拽布局实现

因为之前的版本我们采用了网格布局来实现智能拖拽, 由于内部定位机制采用的是绝对定位(absolute), 所以是实现层级和固定组件比较困难, 如果组件的呈现完全脱离了定位的束缚, 我们就可以实现以上的困境了. 所以这里我们调研了一种方案——拖拽排序机制.

自然流布局的规律就是默认情况下html页面是基于dom出现的顺序来排列的, 也就是我们说的堆叠.

H5制作

我们可以遵循这样的设计, 通过排序的方式改变组件的位置从而实现自然流布局的页面搭建.

那么我们再回到上面说的布局问题, 比如说要想实现栅格化布局, 我们只需要定义一个flex容器, 将组件拖拽到容器里就好了, 这样也就解决了嵌套的问题. 同时我们还可以设计嵌套容器的栅格数, 这样就可以实现类似如下的效果:

H5编辑器

拖拽排序的库我们可以使用: sortable Vue.Draggable * react-dnd

还有很多优秀的库, 这里就不一一举例了.

3. 如何实现层级和嵌套

其实在上面的实现思路中我们已经解决了嵌套的问题了, 即提供拖放的容器组件, 利用笔者在上文中介绍的拖放api即可实现. 对于组件层级来说, 因为我们采用的是自然流布局, 所以我们可以轻松的设置元素的定位属性, 比如我们提供一个定位的设置:

拖拽搭建HTML5


关于如何设计一个动态的属性编辑器, 笔者之前文章中也就详细地介绍, 大家可以参考:

  • 表单编辑器实现(FormEditor)

以上就是自然流布局的基本实现方式, 后续笔者也会在github上同步我们最新的成果.

H5-Dooring编辑器wiki: https://github.com/MrXujiang/h5-Dooring/wiki

最后

觉得有用 ?喜欢就收藏,顺便点个赞吧,你的支持是我最大的鼓励!搜 “趣谈前端”,发现更多有趣的H5游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战.

话不说,先上效果吧。

Axure软件其实是一款原型设计工具,可以设置网页、app等原型,由于它基于html构架,其中又包含了一些基本的函数,所以我们可以通过制作一些游戏更加熟练地使用、了解这些函数。本文之后将根据我的另一篇文章中《以「用户为中心」的产品设计详述》提到的「五大用户体验要素」一一讲解构建过程。

战略层

通过使用Axure制作一些小游戏,将多种函数及逻辑关系设定其中,达到增强自己逻辑思维能力及熟练使用Axure的目的。

本例准备制作一款「人机乒乓球」游戏,基本游戏规则是:玩家通过移动桌面底部球拍,是乒乓球在下落时正好落到球拍上,然后反弹乒乓球。

范围层

本例要实现的基本功能包括:

  • 设置乒乓球移动速度:玩家可以自由选择多种速度等级,等级越高速度越快,每回合分数越高。
  • 设置玩家名称:玩家可以自由输入自己的名称,在游戏中显示。
  • 多种移动球拍方式:鼠标直接拖动球拍、点击键盘「←」、「→」按钮、点击信息栏「左」、「右」图标均可控制球拍移动
  • 游戏暂停及恢复功能:游戏时可以随时暂停及恢复游戏
  • 实时显示分数及难度
  • 游戏结束后可以重新开始

结构层

流程梳理

通过「范围层」的梳理,我们可以简单设置出整个游戏的基本流程图:

难点分析

在游戏中,乒乓球移动是最重要的,所以我们第一个考虑的是「循环」,通过获得一个恒定的循环时间,控制乒乓球恒定速度移动,但是因为我们又可以选择乒乓球移动速度,所以我们需要得到一个基准速度v,然后在基准速度上直接按倍数增加移动速度。

首先在页面载入时设置动态面板「bit_time」和「bit_ball」向后循环,循环间隔1毫秒,动态面板「database」中的「time_begin」获得系统载入时的时间戳。

在动态面板「bit_time」中,我们设置「database」动态面板中的「time_now」获得系统现在时刻的时间戳,「time_bit」=time_now-time_begin。

此时「time_bit」就是一个以毫秒为单位不断增长的数值,它代表着系统现在时刻与之前time_begin的时间差值。

之后我们在「time_ball」动态面板设置触发条件:当「time_bit」>=level(level为小球速度等级,默认50)时,「time_begin」重新赋值为当前系统时间。这时「time_ball」就形成了一个每50毫秒自动循环运行的程序,如果level为100时,「time_ball」就会每100毫秒自动出发一次。

以上我们就获得了一个可以控制的定时循环机制,其它功能都是在这个机制上实现的。

功能

1. 加载初始化实现

第一步永远是最难的,我在这里设置了6个全局变量,方便对整个游戏的配置。

  • Level:游戏难度等级,初始为50
  • location_x:乒乓球在移动时X轴方向的位移量
  • game_status:游戏状态,包括暂停、首页、设置、游戏中、结束等,初始化为首页
  • angle:乒乓球移动速度,初始化为100
  • score:游戏难度对应基准分数倍数,分别为1,2,3,5,10,初始化为3
  • score1:乒乓球移动时基准分,初始化为0

看了上述这些全局变量你可能还不明白,但请你一定要记住,因为每一个都非常重要之后我会详细介绍。

除了初始化全局变量外,还需要初始化以下数据:

  • 游戏桌面动态模板:初始化为显示首页
  • 设置动态模板:初始化为隐藏
  • 信息栏动态模板:初始化禁用所有功能

2. 设置游戏难度

游戏难度体现在乒乓球移动速度上,当难度越高时速度越快,本例中共有5个等级,默认为中间等级。当玩家选择对应等级时,系统将等级赋值给全局变量Level,通过上文的「time_ball」控制乒乓球移动速度,难度越高,Level值越低,则「time_bit」越短,乒乓球越快。

3. 游戏实时显示分数

游戏分数需要实时更新,笔者开始通过判断乒乓球接触球拍且能成功返回时,分数增加,但是实现起来有些bug,所以舍弃。

本例中通过计算乒乓球移动桌面的次数实现分数增加,当乒乓球从最上面到最下面时或从最下面移到最上面时(恒定移动6次,后文详述),score1自增,分数=score*score1/6.

4. 游戏暂停及恢复

通过全局变量game_status的值控制。

当game_status=begin时,所有与游戏进行的相关功能才可以使用,例如乒乓球移动、分数增加、移动球拍等,所以当点击「暂停」时,game_status=pause,乒乓球即自动停止运动。

5. 乒乓球移动速度

整个桌面大小为600*600.为了制作方便,乒乓球固定每次在y轴移动angle==100距离,通过每次移动的时间间隔控制乒乓球移动速度。如初始等级正常,level==50时,乒乓球每次移动时间间隔为50毫秒。

6. 乒乓球随机移动方向

为了使游戏逼真,每次从上桌面弹出的乒乓球角度均为随机值,本例为了制作方便,设置乒乓球初始从(300,0)坐标开始向下移动,如果碰到球拍,乒乓球按照原方向返回(300,0)坐标。

本例目前控制乒乓球初始移动角度不会弹到左、右桌壁(即初始角度较小)。

本例通过函数location_x=[[Math.tan((Math.random()*40-20)/57)]]得到初始移动X轴方向移动位移,乒乓球每次X轴的移动距离为location_x*angle.

7. 球拍触碰乒乓球反弹

球拍的宽度恒定为120,所以判断当乒乓球移动到桌面底部时,如果「-120<球拍.x-乒乓球.x<120」时,则判断此次成功碰到乒乓球,改变乒乓球y轴移动全局变量angle=-angle,乒乓球反弹。

框架层

根据之前逻辑梳理,我们需要制作以下几个动态面板:

游戏桌面面板:包括首页、游戏中、游戏结束状态

设置面板:

信息栏面板:

bit_time:获得基准时间值

bit_ball:控制小球移动

bit_down:判断小球接触球拍时成功或失败

bit_status_up:小球触碰上端桌面时随便方向向下移动

bit_score:获得实时分数

表现层

以下为最初设计的三个页面效果图

总结

虽然看似是一个很简单的小游戏,但在制作过程中却遇到了很多难题,有的可以直接解决,有的却要花费很大精力绕道实现。不过当完成这款游戏后,相信你对各种通过循环功能实现的页面轮播图等功能实现会轻松自如了,而通过全局变量去控制各种逻辑状态、通过键盘控制面板移动等也是信手拈来了。

本游戏目前还十分简单,如果有时间,我近期会做如下改进:

自动识别屏幕,使桌面宽度为屏幕宽度,且乒乓球及球拍大小也会对应变换

乒乓球可以撞击桌面左、右两边

通过中继器储存每次称号及分数,并在结束后实时显示

优化速度控制方式,使手机端更加流畅

增加更多不确定因素

欢迎大家随时交流,谢谢!

本文由 @escher 原创发布于人人都是产品经理。未经许可,禁止转载。

题图来自 Pixabay,基于 CC0 协议

去年曾写过一个类似的拼拼乐小游戏,技术栈采用自己的Xuery框架和原生javascript实现的,脚手架采用gulp来实现,为了满足对vue的需求,这次我使用vue生态将其重构,脚手架采用比较火的vue-cli

前言

为了加深大家对vue的了解和vue项目实战,笔者采用vue生态来重构此项目,方便大家学习和探索。技术栈如下:

  • vue-cli4 基于vue的脚手架
  • Xuery 笔者基于原生js二次封装的dom库
  • vue mvvm库

因为该应用属于H5游戏,为了轻量化项目, 我没有采用第三方ui库, 如果大家想采用基于vue的第三方移动端ui库,推荐如下:

  • Mint 饿了么推出的移动端ui库
  • NutUI 一套京东风格的移动端组件库
  • muse-ui 基于MaterialUI风格的移动端UI组件
  • cube-ui 滴滴团队开发的移动端UI组件库
  • vant 有赞团队的电商风格的移动端组件库
  • atom-design atom风格的移动端ui组件库 *
  • mand-mobile 滴滴团队研发的基于金融场景的移动端ui组件库

以上笔者推荐的都是社区比较完善,bug比较少的组件库,大家可以感受一下。

回到我们的小游戏开发,我们更多的是javascriptcss3的掌握程度,在学习完这篇文章之后相信大家对javascriptcss3的编程能力都会有极大的提升,后面还会介绍如何使用canvas实现生成战绩海报图的功能。

正文

我们先来看看游戏的预览界面:

本文的算法实现方式在之前的拼拼乐文章中已经说明,这里主要介绍核心算法, 至于vue-cli的使用方法,笔者之前也写过对应的文章,大家可以研究学习一下。vue-cli搭建项目方式如下:

// 安装
yarn global add @vue/cli

// 创建项目
vue create pinpinle

// 进入项目并启动
cd pinpinle && yarn start

关于vue-cli3配置实战,可以移步我之前的文章: 一张图教你快速玩转vue-cli3

H5游戏核心功能介绍

目前我主要整理了如下核心功能,接下来会一一带大家实现:

  • 实现纯javascript上传预览图片
  • 实现拼图分割功能
  • 实现洗牌算法
  • 实现生成战绩海报功能

1. 实现纯javascript上传预览图片

文件上传预览主要采用FileReader API实现,原理就是将file对象传给FileReader的readAsDataURL然后转化为data:URL格式的字符串(base64编码)以表示所读取文件的内容。 具体代码如下:

// 2.文件上传解析
let file=$('#file');
file.on('change', function(e){
    var file=this.files[0];
    var fileReader=new FileReader();
    // 读取完成触发的事件
    fileReader.onload=function(e) {
        $('.file-wrap')[0].style.backgroundImage='url(' + fileReader.result + ')';
        imgSrc=fileReader.result;
    }

    file && fileReader.readAsDataURL(file);
})

2. 实现拼图分割功能

一般我们处理这种拼图游戏都会有如下方案: 用canvas分割图片 采用n张不同的切好的切片图片(方法简单,但是会造成多次请求) * 动态背景分割

经过权衡,笔者想出了第三种方法,也是自认为比较优雅的方法,即动态背景分割,我们只需要使用1张图片,然后利于css的方式切割图片,有点经典的雪碧图的感觉,如下:

本质就是我们设置九个div,每个div都使用同一张图片,并且图片大小等于游戏画布大小,但是我们通过backgroundPosition(背景定位)的方式来实现切割图片。这样做的另一个好处是方便我们实现洗牌逻辑

3. 实现洗牌算法

洗牌逻辑依托于随机算法,这里我们结合坐标系,实现一个随机生成二维坐标系的逻辑,然后通过改变每个切片的translate位置,配合过渡动画,即可实现洗牌功能和洗牌动画。

3.1 数组乱序算法

数组乱序比较简单,代码如下:

// 数组乱序
function upsetArr(arr) {
    arr.sort(function(a,b){
        return Math.random() > 0.5 ? -1 : 1
    })
}

3.2 洗牌逻辑

洗牌逻辑基于数组乱序,具体逻辑如下:

// 洗牌方法
function shuffle(els, arr) {
    upsetArr(arr);
    for(var i=0, len=els.length; i< len; i++) {
        var el=els[i];
        el.setAttribute('index', i);  // 将打乱后的数组索引缓存到元素中
        el.style.transform='translate(' + arr[i].x + 'vw,' + arr[i].y + 'vh'+ ')';
    }
}

3.3 生成n纬矩阵坐标

n维矩阵主要用来做洗牌和计算成功率的,具体实现如下:

// 生成n维矩阵坐标
function generateMatrix(n, dx, dy) {
    var arr=[], index=0;
    for(var i=0; i< n; i++) {
        for(var j=0; j< n; j++) {
            arr.push({x: j*dx, y: i*dy, index: index});
            index++;
        }
    }
    return arr
}

3.4 置换算法

置换算法主要用来切换拼图的,比如用户想移动拼图,可以用置换来实现:

// 数组置换
function swap(arr, indexA, indexB) {
    let cache=arr[indexA];
    arr[indexA]=arr[indexB];
    arr[indexB]=cache;
}

4. 实现生成战绩海报功能

生成战绩海报笔者采用canvas来实现,对于canvas的api不熟悉的可以查看MDN,讲的比较详细。这里笔者简单实现一个供大家参考:

function generateImg() {
    var canvas=document.createElement("canvas");

    if(canvas.getContext) {
        var winW=window.innerWidth,
            winH=window.innerHeight,
        ctx=canvas.getContext('2d');
        canvas.width=winW;
        canvas.height=winH;

        // 绘制背景
        // ctx.fillStyle='#06c';
        var linear=ctx.createLinearGradient(0, 0, 0, winH);
        linear.addColorStop(0, '#a1c4fd');
        linear.addColorStop(1, '#c2e9fb');
        ctx.fillStyle=linear;
        ctx.fillRect(0, 0, winW, winH);
        ctx.fill();

        // 绘制顶部图像
        var imgH=0;
        img=new Image();
        img.src=imgSrc;
        img.onload=function(){
            // 绘制的图片宽为.7winW, 根据等比换算绘制的图片高度为 .7winW*imgH/imgW
            imgH=.6*winW*this.height/this.width;
            ctx.drawImage(img, .2*winW, .1*winH, .6*winW, imgH);

            drawText();
            drawTip();
            drawCode();
        }

        // 绘制文字
        function drawText() {
            ctx.save();
            ctx.fillStyle='#fff';
            ctx.font=20 + 'px Helvetica';
            ctx.textBaseline='hanging';
            ctx.textAlign='center';
            ctx.fillText('我只用了' + (180 -dealtime) + 's,' + '快来挑战!', winW/2, .15*winH + imgH);
            ctx.restore();
        }

        // 绘制提示文字
        function drawTip() {
            ctx.save();
            ctx.fillStyle='#000';
            ctx.font=14 + 'px Helvetica';
            ctx.textBaseline='hanging';
            ctx.textAlign='center';
            ctx.fillText('关注下方二维码开始游戏', winW/2, .25*winH + imgH);
            ctx.restore();
        }


        // 绘制二维码
        function drawCode() {
            var imgCode=new Image();
            imgCode.src='/piecePlay/images/logo.png';
            imgCode.onload=function(){
                ctx.drawImage(imgCode, .35*winW, .3*winH + imgH, .3*winW, .3*winW);

                // 生成预览图
                var img=new Image();
                img.src=convertCanvasToImage(canvas, 1).src;
                img.className='previewImg';
                img.onload=function(){
                    $('.preview-page')[0].appendChild(this);
                    startDx=startDx - 100;
                    transformX(wrap, startDx + 'vw');
                }
            }
        }  
    } else {
        alert('浏览器不支持canvas!')
    }
}

H5拼图小游戏笔者已在github开源, 感兴趣的可以学习参考。以上的逻辑部分的代码可以直接整合到vue项目中即可,由于实现比较简单, 这里笔者就不详细介绍了。

H5可视化编辑器H5-Dooring

目前我也在持续更新H5编辑器H5-Dooring, 它是一款轻松拖拽式制作H5页面的工具, 功能非常强大, 感兴趣的朋友也可以搜索体验一下.

最后

如果想学习更多H5游戏, webpack,node,gulp,css3,javascript,nodeJS,canvas数据可视化等前端知识和实战,欢迎在《趣谈前端》学习讨论,共同探索前端的边界。