drawio-offline
Version:
diagrams.net desktop
909 lines (855 loc) • 26.9 kB
JavaScript
/**
* Copyright (c) 2006-2018, JGraph Ltd
* Copyright (c) 2006-2018, Gaudenz Alder
*/
/**
* Class: mxWebColaAdaptor
*
* Extends WebCola's cola object to act as both adaptor and layout in WebCola for mxGraph.
*
* Constructor: mxWebColaAdaptor
*
* Constructs a new WebCola-based adaptor for given mxGraph.
*
* Arguments:
*
* graph - <mxGraph> that contains the cells.
* dimension - <[]> array containing [width, height] of canvas in points
* movableVertices - <[]> IDs of vertices that are movable; if undefined all vertices are movable
* options - <{}> WebCola options for layout/adapter
*
**/
var doNothing = function()
/**
* Empty method for default event handlers
*/
{
}
function mxWebColaAdaptor(graph, dimension, movableVertices, options)
/**
* Constructs a WebCola adaptor for mxGraph
* @param graph mxGraph instance
* @param dimension array containing [width, height] of drawing canvas in points
* @param movableVertices set containing IDs of vertices that are movable; if undefined all vertices are movable
* @param options WebCola options for layout/adapter
* @constructor
*/
{
this.graph = graph;
this.dimension = dimension;
if (typeof dimension === 'undefined')
{
this.dimension = [600, 600];
}
// compute vertex/group degrees from incidence
this.vertexDegrees = new mxDictionary();
this.groupDegrees = new mxDictionary();
this.computeVertexDegrees();
// convert draw.io graph to WebCola's nodes/links
var layoutResult = this.graphToLayout(graph, movableVertices);
this.nodes = layoutResult.nodes;
this.links = layoutResult.links;
this.groups = layoutResult.groups;
this.cellToNode = layoutResult.cellToNode;
this.isStopped = false;
this.options = {};
// assign default values
for (var key in this.defaultValues)
{
this.options[key] = this.defaultValues[key];
}
// if options were passed, override defaults for keys available in options
if (options != null)
{
for (var key in options)
{
this.options[key] = options[key];
}
}
}
// default layout options
mxWebColaAdaptor.prototype.defaultValues = {
doAnimations: true, // whether to show the layout as it's running
// doAnimations: false, // whether to show the layout as it's running
skipFrames: 1, // number of ticks per frame; higher is faster but more jerky
maxSimulationTime: 4000, // max length in ms to run the layout
ungrabifyWhileSimulating: false, // so you can't drag nodes during layout
fit: true, // on every layout reposition of nodes, fit the viewport
padding: 30, // padding around the simulation
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node
// layout event callbacks
ready: function ready() {}, // on layoutready
stop: function stop() {}, // on layoutstop
// positioning options
randomize: false, // use random node positions at beginning of layout
avoidOverlap: true, // if true, prevents overlap of node bounding boxes
handleDisconnected: true, // if true, avoids disconnected components from overlapping
nodeSpacing: function nodeSpacing(node) {
return 10;
}, // extra spacing around nodes
flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 }
alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } }
gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]
// different methods of specifying edge length
// each can be a constant numerical value or a function like `function( edge ){ return 2; }`
edgeLength: undefined, // sets edge length directly in simulation
edgeSymDiffLength: undefined, // symmetric diff edge length in simulation
edgeJaccardLength: undefined, // jaccard edge length in simulation
// iterations of cola algorithm; uses default values on undefined
unconstrIter: undefined, // unconstrained initial layout iterations
userConstIter: undefined, // initial layout iterations with user-specified constraints
allConstIter: undefined, // initial layout iterations with all constraints including non-overlap
// infinite layout options
keepRunning: false // overrides all other options for a forces-all-the-time mode
};
mxWebColaAdaptor.prototype.updatePositions = function(isUndoable)
/**
* Default method for updating positions
* Should be overridden by the caller/user of the adaptor
*/
{
console.log("colaAdaptor: updatePositions");
// TODO: do all the positions here
}
mxWebColaAdaptor.prototype.kick = function (colaAdaptor)
/**
* Starts WebCola computation on the given adaptor
*/
{
console.log("colaAdaptor: step");
if ('doAnimations' in this.options && this.options.doAnimations)
{
doRendering(this.callback);
}
else
{
// run until the end
while (!this.process(colaAdaptor))
{
}
}
}
mxWebColaAdaptor.prototype.step = function (colaAdaptor)
/**
* Notifies about a single layout computation step on WebCola adaptor
*/
{
if ('doAnimations' in this.options && this.options.doAnimations)
{
this.updatePositions(false);
}
}
mxWebColaAdaptor.prototype.frameSteps = function(colaAdaptor)
/**
* Runs multiple ticks on WebCola adaptor until finished
*/
{
var result = void 0;
for (var i = 0; i < this.options.skipFrames && !result; i++) {
result = result || this.process(colaAdaptor);
}
return result;
}
mxWebColaAdaptor.prototype.process = function(colaAdaptor)
/**
* Executes the whole layout computation on WebCola adaptor
*/
{
if (this.isStopped)
{
this.finish();
return true;
}
var result = colaAdaptor.tick();
if (result && this.options.keepRunning) {
colaAdaptor.resume();
}
return result;
}
mxWebColaAdaptor.prototype.renderingChain = function(colaAdaptor)
/**
* This keeps rendering new simulation frames until end is reached
*/
{
if (this.process(colaAdaptor))
{
return;
}
doRendering(this.callback);
}
mxWebColaAdaptor.prototype.finish = function()
{
}
mxWebColaAdaptor.prototype.run = function()
/**
* Runs the layout computation on given nodes/links/groups
* @returns Nothing
*/
{
var layout = this;
var options = this.options;
var colaAdaptor = layout.adaptor = cola.adaptor
({
trigger: function (evt)
{
var START = cola.EventType ? cola.EventType.start : 'start';
var TICK = cola.EventType ? cola.EventType.tick : 'tick';
var END = cola.EventType ? cola.EventType.end : 'end';
switch (evt.type)
{
case START:
{
// colaAdaptor.start();
}
break;
case TICK:
{
layout.step();
}
break;
case END:
{
console.log("colaAdaptor: end");
layout.updatePositions(true);
if (!options.keepRunning)
{
layout.finish();
}
}
break;
}
},
kick: function ()
{
layout.kick(colaAdaptor);
},
finish: function()
{
layout.finish();
},
on: doNothing,
drag: doNothing
});
colaAdaptor.nodes(this.nodes)
.links(this.links)
.groups(this.groups)
.size(this.dimension)
.linkDistance(function (link)
{
return link.length;
});
layout.callback = function()
{
layout.renderingChain(colaAdaptor);
}
colaAdaptor.avoidOverlaps(options.avoidOverlap)
.handleDisconnected(options.handleDisconnected)
// .constraints(constraints)
// .start(100, 100, 100);
.start();
return this.adaptor;
}
function getScreenConstraints(layout, width, height)
/**
* Returns a set of constraints covering limits of screen
* @param layout
* @param width
* @param height
* @returns {Array}
*/
{
var gap = 20;
var size = layout._nodes.length;
var topLeft = {x: 0, y: 0, fixed: true, index: size};
var bottomRight = {x: width, y: height, fixed: true, index: size + 1};
layout._nodes.push(topLeft);
layout._nodes.push(bottomRight);
var constraints = [];
for (var i = 0; i < size; i++) {
var index = layout._nodes[i].index;
constraints.push({ axis: 'x', type: 'separation', left: topLeft.index, right: index, gap: gap });
constraints.push({ axis: 'y', type: 'separation', left: topLeft.index, right: index, gap: gap });
constraints.push({ axis: 'x', type: 'separation', left: index, right: bottomRight.index, gap: gap });
constraints.push({ axis: 'y', type: 'separation', left: index, right: bottomRight.index, gap: gap });
}
return constraints;
}
mxWebColaAdaptor.prototype.graphToLayout = function(graph, movableVertices)
/**
* Returns a WebCola layout set up for the given Draw.io graph
* In WebCola's TypeScript source: vertex cell -> InputNode
* edge cell -> Link
* parent/child -> Group
* @param graph Draw.io graph object
* @param fixedVertices Vertices that shouldn't be moved (dictionary with {id: True} pairs, id is vertex id)
* optional, if undefined all vertices are considered movable
* @returns list of WebCola nodes, list of WebCola links, and a dictionary from Draw.io cell ID to WebCola node ID
* returned as a dictionary: {nodes: ..., links: ..., cellToNode: ...}
*/
{
var activeMaps = this.findActiveVertices(graph); // list of all active vertices, i.e. with no collapsed parents
var activeVertices = activeMaps.activeVertices; // inactive vertex to its nearest active parent map
var inactiveToActiveMap = activeMaps.inactiveToActiveMap;
var model = graph.getModel();
var cells = model.cells;
var view = graph.getView();
var cellSpacing = 20;
// Ignores cells that have no states
var tmp = {};
for (var id in cells)
{
if (view.getState(cells[id]) != null)
{
tmp[id] = cells[id];
}
}
cells = tmp;
var nodeCells = {};
var linkCells = {};
var cellIds = {};
var edgeIds = {};
var colaId = 0;
var nodes = [];
var links = [];
// process nodes first
for (var id in cells)
{
var cell = cells[id];
var state = view.getState(cell);
var bounds = view.getBoundingBox(state, true);
var bounds = model.getGeometry(cell);
var isFirst = true;
// if (cell.isVertex() && this.isLeafOrCollapsed(cell)) {
// only active vertices should be considered (i.e. not hidden by a collapsed or layouted vertex)
// if (cell.isVertex() && activeVertices[cell.id])
if (cell.isVertex() && this.isLeafOrCollapsed(cell) && activeVertices[cell.id])
{
var node = {};
// node.x = bounds.getCenterX();
// node.y = bounds.getCenterY();
node.width = bounds.width + cellSpacing;
node.height = bounds.height + cellSpacing;
node.index = colaId;
node.name = cell.value;
node.fixed = false;
if (typeof movableVertices !== 'undefined' && !(id in movableVertices))
{
node.fixed = true;
}
nodes.push(node);
cellIds[id] = colaId;
nodeCells[colaId] = cell;
colaId++;
}
}
// now edges can be processed as well
for (var id in cells)
{
var cell = cells[id];
var state = view.getState(cell);
if (cell.isEdge() && cell.getTerminal(true) != null && cell.getTerminal(false) != null)
{
// attach edges to lowest active vertex corresponding to each of their terminals
var terminal_id1 = inactiveToActiveMap[cell.source.id];
var terminal_id2 = inactiveToActiveMap[cell.target.id];
if (terminal_id1 == terminal_id2)
{
// both terminals are under the same active parent, no need to make an invisible edge
continue;
}
// if either of terminals are groups, we need to insert complete graph between nodes within these groups
var terminal1 = cells[terminal_id1];
var terminal2 = cells[terminal_id2];
var addedLinks = [];
if (this.isGroup(terminal1) || this.isGroup(terminal2))
{
addedLinks = this.addGroupConstraintLinks(terminal1, terminal2, activeVertices, inactiveToActiveMap, cellIds);
}
else
{
// link = {}
// link.source = cellIds[cell.source.id];
// link.target = cellIds[cell.target.id];
var link = this.createLink(terminal_id1, terminal_id2, cellIds);
addedLinks.push(link);
}
for (var i = 0; i < addedLinks.length; i++)
{
var link = addedLinks[i];
links.push(link);
edgeIds[cell] = id;
linkCells[link] = cell;
}
}
}
links = this.getUniqueLinks(links);
// finally, groups need to be extracted
// mxGraph.getCellsForGroup
// mxGraphModel.getChildCount
// mxGraph.getBoundsForGroup
// first, get all possible parents and their children
var groupParents = {};
var directParentChildren = {};
for (var id in cells)
{
var cell = cells[id];
if (!cell.isVertex() || !this.isLeafOrCollapsed(cell))
continue;
var parent = cell.getParent();
if (parent.isVertex())
{
groupParents[parent.id] = parent;
if (!(parent.id in directParentChildren))
{
directParentChildren[parent.id] = {}
}
directParentChildren[parent.id][id] = cell;
}
}
// now go through all parents/children and build a group hierarchy for WebCola
var preliminaryGroups = [];
var groupId = 0;
var groupToParent = {}
for (var parentId in groupParents)
{
var parentChildren = directParentChildren[parentId];
var groupNodes = []
for (var childId in parentChildren)
{
if (activeVertices[childId])
{
groupNodes.push(cellIds[childId]);
}
}
preliminaryGroups.push({id: groupId, parentId: parentId, nodes: parentChildren, leaves: groupNodes, groups: []});
groupToParent[groupId] = parentId;
groupId++;
}
// here scan newly formed groups if their parent is a child of any of the nodes in any of the groups
for (var i = 0; i < preliminaryGroups.length; i++)
{
var parentGroup = preliminaryGroups[i];
var parentId = parentGroup.parentId;
for (var j = 0; j < preliminaryGroups.length; j++)
{
if (i == j)
continue;
var groupParentId = cells[preliminaryGroups[j].parentId].getParent().id;
if (parentId == groupParentId)
parentGroup.groups.push(j);
}
}
// finalize groups
var groups = [];
for (var i = 0; i < preliminaryGroups.length; i++)
{
var group = preliminaryGroups[i];
var graphGroup = {};
if (group.leaves.length > 0)
{
graphGroup["leaves"] = group.leaves;
}
if (group.groups.length > 0)
{
graphGroup["groups"] = group.groups;
}
if (graphGroup.hasOwnProperty("leaves") || graphGroup.hasOwnProperty("groups"))
{
groups.push(graphGroup);
}
}
return {nodes: nodes, links: links, groups: groups, cellToNode: cellIds};
};
mxWebColaAdaptor.prototype.createLink = function(sourceId, targetId, cellIds)
/**
* Creates a default version of a WebCola link/edge
* @param sourceId ID of the edge's source vertex cell
* @param targetId ID of the edge's target vertex cell
* @param cellIds cell ID to WebCola's node ID mapping
* @returns a WebCola link corresponding to the edge [sourceId, targetId]
* in WebCola node IDs
*/
{
var link = {};
link.source = cellIds[sourceId];
link.target = cellIds[targetId];
link.weight = 0.5;
link.length = 200; //Graph.prototype.defaultEdgeLength;
return link;
}
mxWebColaAdaptor.prototype.computeVertexDegrees = function()
/**
* Computes group vertex and vertex degrees. Useful to stop layouting for groups
* with no internal, incoming or outgoing edges.
*/
{
var model = this.graph.getModel();
var cells = model.cells;
// compute individual vertex degrees
for (var id in cells)
{
var cell = cells[id];
if (cell.isEdge() && cell.source != null && cell.target != null)
{
// scan all edges, ignore other types
var sourceId = cell.source.id;
var targetId = cell.target.id;
var source = cells[sourceId];
var target = cells[targetId];
if (sourceId == targetId)
{
// self-loops are irrelevant
continue;
}
var sourceDegree = this.vertexDegrees.get(source);
if (typeof sourceDegree == "undefined")
{
sourceDegree = 0;
}
sourceDegree++;
this.vertexDegrees.put(source, sourceDegree);
var targetDegree = this.vertexDegrees.get(target);
if (typeof targetDegree == "undefined")
{
targetDegree = 0;
}
targetDegree++;
this.vertexDegrees.put(target, targetDegree);
}
}
// compute sub-group degree, i.e. sum of all degrees of children
// algorithm goes through all vertices, then for each vertex it goes on its material
// path to root and adds its contribution to all vertices on this path
// this should for each vertex place exactly the sum of degrees of all its vertices
// and itself, nothing less, nothing more
for (var id in cells)
{
var cell = cells[id];
if (cell.isVertex())
{
// scan all vertices, ignore other types
var vertexDegree = this.vertexDegrees.get(cell);
if (typeof vertexDegree == "undefined")
{
vertexDegree = 0;
}
var parent = cell;
while (parent != null && typeof parent != "undefined")
{
var groupDegree = this.groupDegrees.get(parent);
if (typeof groupDegree == "undefined")
{
groupDegree = 0;
}
groupDegree += vertexDegree;
this.groupDegrees.put(parent, groupDegree);
parent = parent.parent;
}
}
}
}
mxWebColaAdaptor.prototype.isZeroConnected = function(groupCell)
/**
* Indicates if all group cell's vertices have no incidental edges
* @params groupCell group cell
* @returns true if the group cell doesn't contain any vertices with edges
*/
{
var groupDegree = this.groupDegrees.get(groupCell);
console.log("Group " + groupCell.id + " degree: " + groupDegree);
if (typeof groupDegree != "undefined" && groupDegree > 0)
{
return false;
}
return true;
}
mxWebColaAdaptor.prototype.isInZeroConnectedGroup = function(cell)
{
var parent = cell.parent;
if (parent == null || typeof parent == "undefined")
{
return this.isZeroConnected(cell);
}
else
{
return this.isZeroConnected(parent);
}
}
mxWebColaAdaptor.prototype.isLeafOrCollapsed = function(cell)
/**
* Returns true if a cell is either a leaf or a collapsed group
* @param cell cell to investigate
* @returns true if a cell is either a leaf or a collapsed group, false otherwise
*/
{
if (cell.isCollapsed() ||
cell.children == null || cell.children.length == 0 ||
typeof this.graph.getCellStyle(cell)['childLayout'] != 'undefined')
{
return true;
}
if (this.isZeroConnected(cell))
{
return true;
}
/*
if (!cell.isCollapsed() && cell.children != null && cell.children.length > 0 && this.graph.getEdges(cell, null, true, true, true, true).length == 0)
{
console.log("cell " + cell.id + " is 0-connected.");
return true;
}
*/
return false;
}
mxWebColaAdaptor.prototype.findActiveVertices = function(graph)
/**
* Scans all groups and finds active vertices, as well as an inactive-vertex-to-active-parent map
* @param graph input graph
*/
{
var inactiveToActiveMap = {};
var activeVertices = {};
var root = graph.getModel().root;
var cellsToExplore = [{vertex: root, isActive: true, activeParent: root}]
while (cellsToExplore.length > 0)
{
var currentCellInfo = cellsToExplore.shift();
var cell = currentCellInfo.vertex;
if (cell.isEdge())
{
// cut at edge group, those are ignored
continue;
}
var isActive = currentCellInfo.isActive;
var activeParent = currentCellInfo.activeParent;
if (cell.isVertex())
{
if (isActive)
{
activeVertices[cell.id] = true;
}
else
{
activeVertices[cell.id] = false;
}
}
// prepare children
// child can be active only if any of its parents is not collapsed
var isActive = isActive && !this.isLeafOrCollapsed(cell);
var children = cell.children;
if (children != null && children.length > 0)
{
for (var i = 0; i < children.length; i++)
{
var child = children[i];
var childActiveParent = isActive? child: activeParent;
cellsToExplore.push({vertex: child, isActive: isActive, activeParent: childActiveParent});
if (child.isVertex())
{
inactiveToActiveMap[child.id] = childActiveParent.id;
}
}
}
}
return {activeVertices: activeVertices, inactiveToActiveMap: inactiveToActiveMap};
}
mxWebColaAdaptor.prototype.getActiveVerticesInGroup = function(groupCell, activeVertices, includeCollapsedGroups)
/**
* Scans all children in group and returns all active vertices inside group
* This method is for creating redundant edges between members of groups to simulate group edges in WebCola
* See https://github.com/tgdwyer/WebCola/issues/38
* @param groupCell group cell
*/
{
var activeChildren = [];
if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
{
activeChildren.push(groupCell);
}
var cellsToExplore = [groupCell];
while (cellsToExplore.length > 0)
{
var cell = cellsToExplore.shift();
if (!cell.isVertex() || !activeVertices[cell])
{
// cut at edge group, those are ignored
continue;
}
if (this.isLeafOrCollapsed(cell))
{
activeChildren.push(cell);
}
else
{
var children = cell.children;
if (children == null || children.length == 0)
{
continue;
}
cellsToExplore = cellsToExplore.concat(children);
}
}
return activeChildren;
}
mxWebColaAdaptor.prototype.getAllVerticesInGroup = function(groupCell, includeCollapsedGroups)
/**
* Scans all children in group and returns all active vertices inside group
* This method is for creating redundant edges between members of groups to simulate group edges in WebCola
* See https://github.com/tgdwyer/WebCola/issues/38
* @param groupCell group cell
*/
{
var result = [];
if (includeCollapsedGroups && this.isLeafOrCollapsed(groupCell))
{
result.push(groupCell);
}
var cellsToExplore = [groupCell];
while (cellsToExplore.length > 0)
{
var cell = cellsToExplore.shift();
if (!cell.isVertex())
{
// cut at edge group, those are ignored
continue;
}
if (this.isLeafOrCollapsed(cell))
{
result.push(cell);
}
else
{
var children = cell.children;
if (children == null || children.length == 0)
{
continue;
}
cellsToExplore = cellsToExplore.concat(children);
}
}
return result;
}
mxWebColaAdaptor.prototype.hasVertexChildren = function(cell)
/**
* Returns true if a (group) cell has vertex children in its subtree
* @param cell (group) cell
* @returns true if if a (group) cell has vertex children in its subtree, false otherwise
*/
{
if (cell.children == null || cell.children.length == 0)
{
return false;
}
var toBeExamined = []
toBeExamined = toBeExamined.concat(cell.children);
while (toBeExamined.length > 0)
{
var cell = toBeExamined.shift();
if (cell.isVertex())
return true;
if (cell.children != null && cell.children.length > 0)
{
toBeExamined = toBeExamined.concat(cell.children);
}
}
return false;
}
mxWebColaAdaptor.prototype.isInCollapsedTree = function(cell)
{
// scan the material path for collapsed group node
while (cell != null)
{
cell = cell.getParent();
if (cell != null && cell.isCollapsed())
{
return true;
}
}
return false;
}
mxWebColaAdaptor.prototype.isGroup = function(cell)
/**
* Returns true if cell is a group (has children)
* @param cell cell
* @returns true if cell is a group (has children); false otherwise
*/
{
return cell.children != null && cell.children.length > 0;
}
mxWebColaAdaptor.prototype.addGroupConstraintLinks = function(groupA, groupB, activeVertices, inactiveToActiveMap, cellIds)
/**
* Adds edges between vertex and group or two groups. Each vertex child of a group must be connected to the vertex/
* group, as this way WebCola simulates edges on the group level (as groups don't exist as vertices in WebCola)
* @param rootCell root cell
*/
{
var result = []
// var childrenA = this.getActiveVerticesInGroup(groupA, activeVertices, false);
// var childrenB = this.getActiveVerticesInGroup(groupB, activeVertices, false);
var childrenA = [groupA];
var childrenB = [groupB];
if (!groupA.isCollapsed())
{
childrenA = this.getAllVerticesInGroup(groupA, activeVertices, false);
}
if (!groupB.isCollapsed())
{
childrenB = this.getAllVerticesInGroup(groupB, activeVertices, false);
}
if (childrenA == null || childrenA.length == 0 || childrenB == null || childrenB.length == 0)
return result;
for (var i = 0; i < childrenA.length; i++)
{
var childA_Id = inactiveToActiveMap[childrenA[i].id];
for (var j = 0; j < childrenB.length; j++)
{
var childB_Id = inactiveToActiveMap[childrenB[j].id];
var link = this.createLink(childA_Id, childB_Id, cellIds);
result.push(link);
}
}
return result;
}
mxWebColaAdaptor.prototype.getUniqueLinks = function(links)
/**
* Returns an array of unique links from an array of links
* @param links array of links containing duplicate links
* @returns array of unique links
*/
{
var result = [];
// TODO: this part is inefficient - O(n^2); Theta(n) should be possible with hashmap
for (var i = 0; i < links.length; i++)
{
var link = links[i];
var shouldBeAdded = true;
for (var j = 0; j < result.length; j++)
{
var existingLink = result[j];
if (link.source == existingLink.source && link.target == existingLink.target)
{
shouldBeAdded = false;
break;
}
}
if (shouldBeAdded)
{
result.push(link);
}
}
return result;
}
var doRendering = void 0;
if ((typeof window === "undefined" ? "undefined" : typeof(window)) !== ( true ? "undefined" : typeof(undefined)))
{
doRendering = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
}
else
{
// if not available, all you get is immediate calls
function doRendering(callback)
{
callback();
};
}