首页

Three.js 基础入门

前端达人

课程介绍

近些年,浏览器的功能越来越强大,渐渐得成为了复杂应用和图形的平台。同时,现有大多数浏览器实现了对 WebGL 的支持,但要直接使用 WebGL 相关接口进行开发,则需要学习复杂的着色器语言,且开发周期长,不利于项目的快速开发。

面对这种情况,Three.js 应运而生,它不但对 WebGL 进行了封装,将复杂的接口简单化,而且基于面向对象思维,将数据结构对象化,非常方便我们开发。Three.js 的发展十分迅速,然而配套的学习材料却比较匮乏,于是便有了当前的这个课程。

本课程作为入门课程,不会深入做源码解析,主要协助初学者了解 Three.js 的数据结构,基础 API 以及相关辅助插件的使用。帮助初学者达到快速入门的目的。

本课程共包含四大部分。

第一部分(第01-02课),入门前概述,带你初步认识 Three.js、框架选择标准、开发工具,源码获取,实现一个“Hello World”辅助工具。
第二部分(第03-08课),基础功能篇,主要包括 Object3D、Scene、Mesh、Group、Geometry、Materials、Lights、Cameras、粒子等相关功能的介绍。
第三部分(第09-15课),进阶篇,主要包括 Controls、Loaders、Animation、Tween、核心对象,与场景之间的交互以及性能优化介绍。
第四部分(第16课),实战篇,带大家利用所学知识实现一个 3D 小案例。

作者简介

郑世强,现就职于上海某网络公司担任前端工程师,CSDN 博客作者,长期活跃于各大论坛,擅长前端开发、WEBGL 开发。

课程内容

第01课:入门前准备

什么是 WebGL?

WebGL(Web 图形库)是一种 JavaScript API,用于在任何兼容的 Web 浏览器中呈现交互式 3D 和 2D 图形,而无需使用插件。WebGL 通过引入一个与 OpenGL ES 2.0 紧密相符合的 API,可以在 HTML5 <canvas> 元素中使用(简介引自 MDN)。

以我的理解,WebGL 给我们提供了一系列的图形接口,能够让我们通过 JavaScript 去使用 GPU 来进行浏览器图形渲染的工具。


什么是 Three.js?

Three.js 是一款 webGL 框架,由于其易用性被广泛应用。Three.js 在 WebGL 的 API 接口基础上,又进行的一层封装。它是由居住在西班牙巴塞罗那的程序员 Ricardo Cabbello Miguel 所开发,他更为人知的网名是 Mr.doob。



Three.js 以简单、直观的方式封装了 3D 图形编程中常用的对象。Three.js 在开发中使用了很多图形引擎的高级技巧,极大地提高了性能。另外,由于内置了很多常用对象和极易上手的工具,Three.js 的功能也非常强大。最后,Three.js 还是完全开源的,你可以在 GitHub 上找到它的源代码,并且有很多人贡献代码,帮助 Mr.doob 一起维护这个框架。

WEBGL 和 Three.js 的关系

WebGL 原生 API 是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js 将入门的门槛降低了一大截,对 WebGL 进行封装,简化我们创建三维动画场景的过程。只要你有一定的 JavaScript 基础,有一定的前端经验,我坚信,用不了多长时间,三维制作会变得很简单。



用最简单的一句话概括:WebGL 和 Three.js 的关系,相当于 JavaScript 和 jQuery 的关系。

功能概述

Three.js 作为 WebGL 框架中的佼佼者,由于它的易用性和扩展性,使得它能够满足大部分的开发需求,Three.js 的具体功能如下:


Three.js 掩盖了 3D 渲染的细节:Three.js 将 WebGL 原生 API 的细节抽象化,将 3D 场景拆解为网格、材质和光源(即它内置了图形编程常用的一些对象种类)。
面向对象:开发者可以使用上层的 JavaScript 对象,而不是仅仅调用 JavaScript 函数。
功能非常丰富:Three.js 除封装了 WebGL 原始 API 之外,Three.js 还包含了许多实用的内置对象,可以方便地应用于游戏开发、动画制作、幻灯片制作、髙分辨率模型和一些特殊的视觉效果制作。
速度很快:Three.js 采用了 3D 图形最佳实践来保证在不失可用性的前提下,保持极高的性能。
支持交互:WebGL 本身并不提供拾取(Picking)功能(即是否知道鼠标正处于某个物体上)。而 Three.js 则固化了拾取支持,这就使得你可以轻松为你的应用添加交互功能。
包含数学库:Three.js 拥有一个强大易用的数学库,你可以在其中进行矩阵、投影和矢量运算。
内置文件格式支持:你可以使用流行的 3D 建模软件导出文本格式的文件,然后使用 Three.js 加载,也可以使用 Three.js 自己的 JSON 格式或二进制格式。
扩展性很强:为 Three.js 添加新的特性或进行自定义优化是很容易的事情。如果你需要某个特殊的数据结构,那么只需要封装到 Three.js 即可。
支持HTML5 Canvas:Three.js 不但支持 WebGL,而且还支持使用 Canvas2D、Css3D 和 SVG 进行渲染。在未兼容 WebGL 的环境中可以回退到其它的解决方案。


缺点

虽然 Three.js 的优势很大,但是它也有它的不足之处:



官网文档非常粗糙,对于新手极度不友好。

国内的相关资源匮乏。

Three.js 所有的资料都是以英文格式存在,对国内的朋友来说又提高了门槛。

Three.js 不是游戏引擎,一些游戏相关的功能没有封装在里面,如果需要相关的功能需要进行二次开发。


Three.js 与其他库的对比

随着 WebGL 的迅速发展,相关的 WebGL 库也丰富起来,接下来介绍几个比较火的 WebGL 库。



与 Babylon.js 对比

Babylon.JS 是最好的 JavaScript 3D 游戏引擎,它能创建专业级三维游戏。主要以游戏开发和易用性为主。与 Three.js 之间的对比:



Three.js 比较全面,而 Babylon.js 专注于游戏方面。

Babylon.js 提供了对碰撞检测、场景重力、面向游戏的照相机,Three.js 本身不自带,需要依靠引入插件实现。

对于 WebGL 的封装,双方做得各有千秋,Three.js 浅一些,好处是易于扩展,易于向更底层学习;Babylon.js 深一些,好处是易用扩展难度大一些。

Three.js 的发展依靠社区推动,出来的比较早,发展比较成熟,Babylon.js 由微软公司在2013推出,文档和社区都比较健全,国内还不怎么火。


与 PlayCanvas 对比

PlayCanvas 是一个基于 WebGL 游戏引擎的企业级开源 JavaScript 框架,它有许多的开发工具能帮你快速创建 3D 游戏。与 Three.js 之间的对比:



PlayCanvas 的优势在于它有云端的在线可视化编辑工具。

PlayCanvas 的扩展性不如 Three.js。

最主要是 PlayCanvas 不完全开源,还商业付费。


与 Cesium 对比

Cesium 是国外一个基于 JavaScript 编写的使用 WebGL 的地图引擎,支持 3D、2D、2.5D 形式的地图展示,可以自行绘制图形,高亮区域。与 Three.js 对比:



Cesium 是一个地图引擎,专注于 Gis,相关项目推荐使用它,其它项目还是算了。

至于库的扩展,其它的配套插件,以及周边的资源都不及Three.js。


总结

通过以上信息我们发现,Three.js 在其库的扩展性,易用性以及功能方面有很好的优势。学习 Three.js 入门 3D 开发不但门槛低,而且学习曲线不会太陡,即使以后转向 WebGL 原生开发,也能通过 Three.js 学习到很多有用的知识。



现在最火的微信小游戏跳一跳也是在 Three.js 的基础上开发出来的。所以,Three.js 是我们必须要学的 WebGL 框架。


入门前准备

浏览器兼容


Three.js 可以使用 WebGL 在所有现代浏览器上渲染场景。对于旧版浏览器,尤其是 Internet Explorer 10 及更低版本,您可能需要回退到其他渲染器(CSS2DRenderer、CSS3DRenderer、SVGRenderer、CanvasRenderer)。



注意:如果您不需要支持这些旧版浏览器,则不推荐使用其他渲染器,因为它们速度较慢并且支持的功能比 WebGLRenderer 更少。


点击查看原图

即可下载当前版本的代码及相关案例,文件下载解压后是这样的:


微信截图_20200529213036.png

其中相关文件夹的内容是:

build:里面含有 Three.js 构建出来的 JavaScript 文件,可以直接引入使用,并有压缩版;
docs:Three.js 的官方文档;
editor:Three.js 的一个网页版的模型编辑器;
examples:Three.js 的官方案例,如果全都学会,必将成为大神;
src:这里面放置的全是编译 Three.js 的源文件;
test:一些官方测试代码,我们一般用不到;
utils:一些相关插件;
其他:开发环境搭建、开发所需要的文件,如果不对 Three.js 进行二次开发,用不到。
还有第三种,就是直接去 GitHub 上下载源码,和在官网上下载的代码一样。

hello World

前面说了这么多,准备了这么多,最后,放上我们的第一个案例吧。由此来打开学习 Three.js 的大门:


<!DOCTYPE html><html><head>    <meta charset=utf-8>    <title>我的第一个Three.js案例</title>    <style>        body {            margin: 0;        }        canvas {            width: 100%;            height: 100%;            display: block;        }    </style></head><body onload="init()"><script src="https://cdn.bootcss.com/three.js/92/three.js"></script><script>    //声明一些全局变量    var renderer, camera, scene, geometry, material, mesh;    //初始化渲染器    function initRenderer() {        renderer = new THREE.WebGLRenderer(); //实例化渲染器        renderer.setSize(window.innerWidth, window.innerHeight); //设置宽和高        document.body.appendChild(renderer.domElement); //添加到dom    }    //初始化场景    function initScene() {        scene = new THREE.Scene(); //实例化场景    }    //初始化相机    function initCamera() {        camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); //实例化相机        camera.position.set(0, 0, 15);    }    //创建模型    function initMesh() {        geometry = new THREE.BoxGeometry( 2, 2, 2 ); //创建几何体        material = new THREE.MeshNormalMaterial(); //创建材质        mesh = new THREE.Mesh( geometry, material ); //创建网格        scene.add( mesh ); //将网格添加到场景    }    //运行动画    function animate() {        requestAnimationFrame(animate); //循环调用函数        mesh.rotation.x += 0.01; //每帧网格模型的沿x轴旋转0.01弧度        mesh.rotation.y += 0.02; //每帧网格模型的沿y轴旋转0.02弧度        renderer.render( scene, camera ); //渲染界面    }    //初始化函数,页面加载完成是调用    function init() {        initRenderer();        initScene();        initCamera();        initMesh();        animate();    }</script></body></html>

请将上面的代码复制到 HTML 文件中,然后使用浏览器打开,我们就会发现下面的效果:

点击查看原图



————————————————

版权声明:本文为CSDN博主「GitChat的博客」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/valada/java/article/details/80871701







JavaScript的padStart()和padEnd()格式化字符串使用技巧

seo达人

用例

让我们从介绍几种不同的填充用例开始。


标签和值

假设你在同一行上有标签和值,例如 name:zhangsan 和 Phone Number:(555)-555-1234。如果把他们放在一起看起来会有点奇怪,会是这样:


Name: zhangsan

Phone Number: (555)-555-1234

你可能想要这个。


Name:           zhangsan

Phone Number:   (555)555-1234

或这个...


       Name: zhangsan

Phone Number: (555)555-1234

金额

在中国,显示价格时通常显示两位数的角、分。所以代替这个...


¥10.1

你会想要这个。


¥10.01

日期

对于日期,日期和月份都需要2位数字。所以代替这个...


2020-5-4

你会想要这个。


2020-05-04

时间

与上面的日期类似,对于计时器,你需要2位数字表示秒,3位数字表示毫秒。所以代替这个...


1:1

你会想要这个。


01:001

padstart()

让我们从 padStart() 以及标签和值示例开始。假设我们希望标签彼此正确对齐,以使值在同一位置开始。


       Name: zhangsan

Phone Number: (555)555-1234

由于 Phone Number 是两个标签中较长的一个,因此我们要在 Name 标签的开头加上空格。为了将来的需要,我们不要把它专门填充到电话号码的长度,我们把它填充到长一点,比如说20个字符。这样一来,如果你在未来使用较长的标签,这一招仍然有效。


在填充之前,这是用于显示此信息的入门代码。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1 + ": " + name);

console.log(label2 + ": " + phoneNumber);


//Name: zhangsan

//Phone Number: (555)-555-1234

现在,让我们填充第一个标签。要调用 padStart(),你需要传递两个参数:一个用于填充字符串的目标长度,另一个用于你希望填充的字符。在这种情况下,我们希望长度为20,而填充字符为空格。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1.padStart(20, " ") + ": " + name);

console.log(label2 + ": " + phoneNumber);


//               Name: zhangsan

////Phone Number: (555)-555-1234

现在填充第二行。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1.padStart(20, " ") + ": " + name);

console.log(label2.padStart(20, " ") + ": " + phoneNumber);


//               Name: zhangsan

////     Phone Number: (555)-555-1234

padEnd()

对于相同的标签和值示例,让我们更改填充标签的方式。让我们将标签向左对齐,以便在末尾添加填充。


初始代码


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log(label1 + ": " + name);

console.log(label2 + ": " + phoneNumber);


//Name: zhangsan

//Phone Number: (555)-555-1234

现在,让我们填充第一个标签,与我们之前所做的类似,但有两个小区别。现在,我们使用 padEnd() 而不是padStart(),并且需要在填充之前将冒号与标签连接起来,这样我们就能确保冒号在正确的位置。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log((label1 + ': ').padEnd(20, ' ') + name);

console.log(label2 + ": " + phoneNumber);


//Name:               zhangsan

//Phone Number: (555)-555-1234

现在两行都已填充。


const label1 = "Name";

const label2 = "Phone Number";

const name = "zhangsan"

const phoneNumber = "(555)-555-1234";


console.log((label1 + ': ').padEnd(20, ' ') + name);

console.log((label2 + ': ').padEnd(20, ' ') + phoneNumber);


//Name:               zhangsan

//Phone Number:       (555)-555-1234

数字(价格、日期、计时器等)呢?

padding函数是专门针对字符串而不是数字的,所以,我们需要先将数字转换为字符串。


价格

让我们看一下显示价格的初始代码。


const rmb = 10;

const cents = 1;

console.log("¥" + rmb + "." + cents); //¥10.1

要填充分,我们需要先将其转换为字符串,然后调用 padStart() 函数,指定长度为1且填充字符为'0';


const rmb = 10;

const cents = 1;

console.log("¥" + rmb + "." + cents.toString().padStart(2,0)); //¥10.01

日期

这是显示日期的初始代码。


const month = 2;

const year = 2020;


console.log(year + "-" + month); //2020-2

现在,让我们填充月份以确保它是两位数。


const month = 2;

const year = 2020;


console.log(year + "-" + month.toString().padStart(2,"0")); // 2020-02

计时器

最后是我们的计时器,我们要格式化两个不同的数字,即秒和毫秒。尽管有相同的原则。这是初始代码。


const seconds = 1;

const ms = 1;


console.log(seconds + ":" + ms); //1:1

现在要填充,我将在单独的行上进行填充,以便于阅读。


const seconds = 1;

const formattedSeconds = seconds.toString().padStart(2,0);

const ms = 1;

const formattedMs = ms.toString().padStart(3,0);


console.log(formattedSeconds + ":" + formattedMs); // 01:001

最后

虽然编写自己的padding函数并不难,但既然已经内置在JavaScript中,为什么还要自己去做呢?有很多有趣的函数已经内置了。在你自己构建一些东西之前,可能值得先快速搜索一下。

2020年越来越火的车载交互该怎么设计?来看前辈的经验总结!

涛涛

这次我们不聊视觉,也不畅想未来,只说说当下 HMI 产品设计与交互体验。

本文内容会涉及一些专业的汽车知识名词,因为篇幅有限,如有些知识名词不太明白可以百度一下。

别看错了,不是HDMI!

说到 HMI 大多数设计师应该是既熟悉又陌生,HMI 是 Human Machine Interface 的缩写,「人机接口」,也叫人机界面,人机界面(又称用户界面或使用者界面)是系统和用户之间进行交互和信息交换的媒介, 它实现信息的内部形式与人类可以接受形式之间的转换,凡参与人机信息交流的领域都存在着人机界面。

听起来是不是觉得这不就是 UI 吗?有什么区别吗?似乎差不多,几乎是没有区别的,只不过是在某些场合和设备上管他叫 UI,比如移动端设备,而在另外某些场所和设备上管他就叫 HMI,比如汽车车机和数控机床。所以这个概念也不用去特别较真,HMI 就权当作是汽车上的 UI 界面吧。毕竟汽车是高科技与工业结合的完美产物,「HMI」念出这个词时候就感觉是蛮专业的!很般配!

HMI前世与今生?

刚才说 HMI 最早更应用于工业上,比如常见的各种机床、制造装备。

或者说让时间再向前推进一点!

而这里通常意义的 HMI 则更加聚焦点,基本特指汽车车机或者车载多媒体设备。

说到这里还是要从车载仪表盘说起,从德国人卡尔·本茨发明世界第一辆汽车,距今已经 100 多年的时间了,在那些还没有 HMI 这个名词的年代,那么他是以什么形态出现的?那就不得不提「仪表盘」了。

当然写这篇文章并不是去评测谁家 HMI 更优秀,而是希望通过一些假设、实验和推断,和大家一起来探讨一下如何更有效地设计 HMI。

屏幕越大越好?车内到底需要几块屏幕?

我们先从屏幕开始。

说到屏幕,设计师都是比较敏感的,因为我们最终的设计交互创意都是需要都是在屏幕上显示展示出来的,HMI 当然也不例外。现在在车载屏幕上你能看到最大尺寸多大?

拿特斯拉为例,Model S 和 Model X 车型都是 17英寸,Model 3 为 15 英尺。

当然他肯定不是最大的,熟悉汽车朋友你应该知道我想说谁了,没错就是他!拥有 48 寸可多段升降屏幕的 BYTON 新能源概念车 M-Byte!48 寸的确很夸张,难道屏幕越来越大就是未来 HMI 的方向吗?

当然这个问题肯定是否定的,为什么?那就要从车载屏幕的作用来说起。

首先我是作为一个曾经就职于汽车公司的设计师,并且是一名地道的汽车发烧友,凭借对汽车还算熟悉和热爱做出一些产品交互分析,以下如有不妥之处还望海涵。

汽车内屏幕的作用

按照功能场景总体可分为三类:主行驶状态信息、附设备状态信息、多媒体 & 外设

不可缺少还需要与使用者、场景结合,我们先来做一个大概的用户画像。

对应这些需求,汽车需要有仪表台(屏)控制和显示的区域有五个。

五个区域分别是:

  • 主驾驶仪表屏
  • 中控台控制(屏)
  • 后排娱乐屏
  • 副驾驶信息屏
  • 扶手控制台(屏)

其中前三个是主流配置,后两个比较少见。

关于汽车设备这块我们不做深入展开了,毕竟这篇文章主要讨论的还是设计,直接看结果!

题外音:屏幕安全性的考量

汽车是比较特殊的设备,基于安全性考虑,汽车内屏幕尺寸不宜太大与太多。

屏幕总体为玻璃材质,但与车窗挡风玻璃的材质不同,当汽车遭遇碰撞的时候,车内屏幕极易破损并形成尖锐物,极大可能会乘坐人员造成二次伤害,所以车内屏幕不易太多,更不易太大。虽然车载屏幕变大变多已不可逆转,而且随着屏幕技术的提升,柔性 OLED 的应用也将会在一定范围解决安全问题。但也需要汽车相关设计者多在安全方面进行考虑,任何产品体验应该建立在安全基础之上的,特别是交通工具。

物理实体按钮过时了?

为什么大屏幕操控成为了当前的 HMI 主流了呢?那不得不去提一下另外一个我们熟悉的设备——手机!

同样一个有限的区域,如果用物理按键那么这个区域只能是固定的功能,而屏幕就可以无限扩展。特别是在汽车中控屏上集成内容会很多,体现就更加突出。

但是在汽车上的全部使用屏幕真的是最佳选择吗?显然这是有待商榷的。

不可否认屏幕的确有很强的扩展性,但是缺点也是明显的:1.触控反馈缺乏 2.交互效率不高

对于这样的判断,我们可以通过两个实验来进行验证。

将类似于 Surface Dial 这种智能按钮交互装置引入汽车的屏幕控制中,每个按钮可以根据情景进行自定义,并且吸附到汽车屏幕的任何位置进行交互操作,相信这一定是一种全新的使用体验。当然这一定是需要解决比如吸附力、安全性等一系列问题。

屏幕触控反馈

虽然目前的屏幕还无法做到完美触控反馈,但已经出现了一些新的硬件技术来试图解决这些问题,比如 Tanvas Touch,其定义为 「手指与触摸界面之间的电子压力控制」。简单来说他们的产品就 「皮肤的磁铁」 一样,能够更加精准地感应手指的动作,最后结果就是比 Apple 的 3D Touch 更加具有压感的触摸操作表现。

原理是利用手指尖触摸显示屏时产生的静电引力来模拟触感,通过电磁脉冲把更的反馈发送到用户的指尖。

Tanvas 也正在与汽车制造商们合作把这项技术嵌入到汽车或屏幕上,让人们更容易感触受到不同物体的表面。

也许在未来我们真的会遇到他。

文章来源:优设    作者:残酷de乐章

数据可视化指南:那些高手才懂的坐标轴设计细节

涛涛

坐标系是能够使每个数组在维度空间内找到映射关系的定位系统,更偏向数学/物理概念。在数据可视化中,最常用的坐标系分为笛卡尔坐标系和极坐标系,本文介绍的坐标轴设计主要也是围绕直角坐标系展开。

什么是坐标轴

在说坐标轴之前先来介绍下什么是坐标系。坐标系是能够使每个数组在维度空间内找到映射关系的定位系统,更偏向数学/物理概念。

维基百科对坐标系的定义是:对于一个 n 维系统,能够使每一个点和一组 n 个标量构成一一对应的系统,它可以用一个有序多元组表示一个点的位置。

数据可视化中,最常用的坐标系有两种:笛卡尔坐标系和极坐标系,均为二维坐标系。

  • 笛卡尔坐标系即直角坐标系,是由相互垂直的两条轴线构成。
  • 极坐标系由极点、极轴组成,坐标系内任何一个点都可以用极径和夹角(逆时针)表示。用到直角坐标系的常见图表有柱状图、折线图、面积图、条形图等。

下文介绍的坐标轴设计主要也是围绕直角坐标系展开,用到极坐标系的图表有饼图、圆环图、雷达图等。

坐标轴是坐标系的构成部分,是定义域轴和值域轴的统称。系的范围更大,而轴包含在系的概念里。由于可视化图表绘制的数据大部分都有一定的现实意义,因此我们可以根据坐标轴对应的变量是连续数据还是离散数据,将坐标轴分成连续轴、时间轴、分类轴三大类。轴的类型不同在设计处理上也有差异。

坐标轴的构成要素

介绍坐标轴设计前,我们先将坐标轴拆分成「原子」要素,具体分为轴线、轴刻度、轴标签、轴标题/单位、网格线。

坐标轴易被忽视的设计细节

根据坐标轴的构成,分类讨论下每个构成要素容易被忽视的设计细节。

轴线一般只考虑是否显示,例如柱状图、折线图等,在有背景网格线的情况下,会隐藏 y 轴线,条形图则是隐藏 x 轴线,以达到信息降噪,突出视觉重点的目的。

轴刻度通常不显示,只有在肉眼无法定位到某个标签对应的数据点时,会显示刻度线,辅助用户定位,比如折线图,或抽样显示的柱状图。

网格线用于定位数据点的值域范围,跟随值域轴的位置单向显示,柱状图采用水平网格,条形图采用垂直网格。样式为虚实线的最多,斑马线由于感知过强,一般不用。

轴标题/单位主要用于说明定义域轴、值域轴的数据含义。当可视化图表标题、图例、轴标签已经能充分表达数据含义,无需单独显示标题/单位,「如无必要,勿增实体」。

轴标签的设计就比较复杂,涉及到的细节点很多,而且对于定义域轴和值域轴上的标签和单位设计要考虑的细节点还有差异。下文将定义域轴和值域轴看成 x 轴和 y 轴,便于说明。

1. x轴标签设计

x 轴标签的设计重点在显示规则上,不同的坐标轴类型有不同的处理方式。

连续轴/时间轴的标签显示

