Skip to content

Instantly share code, notes, and snippets.

@jwalton
Last active October 13, 2022 07:03
Show Gist options
  • Save jwalton/6670630 to your computer and use it in GitHub Desktop.
Save jwalton/6670630 to your computer and use it in GitHub Desktop.
CSS3 Transitions for Dashing Dashboards
# Flying Widgets v0.1.1
#
# To use, put this file in assets/javascripts/cycleDashboard.coffee. Then find this line in
# application.coffee:
#
# $('.gridster ul:first').gridster
#
# And change it to:
#
# $('.gridster > ul').gridster
#
# Finally, put multiple gridster divs in your dashboard, and add a call to Dashing.cycleDashboards()
# to the javascript at the top of your dashboard:
#
# <script type='text/javascript'>
# $(function() {
# Dashing.widget_base_dimensions = [370, 340]
# Dashing.numColumns = 5
# Dashing.cycleDashboards({timeInSeconds: 15, stagger: true});
# });
# </script>
#
# <% content_for :title do %>Loop Dashboard<% end %>
#
# <div class="gridster">
# <ul>
# <!-- Page 1 of widgets goes here. -->
# <li data-row="1" data-col="1" data-sizex="1" data-sizey="1">
# <div data-view="Image" data-image="/inverted-logo.png" style="background-color:#666766"></div>
# </li>
#
# </ul>
# </div>
#
# <div class="gridster">
# <ul>
# <!-- Page 2 of widgets goes here. -->
# </ul>
# </div>
#
# Some generic helper functions
sleep = (timeInSeconds, fn) -> setTimeout fn, timeInSeconds * 1000
isArray = (obj) -> Object.prototype.toString.call(obj) is '[object Array]'
isString = (obj) -> Object.prototype.toString.call(obj) is '[object String]';
isFunction = (obj) -> obj && obj.constructor && obj.call && obj.apply
#### Show/Hide functions.
#
# Every member of `showFunctions` and `hideFunctions` must be one of:
#
# * A `{start, end, transition}` object (transition defaults to 'all 1s'.)
# * A `{transitionFunction}` object.
# * A `fn($dashboard, widget, originalLocations)` which returns one of the above.
#
# The easiest way to define a transition is just to specify start and end CSS proprties for each
# widget with a `{start, end}` object. The `fadeOut` and `fadeIn` are some of the simplest
# examples below. Sometimes you might need slightly more control, in which case `start` and
# `end` can each be functions of the form `($widget, index)`, where $widget is the jquery object
# for the widget being transformed, and index is the index of the widget within the dashboard.
# The function form is handy when you want to do something different for each widget, depending
# on it's location.
#
# For even more control, you can specify a `fn($dashboard, widgets, originalLocations)` function
# in place of the entire object. This is handy when you have some setup work to do for your
# transition, such as detecting the width of the page so you can move all widgets off-screen.
#
# For the ultimate in control, you can specify a
# `transitionFunction{$dashboard, options, done}` object. It will be up to you to
# do whatever you need to do in order to hide or display the dashboard. The CSS of every widget
# will be reset to something sane when the function completes, but otherwise it's entirely
# up to you. Params are:
#
# * `$dashboard` - jquery object of the dashboard to show/hide.
# * `options.stagger` - True if transition should be staggered.
# * `options.widgets` - An array of all widgets in the dashboard.
# * `options.originalLocations` - An array of CSS data about the location, opacity, etc... of
# each widget.
# * `done()` - Async callback. Make sure you call this!
#
hideFunctions = {
toRight: ($dashboard, widgets, originalLocations) ->
documentWidth = $(document).width()
return {end: (($widget) -> {left: documentWidth, opacity: 0})}
shrink: {
start: {
opacity: 1,
transform: 'scale(1,1)',
"-webkit-transform": 'scale(1,1)'
},
end: {
transform: 'scale(0,0)',
"-webkit-transform": 'scale(0,0)',
opacity: 0
}
}
fadeOut: {
start: {opacity: 1}
end: {opacity: 0}
}
explode: {
start: {
opacity: 1
transform: 'scale(1,1)',
"-webkit-transform": 'scale(1,1)'
}
end: {
opacity: 0
transform: 'scale(2,2)',
"-webkit-transform": 'scale(2,2)'
}
}
# 3D spinning transition. It's cool, but it crashes Chrome if you sleep and wake your machine.
# spin: {
# transitionFunction: ($dashboard, options, done) ->
# # Add perspective to the container, so we can spin the dashboard in 3D
# $parent = $dashboard.parent()
# $parent.css({perspective: 500, "-webkit-perspective": 500})
# # Do the transition
# moveWithTransition [$dashboard], {
# transition: 'all 1s',
# start: {
# transform: 'rotateY(0deg)'
# "-webkit-transform": 'rotateY(0deg)'
# }
# end: {
# transform: 'rotateY(90deg)'
# "-webkit-transform": 'rotateY(90deg)'
# },
# timeInSeconds: 1}, ->
# $dashboard.hide()
# # Remove changes
# sleep 0, ->
# $dashboard.parent().css({perspective: '', "-webkit-perspective": ''})
# $dashboard.css({
# transform: ''
# "-webkit-transform": ''
# })
# done()
# chainsTo: {
# transitionFunction: ($dashboard, options, done) ->
# # Add perspective to the container, so we can spin the dashboard in 3D
# $parent = $dashboard.parent()
# $parent.css({perspective: 500, "-webkit-perspective": 500})
# $dashboard.show()
# # Do the transition
# moveWithTransition [$dashboard], {
# transition: 'all 1s',
# start: {
# transform: 'rotateY(-90deg)'
# "-webkit-transform": 'rotateY(-90deg)'
# }
# end: {
# transform: 'rotateY(0deg)'
# "-webkit-transform": 'rotateY(0deg)'
# },
# timeInSeconds: 1}, ->
# # Remove changes
# sleep 0, ->
# $dashboard.parent().css({perspective: '', "-webkit-perspective": ''})
# $dashboard.css({
# transform: ''
# "-webkit-transform": ''
# })
# done()
# }
# }
}
# Handy function for reversing simple transitions
reverseTransition = (obj) ->
if isFunction(obj) or obj.transitionFunction?
throw new Error("Can't reverse transition")
return {start: obj.end, end: obj.start, transition: obj.transition}
showFunctions = {
fromLeft: ($dashboard, widgets, originalLocations) ->
start: (($widget, index) -> {left: "#{-$widget.width() - $dashboard.width()}px", opacity: 0}),
end: (($widget, index) -> originalLocations[index]),
fromTop: ($dashboard, widgets, originalLocations) ->
start: (($widget, index) -> {top: "#{-$widget.height() - $dashboard.height()}px", opacity: 0}),
end: (($widget, index) -> return originalLocations[index]),
zoom: reverseTransition(hideFunctions.shrink)
fadeIn: reverseTransition(hideFunctions.fadeOut)
implode: reverseTransition(hideFunctions.explode)
}
# Move an element from one place to another using a CSS3 transition.
#
# * `elements` - One or more elements to move, in an array.
# * `transition` - The transition string to apply (e.g.: 'left 1s ease 0s')
# * `start` - This can be an object (e.g. `{left: 0px}`) or a `fn($el, index)`
# which returns such an object. This is the location the object will start at.
# If start is omitted, then the current location of the object will be used
# as the start.
# * `end` - As with `start`, this can be an object or a function. `end` is required.
# * `timeInSeconds` - The time required to complete the transition. This function will
# wait this long before calling `done()`.
# * `offset` is an offset for the index passed into `start()` and `end()`. Handy when
# you want to split up an array of
# * `done()` - Async callback.
moveWithTransition = (elements, {transition, start, end, timeInSeconds, offset}, done) ->
transition = transition or ''
timeInSeconds = timeInSeconds or 0
end = end or {}
offset = offset or 0
origTransitions = []
moveToStart = () ->
for el, index in elements
$el = $(el)
origTransitions[index + offset] = $el.css 'transition'
$el.css transition: 'left 0s ease 0s'
$el.css(if isFunction start then start($el, index + offset) else start)
moveToEnd = () ->
for el, index in elements
$el = $(el)
$el.css transition: transition
$el.css(if isFunction end then end($el, index + offset) else end)
sleep Math.max(0, timeInSeconds), ->
$el.css transition: origTransitions[index + offset]
done? null
if start
moveToStart()
sleep 0, -> moveToEnd()
else
moveToEnd()
# Runs a function which shows or hides the dashboard. This function ensures that all the
# dashboards widgets end up where they started.
#
# Transitions should be a `{start, end}` object suitable for passing to moveWithTransition,
# or a `transitions($dashboard, widgets, originalLocations)` function which returns such an object.
#
showHideDashboard = (visible, stagger, $dashboard, transitions, done) ->
$dashboard = $($dashboard)
$ul = $dashboard.children('ul')
$widgets = $ul.children('li')
# Record the original location, opacity, other CSS attributes we might want to edit
originalLocations = []
$widgets.each (index, widget) ->
$widget = $(widget)
originalLocations[index] = {
left: $widget.css 'left'
top: $widget.css 'top'
width: $widget.css 'width'
height: $widget.css 'height'
opacity: $widget.css 'opacity'
transform: $widget.css 'transform'
"-webkit-transform": $widget.css '-webkit-transform'
}
widgets = $.makeArray($widgets)
if isFunction transitions
transitions = transitions($dashboard, widgets, originalLocations)
origDone = done
done = () ->
sleep 0, () ->
# Make sure the dashboard is in a sane state.
$dashboard.toggle( visible )
sleep 0, () ->
# Clear any styles we've set on the widgets.
#
# TODO: It would be nice to record the styles before we start, and then restore them
# here, but I've found that if my laptop goes to sleep, when it wakes up, when
# displaying the dashboard on Chrome, it sometimes picks up bad values for
# `originalLocations`. By always forcing the style to a sane known value, we know
# everything will work out in the end.
#
$dashboard.children('ul').children('li').attr 'style', 'position: absolute'
origDone?()
transitionString = "all 1s"
if transitions.transitionFunction
# Show/hide the dashboard with a custom function
transitionFunction = transitions.transitionFunction
else if !stagger
transitionFunction = ($dashboard, {widgets, originalLocations}, fnDone) ->
moveWithTransition widgets, {
end: transitions.start
}, -> sleep 0, ->
if visible then $dashboard.show()
moveWithTransition widgets, {
start: transitions.start,
end: transitions.end,
transition: transitions.transition or transitionString,
timeInSeconds: 1
}, fnDone
else
transitionFunction = ($dashboard, {widgets, originalLocations}, fnDone) ->
singleWidgetFn = (widget, index) ->
moveWithTransition [widget], {
end: transitions.start,
offset: index
}, -> sleep 0, ->
if visible then $dashboard.show()
sleep (Math.random()/2), () ->
moveWithTransition [widget], {
start: transitions.start,
end: transitions.end,
transition: transitions.transition or transitionString,
timeInSeconds: 1,
offset: index
}, ->
for widget, index in widgets
singleWidgetFn(widget, index)
sleep 1.5, fnDone
# Show or hide the dashboard
transitionFunction $dashboard, {stagger, widgets, originalLocations}, done
# Select a member at random from an object.
#
# If 'allowedMembers' is an array of strings, then only the corresponding members will be
# considered for selection.
#
# Returns a "{key, value}" object.
pickMember = (object, allowedMembers=null) ->
answer = null
functionArray = []
if allowedMembers?
if not isArray allowedMembers then allowedMembers = [allowedMembers]
for memberName in allowedMembers
if memberName of object then functionArray.push {key: memberName, value: object[memberName]}
else
for memberName, member of object
functionArray.push {key: memberName, value: member}
if functionArray.length > 0
index = Math.floor(Math.random()*functionArray.length);
answer = functionArray[index]
return answer
# Cycle the dashboard to the next dashboard.
#
# If a transition is already in progress, this function does nothing.
Dashing.cycleDashboardsNow = do () ->
transitionInProgress = false
visibleIndex = 0
(options = {}) ->
return if transitionInProgress
transitionInProgress = true
{stagger, fastTransition} = options
stagger = !!stagger
fastTransition = !!fastTransition
$dashboards = $('.gridster')
# Work out which dashboard to show
oldVisibleIndex = visibleIndex
visibleIndex++
if visibleIndex >= $dashboards.length
visibleIndex = 0
if oldVisibleIndex == visibleIndex
# Only one dashboard. Disable fast transitions
fastTransition = false
doneCount = 0
doneFn = () ->
doneCount++
# Only set transitionInProgress to false when both the show and the hide functions
# are finished.
if doneCount is 2
transitionInProgress = false
# Hide the old dashboard
hideFunction = pickMember hideFunctions
showNewDashboard = () ->
options.onTransition?($($dashboards[visibleIndex]))
showFunction = null
chainsTo = hideFunction.value.chainsTo
if isString chainsTo
showFunction = showFunctions[chainsTo]
else if chainsTo?
showFunction = {key: "chainsTo", value: chainsTo}
if !showFunction
showFunction = pickMember showFunctions
# console.log "Showing dashboard #{visibleIndex} #{showFunction.key}"
showHideDashboard true, stagger, $dashboards[visibleIndex], showFunction.value, () ->
doneFn()
# console.log "Hiding dashboard #{oldVisibleIndex} #{hideFunction.key}"
showHideDashboard false, stagger, $dashboards[oldVisibleIndex], hideFunction.value, () ->
if !fastTransition
showNewDashboard()
doneFn()
# If fast transitions are enabled, then don't wait for the hiding animation to complete
# before showing the new dashboard.
if fastTransition then showNewDashboard()
return null
# Adapted from http://stackoverflow.com/questions/1403888/get-url-parameter-with-javascript-or-jquery
getURLParameter = (name) ->
encodedParameter = (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[null,null])[1]
return if encodedParameter? then decodeURI(encodedParameter) else null
# Cause dashing to cycle from one dashboard to the next.
#
# Dashboard cycling can be bypassed by passing a "page" parameter in the url. For example,
# going to http://dashboardserver/mydashboard?page=2 will show the second dashboard in the list
# and will not cycle.
#
# Options:
# * `timeInSeconds` - The time to display each dashboard, in seconds. If 0, then dashboards will
# not automatically cycle, but can be cycled manually by calling `cycleDashboardsNow()`.
# * `stagger` - If this is true, each widget will be transitioned individually at slightly
# randomized times. This gives a more random look. If false, then all wigets will be moved
# at the same time. Note if `timeInSeconds` is 0, then this option is ignored (but can, instead,
# be passed to `cycleDashboardsNow()`.)
# * `fastTransition` - If true, then we will run the show and hide transitions simultaneously.
# This gets your new dashboard up onto the screen faster.
# * `onTransition($newDashboard)` - A function to call before a dashboard is displayed.
#
Dashing.cycleDashboards = (options) ->
timeInSeconds = if options.timeInSeconds? then options.timeInSeconds else 20
$dashboards = $('.gridster')
startDashboardParam = getURLParameter('page')
startDashboard = parseInt(startDashboardParam) or 1
startDashboard = Math.max startDashboard, 1
startDashboard = Math.min startDashboard, $dashboards.length
$dashboards.each (dashboardIndex, dashboard) ->
# Hide all but the first dashboard.
$(dashboard).toggle(dashboardIndex is (startDashboard - 1))
# Set all dashboards to position: absolute so they stack one on top of the other
$(dashboard).css "position": "absolute"
# If the user specified a dashboard, then don't cycle from one dashboard to the next.
if !startDashboardParam? and (timeInSeconds > 0)
cycleFn = () -> Dashing.cycleDashboardsNow(options)
setInterval cycleFn, timeInSeconds * 1000
$(document).keypress (event) ->
# Cycle to next dashboard on space
if event.keyCode is 32 then Dashing.cycleDashboardsNow(options)
return true
# Customized version of `Dashing.gridsterLayout()` which supports multiple dashboards.
Dashing.cycleGridsterLayout = (positions) ->
#positions = positions.replace(/^"|"$/g, '') # ??
positions = JSON.parse(positions)
$dashboards = $(".gridster > ul")
if isArray(positions) and ($dashboards.length == positions.length)
Dashing.customGridsterLayout = true
for position, index in positions
$dashboard = $($dashboards[index])
widgets = $dashboard.children("[data-row^=]")
for widget, index in widgets
$(widget).attr('data-row', position[index].row)
$(widget).attr('data-col', position[index].col)
else
console.log "Warning: Could not apply custom layout!"
# Redefine functions for saving layout
sleep 0.1, () ->
Dashing.getWidgetPositions = ->
dashboardPositions = []
for dashboard in $(".gridster > ul")
dashboardPositions.push $(dashboard).gridster().data('gridster').serialize()
return dashboardPositions
Dashing.showGridsterInstructions = ->
newWidgetPositions = Dashing.getWidgetPositions()
if !isArray(newWidgetPositions[0])
$('#save-gridster').slideDown()
$('#gridster-code').text("
Something went wrong - reload the page and try again.
")
else
unless JSON.stringify(newWidgetPositions) == JSON.stringify(Dashing.currentWidgetPositions)
Dashing.currentWidgetPositions = newWidgetPositions
$('#save-gridster').slideDown()
$('#gridster-code').text("
<script type='text/javascript'>\n
$(function() {\n\n
\ \ Dashing.cycleGridsterLayout('#{JSON.stringify(Dashing.currentWidgetPositions)}')\n
});\n
</script>
")

Flying Widgets adds CSS3 transitions to your dashboard, allowing you to cycle through multiple widget sets on a single TV without page reloads, using stylish CSS3 transitions. You can even still re-order your widgets and save their locations!

Note that sinatra-cyclist is a potential alternative if the machine you use to display your dashboards is lacking in graphics horsepower.

To use, put this file in assets/javascripts/cycleDashboard.coffee. Then find this line in application.coffee:

    $('.gridster ul:first').gridster

And change it to:

    $('.gridster > ul').gridster

Finally, put multiple gridster divs in your dashboard, and add a call to Dashing.cycleDashboards() to the javascript at the top of your dashboard:

<script type='text/javascript'>
$(function() {
  Dashing.widget_base_dimensions = [370, 340]
  Dashing.numColumns = 5
  Dashing.cycleDashboards({timeInSeconds: 15, stagger: true});
});
</script>

<% content_for :title do %>Loop Dashboard<% end %>

<div class="gridster">
  <ul>
    <!-- Page 1 of widgets goes here. -->
    <li data-row="1" data-col="1" data-sizex="1" data-sizey="1">
      <div data-view="Image" data-image="/inverted-logo.png" style="background-color:#666766"></div>
    </li>

  </ul>
</div>

<div class="gridster">
  <ul>
    <!-- Page 2 of widgets goes here. -->
  </ul>
</div>

Now if you go to your dashboard, you should see slick CSS3 powered transitions as the page automatically cycles from one dashboard to the next. Note that if you want to see just one dashboard without cycling, you can add a "?page=x" at the end of your dashboard URL, replacing "x" with the dashboard you want to see.

@Flygsand
Copy link

@PareidoliaX I've implemented this in my fork. Example:

Dashing.cycleDashboards({timeInSeconds: 15, showFunction: 'fadeIn', hideFunction: 'fadeOut'})

@artkrz Does it have to do with sizing? Try my fork and see if that fixes your problem.

@djl197 Can you gist your code?

@donholly
Copy link

THANK YOU

@orenkatz
Copy link

orenkatz commented Jun 9, 2014

Is there a way to use different widget size for the 2 pages?
For example the first will be:
Dashing.widget_base_dimensions = [370, 340]
And the second will be:
Dashing.widget_base_dimensions = [90, 90]

Thanks!
Oren.

@rahulsom
Copy link

My last column doesn't get utilized. I need to increment numColumns for it to render correctly. Has anyone noticed that?

@johnhilberts
Copy link

@jwalton and @protomouse I've added the ability to specify how long each individual dashboard would stick around for on screen in my gist:
https://gist.github.com/johnhilberts/737ea60f2766632aa007

I've also updated the readme to show an example of how to implement this.
Care to take the changes on?

@poalcospe
Copy link

Hi, very usefull widget ! I'm trying to use it with canvas js but I have some issue with height and width parameters. When i'm switching dashboard, both parameters are not automatically updated. But just pressing F12 for example update it. Looking at the javascript in chrome, I can see in live those values which not change. I think it's a problem with canvas js, somewhere in the js, the canvas is not resized.

Does anyone have already face it or have any explaination ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment