![HexaFlip: A Flexible 3D Cube Plugin](https://i-blog.csdnimg.cn/blog_migrate/7ac386b30dd9480911de13b15310c329.png)
Today I’d like to share my process for creating a flexible JavaScript UI plugin I’ve dubbed HexaFlip.
今天,我想分享我创建一个被称为HexaFlip的灵活JavaScript UI插件的过程。
Lately I’ve taken to building simple experimental UI plugins that manipulate elements with a combination of JavaScript and modern CSS techniques (e.g. oriDomi and Maskew). As Codrops is known for featuring some progressive techniques in browser-based UI design, HexaFlip should fit in nicely.
最近,我开始构建简单的实验性UI插件,以结合JavaScript和现代CSS技术(例如oriDomi和Maskew )来操纵元素。 由于Codrops以在基于浏览器的UI设计中采用一些渐进技术而闻名,因此HexaFlip应该很合适。
I originally developed a simpler version of HexaFlip for an iPhone app I built called ChainCal where it served as a time-picker interface for setting alarms. Most mobile time-picker widgets are fashioned after a dial, but I reasoned that rotating cubes would serve for a more unique and memorable experience. As we all know, a cube has six (i.e. “hexa”) faces, but when rotating it around a single axis, we only have four to work with (front, top, back, and bottom). Thus if we built a cube interface using CSS alone, our interface would be limited to four options per choice. HexaFlip solves this issue and playfully challenges the user’s expectations by allowing the cube to cycle over a list of any length.
我最初为我构建的名为ChainCal的iPhone应用程序开发了一个更简单的HexaFlip版本,该应用程序用作设置警报的时间选择器界面。 大多数移动式时间选择器小部件都是在拨号后形成的,但我认为旋转立方体将提供更独特和令人难忘的体验。 众所周知,一个立方体有六个(即“六个”)面,但是当绕一个轴旋转时,我们只有四个面(前,顶,后和底)可以使用。 因此,如果仅使用CSS构建多维数据集接口,则每个选择的接口将被限制为四个选项。 HexaFlip解决了这个问题,并通过允许多维数据集在任意长度的列表上循环来挑战用户的期望。
标记 (The Markup)
Since HexaFlip is designed to be used as a plugin, it generates its own markup based on the options given to it. For each demo, we only require a single element:
由于HexaFlip旨在用作插件,因此它会根据提供给它的选项生成自己的标记。 对于每个演示,我们只需要一个元素:
<div id="hexaflip-demo1" class="demo"></div>
The id and class are for convenience, should we want to add specific styles to our instance.
id和class是为了方便起见,如果我们想为实例添加特定的样式。
CSS (The CSS)
(Note: For brevity, the following snippets don’t use any vendor prefixes, though the included source does.)
(注意:为简洁起见,以下代码段不使用任何供应商前缀,尽管包含的源代码也使用了这些前缀。)
Inside every element passed to a HexaFlip instance, HexaFlip automatically builds markup for cubes (classed with hexaflip-cube
) based on the number of option sets supplied to it.
在传递给HexaFlip实例的每个元素中,HexaFlip会根据提供给它的选项集的数量,自动为多维数据集(用hexaflip-cube
分类)构建标记。
.hexaflip-cube {
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
text-rendering: optimizeLegibility;
font-smoothing: antialiased;
cursor: move;
cursor: grab;
display: inline-block;
position: relative;
transform-style: preserve-3d;
transition: transform 0.4s;
}
To prompt the user to manipulate the cube with the mouse, we set a cursor property on it. The reason both move
and grab
are set is because grab
is currently vendor-specific and we need a fallback. display
is set to inline-block
so the cubes sit next to each other and we set position: relative
to allow the JS to later set the z-index
. transform-style
is especially important for the use case of a cube because each of the six faces are independently manipulated in their own 3D space. Without setting preserve-3d
, the cubes would appear flat. Finally we set a transition
property on cubes so their rotations can be tweened.
为了提示用户使用鼠标来操作多维数据集,我们在其上设置了光标属性。 之所以要设置move
和grab
,是因为grab
当前是特定于供应商的,我们需要一个后备。 display
设置为inline-block
因此多维数据集彼此相邻放置,并设置position: relative
以允许JS稍后设置z-index
。 transform-style
对于多维数据集的用例尤为重要,因为六个面中的每个面都在各自的3D空间中被独立操纵。 如果不设置preserve-3d
,则多维数据集将显示为平坦。 最后,我们在多维数据集上设置了transition
属性,以便可以补间它们的旋转。
Next we have a class called no-tween
that is automatically toggled on the cubes by the JS when the user moves over a face with either a mouse button or finger down.
接下来,我们有一个名为no-tween
的类,当用户用鼠标按钮或手指向下移动到脸上时,JS将在多维数据集上自动切换该类。
.hexaflip-cube.no-tween {
transition-duration: 0;
}
This class simply disables tweening in that context so movement is immediate during interaction.
此类仅在该上下文中禁用补间,因此在交互过程中可立即移动。
Each cube’s faces are simply nested divs so we use an immediate child selector (>
) to style them.
每个多维数据集的面都只是嵌套的div,因此我们使用直接子选择器( >
)对其进行样式设置。
.hexaflip-cube > div {
width: 100%;
overflow: hidden;
height: 100.5%;
position: absolute;
user-select: none;
background-size: cover;
text-align: center;
background-color: #333;
color: #fff;
font-weight: 100;
text-shadow: 0 -2px 0 rgba(0,0,0,0.3);
line-height: 1.5;
}
You may notice that the height
is set to 100.5%
. This is done to add some extra “bleed” to the size of each face to mitigate a common issue seen during 3D CSS transforms where edges don’t match up perfectly and “cracks” appear in the object. Each face is given absolute positioning so they stack on top of each other before they’re transformed in 3D space. We also give them user-select: none
so the text of the face isn’t accidentally selected when dragging the mouse to rotate the cube. background-size: cover
is applied so that if the user wants to display images on the cube, they fill the entire face without distorting either dimension. The rest of the CSS properties are simply stylistic and can be overridden if you’d like to customize your HexaFlip instance.
您可能会注意到height
设置为100.5%
。 这样做是为了在每个面Kong的大小上添加一些额外的“出血”,以缓解3D CSS变换中出现的常见问题,即边缘不完全匹配并且“裂缝”出现在对象中。 每个面都经过绝对定位,因此在3D空间中进行变换之前,它们彼此堆叠。 我们还为它们提供了user-select: none
因此拖动鼠标旋转立方体时不会意外选择面部文本。 background-size: cover
应用background-size: cover
,以便如果用户希望在立方体上显示图像,则它们会填满整个面部而不会扭曲任何一个尺寸。 其余CSS属性只是样式,如果您想自定义HexaFlip实例,可以将其覆盖。
These classes refer to the side faces that aren’t displayed directly toward the user. They’re given a gray color by default:
这些类是指未直接向用户显示的侧面。 默认情况下,它们的颜色为灰色:
.hexaflip-left, .hexaflip-right {
background-color: #555 !important;
}
True to its roots, HexaFlip can still be used as a time-picker and when used in this mode, a specific class is applied. This final CSS definition simply colors alternating faces red (as they originally appeared in ChainCal) using the :nth-child(odd)
pseudo-class.
从本质上讲,HexaFlip仍然可以用作时间选择器,并且在此模式下使用时,将应用特定的类。 这个最终CSS定义只是使用:nth-child(odd)
伪类将交替的面Kong着色为红色(最初出现在ChainCal中)。
.hexaflip-timepicker .hexaflip-cube:last-child > div:nth-child(odd) {
background-color: #ff575b;
}
JavaScript(The JavaScript)
(Note: HexaFlip was originally written in CoffeeScript and the original source is included in the download. For the purposes of this article I’ll be walking through the generated JavaScript.)
(注意:HexaFlip最初是用CoffeeScript编写的,下载时包括了原始资源。出于本文的目的,我将逐步介绍所生成JavaScript。)
We start by defining an immediately invoked function to create a new scope context so that we don’t touch the global environment. This is especially good practice when building a plugin designed for integration into other developers’ projects.
我们首先定义一个立即调用的函数来创建新的作用域上下文,以免影响全局环境。 在构建旨在集成到其他开发人员项目中的插件时,这是特别好的做法。
(function() {
//...
}).call(this);
After defining some variables, we must tackle the issue of detecting CSS feature support and which vendor prefix to use for properties. The following section defines a function to cycle through the major vendor prefixes to find a compatible match. If no match is found, the function returns false
to denote the browser’s lack of support for that property.
定义一些变量之后,我们必须解决检测CSS功能支持以及用于属性的供应商前缀的问题。 以下部分定义了一个功能,用于循环浏览主要的供应商前缀,以找到兼容的匹配项。 如果未找到匹配项,则该函数返回false
,表示浏览器对该属性缺乏支持。
prefixList = ['webkit', 'Moz', 'O', 'ms'];
prefixProp = function(prop) {
var prefix, prefixed, _i, _len;
if (document.body.style[prop.toLowerCase()] != null) {
return prop.toLowerCase();
}
for (_i = 0, _len = prefixList.length; _i < _len; _i++) {
prefix = prefixList[_i];
prefixed = prefix + prop;
if (document.body.style[prefixed] != null) {
return prefixed;
}
}
return false;
For our purposes we need to test two specific CSS3 properties (transform and perspective) and store them in an object literal simply called css
:
出于我们的目的,我们需要测试两个特定CSS3属性(转换和透视),并将它们存储在对象文字中,简称为css
:
css = {};
_ref = ['Transform', 'Perspective'];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
prop = _ref[_i];
css[prop.toLowerCase()] = prefixProp(prop);
}
Next we have a set of default options for new HexaFlip instances. When a user doesn't supply a specific option when creating a new HexaFlip instance, the default value in this object is used:
接下来,我们为新的HexaFlip实例提供了一组默认选项。 当用户在创建新的HexaFlip实例时不提供特定选项时,将使用此对象中的默认值:
defaults = {
size: 280,
margin: 10,
fontSize: 185,
perspective: 1000,
touchSensitivity: 1
};
Additionally we define some static properties that apply to all instances of HexaFlip:
另外,我们定义了一些静态属性,这些属性适用于HexaFlip的所有实例:
cssClass = baseName.toLowerCase();
faceNames = ['front', 'bottom', 'back', 'top', 'left', 'right'];
faceSequence = faceNames.slice(0, 4);
urlRx = /^((((https?)|(file)):)?//)|(data:)|(..?/)/i;
urlRx
is a simple regular expression that we'll later use to test if a value is a URL. If the string begins with http
, https
, file
, data:
, //
, ./
, or ../
, HexaFlip assumes it's a URL and loads its image on the cube face.
urlRx
是一个简单的正则表达式,稍后我们将使用它来测试值是否为URL。 如果字符串以http
, https
, file
, data:
, //
,. ./
或../
开头,则HexaFlip假定它是一个URL并将其图像加载到多维数据集表面上。
Once we have that bootstrapping out of the way, we can define HexaFlip's behavior via its constructor and prototype. If you're not familiar with this pattern, we're going to create a function that builds every instance of HexaFlip. By attaching properties to this function's prototype object, we define pieces of functionality for every instance that's created in the future. Underscore-prefixed properties denote that the property is only meant to be used internally by HexaFlip, and the user typically shouldn't need to access them.
一旦我们完成了自举,就可以通过其构造函数和原型定义HexaFlip的行为。 如果您不熟悉这种模式,我们将创建一个函数来构建HexaFlip的每个实例。 通过将属性附加到该函数的原型对象,我们为将来创建的每个实例定义了一些功能。 下划线前缀的属性表示该属性只能由HexaFlip在内部使用,并且用户通常不需要访问它们。
The constructor itself accepts three arguments: the target DOM element, an object literal containing sets to display, and an optional object literal of customizations to override the defaults
map. These arguments are attached to the instance itself (this
):
构造函数本身接受三个参数:目标DOM元素,包含要显示的集合的对象常量以及用于覆盖defaults
映射的自定义的可选对象常量。 这些参数附加到实例本身( this
):
function HexaFlip(el, sets, options) {
var cube, cubeFragment, i, image, key, midPoint, option, set, setsKeys, setsLength, val, value, z, _j, _len1, _ref1, _ref2;
this.el = el;
this.sets = sets;
this.options = options != null ? options : {};
For every key in sets
, a new cube will be created. The value of each key should be an array of values to be displayed on the cube faces.
对于sets
每个键,将创建一个新的多维数据集。 每个键的值应该是要在多维数据集面上显示的值的数组。
Before continuing, the constructor checks to see if CSS transforms are supported and if an element was passed. If either test fails, the constructor immediately returns.
在继续之前,构造函数将检查是否支持CSS转换以及是否传递了元素。 如果任一测试失败,则构造函数立即返回。
if (!(css.transform && this.el)) {
return;
}
This block fills in any missing options with the defaults we defined earlier:
此块使用我们先前定义的默认值来填充所有缺少的选项:
for (option in defaults) {
value = defaults[option];
this[option] = (_ref1 = this.options[option]) != null ? _ref1 : defaults[option];
}
if (typeof this.fontSize === 'number') {
this.fontSize += 'px';
}
If the user doesn't pass any sets, we will continue with setting up this instance as a time-picker. The following block contains some simple loops that populate the sets with hours and minutes in intervals of ten:
如果用户未通过任何设置,我们将继续将此实例设置为时间选择器。 以下块包含一些简单的循环,这些循环以小时和分钟(以十为间隔)填充集合:
if (!this.sets) {
this.el.classList.add(cssClass + '-timepicker');
this.sets = {
hour: (function() {
var _j, _results;
_results = [];
for (i = _j = 1; _j <= 12; i = ++_j) {
_results.push(i + '');
}
return _results;
})(),
minute: (function() {
var _j, _results;
_results = [];
for (i = _j = 0; _j <= 5; i = ++_j) {
_results.push(i + '0');
}
return _results;
})(),
meridian: ['am', 'pm']
};
}
Next, we loop over the sets and perform a number of operations. For the primary task, we create an object for each cube using _createCube
(which will be explained momentarily) and append their elements to a document fragment. Using the total number of given sets, we give the cube elements a sequence of z indexes so they stack properly. By splitting the sets length in half, we increment the indexes for the first half of the set and decrement them passing the midpoint. Due to 3D perspective, the view of the cubes is only straight ahead toward the center of the container element, and towards the edges, the cubes' sides are visible. If we didn't perform this stacking manipulation, the cubes in the latter half of the sets would sit on top of each other incorrectly and the illusion would be thrown off.
接下来,我们遍历集合并执行许多操作。 对于主要任务,我们使用_createCube
(将在稍后说明)为每个多维数据集创建一个对象,并将其元素附加到文档片段中。 使用给定集合的总数,我们给多维数据集元素一个z索引序列,以便它们正确堆叠。 通过将集合长度分成两半,我们可以增加集合前半部分的索引,并减少通过中点的索引。 由于3D透视图,立方体的视图仅朝着容器元件的中心正前方,而朝向边缘,则立方体的侧面可见。 如果我们不执行此堆叠操作,则集合后半部分中的多维数据集将错误地彼此叠置,并且会抛出幻觉。
setsKeys = Object.keys(this.sets);
setsLength = setsKeys.length;
cubeFragment = document.createDocumentFragment();
i = z = 0;
midPoint = setsLength / 2 + 1;
this.cubes = {};
_ref2 = this.sets;
for (key in _ref2) {
set = _ref2[key];
cube = this.cubes[key] = this._createCube(key);
if (++i < midPoint) {
z++;
} else {
z--;
}
cube.el.style.zIndex = z;
this._setContent(cube.front, set[0]);
cubeFragment.appendChild(cube.el);
for (_j = 0, _len1 = set.length; _j < _len1; _j++) {
val = set[_j];
if (urlRx.test(val)) {
image = new Image;
image.src = val;
}
}
}
In the conclusion of that loop you'll notice another loop that iterates over the values in a set and uses the regular expression defined earlier to check for URLs. If there's a match, we can assume the user has passed an image and we construct a new image object in memory to force the browser to preload it. The goal is that images aren't loaded on demand when the end user spins the cube as that would degrade the experience.
在该循环的结尾,您会注意到另一个循环,该循环遍历集合中的值并使用前面定义的正则表达式检查URL。 如果存在匹配项,则可以假定用户已传递图像,并在内存中构造了一个新的图像对象以强制浏览器对其进行预加载。 目的是当最终用户旋转多维数据集时不会按需加载图像,因为这会降低体验。
The constructor concludes its work by setting a correct height, width, and perspective for the container element and appends the cubes.
构造函数通过为容器元素设置正确的高度,宽度和透视图来结束其工作,并附加多维数据集。
this.cubes[setsKeys[0]].el.style.marginLeft = '0';
this.cubes[setsKeys[setsKeys.length - 1]].el.style.marginRight = '0';
this.el.classList.add(cssClass);
this.el.style.height = this.size + 'px';
this.el.style.width = ((this.size + this.margin * 2) * setsLength) - this.margin * 2 + 'px';
this.el.style[css.perspective] = this.perspective + 'px';
this.el.appendChild(cubeFragment);
Next, let's look at our first method, previously used in the constructor:
接下来,让我们看一下先前在构造函数中使用的第一个方法:
HexaFlip.prototype._createCube = function(set) {
var cube, eString, eventPair, eventPairs, rotate3d, side, _fn, _j, _k, _l, _len1, _len2, _len3,
_this = this;
cube = {
set: set,
offset: 0,
y1: 0,
yDelta: 0,
yLast: 0,
el: document.createElement('div')
};
cube.el.className = "" + cssClass + "-cube " + cssClass + "-cube-" + set;
cube.el.style.margin = "0 " + this.margin + "px";
cube.el.style.width = cube.el.style.height = this.size + 'px';
cube.el.style[css.transform] = this._getTransform(0);
Each cube is just an object literal that holds properties including a DOM element. To create a three dimensional cube from six flat divs, we loop through the faces and apply a specific style setting for rotational axis and angle based on the face name:
每个多维数据集只是一个对象文字,其中包含DOM元素的属性。 要从六个平面div创建三维立方体,我们遍历各个面,并根据面名称为旋转轴和角度应用特定的样式设置:
for (_j = 0, _len1 = faceNames.length; _j < _len1; _j++) {
side = faceNames[_j];
cube[side] = document.createElement('div');
cube[side].className = cssClass + '-' + side;
rotate3d = (function() {
switch (side) {
case 'front':
return '0, 0, 0, 0deg';
case 'back':
return '1, 0, 0, 180deg';
case 'top':
return '1, 0, 0, 90deg';
case 'bottom':
return '1, 0, 0, -90deg';
case 'left':
return '0, 1, 0, -90deg';
case 'right':
return '0, 1, 0, 90deg';
}
})();
cube[side].style[css.transform] = "rotate3d(" + rotate3d + ") translate3d(0, 0, " + (this.size / 2) + "px)";
cube[side].style.fontSize = this.fontSize;
cube.el.appendChild(cube[side]);
}
Finally, _createCube
attaches event listeners for both, mouse and touch interaction and returns the cube object:
最后, _createCube
为鼠标和触摸交互附加事件侦听器,并返回多维数据集对象:
eventPairs = [['TouchStart', 'MouseDown'], ['TouchMove', 'MouseMove'], ['TouchEnd', 'MouseUp'], ['TouchLeave', 'MouseLeave']];
mouseLeaveSupport = 'onmouseleave' in window;
for (_k = 0, _len2 = eventPairs.length; _k < _len2; _k++) {
eventPair = eventPairs[_k];
_fn = function(fn, cube) {
if (!((eString === 'TouchLeave' || eString === 'MouseLeave') && !mouseLeaveSupport)) {
return cube.el.addEventListener(eString.toLowerCase(), (function(e) {
return _this[fn](e, cube);
}), true);
} else {
return cube.el.addEventListener('mouseout', (function(e) {
return _this._onMouseOut(e, cube);
}), true);
}
};
for (_l = 0, _len3 = eventPair.length; _l < _len3; _l++) {
eString = eventPair[_l];
_fn('_on' + eventPair[0], cube);
}
}
this._setSides(cube);
return cube;
};
The next method is relied on by a few other methods and performs some simple string concatenation for creating CSS transform values:
其他一些方法还依赖next方法,并执行一些简单的字符串连接来创建CSS转换值:
HexaFlip.prototype._getTransform = function(deg) {
return "translateZ(-" + (this.size / 2) + "px) rotateX(" + deg + "deg)";
};
The reason we set a negative Z value for the translate operation is because the front face is extended toward the user in 3D space and its texture would appear somewhat blurry otherwise.
我们为平移操作设置负Z值的原因是因为正面在3D空间中朝向用户延伸,否则其纹理会显得有些模糊。
Next we have _setContent
which accepts a cube face element and a value to display on the face:
接下来,我们有了_setContent
,它接受一个多维数据集face元素和一个要在该face上显示的值:
HexaFlip.prototype._setContent = function(el, content) {
var key, style, val, value;
if (!(el && content)) {
return;
}
if (typeof content === 'object') {
style = content.style, value = content.value;
for (key in style) {
val = style[key];
el.style[key] = val;
}
} else {
value = content;
}
if (urlRx.test(value)) {
el.innerHTML = '';
return el.style.backgroundImage = "url(" + value + ")";
} else {
return el.innerHTML = value;
}
};
For the sake of flexibility, HexaFlip allows the user to pass objects within set arrays so any value can have a specific styling. When a value is an object (rather than a string or number), we loop through the style
property's pairs of CSS keys and values and apply them to the face. This means you could supply a set like this
为了灵活起见,HexaFlip允许用户在集合数组中传递对象,因此任何值都可以具有特定的样式。 当值是一个对象(而不是字符串或数字)时,我们遍历style
属性CSS键和值对,并将它们应用于面部。 这意味着您可以提供这样的一套
[
'hello',
{
value: 'i am green',
style: {
backgroundColor: '#00ff00'
}
}
]
where the first element (displaying "hello") would have default styling, but the second would always appear with a green background.
第一个元素(显示“ hello”)将具有默认样式,但第二个元素将始终以绿色背景显示。
Finally _setContent
checks for a URL value and sets the background image accordingly.
最后, _setContent
检查URL值并相应地设置背景图像。
_setSides
is the most important method in HexaFlip as it maps the values in a set to the four active faces of a cube:
_setSides
是_setSides
中最重要的方法,因为它会将集合中的值映射到多维数据集的四个活动面:
HexaFlip.prototype._setSides = function(cube) {
var bottomAdj, faceOffset, offset, set, setLength, setOffset, topAdj;
cube.el.style[css.transform] = this._getTransform(cube.yDelta);
cube.offset = offset = Math.floor(cube.yDelta / 90);
if (offset === cube.lastOffset) {
return;
}
cube.lastOffset = faceOffset = setOffset = offset;
set = this.sets[cube.set];
setLength = set.length;
if (offset < 0) {
faceOffset = setOffset = ++offset;
if (offset < 0) {
if (-offset > setLength) {
setOffset = setLength - -offset % setLength;
if (setOffset === setLength) {
setOffset = 0;
}
} else {
setOffset = setLength + offset;
}
if (-offset > 4) {
faceOffset = 4 - -offset % 4;
if (faceOffset === 4) {
faceOffset = 0;
}
} else {
faceOffset = 4 + offset;
}
}
}
if (setOffset >= setLength) {
setOffset %= setLength;
}
if (faceOffset >= 4) {
faceOffset %= 4;
}
topAdj = faceOffset - 1;
bottomAdj = faceOffset + 1;
if (topAdj === -1) {
topAdj = 3;
}
if (bottomAdj === 4) {
bottomAdj = 0;
}
this._setContent(cube[faceSequence[topAdj]], set[setOffset - 1] || set[setLength - 1]);
return this._setContent(cube[faceSequence[bottomAdj]], set[setOffset + 1] || set[0]);
};
In a nutshell, this method calculates the number of times a cube has been rotated from its initial state (zero degrees) and transposes that number to a position in that cube's array. This method handles a number of cases such as if the number of values in the set exceeds the number of faces, if the number of rotations exceeds the length of the set, and backwards rotations as well. The key to the illusion of showing more than four values as the user rotates lies in deriving the current topAdj
and bottomAdj
or top and bottom adjacent sides. These sides are relative to the side currently facing the user and since they aren't visible at the moment they sit at the top and bottom, their content can be immediately swapped without tipping off the user to our trick. With this strategy in mind, our code can make sure the adjacent top and bottom sides will always be the previous and next values in the array.
简而言之,此方法计算多维数据集从其初始状态(零度)开始旋转的次数,然后将该数字转置到该多维数据集数组中的某个位置。 此方法处理多种情况,例如,如果集合中的值数超过了面数,旋转数超过了集合的长度,还向后旋转。 在用户旋转时显示四个以上值的错觉的关键在于推导当前的topAdj
和bottomAdj
或顶部和底部相邻边。 这些侧面是相对于当前面向用户的侧面而言的,由于它们位于顶部和底部时不可见,因此可以立即交换其内容,而不会导致用户陷入困境。 考虑到这种策略,我们的代码可以确保相邻的顶部和底部始终是数组中的上一个和下一个值。
Next, we have a collection of methods that handle mouse and touch interaction. _onTouchStart
is called during a click (or touch) and sets a property (touchStarted
) on the target cube to note that the mouse button is currently active. The method then immediately disables the tweening provided by CSS transitions by adding the .no-tween
class. This is done so the cube rotates fluidly with mouse movement in our next method. Finally, the starting position of the mouse (or touch) is recorded so we can later calculate how far it has moved:
接下来,我们有处理鼠标和触摸交互的方法的集合。 _onTouchStart
在单击(或触摸)期间被调用,并在目标多维数据集上设置属性( touchStarted
)以注意鼠标按钮当前处于活动状态。 然后,该方法通过添加.no-tween
类,立即禁用CSS过渡提供的补.no-tween
。 这样做是为了使立方体在我们的下一个方法中随着鼠标移动而流畅地旋转。 最后,记录了鼠标(或触摸)的起始位置,以便我们以后可以计算其移动的距离:
HexaFlip.prototype._onTouchStart = function(e, cube) {
e.preventDefault();
cube.touchStarted = true;
e.currentTarget.classList.add('no-tween');
if (e.type === 'mousedown') {
return cube.y1 = e.pageY;
} else {
return cube.y1 = e.touches[0].pageY;
}
};
Movement immediately followed by the click is handled by _onTouchMove
:
紧随点击之后的移动由_onTouchMove
处理:
HexaFlip.prototype._onTouchMove = function(e, cube) {
if (!cube.touchStarted) {
return;
}
e.preventDefault();
cube.diff = (e.pageY - cube.y1) * this.touchSensitivity;
cube.yDelta = cube.yLast - cube.diff;
return this._setSides(cube);
};
This method is called many times in succession as the user rotates the cube and constantly calculates the distance in pixels moved since we originally recorded y1
in the last method. The cube's yDelta
is the current distance travelled plus all previous rotations in the past. By calling _setSides
, the cube's faces update and display the correct cycle of values. _setSides
also applies the total yDelta
to the cube's DOM element's transform style and the result is that the cube appears to rotate analogous to the user's movements.
自从我们在最后一种方法中最初记录y1
以来,随着用户旋转立方体并不断计算以像素为单位的移动距离,此方法被连续多次调用。 多维数据集的yDelta
是当前行进的距离加上过去所有以前的旋转。 通过调用_setSides
,多维数据集的面将更新并显示正确的值循环。 _setSides
还将总yDelta
应用于多维数据集的DOM元素的变换样式,结果是该多维数据集看起来类似于用户的移动而旋转。
When the mouse button is released, _onTouchEnd
is called:
释放鼠标按钮时,将调用_onTouchEnd
:
HexaFlip.prototype._onTouchEnd = function(e, cube) {
var mod;
cube.touchStarted = false;
mod = cube.yDelta % 90;
if (mod < 45) {
cube.yLast = cube.yDelta + mod;
} else {
if (cube.yDelta > 0) {
cube.yLast = cube.yDelta + mod;
} else {
cube.yLast = cube.yDelta - (90 - mod);
}
}
if (cube.yLast % 90 !== 0) {
cube.yLast -= cube.yLast % 90;
}
cube.el.classList.remove('no-tween');
return cube.el.style[css.transform] = this._getTransform(cube.yLast);
};
In most cases, the user will release the mouse button while the cube is somewhat askew (at an angle that isn't a multiple of ninety). Rather than leaving the cube rotated in a haphazard way, this method calculates the remainder between the current rotation and ninety, and finds the nearest clean value. Before applying this rotation transform, the no-tween
class is removed and the result is a smooth snapping behavior, where the cubes always drift back into a proper position.
在大多数情况下,当多维数据集有些歪斜(角度不是90的倍数)时,用户将释放鼠标按钮。 该方法不是以任意方式旋转多维数据集,而是计算当前旋转和90之间的余数,并找到最接近的净值。 在应用此旋转变换之前,先删除no-tween
类,然后得到平滑的捕捉行为,在该行为中,多维数据集总是漂移回到适当的位置。
Finally we have two simple interaction-related methods which handle when the user's mouse/finger leave the cube. The latter is a necessary polyfill for browsers that don't support the mouseleave
event:
最后,我们有两个简单的与交互相关的方法,它们处理用户的鼠标/手指何时离开立方体。 对于不支持mouseleave
事件的浏览器,后者是必需的mouseleave
:
HexaFlip.prototype._onTouchLeave = function(e, cube) {
if (!cube.touchStarted) {
return;
}
return this._onTouchEnd(e, cube);
};
HexaFlip.prototype._onMouseOut = function(e, cube) {
if (!cube.touchStarted) {
return;
}
if (e.toElement && !cube.el.contains(e.toElement)) {
return this._onTouchEnd(e, cube);
}
};
Next, we have two methods designed for use by other developers. The utility of our cube interfaces would be quite limited if the values of their current positions couldn't be read or manipulated externally. To programmatically change which faces are displayed on the cubes, we have a method called setValue
that accepts an object literal with a key for every cube set, with a corresponding value to display:
接下来,我们有两种方法供其他开发人员使用。 如果无法从外部读取或操纵其当前位置的值,则我们的多维数据集接口的实用程序将受到很大限制。 为了以编程方式更改在多维数据集上显示哪些面,我们有一个名为setValue
的方法,该方法接受带有每个多维数据集键的对象文字,并带有要显示的对应值:
HexaFlip.prototype.setValue = function(settings) {
var cube, index, key, value, _results;
_results = [];
for (key in settings) {
value = settings[key];
if (!(this.sets[key] && !this.cubes[key].touchStarted)) {
continue;
}
value = value.toString();
cube = this.cubes[key];
index = this.sets[key].indexOf(value);
cube.yDelta = cube.yLast = 90 * index;
this._setSides(cube);
_results.push(this._setContent(cube[faceSequence[index % 4]], value));
}
return _results;
};
The logic is simple: we get the value's position in the array with indexOf
and rotate the cube ninety degrees for every offset from zero.
逻辑很简单:我们使用indexOf
获取值在数组中的位置,并针对每个从零开始的偏移将多维数据集旋转90度。
getValue
performs the opposite task and retrieves the current values of the cubes. While it may be obvious to the user via simply looking, external code needs a way of knowing which cube faces and corresponding values are facing the user:
getValue
执行相反的任务,并检索多维数据集的当前值。 通过简单的查找对于用户来说可能很明显,但是外部代码需要一种方法来知道哪些多维数据集面和相应的值面向用户:
HexaFlip.prototype.getValue = function() {
var cube, offset, set, setLength, _ref1, _results;
_ref1 = this.cubes;
_results = [];
for (set in _ref1) {
cube = _ref1[set];
set = this.sets[set];
setLength = set.length;
offset = cube.yLast / 90;
if (offset < 0) {
if (-offset > setLength) {
offset = setLength - -offset % setLength;
if (offset === setLength) {
offset = 0;
}
} else {
offset = setLength + offset;
}
}
if (offset >= setLength) {
offset %= setLength;
}
if (typeof set[offset] === 'object') {
_results.push(set[offset].value);
} else {
_results.push(set[offset]);
}
}
return _results;
};
Above, we loop through the cubes and determine what position in the array they are each showing based on their count of ninety degree rotations. The result is an array with a value for each cube.
在上方,我们遍历多维数据集,并根据其90度旋转次数确定它们在数组中显示的位置。 结果是一个数组,其中每个多维数据集都有一个值。
Finally, we have two convenience methods, flip
and flipBack
. These methods advance all of the cubes forward or backwards by one ninety degree rotation, respectively. While this behavior is entirely possible by using setValue
, it would be tedious as the developer would have to get the current values with getValue
and then refer to the original set arrays and determine their successive values. flip
accepts an argument that reverses the rotation so flipBack
simply piggybacks off its functionality.
最后,我们有两种便捷的方法, flip
和flipBack
。 这些方法分别使所有立方体向前或向后旋转90度。 尽管通过使用setValue
可以完全实现这种行为,但是这很setValue
,因为开发人员必须使用getValue
获取当前值,然后引用原始set数组并确定其连续值。 flip
接受一个可反转旋转的参数,因此flipBack
只是背负了其功能。
HexaFlip.prototype.flip = function(back) {
var cube, delta, set, _ref1, _results;
delta = back ? -90 : 90;
_ref1 = this.cubes;
_results = [];
for (set in _ref1) {
cube = _ref1[set];
if (cube.touchStarted) {
continue;
}
cube.yDelta = cube.yLast += delta;
_results.push(this._setSides(cube));
}
return _results;
};
HexaFlip.prototype.flipBack = function() {
return this.flip(true);
};
As with setValue
, we'll ignore any cube if the user is currently manipulating it.
与setValue
,如果用户当前正在操作它,我们将忽略任何立方体。
That's it! Hopefully you've gained some insight into the thought process and best practices regarding flexible UI plugins.
而已! 希望您对有关灵活的UI插件的思考过程和最佳实践有所了解。
演示版 (Demos)
Time Picker: Drag the cubes or use the select menus to set a time.
Image Cycle: Notice that the number of images exceeds the four cube faces.
Visual Password Experiment: The password is “red yellow blue green.” See if you can get it.
The third demo features illustrations by Jason Custer.
第三个演示包含Jason Custer的插图。
If you find any bugs or have some improvements to contribute, submit them to the GitHub repository.
如果您发现任何错误或需要做出一些改进,请将其提交到GitHub存储库。
翻译自: https://tympanus.net/codrops/2013/03/07/hexaflip-a-flexible-3d-cube-plugin/