连续轴/时间轴,是由一组前后包含同等差值的等差数列组成,缺少几个数值也能明显看出中间的对应关系。当多个标签在容器内全显示发生重叠,我们可以利用抽样显示的手段来避免这种情况。这里不推荐使用旋转,一方面从美观度上,旋转可能会破坏界面整体协调,另一方面,连续/时间轴非必须显示所有轴标签,抽样标签已经能满足用户对当前数组定义域的理解。

介绍一种常见的抽样方式:等分抽样

当多个标签在 x 轴无法完全显示,优先保留首尾标签,其余标签按同等步长间隔显示。间隔等分的前提是间隔数是合数,能被 1 和其本身以外的数整除。如果间隔数为质数,就需要「-1」转成合数。

举个例子:11 个标签,间隔数是 10,能被 2 和 5 整除,即分成 2 等分和 5 等分。12 个标签,间隔数是 11,无法等分,需要在间隔数基础上再「-1」,转成合数 10 后再等分,此时最后一个标签显示在倒数第二个数据点上。

有人会问了,能被那么多数等分,到底该选哪个呢?这就要根据标签长度来定,选择能放下最多标签的等分值。由于连续轴/时间轴,一般是数值、日期、时间等,字符长度有限,即使抽样后也能保证显示出一定数量的标签。

等分抽样不太适用于表达某个时间周期内的走势折线图,因为最后一个标签不一定对应最后一个数据点。对于这类折线图,能清楚表明起始时间和末尾时间,相比显示更多时间标签重要性来的更高。设计上可以只显示首尾标签,或首尾 + 中间值。

分类轴的标签显示

分类轴是由几组离散数据组成,相互之间独立存在,无紧密逻辑关联。若采用抽样规则,隐藏一些标签,用户对图表认知就会有困难,违背了数据可视化清晰、有效的设计原则。分类轴最佳处理方式是标签旋转 45 度,若 45 度仍显示不下,继续旋转 90 度。如果 90 度还是放不下就要考虑结合图表交互或反转图表。

标签旋转方向也有讲究,因为人的视觉习惯是从左到右,从上到下,标签顺时针旋转 45 度更符合用户的浏览动线。

分类轴标签字段有长有短,长文本标签直接旋转不仅影响美观,而且也不利于用户阅读。如果数据量比较少只有 2~4 个,长文本标签更适合水平展示,显示不下省略即可;如果数据量比较多,就限定字符数后旋转。

2. y轴标签设计

y 轴标签的设计重点在标签数量、取值范围和数据格式上。标签显示区域一般根据最长标签宽度自适应缩放。如果数组是固定的,就写成固定宽度,节省图表计算量,提高渲染速度。

y轴标签数量

标签数量不建议过多,太多的标签必定导致横向网格线变多,造成元素冗余,干扰图形信息表达。根据 7±2 设计原则,y 轴标签数量最多不超过这个范围。

y轴标签取值范围

y 轴标签的取值范围决定了图形在整个绘图区域的显示高度。

折线图 y 轴标签取值一般保证图形约占绘图区域的 2/3,以更有效的传达数据波动幅度,避免掩盖和夸大变化趋势。2/3 即斐波那契数列第二位起,相邻两数之比,也是黄金分割最简单的计算方法。

柱状图的 y 轴标签取值应从 0 基线开始,以恰当反映数值。如果展示被截断的柱状图,可能会误导观众做出错误的判断。

y轴标签数据格式

y 轴标签的数据格式在 ant.vision 写的比较详细,重复内容不在此说明,重点讲下一些特殊的设计细节。标签保留的小数位数保持统一,不要因为某些轴标签是整数值,就略去小数点。

正负向的 y 轴标签,由于负值带「-」符号,整个 y 轴看起来会有视觉偏差,特别是双轴图的右 y 轴更明显。这里建议正负向 y 轴给正值标签带上「+」,以达到视觉平衡的效果。

总结

写了那么多关于坐标轴的设计,你是不是恍然大悟,原来小小的坐标轴还有如此之多的细节。往常我们做图表设计,可能只是用网上自动生成的图表简单调整下,或者按照通用样式来设计。然而,通用样式虽然能表达数据意义,但也缺少了对图表细节的把控,失了精致优雅的感觉。

作为数据可视化设计的一小部分,就是这些设计细节,决定了图表最终的传达效果。

文章来源:优设    作者:米粒的DesignNote

手机appUI界面设计赏析(一)

前端达人

与传统PC桌面不同,手机屏幕的尺寸更加小巧操作,方式也已触控为主,APP界面设计不但要保证APP功能的完整性和合理性,又要保证APP的功能性和实用性,在保证其拥有流畅的操作感受的同时,满足人们的审美需求。

接下来为大家介绍几款手机appui界面设计

点击查看原图

--手机appUI设计--

点击查看原图

--手机appUI设计--

点击查看原图

--手机appUI设计--

点击查看原图

--手机appUI设计--

点击查看原图

--手机appUI设计--

点击查看原图


--手机appUI设计--



微信图片_20200529093951.jpg

--手机appUI设计--


微信图片_20200529093948.png

--手机appUI设计--


微信图片_20200529093946.png


--手机appUI设计--


点击查看原图

--专业又贴心医疗App页面设计--


微信图片_20200529093941.jpg

--专业又贴心医疗App页面设计--微信图片_20200529093938.jpg

--专业又贴心医疗App页面设计--微信图片_20200529093936.jpg

--专业又贴心医疗App页面设计--微信图片_20200529093933.jpg

--专业又贴心医疗App页面设计--微信图片_20200529093930.jpg

--手机appUI设计--


微信图片_20200529093928.jpg

--手机appUI设计--


微信图片_20200529093925.jpg

--手机appUI设计--


微信图片_20200529093921.jpg

--手机appUI设计--


点击查看原图

--手机appUI设计--


点击查看原图

--手机appUI设计--


点击查看原图

--手机appUI设计--


点击查看原图

--手机appUI设计--


点击查看原图

--手机appUI设计--


点击查看原图


--手机appUI设计--


点击查看原图

--手机appUI设计--


点击查看原图


--手机appUI设计--


--手机appUI设计--


点击查看原图



--手机appUI设计--

(以上图片均来源于网络)



  蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服



   更多精彩文章:

       手机appUI界面设计赏析(二)




JavaScript版数据结构与算法——基础篇(一)

前端达人

数组

数组——最简单的内存数据结构

数组存储一系列同一种数据类型的值。( Javascript 中不存在这种限制)

对数据的随机访问,数组是更好的选择,否则几乎可以完全用 「链表」 来代替

在很多编程语言中,数组的长度是固定的,当数组被填满时,再要加入新元素就很困难。Javascript 中数组不存在这个问题。

但是 Javascript 中的数组被实现成了对象,与其他语言相比,效率低下。

数组的一些核心方法

方法 描述
push 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。(改变原数组)
pop 方法从数组中删除最后一个元素,并返回该元素的值。(改变原数组)
shift 方法从数组中删除第一个元素,并返回该元素的值,如果数组为空则返回 undefined 。(改变原数组)
unshift 将一个或多个元素添加到数组的开头,并返回该数组的新长度(改变原数组)
concat 连接两个或多个数组,并返回结果(返回一个新数组,不影响原有的数组。)
every 对数组中的每个元素运行给定函数,如果该函数对每个元素都返回 true,则返回 true。若为一个空数组,,始终返回 true。 (不会改变原数组,[].every(callback)始终返回 true)
some 对数组中的每个元素运行给定函数,如果任一元素返回 true,则返回 true。若为一个空数组,,始终返回 false。(不会改变原数组,)
forEach 对数组中的每个元素运行给定函数。这个方法没有返回值,没有办法中止或者跳出 forEach() 循环,除了抛出一个异常(foreach不直接改变原数组,但原数组可能会被 callback 函数该改变。)
map 对数组中的每个元素运行给定函数,返回每次函数调用的结果组成的数组(map不直接改变原数组,但原数组可能会被 callback 函数该改变。)
sort 按照Unicode位点对数组排序,支持传入指定排序方法的函数作为参数(改变原数组)
reverse 方法将数组中元素的位置颠倒,并返回该数组(改变原数组)
join 将所有的数组元素连接成一个字符串
indexOf 返回第一个与给定参数相等的数组元素的索引,没有找到则返回 -1
lastIndexOf 返回在数组中搜索到的与给定参数相等的元素的索引里最大的值,没有找到则返回 -1
slice 传入索引值,将数组里对应索引范围内的元素(浅复制原数组中的元素)作为新数组返回(原始数组不会被改变)
splice 删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容(改变原数组)
toString 将数组作为字符串返回
valueOf 和 toString 类似,将数组作为字符串返回

