From a4a55be17fbb2eebfe0a5887aa2cf5b786abd9fa Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Tue, 23 May 2017 17:05:17 +0200 Subject: [PATCH 01/32] Initial attempt of performance optimizations --- src/core/ElementOutput.js | 24 +++++++++++++++++++++--- src/core/SpecParser.js | 4 +++- src/core/Surface.js | 4 ++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 3649ab05..20509274 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -170,12 +170,14 @@ define(function(require, exports, module) { var result = 'matrix3d('; for (var i = 0; i < 15; i++) { - result += (m[i] < 0.000001 && m[i] > -0.000001) ? '0,' : m[i] + ','; + var shouldOperationBeRounded = ![0, 5, 10].includes(i); + result += (m[i] < 0.000001 && m[i] > -0.000001) ? '0,' : + (shouldOperationBeRounded ? Math.round(m[i]) : m[i]) + + ','; } result += m[15] + ')'; return result; } - /** * Directly apply given FamousMatrix to the document element as the * appropriate webkit CSS style. @@ -302,7 +304,23 @@ define(function(require, exports, module) { */ ElementOutput.prototype.attach = function attach(target) { this._element = target; - _addEventListeners.call(this, target); + + // create an observer instance + var observer = new MutationObserver((mutations) =>{ + if(window.observeAll){ + debugger; + } + + }); + + // configuration of the observer: + var config = { attributes: true, childList: true, characterData: true, attributeOldValue: true }; + + // pass in the target node, as well as the observer options + observer.observe(target, config); + + + _addEventListeners.call(this, target); }; /** diff --git a/src/core/SpecParser.js b/src/core/SpecParser.js index 2aa281ff..0ee4535c 100644 --- a/src/core/SpecParser.js +++ b/src/core/SpecParser.js @@ -131,7 +131,9 @@ define(function(require, exports, module) { var nextSizeContext = sizeContext; if (spec.opacity !== undefined) opacity = parentContext.opacity * spec.opacity; - if (spec.transform) transform = Transform.multiply(parentContext.transform, spec.transform); + if (spec.hide) transform = Transform.scale(0, 0, 0); + else if (spec.transform) transform = Transform.multiply(parentContext.transform, spec.transform); + if (spec.origin) { origin = spec.origin; nextSizeContext = parentContext.transform; diff --git a/src/core/Surface.js b/src/core/Surface.js index 235ae846..f4bf51f3 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -381,7 +381,7 @@ define(function(require, exports, module) { if (size[1] === undefined) size[1] = origSize[1]; if (size[0] === true || size[1] === true) { if (size[0] === true){ - if (this._trueSizeCheck || (this._size[0] === 0)) { + if (this._trueSizeCheck) { var width = target.offsetWidth; if (this._size && this._size[0] !== width) { this._size[0] = width; @@ -393,7 +393,7 @@ define(function(require, exports, module) { } } if (size[1] === true){ - if (this._trueSizeCheck || (this._size[1] === 0)) { + if (this._trueSizeCheck ) { var height = target.offsetHeight; if (this._size && this._size[1] !== height) { this._size[1] = height; From 75ee527866871012737124adc5745a95a7b16f26 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Tue, 23 May 2017 17:21:22 +0200 Subject: [PATCH 02/32] Removed unnecessary rounding operations --- src/core/ElementOutput.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 20509274..8ecc29ec 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -170,10 +170,7 @@ define(function(require, exports, module) { var result = 'matrix3d('; for (var i = 0; i < 15; i++) { - var shouldOperationBeRounded = ![0, 5, 10].includes(i); - result += (m[i] < 0.000001 && m[i] > -0.000001) ? '0,' : - (shouldOperationBeRounded ? Math.round(m[i]) : m[i]) - + ','; + result += (m[i] < 0.000001 && m[i] > -0.000001) ? '0,' : m[i] + ','; } result += m[15] + ')'; return result; From 29935c7268a644f1cd8f22dd8e8e3cbfe5f10720 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 25 May 2017 11:49:45 +0200 Subject: [PATCH 03/32] Made the hide parameter propagate appropriately --- src/core/Context.js | 2 ++ src/core/Group.js | 1 + src/core/SpecParser.js | 11 ++++++++--- src/surfaces/ContainerSurface.js | 8 ++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/core/Context.js b/src/core/Context.js index 7b2ff4cd..f5160dce 100644 --- a/src/core/Context.js +++ b/src/core/Context.js @@ -146,6 +146,8 @@ define(function (require, exports, module) { if (contextParameters.origin) this._nodeContext.origin = contextParameters.origin; if (contextParameters.align) this._nodeContext.align = contextParameters.align; if (contextParameters.size) this._nodeContext.size = contextParameters.size; + if (contextParameters.size) this._nodeContext.size = contextParameters.size; + this._nodeContext.hide = contextParameters.hide; if (contextParameters.allocator) { this._nodeContext.allocator = contextParameters.allocator; } else { diff --git a/src/core/Group.js b/src/core/Group.js index 2b37b2d5..4a765780 100644 --- a/src/core/Group.js +++ b/src/core/Group.js @@ -147,6 +147,7 @@ define(function (require, exports, module) { allocator: this._allocator, transform: Transform.translate(-origin[0] * size[0], -origin[1] * size[1], 0), origin: origin, + hide: context.opacity === 0 || context.hide, size: size }); return result; diff --git a/src/core/SpecParser.js b/src/core/SpecParser.js index 0ee4535c..7ecd61b5 100644 --- a/src/core/SpecParser.js +++ b/src/core/SpecParser.js @@ -99,13 +99,15 @@ define(function(require, exports, module) { if (typeof spec === 'number') { id = spec; - transform = parentContext.transform; + var hide = parentContext.hide || parentContext.opacity === 0; + transform = hide ? Transform.scale(0, 0, 0) : parentContext.transform; align = parentContext.align || _zeroZero; if (parentContext.size && align && (align[0] || align[1])) { var alignAdjust = [align[0] * parentContext.size[0], align[1] * parentContext.size[1], 0]; transform = Transform.thenMove(transform, _vecInContext(alignAdjust, sizeContext)); } this.result[id] = { + hide: hide, transform: transform, opacity: parentContext.opacity, origin: parentContext.origin || _zeroZero, @@ -128,10 +130,12 @@ define(function(require, exports, module) { origin = parentContext.origin; align = parentContext.align; size = parentContext.size; + /* If parent is hidden, also this element should be hidden */ + var hide = spec.hide || parentContext.hide || opacity === 0; var nextSizeContext = sizeContext; if (spec.opacity !== undefined) opacity = parentContext.opacity * spec.opacity; - if (spec.hide) transform = Transform.scale(0, 0, 0); + if (hide) transform = Transform.scale(0, 0, 0); else if (spec.transform) transform = Transform.multiply(parentContext.transform, spec.transform); if (spec.origin) { @@ -169,7 +173,8 @@ define(function(require, exports, module) { opacity: opacity, origin: origin, align: align, - size: size + size: size, + hide: hide }, nextSizeContext); } }; diff --git a/src/surfaces/ContainerSurface.js b/src/surfaces/ContainerSurface.js index c238c36a..0350f054 100644 --- a/src/surfaces/ContainerSurface.js +++ b/src/surfaces/ContainerSurface.js @@ -95,20 +95,16 @@ define(function (require, exports, module) { * @private * @method commit * @param {Context} context commit context - * @param {Transform} transform unused TODO - * @param {Number} opacity unused TODO - * @param {Array.Number} origin unused TODO - * @param {Array.Number} size unused TODO * @return {undefined} TODO returns an undefined value */ - ContainerSurface.prototype.commit = function commit(context, transform, opacity, origin, size) { + ContainerSurface.prototype.commit = function commit(context) { var previousSize = this._size ? [this._size[0], this._size[1]] : null; var result = Surface.prototype.commit.apply(this, arguments); if (this._shouldRecalculateSize || (previousSize && (this._size[0] !== previousSize[0] || this._size[1] !== previousSize[1]))) { this.context.setSize(); this._shouldRecalculateSize = false; } - this.context.update(); + this.context.update({hide: context.opacity === 0 || context.hide}); return result; }; From 4f921cefa78977b93921e8d7ab2dc6ed4b13a5b6 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Wed, 31 May 2017 16:34:47 +0200 Subject: [PATCH 04/32] Added function to retrieve a singleton canvas --- src/core/Engine.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/Engine.js b/src/core/Engine.js index 4d3940e7..8bda95ee 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -183,6 +183,15 @@ define(function (require, exports, module) { document.documentElement.classList.add('famous-root'); } + var canvas; + Engine.getCachedCanvas = function() { + if(!canvas){ + canvas = document.createElement('canvas'); + document.createDocumentFragment().appendChild(canvas); + } + return canvas; + }; + /** * Add event handler object to set of downstream handlers. * From 6a1e1f26aea465cd987cca47e8077897654338f0 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 1 Jun 2017 14:00:26 +0200 Subject: [PATCH 05/32] Removed dubious code causing unnecessary reflows --- src/surfaces/ContainerSurface.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/surfaces/ContainerSurface.js b/src/surfaces/ContainerSurface.js index 0350f054..85420b02 100644 --- a/src/surfaces/ContainerSurface.js +++ b/src/surfaces/ContainerSurface.js @@ -100,10 +100,7 @@ define(function (require, exports, module) { ContainerSurface.prototype.commit = function commit(context) { var previousSize = this._size ? [this._size[0], this._size[1]] : null; var result = Surface.prototype.commit.apply(this, arguments); - if (this._shouldRecalculateSize || (previousSize && (this._size[0] !== previousSize[0] || this._size[1] !== previousSize[1]))) { - this.context.setSize(); - this._shouldRecalculateSize = false; - } + this.context.setSize(context.size); this.context.update({hide: context.opacity === 0 || context.hide}); return result; }; From 0275a8bc2858e0a0250ee1597a54d60fd0d5a0f4 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 09:29:23 +0200 Subject: [PATCH 06/32] Added assignment to textContent instead of innerHTML when no tags present in Surface --- src/core/Surface.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/Surface.js b/src/core/Surface.js index f4bf51f3..bf293215 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -493,11 +493,17 @@ define(function(require, exports, module) { target.removeChild(target.firstChild); target.appendChild(content); } else { - target.innerHTML = content; + /* textContent proved to be faster: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ + if(content.includes('<')){ + target.innerHTML = content; + } else { + target.textContent = content; + } } this.content = target.innerHTML; }; + /** * FIX for famous-bug: https://github.com/Famous/famous/issues/673 * From a755dbefa586c3b0a8a3f4071ce03bef790969d7 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 11:31:51 +0200 Subject: [PATCH 07/32] Added global event system to reduce the number of calls to element.addEventListener --- src/core/DOMEventHandler.js | 62 +++++++++++++++++++++++++++++++++++++ src/core/ElementOutput.js | 9 +++--- 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/core/DOMEventHandler.js diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js new file mode 100644 index 00000000..ca88daa3 --- /dev/null +++ b/src/core/DOMEventHandler.js @@ -0,0 +1,62 @@ + +/** + * Created by lundfall on 01/06/2017. + */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Owner: mark@famo.us + * @license MPL 2.0 + * @copyright Famous Industries, Inc. 2015 + */ + +define(function (require, exports, module) { + + var DOMEventHandler = {}; + var EventEmitter = require('./EventEmitter.js'); + var DOMBuffer = require('./DOMBuffer'); + + //TODO Add more to complete list + var singleElementEvents = [ + 'submit', 'focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll' + ]; + + var initializedListeners = {}; + + DOMEventHandler.isNativeEvent = function(eventName) { + return typeof document.body["on" + eventName] !== "undefined"; + }; + + DOMEventHandler.addEventListener = function(id, element, type, callback){ + if(!DOMEventHandler.isNativeEvent(type)){ + return; + } + + if(singleElementEvents.includes(type)){ + return element.addEventListener(type, this.eventForwarder); + } + DOMBuffer.setAttribute(element, 'data-arvaid', id); + var eventEmitter = initializedListeners[type]; + if(!eventEmitter){ + eventEmitter = initializedListeners[type] = new EventEmitter(); + window.addEventListener(type, function (event) { + var recievedID = event.target && event.target.getAttribute && event.target.getAttribute('data-arvaid'); + if(recievedID){ + eventEmitter.emit(recievedID, event); + } + }); + } + eventEmitter.on(id, callback); + + }; + + DOMEventHandler.removeEventListener = function(id, type, callback) { + if(initializedListeners[type]){ + initializedListeners[type].removeListener(id, callback); + } + }; + + module.exports = DOMEventHandler; +}); diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 8ecc29ec..d0cf2bf8 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -11,6 +11,7 @@ define(function(require, exports, module) { var Entity = require('./Entity'); var EventHandler = require('./EventHandler'); var Transform = require('./Transform'); + var DOMEventHandler = require('./DOMEventHandler'); var usePrefix = !('transform' in document.documentElement.style); var devicePixelRatio = window.devicePixelRatio || 1; @@ -60,12 +61,12 @@ define(function(require, exports, module) { * @return {EventHandler} this */ ElementOutput.prototype.on = function on(type, fn) { - if (this._element) this._element.addEventListener(type, this.eventForwarder); + if (this._element) DOMEventHandler.addEventListener(this.id, this._element, type, this.eventForwarder); this._eventOutput.on(type, fn); }; ElementOutput.prototype.once = function on(type, fn) { - if (this._element) this._element.addEventListener(type, this.eventForwarder); + if (this._element) DOMEventHandler.addEventListener(this.id, this._element, type, this.eventForwarder); this._eventOutput.once(type, fn); }; @@ -142,7 +143,7 @@ define(function(require, exports, module) { // Calling this enables methods like #on and #pipe. function _addEventListeners(target) { for (var i in this._eventOutput.listeners) { - target.addEventListener(i, this.eventForwarder); + DOMEventHandler.addEventListener(this.id, target, i, this.eventForwarder); } } @@ -150,7 +151,7 @@ define(function(require, exports, module) { // document element. This occurs just before detach from the document. function _removeEventListeners(target) { for (var i in this._eventOutput.listeners) { - target.removeEventListener(i, this.eventForwarder); + DOMEventHandler.removeEventListener(this.id, i, this.eventForwarder) } } From 09aef81a33d8602924c31f93ad68feee96109e96 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 12:10:10 +0200 Subject: [PATCH 08/32] Got commits slightly mixed up, correcting mistake in DOMEventHandler --- src/core/DOMEventHandler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index ca88daa3..791eb534 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -16,7 +16,6 @@ define(function (require, exports, module) { var DOMEventHandler = {}; var EventEmitter = require('./EventEmitter.js'); - var DOMBuffer = require('./DOMBuffer'); //TODO Add more to complete list var singleElementEvents = [ @@ -37,7 +36,7 @@ define(function (require, exports, module) { if(singleElementEvents.includes(type)){ return element.addEventListener(type, this.eventForwarder); } - DOMBuffer.setAttribute(element, 'data-arvaid', id); + element.setAttribute('data-arvaid', id); var eventEmitter = initializedListeners[type]; if(!eventEmitter){ eventEmitter = initializedListeners[type] = new EventEmitter(); From 7165c1d56cfacbd589851868d51854005be9902b Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 12:43:22 +0200 Subject: [PATCH 09/32] Implemented a flushing of all dom updates at the end of each frame --- src/core/DOMBuffer.js | 89 ++++++++++++++++++++++++++++++++++++ src/core/ElementAllocator.js | 20 ++++---- src/core/ElementOutput.js | 21 +++++---- src/core/Engine.js | 4 ++ src/core/Surface.js | 70 +++++++++++++++------------- src/surfaces/InputSurface.js | 10 ++-- 6 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 src/core/DOMBuffer.js diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js new file mode 100644 index 00000000..16c7a976 --- /dev/null +++ b/src/core/DOMBuffer.js @@ -0,0 +1,89 @@ +/** + * Created by lundfall on 02/06/2017. + */ + + +define(function (require, exports, module) { + + var DOMBuffer = {}; + /*var enqueuedAttributes = []; + var enqueuedProperties = []; + var enqueuedAdditions = []; + var enqueuedRemovals = []; + var enqueuedAttributeRemovals = []; + var enqueuedChildRemovals = []; + var enqueuedChildAppendices = []; + var enqueuedInsertBefore = [];*/ + var enqueuedOperations = []; + + + DOMBuffer.assignProperty = function (object, property, value) { + enqueuedOperations.push({data: [object, property, value], operation: 'assignProperty'}); + }; + + DOMBuffer.setAttribute = function (element, attribute, value) { + enqueuedOperations.push({data: [element, attribute, value], operation: 'setAttribute'}); + }; + + DOMBuffer.addToObject = function (object, value) { + enqueuedOperations.push({data: [object, value], operation: 'addToObject'}); + }; + + DOMBuffer.removeFromObject = function (object, attribute) { + enqueuedOperations.push({data: [object, attribute], operation: 'removeFromObject'}); + }; + + DOMBuffer.removeAttribute = function (element, attribute) { + enqueuedOperations.push({data: [element, attribute], operation: 'removeAttribute'}); + }; + + DOMBuffer.removeChild = function (parent, childToRemove) { + enqueuedOperations.push({data: [parent, childToRemove], operation: 'removeChild'}); + }; + + DOMBuffer.appendChild = function (parent, childToAppend) { + enqueuedOperations.push({data: [parent, childToAppend], operation: 'appendChild'}); + }; + + DOMBuffer.insertBefore = function (parent, childBefore, childToInsert) { + enqueuedOperations.push({data: [parent, childBefore, childToInsert], operation: 'insertBefore'}); + }; + + DOMBuffer.flushUpdates = function () { + for(var index = 0; index < enqueuedOperations.length ; index++){ + var enqueuedOperation = enqueuedOperations[index]; + var operationName = enqueuedOperation.operation; + var data = enqueuedOperation.data; + switch (operationName){ + case 'appendChild': + data[0].appendChild(data[1]); + break; + case 'insertBefore': + data[0].insertBefore(data[1], data[2]); + break; + case 'setAttribute': + data[0].setAttribute(data[1], data[2]); + break; + case 'removeChild': + data[0].removeChild(data[1]); + break; + case 'removeAttribute': + data[0].removeAttribute(data[1]); + break; + case 'addToObject': + data[0].add(data[1]); + break; + case 'removeFromObject': + data[0].remove(data[1]); + break; + case 'assignProperty': + data[0][data[1]] = data[2]; + break; + } + } + enqueuedOperations = []; + }; + + module.exports = DOMBuffer; +}); + diff --git a/src/core/ElementAllocator.js b/src/core/ElementAllocator.js index 9a6b6091..fb6b8f5b 100644 --- a/src/core/ElementAllocator.js +++ b/src/core/ElementAllocator.js @@ -9,6 +9,7 @@ define(function (require, exports, module) { var Context = require('./Context.js'); + var DOMBuffer = require('./DOMBuffer'); /** * Internal helper object to Context that handles the process of @@ -42,11 +43,13 @@ define(function (require, exports, module) { if (container === oldContainer) return; if (oldContainer instanceof DocumentFragment) { - container.appendChild(oldContainer); + DOMBuffer.appendChild(container, oldContainer); } else { - while (oldContainer.hasChildNodes()) { - container.appendChild(oldContainer.firstChild); + var children = oldContainer.childNodes || []; + //TODO Confirm that this works + for(var i = 0;i< children.length; i++){ + DOMBuffer.appendChild(container, children[i]); } } @@ -71,18 +74,19 @@ define(function (require, exports, module) { var isNested = !!options.isNested; type = type.toLowerCase(); var detachedList = isNested ? this.detachedAllocators : this.detachedHtmlElements; + var result; if (!(type in detachedList)) detachedList[type] = []; var nodeStore = detachedList[type]; var result; - if (nodeStore.length > 0 && !insertFirst) { + /*if (nodeStore.length > 0 && !insertFirst) { result = nodeStore.pop(); } - else { + else {*/ result = this._allocateNewHtmlOutput(type, insertFirst); if (isNested) { result = this._allocateNewAllocator(result); } - } + // } return result; }; @@ -106,9 +110,9 @@ define(function (require, exports, module) { ElementAllocator.prototype._allocateNewHtmlOutput = function _allocateNewElementOutput(type, insertFirst) { var result = document.createElement(type); if (insertFirst) { - this.container.insertBefore(result, this.container.firstChild); + DOMBuffer.insertBefore(this.container, result, this.container.firstChild); } else { - this.container.appendChild(result); + DOMBuffer.appendChild(this.container, result); } return result; }; diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index d0cf2bf8..bb897ef7 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -12,6 +12,7 @@ define(function(require, exports, module) { var EventHandler = require('./EventHandler'); var Transform = require('./Transform'); var DOMEventHandler = require('./DOMEventHandler'); + var DOMBuffer = require('./DOMBuffer'); var usePrefix = !('transform' in document.documentElement.style); var devicePixelRatio = window.devicePixelRatio || 1; @@ -191,12 +192,12 @@ define(function(require, exports, module) { var _setMatrix; if (usePrefix) { _setMatrix = function(element, matrix) { - element.style.webkitTransform = _formatCSSTransform(matrix); + DOMBuffer.assignProperty(element.style, 'webkitTransform', _formatCSSTransform(matrix)); }; } else { _setMatrix = function(element, matrix) { - element.style.transform = _formatCSSTransform(matrix); + DOMBuffer.assignProperty(element.style, 'transform', _formatCSSTransform(matrix)); }; } @@ -215,11 +216,11 @@ define(function(require, exports, module) { // Shrink given document element until it is effectively invisible. var _setInvisible = usePrefix ? function(element) { - element.style.webkitTransform = 'scale3d(0.0001,0.0001,0.0001)'; - element.style.opacity = 0; + DOMBuffer.assignProperty(element.style, 'webkitTransform', 'scale3d(0.0001,0.0001,0.0001)'); + DOMBuffer.assignProperty(element.style, 'opacity', '0'); } : function(element) { - element.style.transform = 'scale3d(0.0001,0.0001,0.0001)'; - element.style.opacity = 0; + DOMBuffer.assignProperty(element.style, 'transform', 'scale3d(0.0001,0.0001,0.0001)'); + DOMBuffer.assignProperty(element.style, 'opacity', '0'); }; function _xyNotEquals(a, b) { @@ -256,12 +257,12 @@ define(function(require, exports, module) { if (this._invisible) { this._invisible = false; - this._element.style.display = ''; + DOMBuffer.assignProperty(this._element.style, 'display', ''); } if (this._opacity !== opacity) { this._opacity = opacity; - target.style.opacity = (opacity >= 1) ? '0.999999' : opacity; + DOMBuffer.assignProperty(target.style, 'opacity', (opacity >= 1) ? '0.999999' : opacity); } if (this._transformDirty || this._originDirty || this._sizeDirty) { @@ -289,7 +290,7 @@ define(function(require, exports, module) { ElementOutput.prototype.cleanup = function cleanup() { if (this._element) { this._invisible = true; - this._element.style.display = 'none'; + DOMBuffer.assignProperty(this._element.style, 'display', 'none'); } }; @@ -334,7 +335,7 @@ define(function(require, exports, module) { _removeEventListeners.call(this, target); if (this._invisible) { this._invisible = false; - this._element.style.display = ''; + DOMBuffer.assignProperty(this._element.style, 'display', ''); } } this._element = null; diff --git a/src/core/Engine.js b/src/core/Engine.js index 8bda95ee..596a1715 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -28,6 +28,8 @@ define(function (require, exports, module) { var ElementAllocator = require('./ElementAllocator'); var EventHandler = require('./EventHandler'); var OptionsManager = require('./OptionsManager'); + var DOMBuffer = require('./DOMBuffer'); + var Engine = {}; @@ -99,6 +101,8 @@ define(function (require, exports, module) { for (i = 0; i < contexts.length; i++) contexts[i].update(); + DOMBuffer.flushUpdates(); + eventHandler.emit('postrender'); }; diff --git a/src/core/Surface.js b/src/core/Surface.js index bf293215..771f9508 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -9,6 +9,7 @@ define(function(require, exports, module) { var ElementOutput = require('./ElementOutput'); + var DOMBuffer = require('./DOMBuffer'); /** * A base class for viewable content and event @@ -254,7 +255,7 @@ define(function(require, exports, module) { // Apply to document all changes from removeClass() since last setup(). function _cleanupClasses(target) { - for (var i = 0; i < this._dirtyClasses.length; i++) target.classList.remove(this._dirtyClasses[i]); + for (var i = 0; i < this._dirtyClasses.length; i++) DOMBuffer.removeFromObject(target.classList, this._dirtyClasses[i]); this._dirtyClasses = []; } @@ -262,7 +263,7 @@ define(function(require, exports, module) { // These will be deployed to the document on call to #setup(). function _applyStyles(target) { for (var n in this.properties) { - target.style[n] = this.properties[n]; + DOMBuffer.assignProperty(target.style, n, this.properties[n]); } } @@ -270,7 +271,7 @@ define(function(require, exports, module) { // These will be deployed to the document on call to #setup(). function _cleanupStyles(target) { for (var n in this.properties) { - target.style[n] = ''; + DOMBuffer.assignProperty(target.style, n, ''); } } @@ -278,11 +279,11 @@ define(function(require, exports, module) { // These will be deployed to the document on call to #setup(). function _applyAttributes(target) { for (var n in this.attributes) { - target.setAttribute(n, this.attributes[n]); + DOMBuffer.setAttribute(target, n, this.attributes[n]); } for (var index in this._dirtyAttributes) { var name = this._dirtyAttributes[index]; - target.removeAttribute(name); + DOMBuffer.removeAttribute(target, name); this._dirtyAttributes.shift(); } } @@ -291,7 +292,7 @@ define(function(require, exports, module) { // These will be deployed to the document on call to #setup(). function _cleanupAttributes(target) { for (var n in this.attributes) { - target.removeAttribute(n); + DOMBuffer.removeAttribute(target, n); } } @@ -312,14 +313,14 @@ define(function(require, exports, module) { if (this.elementClass) { if (this.elementClass instanceof Array) { for (var i = 0; i < this.elementClass.length; i++) { - target.classList.add(this.elementClass[i]); + DOMBuffer.addToObject(target.classList, this.elementClass[i]); } } else { - target.classList.add(this.elementClass); + DOMBuffer.addToObject(target.classList, this.elementClass); } } - target.style.display = ''; + DOMBuffer.assignProperty(target.style, 'display', ''); this.attach(target); this._opacity = null; this._currentTarget = target; @@ -357,7 +358,7 @@ define(function(require, exports, module) { if (this._classesDirty) { _cleanupClasses.call(this, target); var classList = this.getClassList(); - for (var i = 0; i < classList.length; i++) target.classList.add(classList[i]); + for (var i = 0; i < classList.length; i++) DOMBuffer.addToObject(target.classList, classList[i]); this._classesDirty = false; this._trueSizeCheck = true; } @@ -418,8 +419,10 @@ define(function(require, exports, module) { if (this._sizeDirty) { if (this._size) { - target.style.width = this.size && this.size[0] === true || this._size[0] === true ? '' : this._size[0] + 'px'; - target.style.height = this.size && this.size[1] === true || this._size[1] === true ? '' : this._size[1] + 'px'; + var resolvedWidth = this.size && this.size[0] === true || this._size[0] === true ? '' : this._size[0] + 'px'; + var resolvedHeight = this.size && this.size[1] === true || this._size[1] === true ? '' : this._size[1] + 'px'; + DOMBuffer.assignProperty(target.style, 'width', resolvedWidth); + DOMBuffer.assignProperty(target.style, 'height', resolvedHeight); } this._eventOutput.emit('resize'); @@ -455,10 +458,10 @@ define(function(require, exports, module) { var target = this._currentTarget; this._eventOutput.emit('recall'); this.recall(target); - target.style.display = 'none'; - target.style.opacity = ''; - target.style.width = ''; - target.style.height = ''; + DOMBuffer.assignProperty(target.style, 'display', 'none'); + DOMBuffer.assignProperty(target.style, 'opacity', ''); + DOMBuffer.assignProperty(target.style, 'width', ''); + DOMBuffer.assignProperty(target.style, 'height', ''); _cleanupStyles.call(this, target); _cleanupAttributes.call(this, target); var classList = this.getClassList(); @@ -467,11 +470,11 @@ define(function(require, exports, module) { if (this.elementClass) { if (this.elementClass instanceof Array) { for (i = 0; i < this.elementClass.length; i++) { - target.classList.remove(this.elementClass[i]); + DOMBuffer.removeFromObject(target.classList, this.elementClass[i]); } } else { - target.classList.remove(this.elementClass); + DOMBuffer.removeFromObject(target.classList, this.elementClass); } } this.detach(target); @@ -489,18 +492,23 @@ define(function(require, exports, module) { Surface.prototype.deploy = function deploy(target) { var content = this.getContent(); if (content instanceof Node) { - while (target.hasChildNodes()) - target.removeChild(target.firstChild); - target.appendChild(content); + var children = target.childNodes || []; + //TODO Confirm that this works + for(var i = 0;i< children.length; i++){ + DOMBuffer.removeChild(target, children[i]); + } + DOMBuffer.appendChild(target, content); + // this.content = target.innerHTML; } else { - /* textContent proved to be faster: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ + /* textContent proved to be faster than innerHTML: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ if(content.includes('<')){ - target.innerHTML = content; + DOMBuffer.assignProperty(target, 'innerHTML', content); } else { - target.textContent = content; + DOMBuffer.assignProperty(target, 'textContent', content); } + // this.content = content; } - this.content = target.innerHTML; + }; @@ -512,16 +520,14 @@ define(function(require, exports, module) { * the next render-cycle. */ Surface.prototype.recall = function recall(target) { - if (!this._contentDirty) { var df = document.createDocumentFragment(); - while (target.hasChildNodes()) { - df.appendChild(target.firstChild); + var children = target.childNodes || []; + //TODO Confirm that this works + for(var i = 0;i< children.length; i++){ + DOMBuffer.appendChild(df, children[i]); } - this.setContent(df); - } - else { this._contentDirty = true; - } + }; /** diff --git a/src/surfaces/InputSurface.js b/src/surfaces/InputSurface.js index 262c248b..60924662 100644 --- a/src/surfaces/InputSurface.js +++ b/src/surfaces/InputSurface.js @@ -9,6 +9,7 @@ define(function(require, exports, module) { var Surface = require('../core/Surface'); + var DOMBuffer = require('../core/DOMBuffer'); /** * A Famo.us surface in the form of an HTML input element. @@ -151,10 +152,11 @@ define(function(require, exports, module) { * @param {Node} target document parent of this container */ InputSurface.prototype.deploy = function deploy(target) { - if (this._placeholder !== '') target.placeholder = this._placeholder; - target.value = this._value; - target.type = this._type; - target.name = this._name; + if (this._placeholder !== '') + DOMBuffer.assignProperty(target, 'placeholder', this._placeholder); + DOMBuffer.assignProperty(target, 'value', this._value); + DOMBuffer.assignProperty(target, 'type', this._type); + DOMBuffer.assignProperty(target, 'name', this._name); }; module.exports = InputSurface; From 6b023fee0bb242fcb2306f855b45c5b108cc6a7e Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 13:07:44 +0200 Subject: [PATCH 10/32] Made the DOMEventHandler use DOMBuffer --- src/core/DOMEventHandler.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index 791eb534..1656739b 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -16,6 +16,7 @@ define(function (require, exports, module) { var DOMEventHandler = {}; var EventEmitter = require('./EventEmitter.js'); + var DOMBuffer = require('./DOMBuffer'); //TODO Add more to complete list var singleElementEvents = [ @@ -36,6 +37,7 @@ define(function (require, exports, module) { if(singleElementEvents.includes(type)){ return element.addEventListener(type, this.eventForwarder); } + DOMBuffer.setAttribute(element, 'data-arvaid', id); element.setAttribute('data-arvaid', id); var eventEmitter = initializedListeners[type]; if(!eventEmitter){ From 8bbf2972bb967332a76794bf4119bf6ab310fb5b Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 13:07:55 +0200 Subject: [PATCH 11/32] Small cleanup of DOMBuffer --- src/core/DOMBuffer.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js index 16c7a976..51a080e1 100644 --- a/src/core/DOMBuffer.js +++ b/src/core/DOMBuffer.js @@ -5,15 +5,16 @@ define(function (require, exports, module) { + /** + * Singleton class optimized for high performance in DOM updates. All DOM updates that are done through this class will + * be cached and can be flushed at the same order the instructions came in. + * + * TODO: Further optimization here could be to introduce a createElement function that returns a placeholder that is + * recognized by the other functions and then we can create all elements at the end of the frame too + * + * @type {{}} + */ var DOMBuffer = {}; - /*var enqueuedAttributes = []; - var enqueuedProperties = []; - var enqueuedAdditions = []; - var enqueuedRemovals = []; - var enqueuedAttributeRemovals = []; - var enqueuedChildRemovals = []; - var enqueuedChildAppendices = []; - var enqueuedInsertBefore = [];*/ var enqueuedOperations = []; From 93ea95414b4aae69a3c816d1d58ad7985afa978b Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 13:09:02 +0200 Subject: [PATCH 12/32] Removed unused origin functionality causing unnecessary updates --- src/core/ElementOutput.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index d0cf2bf8..6d75dd4f 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -205,13 +205,7 @@ define(function(require, exports, module) { return (100 * origin[0]) + '% ' + (100 * origin[1]) + '%'; } - // Directly apply given origin coordinates to the document element as the - // appropriate webkit CSS style. - var _setOrigin = usePrefix ? function(element, origin) { - element.style.webkitTransformOrigin = _formatCSSOrigin(origin); - } : function(element, origin) { - element.style.transformOrigin = _formatCSSOrigin(origin); - }; + // Shrink given document element until it is effectively invisible. var _setInvisible = usePrefix ? function(element) { @@ -274,7 +268,6 @@ define(function(require, exports, module) { this._origin[1] = origin[1]; } else this._origin = null; - _setOrigin(target, this._origin); this._originDirty = false; } From dba82cec4fb7f116f9ba2eda225495ed1bc54227 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 13:12:18 +0200 Subject: [PATCH 13/32] Removed duplicate instruction to set attribute on element --- src/core/DOMEventHandler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index 1656739b..ca88daa3 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -38,7 +38,6 @@ define(function (require, exports, module) { return element.addEventListener(type, this.eventForwarder); } DOMBuffer.setAttribute(element, 'data-arvaid', id); - element.setAttribute('data-arvaid', id); var eventEmitter = initializedListeners[type]; if(!eventEmitter){ eventEmitter = initializedListeners[type] = new EventEmitter(); From 47107a5d2ca202d59a5cd46863213c7c5ac446f8 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 16:33:03 +0200 Subject: [PATCH 14/32] Removed comment that turned out to be inadequate --- src/core/DOMBuffer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js index 51a080e1..1369d18f 100644 --- a/src/core/DOMBuffer.js +++ b/src/core/DOMBuffer.js @@ -9,8 +9,6 @@ define(function (require, exports, module) { * Singleton class optimized for high performance in DOM updates. All DOM updates that are done through this class will * be cached and can be flushed at the same order the instructions came in. * - * TODO: Further optimization here could be to introduce a createElement function that returns a placeholder that is - * recognized by the other functions and then we can create all elements at the end of the frame too * * @type {{}} */ @@ -18,6 +16,7 @@ define(function (require, exports, module) { var enqueuedOperations = []; + DOMBuffer.assignProperty = function (object, property, value) { enqueuedOperations.push({data: [object, property, value], operation: 'assignProperty'}); }; From 5892ca13a0ee3d9bfdb2d3814729773e09a95866 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 16:33:17 +0200 Subject: [PATCH 15/32] Removed commented out allocation code --- src/core/ElementAllocator.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/ElementAllocator.js b/src/core/ElementAllocator.js index fb6b8f5b..8dd373ba 100644 --- a/src/core/ElementAllocator.js +++ b/src/core/ElementAllocator.js @@ -62,6 +62,7 @@ define(function (require, exports, module) { * @private * @method allocate * + * @param {Object} options * @param {String} options.type type of element, e.g. 'div' * @param {Boolean} options.insertFirst Whether it should be allocated from the top instead of the bottom * or at the end. Defaults to false (at the bottom). @@ -74,19 +75,18 @@ define(function (require, exports, module) { var isNested = !!options.isNested; type = type.toLowerCase(); var detachedList = isNested ? this.detachedAllocators : this.detachedHtmlElements; - var result; if (!(type in detachedList)) detachedList[type] = []; var nodeStore = detachedList[type]; var result; - /*if (nodeStore.length > 0 && !insertFirst) { + if (nodeStore.length > 0 && !insertFirst) { result = nodeStore.pop(); } - else {*/ + else { result = this._allocateNewHtmlOutput(type, insertFirst); if (isNested) { result = this._allocateNewAllocator(result); } - // } + } return result; }; From dc015652abfc22dad251f2f189b7c6b2bfee8118 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 16:34:18 +0200 Subject: [PATCH 16/32] Removed code litter --- src/core/ElementOutput.js | 16 ---------------- src/core/Surface.js | 2 -- 2 files changed, 18 deletions(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 0fd8275a..62f6478f 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -296,22 +296,6 @@ define(function(require, exports, module) { */ ElementOutput.prototype.attach = function attach(target) { this._element = target; - - // create an observer instance - var observer = new MutationObserver((mutations) =>{ - if(window.observeAll){ - debugger; - } - - }); - - // configuration of the observer: - var config = { attributes: true, childList: true, characterData: true, attributeOldValue: true }; - - // pass in the target node, as well as the observer options - observer.observe(target, config); - - _addEventListeners.call(this, target); }; diff --git a/src/core/Surface.js b/src/core/Surface.js index 771f9508..6c4f0d3c 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -498,7 +498,6 @@ define(function(require, exports, module) { DOMBuffer.removeChild(target, children[i]); } DOMBuffer.appendChild(target, content); - // this.content = target.innerHTML; } else { /* textContent proved to be faster than innerHTML: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ if(content.includes('<')){ @@ -506,7 +505,6 @@ define(function(require, exports, module) { } else { DOMBuffer.assignProperty(target, 'textContent', content); } - // this.content = content; } }; From 1421262b2d01c5b7ebe7edc82032bee08e04ff86 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Fri, 2 Jun 2017 16:34:18 +0200 Subject: [PATCH 17/32] Cherry picked removing MutationObserver --- src/core/ElementOutput.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 6d75dd4f..a01fb4b1 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -295,22 +295,6 @@ define(function(require, exports, module) { */ ElementOutput.prototype.attach = function attach(target) { this._element = target; - - // create an observer instance - var observer = new MutationObserver((mutations) =>{ - if(window.observeAll){ - debugger; - } - - }); - - // configuration of the observer: - var config = { attributes: true, childList: true, characterData: true, attributeOldValue: true }; - - // pass in the target node, as well as the observer options - observer.observe(target, config); - - _addEventListeners.call(this, target); }; From 3044c15fed338bf5a079638b2bbf22dbe171d5fd Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Tue, 6 Jun 2017 14:58:22 +0200 Subject: [PATCH 18/32] Added protection against exception in DOMBuffer --- src/core/DOMBuffer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js index 1369d18f..4656bf7c 100644 --- a/src/core/DOMBuffer.js +++ b/src/core/DOMBuffer.js @@ -65,7 +65,9 @@ define(function (require, exports, module) { data[0].setAttribute(data[1], data[2]); break; case 'removeChild': - data[0].removeChild(data[1]); + if(data[0].childNodes.length && data[0].contains(data[1])){ + data[0].removeChild(data[1]); + } break; case 'removeAttribute': data[0].removeAttribute(data[1]); From 935af81fbe406f554bd8c81fbede1a8aa3e01518 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Tue, 6 Jun 2017 14:59:43 +0200 Subject: [PATCH 19/32] Added protection against undefined functions in Surface --- src/core/Surface.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/Surface.js b/src/core/Surface.js index bf293215..3f3a50e7 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -493,8 +493,9 @@ define(function(require, exports, module) { target.removeChild(target.firstChild); target.appendChild(content); } else { - /* textContent proved to be faster: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ - if(content.includes('<')){ + + /* textContent proved to be faster than innerHTML: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ + if(content && content.includes && content.includes('<')){ target.innerHTML = content; } else { target.textContent = content; From d0205e61cc9b9690c1477e5d48409cf85a0b10f8 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Tue, 6 Jun 2017 16:01:02 +0200 Subject: [PATCH 20/32] Revert "Removed unused origin functionality causing unnecessary updates" This reverts commit 93ea95414b4aae69a3c816d1d58ad7985afa978b. --- src/core/ElementOutput.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 2006bf09..2afc2072 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -205,7 +205,13 @@ define(function(require, exports, module) { return (100 * origin[0]) + '% ' + (100 * origin[1]) + '%'; } - + // Directly apply given origin coordinates to the document element as the + // appropriate webkit CSS style. + var _setOrigin = usePrefix ? function(element, origin) { + element.style.webkitTransformOrigin = _formatCSSOrigin(origin); + } : function(element, origin) { + element.style.transformOrigin = _formatCSSOrigin(origin); + }; // Shrink given document element until it is effectively invisible. var _setInvisible = usePrefix ? function(element) { @@ -268,6 +274,7 @@ define(function(require, exports, module) { this._origin[1] = origin[1]; } else this._origin = null; + _setOrigin(target, this._origin); this._originDirty = false; } From a0102a742b22adce565795dcc1aaac0115df979f Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Wed, 7 Jun 2017 09:40:59 +0200 Subject: [PATCH 21/32] Added more precise timing function that the engine can use to measure time --- src/core/Engine.js | 20 +++++++++++++++++--- src/utilities/Timer.js | 22 ++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/core/Engine.js b/src/core/Engine.js index 8bda95ee..c2e32afe 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -28,6 +28,16 @@ define(function (require, exports, module) { var ElementAllocator = require('./ElementAllocator'); var EventHandler = require('./EventHandler'); var OptionsManager = require('./OptionsManager'); + var Utility = require('../utilities/Utility'); + + /* Precise function for comparing time stamps*/ + var getTime = (typeof window !== 'undefined' && window.performance && window.performance.now) ? + function() { + return window.performance.now(); + } + : function() { + return Date.now(); + }; var Engine = {}; @@ -40,7 +50,9 @@ define(function (require, exports, module) { var deferQueue = []; - var lastTime = Date.now(); + /* The last timestamp of the previous frame */ + var lastTime = getTime(); + var frameTime; var frameTimeLimit; var loopEnabled = true; @@ -75,7 +87,7 @@ define(function (require, exports, module) { currentFrame++; nextTickFrame = currentFrame; - var currentTime = Date.now(); + var currentTime = getTime(); this._lastFrameTimeDelta = currentTime - lastTime; // skip frame if we're over our framerate cap @@ -93,7 +105,7 @@ define(function (require, exports, module) { while (numFunctions--) (nextTickQueue.shift())(currentFrame); // limit total execution time for deferrable functions - while (deferQueue.length && (Date.now() - currentTime) < MAX_DEFER_FRAME_TIME) { + while (deferQueue.length && (getTime() - currentTime) < MAX_DEFER_FRAME_TIME) { deferQueue.shift().call(this); } @@ -422,6 +434,8 @@ define(function (require, exports, module) { nextTickQueue.push(fn); }; + Engine.now = getTime; + /** * Queue a function to be executed sometime soon, at a time that is * unlikely to affect frame rate. diff --git a/src/utilities/Timer.js b/src/utilities/Timer.js index 7ea87f6e..17669e5f 100644 --- a/src/utilities/Timer.js +++ b/src/utilities/Timer.js @@ -23,13 +23,7 @@ define(function(require, exports, module) { var _event = 'prerender'; - var getTime = (typeof window !== 'undefined' && window.performance && window.performance.now) ? - function() { - return window.performance.now(); - } - : function() { - return Date.now(); - }; + /** * Add a function to be run on every prerender @@ -58,9 +52,9 @@ define(function(require, exports, module) { * @return {function} function passed in as parameter */ function setTimeout(fn, duration) { - var t = getTime(); + var t = FamousEngine.now(); var callback = function() { - var t2 = getTime(); + var t2 = FamousEngine.now(); if (t2 - t >= duration) { fn.apply(this, arguments); FamousEngine.removeListener(_event, callback); @@ -82,12 +76,12 @@ define(function(require, exports, module) { * @return {function} function passed in as parameter */ function setInterval(fn, duration) { - var t = getTime(); + var t = FamousEngine.now(); var callback = function() { - var t2 = getTime(); + var t2 = FamousEngine.now(); if (t2 - t >= duration) { fn.apply(this, arguments); - t = getTime(); + t = FamousEngine.now(); } }; return addTimerFunction(callback); @@ -171,10 +165,10 @@ define(function(require, exports, module) { return function() { ctx = this; args = arguments; - timestamp = getTime(); + timestamp = FamousEngine.now(); var fn = function() { - var last = getTime - timestamp; + var last = FamousEngine.now() - timestamp; if (last < wait) { timeout = setTimeout(fn, wait - last); From 1f777f9e7a96839fe3f53faa81719838601ecd34 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Wed, 7 Jun 2017 09:41:44 +0200 Subject: [PATCH 22/32] Enabled touch move by default to prevent non-passive listener --- src/core/Engine.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/core/Engine.js b/src/core/Engine.js index c2e32afe..971c9d01 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -153,13 +153,20 @@ define(function (require, exports, module) { handleResize(); } - Engine.touchMoveEnabled = false; - - Engine.enableTouchMove = function enableTouchMove() { - if (!this.touchMoveEnabled) { - console.log("Warning: Touch move enabled. Outcomes might be unwated"); + Engine.touchMoveEnabled = true; + + Engine.disableTouchMove = function disableTouchMove() { + if (this.touchMoveEnabled) { + // prevent scrolling via browser + window.addEventListener('touchmove', function (event) { + if (event.target.tagName === 'TEXTAREA' || this.touchMoveEnabled) { + return true; + } else { + event.preventDefault(); + } + }.bind(this), { capture: true, passive: false }); + this.touchMoveEnabled = false; } - this.touchMoveEnabled = true; }; From 80e8af1f4d507dc37b2bf8b05e378b79b3bf0911 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Wed, 7 Jun 2017 12:32:16 +0200 Subject: [PATCH 23/32] Rounded off the z index assignment since it in some cases wasn't set when z index was floating point --- src/core/ElementOutput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 05eddf92..a1a0cf04 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -285,7 +285,7 @@ define(function(require, exports, module) { _setMatrix(target, aaMatrix); /* Since a lot of browsers are buggy, they need the z-index to be set as well besides the 3d transformation * matrix to successfully place things on top of each other*/ - DOMBuffer.assignProperty(target.style, 'zIndex', aaMatrix[14]); + DOMBuffer.assignProperty(target.style, 'zIndex', Math.round(aaMatrix[14])); this._transformDirty = false; } }; From 6b0556dbf205defe56049b470ef2f16e375a47b7 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 09:47:33 +0200 Subject: [PATCH 24/32] Added missing non-bubbling events to DOMEventHandler and made the removeEventListener work correctly for non-bubbling events --- src/core/DOMEventHandler.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index 791eb534..e680aa79 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -19,7 +19,7 @@ define(function (require, exports, module) { //TODO Add more to complete list var singleElementEvents = [ - 'submit', 'focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll' + 'submit', 'focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll', 'mousewheel', 'wheel' ]; var initializedListeners = {}; @@ -34,7 +34,7 @@ define(function (require, exports, module) { } if(singleElementEvents.includes(type)){ - return element.addEventListener(type, this.eventForwarder); + return element.addEventListener(type, callback); } element.setAttribute('data-arvaid', id); var eventEmitter = initializedListeners[type]; @@ -51,7 +51,10 @@ define(function (require, exports, module) { }; - DOMEventHandler.removeEventListener = function(id, type, callback) { + DOMEventHandler.removeEventListener = function(element, id, type, callback) { + if(singleElementEvents.includes(type)){ + return element.addEventListener(type, callback); + } if(initializedListeners[type]){ initializedListeners[type].removeListener(id, callback); } From fba808488c90bcaac8ed8985b5974876076ff34f Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 09:47:51 +0200 Subject: [PATCH 25/32] Changed the call to removeEventListener to reflect change in DOMEventHandler --- src/core/ElementOutput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ElementOutput.js b/src/core/ElementOutput.js index 2afc2072..a11714c4 100644 --- a/src/core/ElementOutput.js +++ b/src/core/ElementOutput.js @@ -151,7 +151,7 @@ define(function(require, exports, module) { // document element. This occurs just before detach from the document. function _removeEventListeners(target) { for (var i in this._eventOutput.listeners) { - DOMEventHandler.removeEventListener(this.id, i, this.eventForwarder) + DOMEventHandler.removeEventListener(target, this.id, i, this.eventForwarder) } } From 915246fe3a633fbe21a585efa1c35cbf7304b2b6 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 09:48:10 +0200 Subject: [PATCH 26/32] Removed unnecessary Array.from call in EventEmitter --- src/core/EventEmitter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/EventEmitter.js b/src/core/EventEmitter.js index 300823d9..3b0d7b9e 100644 --- a/src/core/EventEmitter.js +++ b/src/core/EventEmitter.js @@ -41,7 +41,6 @@ define(function(require, exports, module) { } var handlers = this.listeners[type]; if (handlers) { - handlers = Array.from(handlers); for (var i = 0; i < handlers.length; i++) { handlers[i].apply(this._owner, args); } From f45990c3438fb80e52b2395efcc2a710cce77e9c Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 11:18:13 +0200 Subject: [PATCH 27/32] Added propagation of data-arvaid to the descendants of the surface --- src/core/DOMBuffer.js | 13 +++++++++++++ src/core/Surface.js | 1 + 2 files changed, 14 insertions(+) diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js index 4656bf7c..a9316ce7 100644 --- a/src/core/DOMBuffer.js +++ b/src/core/DOMBuffer.js @@ -29,6 +29,10 @@ define(function (require, exports, module) { enqueuedOperations.push({data: [object, value], operation: 'addToObject'}); }; + DOMBuffer.setAttributeOnDescendants = function (element, attribute, attributeValue) { + enqueuedOperations.push({data: [element, attribute, attributeValue], operation: 'setAttributeOnDescendants'}); + }; + DOMBuffer.removeFromObject = function (object, attribute) { enqueuedOperations.push({data: [object, attribute], operation: 'removeFromObject'}); }; @@ -81,6 +85,15 @@ define(function (require, exports, module) { case 'assignProperty': data[0][data[1]] = data[2]; break; + case 'setAttributeOnDescendants': + /* Gets all the descendants for element + * https://stackoverflow.com/questions/26325278/how-can-i-get-all-descendant-elements-for-parent-container + * */ + var descendants = data[0].querySelectorAll("*"); + for(var i=0; i < descendants.length; i++){ + descendants[i].setAttribute(data[1], data[2]); + } + break; } } enqueuedOperations = []; diff --git a/src/core/Surface.js b/src/core/Surface.js index 9789a8f2..3ed7dbf6 100644 --- a/src/core/Surface.js +++ b/src/core/Surface.js @@ -504,6 +504,7 @@ define(function (require, exports, module) { /* textContent proved to be faster than innerHTML: https://jsperf.com/innerhtml-vs-textcontent-with-checks/1 */ if (content && content.includes && content.includes('<')) { DOMBuffer.assignProperty(target, 'innerHTML', content); + DOMBuffer.setAttributeOnDescendants(target, 'data-arvaid', this.id); } else { DOMBuffer.assignProperty(target, 'textContent', content); } From 5a7191b1d33cd8e42e8dd246ed356f8f8f6030bb Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 12:33:02 +0200 Subject: [PATCH 28/32] Adjusted whitepsace in DOMBuffer --- src/core/DOMBuffer.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/core/DOMBuffer.js b/src/core/DOMBuffer.js index a9316ce7..af106a2b 100644 --- a/src/core/DOMBuffer.js +++ b/src/core/DOMBuffer.js @@ -16,49 +16,48 @@ define(function (require, exports, module) { var enqueuedOperations = []; - DOMBuffer.assignProperty = function (object, property, value) { - enqueuedOperations.push({data: [object, property, value], operation: 'assignProperty'}); + enqueuedOperations.push({ data: [object, property, value], operation: 'assignProperty' }); }; DOMBuffer.setAttribute = function (element, attribute, value) { - enqueuedOperations.push({data: [element, attribute, value], operation: 'setAttribute'}); + enqueuedOperations.push({ data: [element, attribute, value], operation: 'setAttribute' }); }; DOMBuffer.addToObject = function (object, value) { - enqueuedOperations.push({data: [object, value], operation: 'addToObject'}); + enqueuedOperations.push({ data: [object, value], operation: 'addToObject' }); }; DOMBuffer.setAttributeOnDescendants = function (element, attribute, attributeValue) { - enqueuedOperations.push({data: [element, attribute, attributeValue], operation: 'setAttributeOnDescendants'}); + enqueuedOperations.push({ data: [element, attribute, attributeValue], operation: 'setAttributeOnDescendants' }); }; DOMBuffer.removeFromObject = function (object, attribute) { - enqueuedOperations.push({data: [object, attribute], operation: 'removeFromObject'}); + enqueuedOperations.push({ data: [object, attribute], operation: 'removeFromObject' }); }; DOMBuffer.removeAttribute = function (element, attribute) { - enqueuedOperations.push({data: [element, attribute], operation: 'removeAttribute'}); + enqueuedOperations.push({ data: [element, attribute], operation: 'removeAttribute' }); }; DOMBuffer.removeChild = function (parent, childToRemove) { - enqueuedOperations.push({data: [parent, childToRemove], operation: 'removeChild'}); + enqueuedOperations.push({ data: [parent, childToRemove], operation: 'removeChild' }); }; DOMBuffer.appendChild = function (parent, childToAppend) { - enqueuedOperations.push({data: [parent, childToAppend], operation: 'appendChild'}); + enqueuedOperations.push({ data: [parent, childToAppend], operation: 'appendChild' }); }; DOMBuffer.insertBefore = function (parent, childBefore, childToInsert) { - enqueuedOperations.push({data: [parent, childBefore, childToInsert], operation: 'insertBefore'}); + enqueuedOperations.push({ data: [parent, childBefore, childToInsert], operation: 'insertBefore' }); }; DOMBuffer.flushUpdates = function () { - for(var index = 0; index < enqueuedOperations.length ; index++){ + for (var index = 0; index < enqueuedOperations.length; index++) { var enqueuedOperation = enqueuedOperations[index]; var operationName = enqueuedOperation.operation; var data = enqueuedOperation.data; - switch (operationName){ + switch (operationName) { case 'appendChild': data[0].appendChild(data[1]); break; @@ -69,7 +68,7 @@ define(function (require, exports, module) { data[0].setAttribute(data[1], data[2]); break; case 'removeChild': - if(data[0].childNodes.length && data[0].contains(data[1])){ + if (data[0].childNodes.length && data[0].contains(data[1])) { data[0].removeChild(data[1]); } break; @@ -90,14 +89,14 @@ define(function (require, exports, module) { * https://stackoverflow.com/questions/26325278/how-can-i-get-all-descendant-elements-for-parent-container * */ var descendants = data[0].querySelectorAll("*"); - for(var i=0; i < descendants.length; i++){ + for (var i = 0; i < descendants.length; i++) { descendants[i].setAttribute(data[1], data[2]); } break; } } enqueuedOperations = []; - }; + }; module.exports = DOMBuffer; }); From 9e50b6c797dc8823b48f41c52618f82cc3af5be6 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 12:34:20 +0200 Subject: [PATCH 29/32] Made DOMEventHandler look at the relatedTarget as well --- src/core/DOMEventHandler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index 386d0e95..78814a87 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -42,7 +42,8 @@ define(function (require, exports, module) { if(!eventEmitter){ eventEmitter = initializedListeners[type] = new EventEmitter(); window.addEventListener(type, function (event) { - var recievedID = event.target && event.target.getAttribute && event.target.getAttribute('data-arvaid'); + var target = event.relatedTarget || event.target; + var recievedID = target && target.getAttribute && target.getAttribute('data-arvaid'); if(recievedID){ eventEmitter.emit(recievedID, event); } From b25d6679d56c8513e397a419ae6b38bd9c867aa5 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 12:47:00 +0200 Subject: [PATCH 30/32] Added touch events to native events to make it work with browser emulation of device --- src/core/DOMEventHandler.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index 78814a87..3f877218 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -26,7 +26,10 @@ define(function (require, exports, module) { var initializedListeners = {}; DOMEventHandler.isNativeEvent = function(eventName) { - return typeof document.body["on" + eventName] !== "undefined"; + return typeof document.body["on" + eventName] !== "undefined" + || + /* Needed because otherwise not able to use mobile emulation in browser! */ + ['touchmove', 'touchstart', 'touchend'].includes(eventName); }; DOMEventHandler.addEventListener = function(id, element, type, callback){ From 246767def1a02c51913fbf65b689b0af1ca83d63 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 12:47:13 +0200 Subject: [PATCH 31/32] Removed faulty single-target events --- src/core/DOMEventHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/DOMEventHandler.js b/src/core/DOMEventHandler.js index 3f877218..7e46e680 100644 --- a/src/core/DOMEventHandler.js +++ b/src/core/DOMEventHandler.js @@ -20,7 +20,7 @@ define(function (require, exports, module) { //TODO Add more to complete list var singleElementEvents = [ - 'submit', 'focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll', 'mousewheel', 'wheel' + 'submit', 'focus', 'blur', 'load', 'unload', 'change', 'reset', 'scroll' ]; var initializedListeners = {}; From d9d2e138750496b923d82c0bb8342a345bac6333 Mon Sep 17 00:00:00 2001 From: Karl Lundfall Date: Thu, 8 Jun 2017 16:48:07 +0200 Subject: [PATCH 32/32] Added priority levels to the engine --- src/core/Engine.js | 56 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/core/Engine.js b/src/core/Engine.js index 95087262..16d2f5b8 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -72,6 +72,13 @@ define(function (require, exports, module) { /** @const */ var MAX_DEFER_FRAME_TIME = 10; + + Engine.PriorityLevels = { + critical: Infinity, + normal: 130, + generous: 0 + }; + /** * Inside requestAnimationFrame loop, step() is called, which: * calculates current FPS (throttling loop if it is over limit set in setFPSCap), @@ -94,7 +101,15 @@ define(function (require, exports, module) { // skip frame if we're over our framerate cap if (frameTimeLimit && this._lastFrameTimeDelta < frameTimeLimit) return; - var i = 0; + this._priorityLevel = Infinity; + var priorityLevels = Object.keys(Engine.PriorityLevels); + for (var i = 0; i < priorityLevels.length; i++) { + var priority = priorityLevels[i]; + var priorityLevelCriteria = Engine.PriorityLevels[priority]; + if (this._lastFrameTimeDelta < priorityLevelCriteria && priorityLevelCriteria <= this._priorityLevel){ + this._priorityLevel = priorityLevelCriteria; + } + } frameTime = currentTime - lastTime; lastTime = currentTime; @@ -110,13 +125,43 @@ define(function (require, exports, module) { deferQueue.shift().call(this); } - for (i = 0; i < contexts.length; i++) contexts[i].update(); + for (var i = 0; i < contexts.length; i++) contexts[i].update(); DOMBuffer.flushUpdates(); eventHandler.emit('postrender'); + + }; + /** + * @example + * + * Engine.restrictAnimations({ + * size: Engine.PriorityLevel.critical, + * opacity: Engine.PriorityLevel.critical + * }) + * + * Instructs the engine to disable the animations for the different properties passed. + * + * @param options + */ + Engine.restrictAnimations = function disableAnimationsWhen(options) { + this._disableAnimationSpec = options; + }; + + Engine.shouldPropertyAnimate = function shouldPropertyAnimate(propertyName){ + if(!this._disableAnimationSpec){ + return true; + } + var priorityLevel = this._disableAnimationSpec[propertyName]; + if(priorityLevel === undefined){ + return true; + } + return this._priorityLevel < priorityLevel; + }; + + Engine.getFrameTimeDelta = function getFrameTimeDelta() { return this._lastFrameTimeDelta; }; @@ -158,6 +203,9 @@ define(function (require, exports, module) { Engine.touchMoveEnabled = true; + Engine.getPriorityLevel = function () { + return this._priorityLevel; + }; Engine.disableTouchMove = function disableTouchMove() { if (this.touchMoveEnabled) { // prevent scrolling via browser @@ -181,6 +229,7 @@ define(function (require, exports, module) { * @method initialize */ function initialize() { + // prevent scrolling via browser window.addEventListener('touchmove', function (event) { if (event.target.tagName === 'TEXTAREA' || this.touchMoveEnabled) { @@ -364,6 +413,9 @@ define(function (require, exports, module) { * @return {Context} new Context within el */ Engine.createContext = function createContext(el) { + + this._priorityLevel = Engine.PriorityLevels.critical; + if (!initialized && options.appMode) Engine.nextTick(initialize.bind(this)); var needMountContainer = false;