oriDomi.coffee | |
---|---|
oriDomiby Dan MotzenbeckerFold up the DOM like paper. | |
| |
Copyright 2012, MIT License | |
Setup | |
Enable strict mode. | 'use strict' |
Set a reference to the global object within this scope. | root = window |
An array to hold references to oriDomi instances so they can be easily freed
from memory via the | instances = [] |
Set a reference to jQuery (or another | $ = root.$ or false |
| devMode = false |
This variable is set to true and negated later if the browser does not support oriDomi. | oriDomiSupport = true |
Create a div for testing CSS3 properties. | testEl = document.createElement 'div' |
Set a list of browser prefixes for testing CSS3 properties. | prefixList = ['Webkit', 'Moz', 'O', 'ms', 'Khtml'] |
A map of the CSS3 properties needed to support oriDomi, with shorthand names as keys. | css =
transform: 'transform'
origin: 'transformOrigin'
transformStyle: 'transformStyle'
transitionProp: 'transitionProperty'
transitionDuration: 'transitionDuration'
transitionEasing: 'transitionTimingFunction'
perspective: 'perspective'
backface: 'backfaceVisibility' |
This function checks for the presence of CSS properties on the test div. | testProp = (prop) -> |
If the un-prefixed property is present, return it. | return prop if testEl.style[prop]? |
Capitalize the property name for camel-casing. | capProp = prop.charAt(0).toUpperCase() + prop.slice 1 |
Loop through the vendor prefix list and return when we find a match. | for prefix in prefixList
if testEl.style[prefix + capProp]?
return prefix + capProp |
If no matches are found, return false to denote that the browser is missing this property. | false |
Loop through the CSS hash and replace each value with the result of | for key, value of css
css[key] = testProp value |
If the returned value is false, warn the user that the browser doesn't support
oriDomi, set | unless css[key]
console.warn 'oriDomi: Browser does not support oriDomi' if devMode
oriDomiSupport = false
break |
CSS3 gradients are used for shading. Testing for them is different because they are prefixed values, not properties. This invokes an anonymous function to loop through vendor-prefixed linear-gradients. | css.gradientProp = do ->
for prefix in prefixList
hyphenated = "-#{ prefix.toLowerCase() }-linear-gradient"
testEl.style.backgroundImage = "#{ hyphenated }(left, #000, #fff)" |
After setting a gradient background on the test div, attempt to retrieve it. | unless testEl.style.backgroundImage.indexOf('gradient') is -1
return hyphenated |
If none of the hyphenated values worked, return the un-prefixed version. | 'linear-gradient' |
Invoke a functional scope to set a hyphenated version of the transform property. | css.transformProp = do -> |
Use a regex to pluck the prefix | prefix = css.transform.match /(\w+)Transform/i
if prefix
"-#{ prefix[1].toLowerCase() }-transform"
else
'transform' |
Set a | css.transitionEnd = do ->
switch css.transitionProp
when 'transitionProperty'
'transitionEnd'
when 'WebkitTransitionProperty'
'webkitTransitionEnd'
when 'MozTransitionProperty'
'transitionend'
when 'OTransitionProperty'
'oTransitionEnd'
when 'MSTransitionProperty'
'msTransitionEnd' |
This function is used to extend option object literals with a set of defaults. It is simple and one dimensional. | extendObj = (target, source) -> |
Check if the extension object is an object literal by casting it and comparing it. | if source isnt Object source
console.warn 'oriDomi: Must pass an object to extend with' if devMode |
Return the original target if its source isn't valid. | return target |
If the target isn't an object, set it to an empty object literal. | if target isnt Object target
target = {} |
Loop through the extension object and copy its values to the target if they don't exist. | for prop of source
if not target[prop]?
target[prop] = source[prop] |
Return the extended target object. | target |
Defaults | |
Empty function to be used as placeholder for callback defaults (instead of creating separate empty functions). | noOp = -> |
Map of oriDomi instance defaults. | defaults = |
The number of vertical panels (for folding left or right). | vPanels: 3 |
The number of horizontal panels (for folding top or bottom). | hPanels: 3 |
The determines the distance in pixels (z axis) of the camera/viewer to the paper. The smaller the value, the more distorted and exaggerated the effects will appear. | perspective: 1000 |
The default shading style is hard, which shows distinct creases in the paper.
Other options include | shading: 'hard' |
Determines the duration of all animations in milliseconds. | speed: 700 |
This CSS class is applied to elements that oriDomi has been invoked so they can be easily targeted later if needed. | oriDomiClass: 'oridomi' |
This is a multiplier that determines the darkness of shading. If you need subtler shading, set this to a value below 1. | shadingIntensity: 1 |
This option allows you to supply the name of a custom easing method defined in one
of your stylesheets. It defaults to a blank string which is interpreted as | easingMethod: '' |
To prevent a possible "flash of unstyled content" you can hide your target elements
and pass this setting as | showOnStart: false |
Currently, Firefox doesn't handle edge anti-aliasing well and oriDomi edges look jagged. This setting forces Firefox to smooth edges, but usually results in poor performance, so it's not recommended for animation-heavy use of oriDomi until Firefox's transform performance improves. | forceAntialiasing: false |
Allow the user to fold the target by dragging a finger or the mouse. | touchEnabled: true |
Coefficient of touch/drag action's distance delta. Higher numbers cause more movement. | touchSensitivity: .25 |
Custom callbacks for touch/drag events. Each one is invoked with a relevant value so they can be used to manipulate objects outside of the oriDomi instance (e.g. sliding panels). x values are returned when folding left and right, y values for top and bottom. These are empty functions by default. Invoked with starting coordinate as first argument. | touchStartCallback: noOp |
Invoked with current movement distance. | touchMoveCallback: noOp |
Inkoked with ending point. | touchEndCallback: noOp |
oriDomi Class | class OriDomi |
The constructor takes two arguments: a target element and an options object literal. | constructor: (@el, options) -> |
If | console.time 'oridomiConstruction' if devMode |
If the browser doesn't support oriDomi, return the element unmodified. | return @el unless oriDomiSupport |
If the constructor wasn't called with the | unless @ instanceof OriDomi
return new oriDomi @el, @settings |
Extend any passed options with the defaults map. | @settings = extendObj options, defaults |
Return if the first argument isn't a DOM element. | if not @el or @el.nodeType isnt 1
console.warn 'oriDomi: First argument must be a DOM element' if devMode
return |
Clone the target element and save a copy of it. | @cleanEl = @el.cloneNode true
@cleanEl.style.margin = '0'
@cleanEl.style.position = 'absolute' |
A much faster version of | @cleanEl.style[css.transform] = 'translate3d(-9999px, 0, 0)' |
Destructure some instance variables from the settings object. | {@shading, @shadingIntensity, @vPanels, @hPanels} = @settings |
Record the current global styling of the target element. | @_elStyle = root.getComputedStyle @el |
Save the original CSS display of the target. If | @displayStyle = @_elStyle.display
@displayStyle = 'block' if @displayStyle is 'none' |
To calculate the full dimensions of the element, create arrays of relevant metric keys. | xMetrics = ['width', 'paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth']
yMetrics = ['height', 'paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth'] |
Add up values for width and height using | @width = 0
@height = 0
@width += @_getMetric metric for metric in xMetrics
@height += @_getMetric metric for metric in yMetrics |
Calculate the panel width and panel height by dividing the total width and height by the requested number of panels in each axis. | @panelWidth = @width / @vPanels
@panelHeight = @height / @hPanels |
Set our current fold angle at | @lastAngle = 0
@isFoldedUp = false |
| @isFrozen = false |
Set an array of anchor names. | @anchors = ['left', 'right', 'top', 'bottom'] |
oriDomi starts oriented with the left anchor. | @lastAnchor = @anchors[0] |
Create object literals to store panels and stages. | @panels = {}
@stages = {} |
Create a stage div to serve as a prototype. | stage = document.createElement 'div' |
The stage should occupy the full width and height of the target element. | stage.style.width = @width + 'px'
stage.style.height = @height + 'px' |
By default, each stage is hidden and absolutely positioned so they stack on top of each other. | stage.style.display = 'none'
stage.style.position = 'absolute' |
Eliminate padding and margins since the stage is already the full width and height. | stage.style.padding = '0'
stage.style.margin = '0' |
Apply 3D perspective and preserve any parent perspective. | stage.style[css.perspective] = @settings.perspective + 'px'
stage.style[css.transformStyle] = 'preserve-3d' |
Loop through the anchors list and create a stage and empty panel set for each. | for anchor in @anchors
@panels[anchor] = []
@stages[anchor] = stage.cloneNode false
@stages[anchor].className = 'oridomi-stage-' + anchor |
If shading is enabled, create an object literal to hold shaders. | if @shading
@shaders = {} |
Loop through each anchor and create a nested object literal. For the left and right anchors, create arrays to hold the left and right shader for each panel. Do the same for top and bottom. | for anchor in @anchors
@shaders[anchor] = {}
if anchor is 'left' or anchor is 'right'
@shaders[anchor].left = []
@shaders[anchor].right = []
else
@shaders[anchor].top = []
@shaders[anchor].bottom = [] |
Create a shader div prototype to clone. | shader = document.createElement 'div'
shader.style[css.transitionProp] = 'opacity'
shader.style[css.transitionDuration] = @settings.speed + 'ms'
shader.style[css.transitionEasing] = @settings.easingMethod
shader.style.position = 'absolute'
shader.style.width = '100%'
shader.style.height = '100%'
shader.style.opacity = '0'
shader.style.top = '0'
shader.style.left = '0'
shader.style.pointerEvents = 'none' |
The content holder is a clone of the target element. Every panel will contain one. | contentHolder = @el.cloneNode true
contentHolder.classList.add 'oridomi-content'
contentHolder.style.margin = '0'
contentHolder.style.position = 'relative'
contentHolder.style.float = 'none' |
Create a prototype mask div to clone. Masks serve to display only a small offset portion of the content they hold. | hMask = document.createElement 'div'
hMask.className = 'oridomi-mask-h'
hMask.style.position = 'absolute'
hMask.style.overflow = 'hidden'
hMask.style.width = '100%'
hMask.style.height = '100%' |
Adding | hMask.style[css.transform] = 'translate3d(0, 0, 0)' |
Add the | hMask.appendChild contentHolder |
If shading is enabled, create top and bottom shaders for the horizontal mask prototype. | if @shading
topShader = shader.cloneNode false
topShader.className = 'oridomi-shader-top'
topShader.style.background = @_getShaderGradient 'top'
bottomShader = shader.cloneNode false
bottomShader.className = 'oridomi-shader-bottom'
bottomShader.style.background = @_getShaderGradient 'bottom'
hMask.appendChild topShader
hMask.appendChild bottomShader |
The bleed variable creates some overlap between the panels to prevent cracks in the paper. | bleed = 1.5 |
The panel element holds both its respective mask and all subsequent sibling panels. | hPanel = document.createElement 'div'
hPanel.className = 'oridomi-panel-h'
hPanel.style.width = '100%'
hPanel.style.height = @panelHeight + bleed + 'px'
hPanel.style.padding = '0'
hPanel.style.position = 'relative' |
The panel element is the target of the transforms. | hPanel.style[css.transitionProp] = css.transformProp
hPanel.style[css.transitionDuration] = @settings.speed + 'ms'
hPanel.style[css.transitionEasing] = @settings.easingMethod
hPanel.style[css.origin] = 'top'
hPanel.style[css.transformStyle] = 'preserve-3d'
hPanel.style[css.backface] = 'hidden' |
Apply a transparent border to force edge smoothing on Firefox. (This setting hurts performance significantly.) | if @settings.forceAntialiasing
hPanel.style.outline = '1px solid transparent' |
Add the horizontal mask prototype to the horizontal panel prototype. | hPanel.appendChild hMask |
Loop through just the horizontal anchors. | for anchor in ['top', 'bottom'] |
Loop through the number of horizontal panels. | for i in [0...@hPanels] |
Clone a copy of the panel prototype for manipulation. | panel = hPanel.cloneNode true |
Set a reference to its inner content. | content = panel.getElementsByClassName('oridomi-content')[0]
if anchor is 'top' |
The | yOffset = -(i * @panelHeight) |
This conditional pushes each panel's position down so they stack on top of each other. | if i is 0
panel.style.top = '0'
else
panel.style.top = @panelHeight + 'px'
else |
For bottom panels, make sure the transform origin is | panel.style[css.origin] = 'bottom' |
For the bottom | yOffset = -((@hPanels * @panelHeight) - (@panelHeight * (i + 1)))
if i is 0
panel.style.top = @panelHeight * (@vPanels - 1) - bleed + 'px'
else
panel.style.top = -@panelHeight + 'px'
content.style.top = yOffset + 'px' |
Store references to the shader divs in the | if @shading
@shaders[anchor].top[i] = panel.getElementsByClassName('oridomi-shader-top')[0]
@shaders[anchor].bottom[i] = panel.getElementsByClassName('oridomi-shader-bottom')[0] |
Store a reference to this panel in the | @panels[anchor][i] = panel |
Append each panel to its previous sibling (unless it's the first panel). | @panels[anchor][i - 1].appendChild panel unless i is 0 |
Append the first panel (containing all of its siblings) to its respective stage. | @stages[anchor].appendChild @panels[anchor][0] |
Now that the horizontal panels are done, we can clone the | vMask = hMask.cloneNode true
vMask.className = 'oridomi-mask-v' |
Create left and right shaders if applicable. | if @shading
leftShader = vMask.getElementsByClassName('oridomi-shader-top')[0]
leftShader.className = 'oridomi-shader-left'
leftShader.style.background = @_getShaderGradient 'left'
rightShader = vMask.getElementsByClassName('oridomi-shader-bottom')[0]
rightShader.className = 'oridomi-shader-right'
rightShader.style.background = @_getShaderGradient 'right' |
Clone the | vPanel = hPanel.cloneNode false
vPanel.className = 'oridomi-panel-v'
vPanel.style.width = @panelWidth + bleed + 'px'
vPanel.style.height = '100%'
vPanel.style[css.origin] = 'left'
vPanel.appendChild vMask |
Repeat a similar panel creation process for vertical panels. | for anchor in ['left', 'right']
for i in [0...@vPanels]
panel = vPanel.cloneNode true
content = panel.getElementsByClassName('oridomi-content')[0]
if anchor is 'left'
xOffset = -(i * @panelWidth)
if i is 0
panel.style.left = '0'
else
panel.style.left = @panelWidth + 'px'
else
panel.style[css.origin] = 'right'
xOffset = -((@vPanels * @panelWidth) - (@panelWidth * (i + 1)))
if i is 0
panel.style.left = @panelWidth * (@vPanels - 1) - 1 + 'px'
else
panel.style.left = -@panelWidth + 'px'
content.style.left = xOffset + 'px'
if @shading
@shaders[anchor].left[i] = panel.getElementsByClassName('oridomi-shader-left')[0]
@shaders[anchor].right[i] = panel.getElementsByClassName('oridomi-shader-right')[0]
@panels[anchor][i] = panel
@panels[anchor][i - 1].appendChild panel unless i is 0
@stages[anchor].appendChild @panels[anchor][0] |
Add a special class to the target element. | @el.classList.add @settings.oriDomiClass |
Remove its padding and set a fixed width and height. | @el.style.padding = '0'
@el.style.width = @width + 'px'
@el.style.height = @height + 'px' |
Remove its background, border, and outline. | @el.style.backgroundColor = 'transparent'
@el.style.backgroundImage = 'none'
@el.style.border = 'none'
@el.style.outline = 'none' |
Show the left stage to start with. | @stages.left.style.display = 'block' |
Create an element to hold stages. | @stageEl = document.createElement 'div' |
Attach touch/drag event listeners. | @stageEl.addEventListener 'touchstart', @_onTouchStart, false
@stageEl.addEventListener 'mousedown', @_onTouchStart, false
@stageEl.addEventListener 'touchend', @_onTouchEnd, false
@stageEl.addEventListener 'mouseup', @_onTouchEnd, false
@enableTouch() if @settings.touchEnabled |
Append each stage to the target element. | @stageEl.appendChild @stages[anchor] for anchor in @anchors |
Show the target if applicable. | if @settings.showOnStart
@el.style.display = 'block'
@el.style.visibility = 'visible' |
Hide the original content and insert the oriDomi version. | @el.innerHTML = ''
@el.appendChild @cleanEl
@el.appendChild @stageEl |
These properties record starting angles for touch/drag events. Initialize both to zero. | [@_xLast, @_yLast] = [0, 0] |
This property determines the effect used during touch/drag events. | @lastOp = method: 'accordion', options: {} |
Cache a jQuery object of the element if applicable. | @$el = $ @el if $ |
Push this instance into the instances collection. | instances.push @ |
If a callback was passed in the constructor options, run it. | @_callback @settings |
End the constructor benchmark if | console.timeEnd 'oridomiConstruction' if devMode |
Internal Methods | |
| _callback: (options) ->
if typeof options.callback is 'function' |
Create a local callback for the animation's end. | onTransitionEnd = (e) => |
Remove the event listener immediately to prevent bubbling. | e.currentTarget.removeEventListener css.transitionEnd, onTransitionEnd, false |
Invoke the callback. | options.callback() |
If there was no transformation (0 degrees) invoke the callback immediately. | if @lastAngle is 0
options.callback() |
Otherwise, attach an event listener to be called on the transition's end. | else
@panels[@lastAnchor][0].addEventListener css.transitionEnd, onTransitionEnd, false |
| _getMetric: (metric) ->
parseInt @_elStyle[metric], 10 |
| _transform: (angle, fracture) ->
switch @lastAnchor
when 'left'
axes = [0, 1, 0, angle]
when 'right'
axes = [0, 1, 0, -angle]
when 'top'
axes = [1, 0, 0, -angle]
when 'bottom'
axes = [1, 0, 0, angle] |
| if fracture
[axes[0], axes[1], axes[2]] = [1, 1, 1]
"rotate3d(#{ axes[0] }, #{ axes[1] }, #{ axes[2] }, #{ axes[3] }deg)" |
| _normalizeAngle: (angle) ->
angle = parseFloat angle, 10
if isNaN angle
0
else if angle > 89
89
else if angle < -89
-89
else
angle |
| _normalizeArgs: (method, args) ->
@unfreeze() if @isFrozen |
Get a valid angle. | angle = @_normalizeAngle args[0] |
Get the full anchor name. | anchor = @_getLonghandAnchor args[1] or @lastAnchor |
Extend the given options with the method's defaults. | options = extendObj args[2], @_methodDefaults[method] |
Store a record of this operation for future touch events. | @lastOp = method: method, options: options, negative: angle < 0 |
If the user is trying to transform using a different anchor, we must first unfold the current anchor for transition purposes. | if anchor isnt @lastAnchor or (method is 'foldUp' and @lastAngle isnt 0) or @isFoldedUp |
Call | @reset => |
Show the stage element of the originally requested anchor. | @_showStage anchor |
Since the anchor changed, update the mouse drag cursor. | @_setCursor() if @_touchEnabled |
Defer this operation until the next event loop to prevent a sudden jump. | setTimeout => |
| args.shift() if method is 'foldUp' |
We can now call the originally requested method. | @[method].apply @, args
, 0 |
Return | false
else |
Set an instance reference to the last called angle and return the normalized arguments. | @lastAngle = angle
[angle, anchor, options] |
| _setShader: (i, anchor, angle) -> |
Store the angle's absolute value and generate an opacity based on | abs = Math.abs angle
opacity = abs / 90 * @shadingIntensity |
With hard shading, opacity is reduced and | if @shading is 'hard'
opacity *= .15
if @lastAngle < 0
angle = abs
else
angle = -abs
else
opacity *= .4 |
This block makes sure left and top shaders appear for negative angles and right and bottom shaders appear for positive ones. | switch anchor
when 'left', 'top'
if angle < 0
a = opacity
b = 0
else
a = 0
b = opacity
when 'right', 'bottom'
if angle < 0
a = 0
b = opacity
else
a = opacity
b = 0 |
Only manipulate shader opacity for the current axis. | if anchor is 'left' or anchor is 'right'
@shaders[anchor].left[i].style.opacity = a
@shaders[anchor].right[i].style.opacity = b
else
@shaders[anchor].top[i].style.opacity = a
@shaders[anchor].bottom[i].style.opacity = b |
This is a simple method used by the constructor to set CSS gradient styles. It accepts an anchor argument to start the gradient slope. | _getShaderGradient: (anchor) ->
"#{ css.gradientProp }(#{ anchor }, rgba(0, 0, 0, .5) 0%, rgba(255, 255, 255, .35) 100%)" |
This method shows the requested stage element and sets a reference to it as the current stage. | _showStage: (anchor) ->
if anchor isnt @lastAnchor
@stages[anchor].style.display = 'block'
@stages[@lastAnchor].style.display = 'none'
@lastAnchor = anchor |
Simple method that returns the correct panel set based on an anchor. | _getPanelType: (anchor) ->
if anchor is 'left' or anchor is 'right'
@vPanels
else
@hPanels |
Converts a shorthand anchor name to a full one. | _getLonghandAnchor: (shorthand) ->
switch shorthand
when 'left', 'l', '4', 4
'left'
when 'right', 'r', '2', 2
'right'
when 'top', 't', '1', 1
'top'
when 'bottom', 'b', '3', 3
'bottom'
else |
Left is always default. | 'left' |
Allows other methods to change the tween duration or disable it altogether. | _setTweening: (speed) -> |
If the speed value is | if typeof speed is 'boolean'
speed = if speed then @settings.speed + 'ms' else '0ms' |
To loop through the shaders, derive the correct pair from the current anchor. | if @lastAnchor is 'left' or @lastAnchor is 'right'
shaderPair = ['left', 'right']
else
shaderPair = ['top', 'bottom'] |
Loop through the panels in this anchor and set the transition duration to the new speed. | for panel, i in @panels[@lastAnchor]
panel.style[css.transitionDuration] = speed
if @shading
@shaders[@lastAnchor][shaderPair[0]][i].style[css.transitionDuration] = speed
@shaders[@lastAnchor][shaderPair[1]][i].style[css.transitionDuration] = speed |
Return null and not the loop's result. | null |
Gives the element a resize cursor to prompt the user to drag the mouse. | _setCursor: ->
if @_touchEnabled
if @lastAnchor is 'left' or @lastAnchor is 'right'
@stageEl.style.cursor = 'ew-resize'
else
@stageEl.style.cursor = 'ns-resize'
else
@stageEl.style.cursor = 'default' |
Map of defaults for each method. Some are empty for now. | _methodDefaults:
accordion: |
Sticky keeps the first panel flat on the page. | sticky: false |
Stairs creates a stairway effect. | stairs: false |
Twist and fracture are similar effects that result in wild non-origami-like splits. | fracture: false
twist: false
curl:
twist: false
ramp: {}
foldUp: {} |
Touch / Drag Event Handlers | |
This method is called when a finger or mouse button is pressed on the element. | _onTouchStart: (e) =>
return unless @_touchEnabled
e.preventDefault() |
Disable tweening to enable instant 1 to 1 movement. | @_setTweening false |
Derive the axis to fold on. | @_touchAxis = if @lastAnchor is 'left' or @lastAnchor is 'right' then 'x' else 'y' |
Set a reference to the last folded angle to accurately derive deltas. | @["_#{ @_touchAxis }Last"] = @lastAngle |
Determine the starting tap's coordinate for touch and mouse events. | if e.type is 'mousedown'
@["_#{ @_touchAxis }1"] = e["page#{ @_touchAxis.toUpperCase() }"]
else
@["_#{ @_touchAxis }1"] = e.targetTouches[0]["page#{ @_touchAxis.toUpperCase() }"] |
Add movement listener. | @stageEl.addEventListener 'touchmove', @_onTouchMove, false
@stageEl.addEventListener 'mousemove', @_onTouchMove, false |
Return that value to an external listener. | @settings.touchStartCallback @["_#{ @_touchAxis }1"] |
Called on touch/mouse movement. | _onTouchMove: (e) =>
return unless @_touchEnabled
e.preventDefault() |
Set a reference to the current x or y position. | if e.type is 'mousemove'
current = e["page#{ @_touchAxis.toUpperCase() }"]
else
current = e.targetTouches[0]["page#{ @_touchAxis.toUpperCase() }"] |
Calculate distance and multiply by | distance = (current - @["_#{ @_touchAxis }1"]) * @settings.touchSensitivity |
Calculate final delta based on starting angle, anchor, and what side of zero the last operation was on. | if @lastOp.negative
if @lastAnchor is 'right' or @lastAnchor is 'bottom'
delta = @["_#{ @_touchAxis }Last"] - distance
else
delta = @["_#{ @_touchAxis }Last"] + distance
delta = 0 if delta > 0
else
if @lastAnchor is 'right' or @lastAnchor is 'bottom'
delta = @["_#{ @_touchAxis }Last"] + distance
else
delta = @["_#{ @_touchAxis }Last"] - distance
delta = 0 if delta < 0 |
Invoke the effect method with the delta as an angle argument. | @[@lastOp.method] delta, @lastAnchor, @lastOp.options |
Pass the delta to the movement callback. | @settings.touchMoveCallback delta |
Teardown process when touch/drag event ends. | _onTouchEnd: (e) =>
return unless @_touchEnabled |
Enable tweening again. | @_setTweening true |
Remove movement listeners. | @stageEl.removeEventListener 'touchmove', @_onTouchMove, false
@stageEl.removeEventListener 'mousemove', @_onTouchMove, false |
Pass callback final value. | @settings.touchEndCallback @["_#{ @_touchAxis }Last"] |
Public Methods | |
Reset handles resetting all panels back to zero degrees. | reset: (callback) -> |
If the stage is folded up, unfold it first. | return @unfold callback if @isFoldedUp
for panel, i in @panels[@lastAnchor]
panel.style[css.transform] = @_transform 0
@_setShader i, @lastAnchor, 0 if @shading |
When called internally, | @_callback callback: callback |
Disables oriDomi slicing by showing the original, untouched target element. This is useful for certain user interactions on the inner content. | freeze: (callback) -> |
Return if already frozen. | if @isFrozen
callback?()
else |
Make sure to reset folding first. | @reset =>
@isFrozen = true |
Swap the visibility of the elements. | @stageEl.style[css.transform] = 'translate3d(-9999px, 0, 0)'
@cleanEl.style[css.transform] = 'translate3d(0, 0, 0)'
callback?() |
Restores the oriDomi version of the element for folding purposes. | unfreeze: -> |
Only unfreeze if already frozen. | if @isFrozen
@isFrozen = false |
Swap the visibility of the elements. | @cleanEl.style[css.transform] = 'translate3d(-9999px, 0, 0)'
@stageEl.style[css.transform] = 'translate3d(0, 0, 0)' |
Set | @lastAngle = 0 |
Removes the oriDomi element and marks its instance for garbage collection. | destroy: (callback) -> |
First restore the original element. | @freeze => |
Remove event listeners. | @stageEl.removeEventListener 'touchstart', @_onTouchStart, false
@stageEl.removeEventListener 'mousedown', @_onTouchStart, false
@stageEl.removeEventListener 'touchend', @_onTouchEnd, false
@stageEl.removeEventListener 'mouseup', @_onTouchEnd, false |
Remove the data reference if using jQuery. | $.data @el, 'oriDomi', null if $ |
Remove the oriDomi element from the DOM. | @el.innerHTML = @cleanEl.innerHTML |
Reset original styles. | changedKeys = ['padding', 'width', 'height', 'backgroundColor', 'backgroundImage', 'border', 'outline']
@el.style[key] = @_elStyle[key] for key in changedKeys |
Free up this instance for garbage collection. | instances[instances.indexOf @] = null
callback?() |
Enables touch events and sets cursor. | enableTouch: ->
@_touchEnabled = true
@_setCursor() |
Disables touch events. | disableTouch: ->
@_touchEnabled = false
@_setCursor() |
oriDomi's most basic effect. Transforms the target like its namesake. | accordion: (angle, anchor, options) ->
normalized = @_normalizeArgs 'accordion', arguments |
If | return unless normalized |
Otherwise, destructure the normalized arguments into some local variables. | [angle, anchor, options] = normalized |
Loop through the panels in this stage. | for panel, i in @panels[anchor] |
If it's an odd-numbered panel, reverse the angle. | if i % 2 isnt 0 and not options.twist
deg = -angle
else
deg = angle |
If sticky, keep the first panel flat. | if options.sticky
if i is 0
deg = 0
else if i > 1 or options.stairs
deg *= 2
else |
Double the angle to counteract the angle of the parent panel. | deg *= 2 unless i is 0 |
In stairs mode, keep all the angles on the same side of 0. | if options.stairs
deg = -deg |
Set the CSS transformation. | panel.style[css.transform] = @_transform deg, options.fracture |
Apply shaders. | if @shading and !(i is 0 and options.sticky) and Math.abs(deg) isnt 180
@_setShader i, anchor, deg |
Ask | @_callback options |
| curl: (angle, anchor, options) ->
normalized = @_normalizeArgs 'curl', arguments
return unless normalized
[angle, anchor, options] = normalized |
Reduce the angle based on the number of panels in this axis. | angle /= @_getPanelType anchor
for panel, i in @panels[anchor]
panel.style[css.transform] = @_transform angle
@_setShader i, anchor, 0 if @shading
@_callback options |
| ramp: (angle, anchor, options) ->
normalized = @_normalizeArgs 'ramp', arguments
return unless normalized
[angle, anchor, options] = normalized |
Rotate the second panel for the lift up. | @panels[anchor][1].style[css.transform] = @_transform angle |
For all but the first two panels, set the angle to 0. | for panel, i in @panels[anchor]
if i > 1
@panels[anchor][i].style[css.transform] = @_transform 0
if @shading
@_setShader i, anchor, 0
@_callback options |
| foldUp: (anchor, callback) -> |
Default to left anchor. | unless anchor
anchor = 'left' |
Check if callback is the first argument. | else if typeof anchor is 'function'
callback = anchor |
| normalized = @_normalizeArgs 'foldUp', [0, anchor, {}]
return unless normalized
anchor = normalized[1] |
Set | @isFoldedUp = true |
Start an iterator at the last panel in this anchor. | i = @panels[anchor].length - 1 |
Rotate 100 degrees. | angle = 100 |
Local function that sets an event listener on the current panel and transforms it. | nextPanel = =>
@panels[anchor][i].addEventListener css.transitionEnd, onTransitionEnd, false
@panels[anchor][i].style[css.transform] = @_transform angle
@_setShader i, anchor, angle if @shading |
Called when each panel finishes folding in. | onTransitionEnd = (e) => |
Remove the listener. | @panels[anchor][i].removeEventListener css.transitionEnd, onTransitionEnd, false |
Hide the panel so it doesn't collide when bending around. | @panels[anchor][i].style.display = 'none' |
Decrement the iterator and check if we're on the first panel. | if --i is 0 |
If so, invoke the callback directly if applicable. | callback?()
else |
Otherwise, defer until the next event loop and fold back the next panel. | setTimeout nextPanel, 0 |
Start the chain of folds. | nextPanel() |
Essentially the inverse of | unfold: (callback) -> |
If the target isn't folded up, there's no reason to call this method and the callback is immediately invoked. | callback?() unless @isFoldedUp |
Reset | @isFoldedUp = false |
Start the iterator on the second panel. | i = 1 |
Rotate back to 0. | angle = 0
nextPanel = => |
Show the panel again. | @panels[@lastAnchor][i].style.display = 'block' |
Wait for the next event loop so the transition listener works. | setTimeout =>
@panels[@lastAnchor][i].addEventListener css.transitionEnd, onTransitionEnd, false
@panels[@lastAnchor][i].style[css.transform] = @_transform angle
@_setShader i, @lastAnchor, angle if @shading
, 0
onTransitionEnd = (e) =>
@panels[@lastAnchor][i].removeEventListener css.transitionEnd, onTransitionEnd, false |
Increment the iterator and check if we're past the last panel. | if ++i is @panels[@lastAnchor].length
callback?()
else
setTimeout nextPanel, 0 |
Start the sequence. | nextPanel() |
Convenience Methods | |
Completely folds in target. | collapse: (anchor, options = {}) ->
options.sticky = false
@accordion -89, anchor, options |
Same as | collapseAlt: (anchor, options = {}) ->
options.sticky = false
@accordion 89, anchor, options |
Simply proxy for calling | reveal: (angle, anchor, options = {}) ->
options.sticky = true
@accordion angle, anchor, options |
Proxy to enable stairs mode on | stairs: (angle, anchor, options = {}) ->
options.stairs = true
options.sticky = true
@accordion angle, anchor, options |
| fracture: (angle, anchor, options = {}) ->
options.fracture = true
@accordion angle, anchor, options |
| twist: (angle, anchor, options = {}) ->
options.fracture = true
options.twist = true
@accordion angle / 10, anchor, options |
Class Members | |
Set a version flag for easy external retrieval. | @VERSION = '0.2.0' |
External function to enable | @devMode = ->
devMode = true |
Attach | root.OriDomi = OriDomi |
Plugin Bridge | |
Only create bridge if jQuery (or the like) exists. | if $ |
Attach an | $.fn.oriDomi = (options) -> |
Return selection if oriDomi is unsupported by the browser. | return @ unless oriDomiSupport |
If | if typeof options is 'string' |
Check if method exists and warn if it doesn't. | unless typeof OriDomi::[options] is 'function'
console.warn "oriDomi: No such method '#{ options }'" if devMode
return |
Loop through the jQuery selection. | for el in @ |
Retrieve the instance of oriDomi attached to the element. | instance = $.data el, 'oriDomi' |
Warn if oriDomi hasn't been initialized on this element. | unless instance?
console.warn "oriDomi: Can't call #{ options }, oriDomi hasn't been initialized on this element" if devMode
return |
Convert arguments to a proper array and remove the first element. | args = Array::slice.call arguments
args.shift() |
Call the requested method with arguments. | instance[options].apply instance, args |
Return selection. | @ |
If not calling a method, initialize oriDomi on the selection. | else
for el in @ |
If the element in the selection already has an instance of oriDomi attached to it, return the instance. | instance = $.data el, 'oriDomi'
if instance
return instance
else |
Create an instance of oriDomi and attach it to the element. | $.data el, 'oriDomi', new OriDomi el, options |
Return the selection. | @
|