是一种遵循后进先出(LIFO)原则的有序集合,新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

通俗来讲,就是你向一个桶里放书本或者盘子,你要想取出最下面的书或者盘子,你必须要先把上面的都先取出来。

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,也被用于浏览器历史记录 (浏览器的返回按钮)。

代码实现

// 封装栈
    function Stack() {
        // 栈的属性
        this.items = []

        // 栈的操作
        // 1.将元素压入栈
        Stack.prototype.push = function (element) {
            this.items.push(element)
        }
        // 2.从栈中取出元素
        Stack.prototype.pop = function () {
            return this.items.pop()
        }
        // 3.查看栈顶元素
        Stack.prototype.peek = function () {
            return this.items[this.items.length - 1]
        }
        // 4.判断是否为空
        Stack.prototype.isEmpty = function () {
            return this.items.length === 0
        }
        // 5.获取栈中元素的个数
        Stack.prototype.size = function () {
            return this.items.length
        }
        // 6.toString()方法
        Stack.prototype.toString = function () {
            let str = ''
            for (let i = 0; i< this.items.length; i++) {
                str += this.items[i] + ' '
            }
            return str
        }

    }

    // 栈的使用
    let s = new Stack()

队列

队列是遵循先进先出(FIFO,也称为先来先服务)原则的一组有序的项。队列在尾部添加新
元素,并从顶部移除元素。添加的元素必须排在队列的末尾。

生活中常见的就是排队

代码实现

function Queue() {
        this.items = []
        // 1.将元素加入队列
        Queue.prototype.enqueue = function (element) {
            this.items.push(element)
        }
        // 2.从队列前端删除元素
        Queue.prototype.dequeue = function () {
            return this.items.shift()
        }
        // 3.查看队列前端元素
        Queue.prototype.front = function () {
            return this.items[0]
        }
        // 4.判断是否为空
        Queue.prototype.isEmpty = function () {
            return this.items.length === 0
        }
        // 5.获取队列中元素的个数
        Queue.prototype.size = function () {
            return this.items.length
        }
        // 6.toString()方法
        Queue.prototype.toString = function () {
            let str = ''
            for (let i = 0; i< this.items.length; i++) {
                str += this.items[i] + ' '
            }
            return str
        }
    }
    
    // 队列使用
    let Q = new Queue()

优先级队列:

代码实现


function PriorityQueue() {
        function QueueElement(element, priority) {
            this.element = element
            this.priority = priority
        }
        this.items = []

        PriorityQueue.prototype.enqueue = function (element, priority) {
            let queueElement = new QueueElement(element,priority)

            // 判断队列是否为空
            if (this.isEmpty()) {
                this.items.push(queueElement)
            } else {
                let added = false // 如果在队列已有的元素中找到满足条件的,则设为true,否则为false,直接插入队列尾部
                for (let i = 0; i< this.items.length; i++) {
                    // 假设priority值越小,优先级越高,排序越靠前
                    if (queueElement.priority < this.items[i].priority) {
                        this.items.splice(i, 0, queueElement)
                        added = true
                        break
                    }
                }
                if (!added) {
                    this.items.push(queueElement)
                }
            }

        }
        
    }
    

链表

链表——存储有序的元素集合,但在内存中不是连续放置的。


链表(单向链表)中的元素由存放元素本身「data」 的节点和一个指向下一个「next」 元素的指针组成。牢记这个特点

相比数组,链表添加或者移除元素不需要移动其他元素,但是需要使用指针。访问元素每次都需要从表头开始查找。

代码实现:
单向链表


function LinkedList() {
        function Node(data) {
            this.data = data
            this.next = null

        }
        this.head = null // 表头
        this.length = 0
        // 插入链表
        LinkedList.prototype.append = function (data) {
            // 判断是否是添加的第一个节点
            let newNode = new Node(data)
            if (this.length == 0) {
                this.head = newNode
            } else {
                let current = this.head
                while (current.next) { 
                // 如果next存在,
                // 则当前节点不是链表最后一个
                // 所以继续向后查找
                    current = current.next
                }
                // 如果next不存在
                 // 则当前节点是链表最后一个
                // 所以让next指向新节点即可
                current.next = newNode
            }
            this.length++
        }
        // toString方法
        LinkedList.prototype.toString = function () {
            let current = this.head
            let listString = ''
            while (current) {
                listString += current.data + ' '
                current = current.next
            }
            return listString
        }
         // insert 方法
        LinkedList.prototype.insert = function (position, data) {
            if (position < 0 || position > this.length) return false
            let newNode = new Node(data)
            if (position == 0) {
                newNode.next = this.head
                this.head = newNode
            } else {
                let index = 0
                let current = this.head
                let prev = null
                while (index++ < position) {
                    prev = current
                    current = current.next
                }
                newNode.next = current
                prev.next = newNode
            }
            this.length++
            return true
        }
        // get方法
        LinkedList.prototype.get = function (position) {
            if (position < 0 || position >= this.length) return null
            let index = 0
            let current = this.head
            while (index++ < position){
                current = current.next
            }
            return current.data
        }
        LinkedList.prototype.indexOf = function (data) {
            let index = 0
            let current = this.head
            while (current) {
                if (current.data == data) {
                    return index
                } else {
                    current = current.next
                    index++
                }
            }

            return  -1
        }
        LinkedList.prototype.update = function (position, data) {
            if (position < 0 || position >= this.length) return false
            let index = 0
            let current = this.head
            while (index++ < position) {
                current = current.next
            }
            current.data = data
            return  true
        }
        LinkedList.prototype.removeAt = function (position) {
            if (position < 0 || position >= this.length) return null
            if (position == 0) {
                this.head = this.head.next
            } else {
                let index = 0
                let current = this.head
                let prev = null
                while (index++ < position) {
                    prev = current
                    current = current.next
                }
                prev.next = current.next
            }
            this.length--
            return  true


        }
        LinkedList.prototype.remove = function (data) {
            let postions = this.indexOf(data)

            return this.removeAt(postions)
        }
        
    }

    let list = new LinkedList()
双向链表:包含表头表尾 和 存储数据的 节点,其中节点包含三部分:一个链向下一个元素的next, 另一个链向前一个元素的prev 和存储数据的 data牢记这个特点

function doublyLinkedList() {
        this.head = null // 表头:始终指向第一个节点,默认为 null
        this.tail = null // 表尾:始终指向最后一个节点,默认为 null
        this.length = 0 // 链表长度

        function Node(data) {
            this.data = data
            this.prev = null
            this.next = null
        }

        doublyLinkedList.prototype.append = function (data) {
            let newNode = new Node(data)

            if (this.length === 0) {
            // 当插入的节点为链表的第一个节点时
            // 表头和表尾都指向这个节点
                this.head = newNode
                this.tail = newNode
            } else {
            // 当链表中已经有节点存在时
            // 注意tail指向的始终是最后一个节点
            // 注意head指向的始终是第一个节点
            // 因为是双向链表,可以从头部插入新节点,也可以从尾部插入
            // 这里以从尾部插入为例,将新节点插入到链表最后
            // 首先将新节点的 prev 指向上一个节点,即之前tail指向的位置
                newNode.prev = this.tail
            // 然后前一个节点的next(及之前tail指向的节点)指向新的节点
            // 此时新的节点变成了链表的最后一个节点
                this.tail.next = newNode
            // 因为 tail 始终指向的是最后一个节点,所以最后修改tail的指向
                this.tail = newNode
            }
            this.length++
        }
        doublyLinkedList.prototype.toString = function () {
            return this.backwardString()
        }
        doublyLinkedList.prototype.forwardString = function () {
            let current = this.tail
            let str = ''

            while (current) {
                str += current.data + ''
                current = current.prev
            }

            return str
        }
        doublyLinkedList.prototype.backwardString = function () {
            let current = this.head
            let str = ''

            while (current) {
                str += current.data + ''
                current = current.next
            }

            return str
        }

        doublyLinkedList.prototype.insert = function (position, data) {
            if (position < 0 || position > this.length) return false
            let newNode = new Node(data)
            if (this.length === 0) {
                this.head = newNode
                this.tail = newNode
            } else {
                if (position == 0) {
                    this.head.prev = newNode
                    newNode.next = this.head
                    this.head = newNode
                } else if (position == this.length) {
                    newNode.prev = this.tail
                    this.tail.next = newNode
                    this.tail = newNode
                } else {
                    let current = this.head
                    let index = 0
                    while( index++ < position){
                        current = current.next
                    }
                    newNode.next = current
                    newNode.prev = current.prev
                    current.prev.next = newNode
                    current.prev = newNode

                }

            }

            this.length++
            return true
        }
        doublyLinkedList.prototype.get = function (position) {
            if (position < 0 || position >= this.length) return null
            let current = this.head
            let index = 0
            while (index++) {
                current = current.next
            }

            return current.data
        }
        doublyLinkedList.prototype.indexOf = function (data) {
            let current = this.head
            let index = 0
            while (current) {
                if (current.data === data) {
                    return index
                }
                current = current.next
                index++
            }
            return  -1
        }
        doublyLinkedList.prototype.update = function (position, newData) {
            if (position < 0 || position >= this.length) return false
            let current = this.head
            let index = 0
            while(index++ < position){
                current = current.next
            }
            current.data = newData
            return true
        }
        doublyLinkedList.prototype.removeAt = function (position) {
            if (position < 0 || position >= this.length) return null
            let current = this.head
            if (this.length === 1) {
                this.head = null
                this.tail = null
            } else {
                if (position === 0) { // 删除第一个节点
                    this.head.next.prev = null
                    this.head = this.head.next
                } else if (position === this.length - 1) { // 删除最后一个节点
                    this.tail.prev.next = null
                    this.tail = this.tail.prev
                } else {
                    let index = 0
                    while (index++ < position) {
                        current = current.next
                    }
                    current.prev.next = current.next
                    current.next.prev = current.prev
                }
            }
            this.length--
            return current.data
        }
        doublyLinkedList.prototype.remove = function (data) {
            let index = this.indexOf(data)
            return this.removeAt(index)
        }
    }


感谢你的阅读~
————————————————
版权声明:本文为CSDN博主「重庆崽儿Brand」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/brand2014/java/article/details/106134844



大数据可视化设计赏析(三)

前端达人

     如今大数据产业正在超出我们的想象悄然发展,而随着大数据时代的到来,越来越多的公司开始意识到数据资源的管理和运用。今天就给大家介绍一些可视化大屏的UI设计。


点击查看原图

    --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

c24b7efe812270eb555c5ab24b8a5fa2626973621ab956-4LUO4k_fw658.gif

 --大屏UI设计--

eebdcf2ab80ccf28a832b463b5efb8d390baa8401fbcda-58EU2O_fw658.jpg


eee7b0bd72a92d26ef0ea8b65921a2fcacf49ae934f18-ScQnAI_fw658.png

f0ab44b8e812af72209891521cbff1fe6ff656b863d09-JxGZiR_fw658.jpg



f5c7bedb9779f20ca239e235a98ef8eae839a5f980e8a-gkXvyM_fw658.png

 --大屏UI设计--


点击查看原图

 --大屏UI设计--

TB2XULnmC0mpuFjSZPiXXbssVXa-680650857的副本.jpg

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--WechatIMG166.jpeg

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

WechatIMG174.jpeg

 --大屏UI设计--

WechatIMG175.jpeg

 --大屏UI设计--

WechatIMG164.jpeg

 --大屏UI设计--

WechatIMG176.jpeg

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

点击查看原图

 --大屏UI设计--

(以上图片均来自于网络)


其实可视化大屏的UI设计并不只是一个简单的设计,其核心就是要以展示数据为核心,不管在多么炫目的情况下都不会影响数据的展示。


  蓝蓝设计www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 平面设计服





    更多精彩文章:

       

 大数据可视化设计赏析(一)

  大数据可视化设计赏析(二)

  大数据可视化设计赏析(三)

  大数据可视化设计赏析(四)

  大数据可视化设计赏析(五)

  大数据可视化设计赏析(六)

  大数据可视化设计赏析(七)




认识 ESLint 和 Prettier

seo达人

ESLint

先说是什么:ESLint 是一个检查代码质量与风格的工具,配置一套规则,他就能检查出你代码中不符合规则的地方,部分问题支持自动修复。


使用这么一套规则有什么用呢?如果单人开发的话倒是没什么了,但是一个团队若是存在两种风格,那格式化之后处理代码冲突就真的要命了,统一的代码风格真的很重要!


(其实以前自己做一个项目的时候,公司电脑和家庭电脑的代码风格配置不一样,在家加班的时候也经常顺手格式化了,这么循环了几次不同的风格,导致 diff 极其混乱

想提高设计转化率,按钮应该放在左边还是右边?

涛涛

任何一名设计师应该都会接触到运营活动页,产品落地页此类需求。而这些落地页设计需求的业务目标衡量标准都相当明确——即转化率。再进一步,与我们的设计输出直接相关的就是首页转化率/点击率。这些数据通过埋点能很轻易地获得,一般情况下,产品经理会提前在需求文档中标明需要埋点的地方(埋点简单说就是测量某个位置或者交互节点的具体数据,例如发生了多少次点击),获得数据用于验证产品最终是否符合预期,是否达到了理想的转化效果。

叮~ 讲到这我们应该明确了一件事,整个落地的设计其实最终都是为那个关键数据服务,无论是点击率还是转化率,达到预期甚至超出预期,那你的设计就完美地完成了任务,这也是验证设计有效性的主要方法,将设计与数据关联,用可量化的数据指标来验证偏感性的视觉工作。

就这样,设计与产品/运营的世纪大战开始了。因为我们都有了一个共同的目标,因此在产品的最终收益、期望效果方面互相都很明确。但在实现手段上,我们很轻易地产生了分歧。主要分歧点就是「按钮在左还是按钮在右」这个问题上。我们需要理解,这不是一个简单的交互问题,因为它其中掺杂了商业内容。如果这是一个交互问题,那我们很容易判断,例如弹窗的主次按钮应该主右副左,这既符合平台规范,也符合用户认知和操作习惯。

然而作为一个强商业属性的落地页,按钮在左或者按钮在右都有其合理性。我选择左,而运营同学代表他们团队要求右。 于是我败下阵来,当然,虽然表面上设计师输了,但我们怎么能服输,于是我想尽办法来验证左侧放置按钮才是更有利于转化的形式。下面我们来看看不同的倾向对应的设计原理。

左与右的矛盾

产生左与右的争执其实主要源于设计与需求方的两个判断方向。首先说一下我的判断逻辑,按照已知经过验证的理论,即 F 阅读顺序(尼尔森的用户阅读视线模型),用户浏览落地页的顺序应当是从左往右自上而下,因此左上角的信息最早触达用户。在当前主流的首图式落地页样式下,首图 banner 中的内容应当置于左侧,以使用户更快地获知产品的关键信息。

在落地页首图的体验文案本身就是一个设计的覆盖范围,因为它直接关系到首页的视觉传达效率,即用户需要花费多长时间、多少精力才能理解你的产品。我们往往在首页体验文案中采用主标题加副标题的形式,着重解释这个产品是个什么东西、用户能从这获得什么,往往通过主副文案搭配的形式,来完成整个大意的阐述。

基于此,核心内容置于左侧,用户在快速扫视时能够第一时间获知产品信息,了解产品利益点,这与我们精心准备整个网站,以及精心准备诱导力文案的方法相契合。这是我做出内容置于左侧的设计决策的主要思路。可以看出,我这里主要参考的是 F 阅读模型这一理论,根据这个经验我得到的结论是 重要的信息应当摆放在左侧以使用户立即触达核心信息,这将有利于接下来的引导或者转化。

另一方面,运营同学又是基于什么考虑决定将核心内容放在右侧的呢?答案是操作习惯,理论化的话可以用费茨定律概括,(目标距离用户距离越短,用户触达的效率越高)。考虑到大部分用户使用右手操作,鼠标也大都悬停在屏幕右侧,因此,按钮置于右侧,用户点击的路径变得更短,也就更容易触达和转化(纯体验角度或者说效率角度)。

你仔细阅读这部分内容,从分歧点到各自的理论支撑实际上都没有太大的漏洞,为什么没有漏洞?因为确实都没有错误,也都存在其合理性。例如我们常用的购物 APP 会把按钮置于右下角,用户操作起来必然比左上角的按钮更加容易。那么在这两种分析都合理的背景下,我们要对比或争论的其实不是哪个判断是错误的,而是哪个判断更有利,更合理,能够带来更多的数据转化。因此,这个问题最终由对错问题,转化为一个优劣问题。

左与右的妥协(一种结论)

有些人很机智,这个时候肯定会想,既然左边最容易触达信息,右边最容易触达按钮操作,那左边放置内容,右侧放置操作不就完美解决了吗?哎呀,读者真聪明。

由于 F 阅读的逻辑,将展示性质的「内容」放置于左侧,使用户更快触达关键信息,由于费茨定律,以及多年来养成的用户习惯(操作组件在右侧,当然现在很多放在中间的情况)将需要执行的操作置于右侧,使用户快速交互并完成任务。有一定道理,甚至在实际落地产品中我们也能看到一些类似的设计,例如豆瓣。 这是一种左与右的妥协

但需要注意的是,豆瓣产品的右侧放置的是较为复杂的交互模块,例如完整的登录注册模块。在该场景下,用户在交互路径更短的右侧区域执行交互效率要明显高于左侧区域。

那么下面开始论述按钮置于左侧的观点

论点一:排版的限制

豆瓣的形式对于落地页产品,可能并不适用。主要有两方面原因。我们都知道,产品落地页首屏的组成为体验文案,主 CTA,插画配图三部分。常规做法是插画作为一组信息置于一侧,文案加按钮作为一组信息置于一侧。因为,体验文案与按钮具有强关联性,同时按钮与文案作为一组信息,才能与另一侧的插画搭配构建平衡的布局,呈现比较优美的视觉效果。

请登录后查看原图,因此,豆瓣那种妥协方式并不适用于商业类落地页。因为内容和操作本身是一体的,这源于排版的规整性的限制,按钮和文案只能同时存在于一侧,如果刻意去追求左侧内容,右侧操作,效果就像下面这样。一方面,只靠文案和按钮无法撑起左右两个区域,一方面文案和按钮被割裂开,用户的视线由文案转到按钮的路径过长,体验较差。(文案与按钮成组后,用户可以在阅读内容产生动机后立即触达交互按钮并完成转化)

论点二:文案与配图孰轻孰重

如果你亲自体验这两种区别的落地页(左图右文/左文右图),你会发现有一个共同点,就是在某个区域的停留时长,没错就是内容区域。以下图的顶部卡片区域为例,在阅读时我的浏览情况是,大致地扫视左侧的插画,然后注视右侧文字区,了解文章的具体内容,并在此区域停留较长时间,毕竟仔细阅读需要花费时间。

这就涉及到一个问题,插画与内容哪个更重要?其实答案很明显,我们只需要舍弃掉其中一项来测试下,看看哪个内容的缺失会对用户理解设计传达的语义产生较大影响。OK,我觉得没必要测试了(虚晃一枪)。很明显,删除插画后,我们仍然可以通过文章的标题来获知文章概要等关键信息,就像落地页首屏的体验文案,即便没有插画我们也能通过首页文案来获知这个产品是什么,能够为我带来什么。

然而如果去掉关键信息,去掉标题与按钮,仅凭插画我们无法分辨当前页面到底在讲述什么东西。设计本身就像是人与人的交流,产品就是我们,而用户则是我们的交流对象,去掉核心的文案,相当于把我们自己变成了哑巴,而去掉插画,最多相当于我们交流时面无表情罢了。

因此,在商业落地页中,我们以转化为核心目标,而能够更快地触达最重要的信息显然是明智之举,因此我们希望将核心的文案内容置于左侧。

(另外,一图胜千言的原理只适用于个别场景,例如数据可视化。设计人员通过将数值数据转化为易于理解的柱状图扇形图,来传达数据结论。而视觉修饰性质的插画则无法做到准确表意,我们通常在产品设计中见到的插画更多的是在情感上和审美上给予我们一定的愉悦,但想要准确描述关键信息,还是需要文字作为核心角色)

论点三:用户会因为便于操作而产生动机?

另一点同样值得我们思考,即用户真的会因为某个按钮更容易点击而被转化吗?或者我们换个形式问,假设你是一名男性,你会因为按钮在鼠标附近而选择点击购买女士内衣吗?你会在自己财务状况较差的时候因为按钮在鼠标附近而点击购买品吗?在大多数理性场景下,我相信你不会这样做。

所以这时候要引入福格模型,用来阐述产生转化的整个路径。福格模型简单来讲就是一个公式:B=MAT。B(behavior) 代表行为,M(motivation) 代表动机也就是用户需求,A(ability) 代表用户使用的门槛,T(trigger) 代表触发。也就是用户行为的产生需要用户需求为基础,需要保证产品的易用性,但是这还不够,在这个基础上我们还需要在产品中通过设计触发用户。完成转化的三个关键要素是,动机、能力、触发,缺一不可。

福格模型帮助我们解决了这个疑问。用户的购买或者转化始于动机,就像我上面举的例子,如果一个用户根本对产品没有需求(男性对女性内衣),那就不会产生动机,在没有动机的情况下,后面两项内容,能力或者触发都没有意义,无法发挥作用。整个转化的流程可以参考下方的示意图。

实际上对于那些有强烈动机购买或使用产品的用户,你的一切设计都没有太大意义,因为用户有强烈诉求的情况下,他会发挥主观能动性去找到转化的入口,主动完成转化。同理,有些用户是完全不会产生动机的,不是目标用户群。

设计策略主要针对的是有动机但不强烈(某种程度上有需求或者被吸引),以及暂时没有动机的两类用户。通过我们的首屏及详细内容,痛点利益点的介绍,来放大用户动机,制造共鸣点,创造美好的想象空间,使用户涌现强烈动机。然后转化就自然而然的产生。

因此,在首屏我们的核心要义是通过内容设计来触发用户动机,而不是想方设法触发操作。走捷径的误触方案设计能保证百分百的触发率,但那种触发没有任何意义。到这里我们应该明确了,用户会因为好的内容所触发的动机而买单,但不会因为你把按钮放在我手边而产生购买冲动。

因此,我的结论是,用户更有可能因为左侧展示的强洞察力的文案而产生动机,而动机是整个转化的起始,也是最关键的一点,有了动机,触发(按钮位置)的效率即便低一点,但转化仍然很有可能继续(就像动机产生了惯性,有了强烈的动机会自发地去寻找触发器,去寻找按钮以自主完成转化,但触发器不会有惯性)

这个观点论述下来,主要涉及到 F 阅读模型,费茨定律以及福格模型,算是很基本的设计原则,也顺便帮大家重温一下。最后,我们再拿一些其他实证来进一步论述,例如国内一线公司的落地页设计。

1. 一线公司落地页布局

2. 全球独角兽企业落地页

文章来源:优设    作者:南山可

B端系统,筛选控件总结

鹤鹤

写在前面


首先我们先从筛选本身讲起吧~

 

筛选可以说是我使用比较频繁的一种交互形式,比如我点外卖,会选择满减优惠力度大,同时我也可以选择在哪一个价格区间内的产品,这就会用到筛选,而到了B端产品上来,一个CRM系统当中,筛选的逻辑也会比移动端的复杂,伴随着:且关系、或关系、大于、小于等等这样复杂的逻辑,也为设计本身增加了很多难度。因此,今天我们就来讨论讨论筛选控件

 


1、筛选存在的意义


筛选存在的对于整个表单来说是非常重要的,它可以帮助用户,在表单茫茫多的数据当中进行快速的定位;可以对表单进行快速划分,缩短用户对于数据的寻找时间;能够满足用户在工作中,实际业务场景的筛选。

对于实际B端场景来说,筛选是日常数据分类的一个重要途径,我们先来看看实际场景到底有哪些?

 

用几个我们CRM用户日常使用的场景来说:

 

比如今天作为一个电话销售人员,想要联系最近注册的用户时,通常会通过筛选来选出最近几天注册过,同时又没有销售更进的客户,进行一个优先级的排布;

 

再比如说,在销售周报当中,销售主管可以通过筛选得到每个人这周完成的状态,也可以通过筛选得出每个人对于线索的更进情况和对客户的流失状态等等,这些都可以通过各种各样的筛选形式来满足用户对于特定情况下的使用



筛选和搜索、导航的区别?

 

筛选可以通过多个筛选条件进行多维度的寻找,而导航、搜索只能通过单一条件进行指定筛选。

虽然在现在很多搜索都可以支持多维度用空格去进行多字段的关键词搜索,但本质上区别不大

所以在B端项目当中,如果你有表单,那你就需要筛选



2、筛选的类型


我们将筛选分为基础筛选和高级筛选两种,两种筛选会根据业务场景不同,在不同的页面去使用

 

2.1、基础筛选


基础筛选一般为系统预设好的筛选字段,具有很强的业务和场景的需求。基础筛选一般分为四个部分:


筛选条件:是指用户可以筛选的范围

筛选项:是指用户可以选择的筛选项目

已选项:是指用户已经选中的筛选项

备选项:是指用户还没有选择的筛选选项



基础筛选更多作为用户快捷筛选的一种方式,因为一般使用场景当中用户几个筛选逻辑为“且”

同时筛选的逻辑也为简单筛选,所以在使用场景上只适合在对筛选要求不高的场景下使用。


2.2、高级筛选


高级筛选一般为筛选中含有运算符,同时筛选当中包含条件关系,比如且关系或者否关系。一般高级筛选包含以下几类关键词

 

筛选关系:是指几个筛选条件之间的关系,一般为 且、或关系,即 且 关系为几个条件之间的并集;或 关系为几个条件之间的联集

筛选字段:是指在筛选当中,所要的筛选项,一般为表单当中的所有可筛选的字段

筛选操作:是指筛选字段和筛选值之间的关系,常见的筛选操作有:大于、小于、是、否、包含、不包含、为空、不为空等等。

筛选值:你所需要筛选的数值



高级筛选一般满足更多的用户场景,为用户多条件多字段、多个筛选关系、多个筛选操作 提供有利保障。




3、筛选的布局


3.1、上下布局


当在筛选器条件少于5个的情况下,最常使用的就是上下布局,这样筛选能与网站保持统一的情况下,上下布局也更方便用户进行阅读

 

当筛选器过多的情况下(一般在5-15个之间),筛选器过多,需要滚屏才能看到筛选结果,用户使用起来会很别扭。所以在5-15个的情况下,一般会将筛选项进行收折,这样保证筛选整体面积不会太大,同时将用户常用的筛选放在前面,可以满足用户基本的业务需求和使用场景



3.2、左右布局


左右布局在PC端一般是以字段选择进行筛选,通俗来讲就是将用户可以筛选的所有字段全部罗列出来,然后通过勾选选,择出你需要筛选的字段,进行筛选器的使用

 

左右布局的好处是能够将筛选的所有条件都直接的展示出来,可以适应很多场景,在筛选器用15个以上时。通过左右布局的方式,能够让筛选条件进行滚动,在最大限度保持用户使用体验




4、筛选的形式


在日常的B端产品中,筛选的形式有哪些?筛选到底应该怎么设计?接下来为大家总结梳理一些在 B端产品 中的筛选玩法,希望为你开启新大陆。


4.1、平铺型



平铺型一般为用户搜索结果数据量过大,使用户搜索出来的结果与其预期差距过大,用户然后可以通过筛选对数据的再一次分类,使用户能够精准寻找其想要的结果。

平铺型一般为筛选条件少于6个,这样能够通过1行或者2行去展示筛选项的结果

 

多用于信息量大的产品,比如电商、视频网站等等。常见的淘宝、京东、腾讯视频PC端 都采取用这样的方式,将所有的筛选条件列出来。

 

平铺型的好处是将筛选项的结果全部或者部分放出,能够帮助用户快速理解筛选项以及快读找到自己想要的结果。

缺点也是很明显,平铺型的控件占比大,需要占据大量面积展示平铺出的筛选结果。

 

比如淘宝PC端,搜索一个产品后花去40%的面积去展示所有的筛选条件,其实就是想引导用户,淘宝搜索过后spu的数量仍然过大,想通过进一步的筛选,让用户明确自己对想要东西。同时因为面积占比大,通常平铺型都是以收折的状态,只有在搜索触发后才会完全展开


4.2、收折型



收折型筛选是一种简单直接的筛选形式,将用户常用的筛选形式通过下拉框的形式进行筛选。每一个筛选条件就是一个下拉框,这种形式看上去很简单,但是在B端场景中,下拉框对于用户来说认知成本低,操作性也较强,同时在用户重度使用时,又能给用户很好的使用体验的一种方式

 

优点:

用户可以直接对其常用的字段筛选进行一步操作,并且没有复杂的筛选关系,全部都是“且”的筛选逻辑,能够保证用户进行快速的筛选选择

 

缺点:

将所有信息全部平铺展开,信息量过于冗杂繁多,同时在做通用性产品时,这种方式很难做到通用性


 

4.3、单侧筛选



单侧筛选是一种更通用的筛选形式,通过对于你想筛选的字段进行勾选,勾选完成后就会出现筛选条件,然后选择筛选字段、筛选操作、筛选值,一般选择完成所有筛选后,还需要点击查询,筛选操作才算完成。

 

整个单侧筛选,大量的筛选条件可以放置在表单的左侧或者右侧,通过表单纵向空间,去承载大量筛选条件。

 

优点:

节省空间、通用性强。因为在很多Saas系统、Paas系统当中,无法针对每一个客户进行设计,就要考虑到系统通用型高,做一些大而全的功能。在每个表单也所需要定制化修改的地方很少,同时能容纳的信息量可以很大。

 

缺点:

就是在后台系统当中只有这一种筛选形式会面临在我常用的几种筛选的字段中,要通过不断寻找,来满足我的筛选需求,操作麻烦。

 

 

我们产品在某一次改版就将筛选由收折式修改为单侧式,因为我们用户使用筛选的场景非常的多,用户每次筛选都要多进行2、3步操作,导致用户进行了大量的吐槽,后来进行修改,将筛选顺序支持手动调整顺序,用户吐槽的次数才慢慢减少。



4.4、表头筛选

 


表头筛选是一种复杂筛选的形式,其最开始是来源于Excel的筛选形式。点击表单的筛选按钮,可以将表头的筛选字段直接带入,方便用户。之后在后台产品的发展中,得以借鉴过来。

 

优点:

可以通过表头的点击,使用户更快捷进入到自己的筛选条件,在通常情况下,在表单越左的数据显然是越重要的,也是使用筛选去筛频率最高的,因此高频的筛选场景基本还是得到满足。


缺点:

用户第一次进入系统很难理解这种交互形式,且在每个表头都会有一个icon,影响用户对于表头的识别。

 

 

4.5、弹窗式



通过点击筛选按钮,展现出筛选弹窗,进行筛选。这种筛选适合在筛选功能在系统中不是很重要的层级。最常见的就是Tapd,在其中筛选不是很强的一个功能,同时也是系统中十分有必要的。

 

优点:

是能够在节省面积的情况下,可以进行很复杂的筛选,同时可以支持复杂情况下的筛选

 

缺点:

弹窗会遮挡一部分表单数据,会影响筛选人的判断,其次筛选条件的添加也相对更加繁琐。

 

 


5、选择更合适的筛选

在我们一系列筛选的调整过后,我们团队也总结了对于我们来说更重要的条件和形式,来和大家分享探讨一下。

 

5.1、使用频率

我们认为影响筛选控件最重要的是用户的使用频率,因为用户的使用频率和使用方式,直接影响到我们筛选是用普通筛选or高级筛选,也会影响到筛选的形式。

 

5.2、满足实际业务所需

筛选功能的做法,取决于我们产品未来是想往哪一个方向发展,如果想把功能做的强大,就得考虑到筛选的后续扩展性。因此满足实际业务也是十分重要。

 

5.3、用户认知成本

在B端系统当中,最可能遇见的就是你给用户设计的路径但是其实用户根本没有往你想的方向去操作。我们系统最开始给用户设计好了很多功能点,但是用户对于这个点的认知成本实在过低,也导致了后面系统功能点很多都被埋没。因为在你设计好了一个功能点后,要适当引导用户,解释这个功能的使用场景才不会让你设计的功能被淹没。

 

 


其实在B端产品中,易用本身就是难且长的过程,在每一个功能的设计都需要你去思考很多方面:用户易用、信息层级、未来扩展,你都要做出取舍,而对于每个模块都需要你思考、结合用户场景,B端web的设计一直都是摸黑前进,我也只是将自己的一段时间的工作进行总结,说的不正确,欢迎大家指正。

 转自:站酷-Cengg 


日历

链接

个人资料

蓝蓝设计的小编 http://www.lanlanwork.com

存档