From 4b715ea1c78136ad3057d8ccfe8a030e5c34a161 Mon Sep 17 00:00:00 2001 From: Madana Gopal Thirumalai Date: Wed, 29 Jan 2020 08:18:36 +0530 Subject: [PATCH] sample apps and supporting files --- rectApp.js | 23765 ++++++++++++++++++++++++ shaderApp.js | 23770 ++++++++++++++++++++++++ simpleImageApp.js | 23761 ++++++++++++++++++++++++ static-ux/fonts/Material-Icons.ttf | Bin 0 -> 128180 bytes static-ux/fonts/roboto-regular.ttf | Bin 0 -> 140752 bytes static-ux/tools/player/img/next.png | Bin 0 -> 452 bytes static-ux/tools/player/img/pause.png | Bin 0 -> 157 bytes static-ux/tools/player/img/play.png | Bin 0 -> 440 bytes static-ux/tools/player/img/prev.png | Bin 0 -> 424 bytes static/._background.png | Bin 0 -> 301 bytes static/._rockies.jpeg | Bin 0 -> 301 bytes static/background.png | Bin 0 -> 440067 bytes static/ll_image.png | Bin 0 -> 15573 bytes static/rockies.jpeg | Bin 0 -> 74469 bytes transitionApp.js | 23821 +++++++++++++++++++++++++ 15 files changed, 95117 insertions(+) create mode 100644 rectApp.js create mode 100644 shaderApp.js create mode 100644 simpleImageApp.js create mode 100644 static-ux/fonts/Material-Icons.ttf create mode 100644 static-ux/fonts/roboto-regular.ttf create mode 100644 static-ux/tools/player/img/next.png create mode 100644 static-ux/tools/player/img/pause.png create mode 100644 static-ux/tools/player/img/play.png create mode 100644 static-ux/tools/player/img/prev.png create mode 100644 static/._background.png create mode 100644 static/._rockies.jpeg create mode 100644 static/background.png create mode 100644 static/ll_image.png create mode 100644 static/rockies.jpeg create mode 100644 transitionApp.js diff --git a/rectApp.js b/rectApp.js new file mode 100644 index 0000000..b3a778e --- /dev/null +++ b/rectApp.js @@ -0,0 +1,23765 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +require('fs'); +require('http'); +require('https'); +var fetch = _interopDefault(require('node-fetch')); + +class StageUtils { + + static mergeNumbers(v1, v2, p) { + return v1 * p + v2 * (1 - p); + }; + + static rgb(r, g, b) { + return (r << 16) + (g << 8) + b + (255 * 16777216); + }; + + static rgba(r, g, b, a) { + return (r << 16) + (g << 8) + b + (((a * 255) | 0) * 16777216); + }; + + static getRgbString(color) { + let r = ((color / 65536) | 0) % 256; + let g = ((color / 256) | 0) % 256; + let b = color % 256; + return 'rgb(' + r + ',' + g + ',' + b + ')'; + }; + + static getRgbaString(color) { + let r = ((color / 65536) | 0) % 256; + let g = ((color / 256) | 0) % 256; + let b = color % 256; + let a = ((color / 16777216) | 0) / 255; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(4) + ')'; + }; + + static getRgbaStringFromArray(color) { + let r = Math.floor(color[0] * 255); + let g = Math.floor(color[1] * 255); + let b = Math.floor(color[2] * 255); + let a = Math.floor(color[3] * 255) / 255; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(4) + ')'; + }; + + static getRgbaComponentsNormalized(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r / 255, g / 255, b / 255, a / 255]; + }; + + static getRgbComponentsNormalized(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + return [r / 255, g / 255, b / 255]; + }; + + static getRgbaComponents(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r, g, b, a]; + }; + + static getArgbNumber(rgba) { + rgba[0] = Math.max(0, Math.min(255, rgba[0])); + rgba[1] = Math.max(0, Math.min(255, rgba[1])); + rgba[2] = Math.max(0, Math.min(255, rgba[2])); + rgba[3] = Math.max(0, Math.min(255, rgba[3])); + let v = ((rgba[3] | 0) << 24) + ((rgba[0] | 0) << 16) + ((rgba[1] | 0) << 8) + (rgba[2] | 0); + if (v < 0) { + v = 0xFFFFFFFF + v + 1; + } + return v; + }; + + static mergeColors(c1, c2, p) { + let r1 = ((c1 / 65536) | 0) % 256; + let g1 = ((c1 / 256) | 0) % 256; + let b1 = c1 % 256; + let a1 = ((c1 / 16777216) | 0); + + let r2 = ((c2 / 65536) | 0) % 256; + let g2 = ((c2 / 256) | 0) % 256; + let b2 = c2 % 256; + let a2 = ((c2 / 16777216) | 0); + + let r = r1 * p + r2 * (1 - p); + let g = g1 * p + g2 * (1 - p); + let b = b1 * p + b2 * (1 - p); + let a = a1 * p + a2 * (1 - p); + + return Math.round(a) * 16777216 + Math.round(r) * 65536 + Math.round(g) * 256 + Math.round(b); + }; + + static mergeMultiColors(c, p) { + let r = 0, g = 0, b = 0, a = 0, t = 0; + let n = c.length; + for (let i = 0; i < n; i++) { + let r1 = ((c[i] / 65536) | 0) % 256; + let g1 = ((c[i] / 256) | 0) % 256; + let b1 = c[i] % 256; + let a1 = ((c[i] / 16777216) | 0); + r += r1 * p[i]; + g += g1 * p[i]; + b += b1 * p[i]; + a += a1 * p[i]; + t += p[i]; + } + + t = 1 / t; + return Math.round(a * t) * 16777216 + Math.round(r * t) * 65536 + Math.round(g * t) * 256 + Math.round(b * t); + }; + + static mergeMultiColorsEqual(c) { + let r = 0, g = 0, b = 0, a = 0, t = 0; + let n = c.length; + for (let i = 0; i < n; i++) { + let r1 = ((c[i] / 65536) | 0) % 256; + let g1 = ((c[i] / 256) | 0) % 256; + let b1 = c[i] % 256; + let a1 = ((c[i] / 16777216) | 0); + r += r1; + g += g1; + b += b1; + a += a1; + t += 1.0; + } + + t = 1 / t; + return Math.round(a * t) * 16777216 + Math.round(r * t) * 65536 + Math.round(g * t) * 256 + Math.round(b * t); + }; + + static mergeColorAlpha(c, alpha) { + let a = ((c / 16777216 | 0) * alpha) | 0; + return (((((c >> 16) & 0xff) * a) / 255) & 0xff) + + ((((c & 0xff00) * a) / 255) & 0xff00) + + (((((c & 0xff) << 16) * a) / 255) & 0xff0000) + + (a << 24); + }; + + static rad(deg) { + return deg * (Math.PI / 180); + }; + + static getTimingBezier(a, b, c, d) { + let xc = 3.0 * a; + let xb = 3.0 * (c - a) - xc; + let xa = 1.0 - xc - xb; + let yc = 3.0 * b; + let yb = 3.0 * (d - b) - yc; + let ya = 1.0 - yc - yb; + + return function (time) { + if (time >= 1.0) { + return 1; + } + if (time <= 0) { + return 0; + } + + let t = 0.5, cbx, cbxd, dx; + + for (let it = 0; it < 20; it++) { + cbx = t * (t * (t * xa + xb) + xc); + dx = time - cbx; + if (dx > -1e-8 && dx < 1e-8) { + return t * (t * (t * ya + yb) + yc); + } + + // Cubic bezier derivative. + cbxd = t * (t * (3 * xa) + 2 * xb) + xc; + + if (cbxd > 1e-10 && cbxd < 1e-10) { + // Problematic. Fall back to binary search method. + break; + } + + t += dx / cbxd; + } + + // Fallback: binary search method. This is more reliable when there are near-0 slopes. + let minT = 0; + let maxT = 1; + for (let it = 0; it < 20; it++) { + t = 0.5 * (minT + maxT); + + cbx = t * (t * (t * xa + xb) + xc); + + dx = time - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc); + } + + if (dx < 0) { + maxT = t; + } else { + minT = t; + } + } + + }; + }; + + static getTimingFunction(str) { + switch (str) { + case "linear": + return function (time) { + return time + }; + case "ease": + return StageUtils.getTimingBezier(0.25, 0.1, 0.25, 1.0); + case "ease-in": + return StageUtils.getTimingBezier(0.42, 0, 1.0, 1.0); + case "ease-out": + return StageUtils.getTimingBezier(0, 0, 0.58, 1.0); + case "ease-in-out": + return StageUtils.getTimingBezier(0.42, 0, 0.58, 1.0); + case "step-start": + return function () { + return 1 + }; + case "step-end": + return function (time) { + return time === 1 ? 1 : 0; + }; + default: + let s = "cubic-bezier("; + if (str && str.indexOf(s) === 0) { + let parts = str.substr(s.length, str.length - s.length - 1).split(","); + if (parts.length !== 4) { + console.warn("Unknown timing function: " + str); + + // Fallback: use linear. + return function (time) { + return time + }; + } + let a = parseFloat(parts[0]); + let b = parseFloat(parts[1]); + let c = parseFloat(parts[2]); + let d = parseFloat(parts[3]); + if (isNaN(a) || isNaN(b) || isNaN(c) || isNaN(d)) { + console.warn("Unknown timing function: " + str); + // Fallback: use linear. + return function (time) { + return time + }; + } + + return StageUtils.getTimingBezier(a, b, c, d); + } else { + console.warn("Unknown timing function: " + str); + // Fallback: use linear. + return function (time) { + return time + }; + } + } + }; + +} + +class Utils { + + static isFunction(value) { + return typeof value === 'function'; + } + + static isNumber(value) { + return typeof value === 'number'; + } + + static isInteger(value) { + return (typeof value === 'number' && (value % 1) === 0); + } + + static isBoolean(value) { + return value === true || value === false; + } + + static isString(value) { + return typeof value === 'string'; + } + + static clone(v) { + if (Utils.isObjectLiteral(v) || Array.isArray(v)) { + return Utils.getDeepClone(v); + } else { + // Copy by value. + return v; + } + } + + static cloneObjShallow(obj) { + let keys = Object.keys(obj); + let clone = {}; + for (let i = 0; i < keys.length; i++) { + clone[keys[i]] = obj[keys[i]]; + } + return clone; + } + + static merge(obj1, obj2) { + let keys = Object.keys(obj2); + for (let i = 0; i < keys.length; i++) { + obj1[keys[i]] = obj2[keys[i]]; + } + return obj1; + } + + static isObject(value) { + let type = typeof value; + return !!value && (type === 'object' || type === 'function'); + } + + static isPlainObject(value) { + let type = typeof value; + return !!value && (type === 'object'); + } + + static isObjectLiteral(value){ + return typeof value === 'object' && value && value.constructor === Object; + } + + static getArrayIndex(index, arr) { + return Utils.getModuloIndex(index, arr.length); + } + + static getModuloIndex(index, len) { + if (len === 0) return index; + while (index < 0) { + index += Math.ceil(-index / len) * len; + } + index = index % len; + return index; + } + + static getDeepClone(obj) { + let i, c; + if (Utils.isFunction(obj)) { + // Copy functions by reference. + return obj; + } + if (Array.isArray(obj)) { + c = []; + let keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + c[keys[i]] = Utils.getDeepClone(obj[keys[i]]); + } + return c; + } else if (Utils.isObject(obj)) { + c = {}; + let keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + c[keys[i]] = Utils.getDeepClone(obj[keys[i]]); + } + return c; + } else { + return obj; + } + } + + static equalValues(v1, v2) { + if ((typeof v1) !== (typeof v2)) return false; + if (Utils.isObjectLiteral(v1)) { + return Utils.isObjectLiteral(v2) && Utils.equalObjectLiterals(v1, v2); + } else if (Array.isArray(v1)) { + return Array.isArray(v2) && Utils.equalArrays(v1, v2); + } else { + return v1 === v2; + } + } + + static equalObjectLiterals(obj1, obj2) { + let keys1 = Object.keys(obj1); + let keys2 = Object.keys(obj2); + if (keys1.length !== keys2.length) { + return false; + } + + for (let i = 0, n = keys1.length; i < n; i++) { + const k1 = keys1[i]; + const k2 = keys2[i]; + if (k1 !== k2) { + return false; + } + + const v1 = obj1[k1]; + const v2 = obj2[k2]; + + if (!Utils.equalValues(v1, v2)) { + return false; + } + } + + return true; + } + + static equalArrays(v1, v2) { + if (v1.length !== v2.length) { + return false; + } + for (let i = 0, n = v1.length; i < n; i++) { + if (!this.equalValues(v1[i], v2[i])) { + return false; + } + } + + return true; + } + + static setToArray(s) { + let result = []; + s.forEach(function (value) { + result.push(value); + }); + return result; + } + + static iteratorToArray(iterator) { + let result = []; + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + result.push(iteratorResult.value); + iteratorResult = iterator.next(); + } + return result; + } + + static isUcChar(charcode) { + return charcode >= 65 && charcode <= 90; + } + +} + +Utils.isNode = (typeof window === "undefined"); +Utils.isWeb = (typeof window !== "undefined"); +Utils.isWPE = Utils.isWeb && (navigator.userAgent.indexOf("WPE") !== -1); +Utils.isSpark = (typeof window === "undefined") && (typeof sparkscene !== "undefined"); + +class Base { + + static defaultSetter(obj, name, value) { + obj[name] = value; + } + + static patchObject(obj, settings) { + if (!Utils.isObjectLiteral(settings)) { + console.error("Settings must be object literal"); + } else { + let names = Object.keys(settings); + for (let i = 0, n = names.length; i < n; i++) { + let name = names[i]; + + this.patchObjectProperty(obj, name, settings[name]); + } + } + } + + static patchObjectProperty(obj, name, value) { + let setter = obj.setSetting || Base.defaultSetter; + + if (name.charAt(0) === "_") { + // Disallow patching private variables. + if (name !== "__create") { + console.error("Patch of private property '" + name + "' is not allowed"); + } + } else if (name !== "type") { + // Type is a reserved keyword to specify the class type on creation. + if (Utils.isFunction(value) && value.__local) { + // Local function (Base.local(s => s.something)) + value = value.__local(obj); + } + + setter(obj, name, value); + } + } + + static local(func) { + // This function can be used as an object setting, which is called with the target object. + func.__local = true; + } + + +} + +class SpacingCalculator { + + static getSpacing(mode, numberOfItems, remainingSpace) { + const itemGaps = (numberOfItems - 1); + let spacePerGap; + + let spacingBefore, spacingBetween; + + switch(mode) { + case "flex-start": + spacingBefore = 0; + spacingBetween = 0; + break; + case "flex-end": + spacingBefore = remainingSpace; + spacingBetween = 0; + break; + case "center": + spacingBefore = remainingSpace / 2; + spacingBetween = 0; + break; + case "space-between": + spacingBefore = 0; + spacingBetween = Math.max(0, remainingSpace) / itemGaps; + break; + case "space-around": + if (remainingSpace < 0) { + return this.getSpacing("center", numberOfItems, remainingSpace); + } else { + spacePerGap = remainingSpace / (itemGaps + 1); + spacingBefore = 0.5 * spacePerGap; + spacingBetween = spacePerGap; + } + break; + case "space-evenly": + if (remainingSpace < 0) { + return this.getSpacing("center", numberOfItems, remainingSpace); + } else { + spacePerGap = remainingSpace / (itemGaps + 2); + spacingBefore = spacePerGap; + spacingBetween = spacePerGap; + } + break; + case "stretch": + spacingBefore = 0; + spacingBetween = 0; + break; + default: + throw new Error("Unknown mode: " + mode); + } + + return {spacingBefore, spacingBetween} + } + +} + +class ContentAligner { + + constructor(layout) { + this._layout = layout; + this._totalCrossAxisSize = 0; + } + + get _lines() { + return this._layout._lines; + } + + init() { + this._totalCrossAxisSize = this._getTotalCrossAxisSize(); + } + + align() { + const crossAxisSize = this._layout.crossAxisSize; + const remainingSpace = crossAxisSize - this._totalCrossAxisSize; + + const {spacingBefore, spacingBetween} = this._getSpacing(remainingSpace); + + const lines = this._lines; + + const mode = this._layout._flexContainer.alignContent; + let growSize = 0; + if (mode === "stretch" && lines.length && (remainingSpace > 0)) { + growSize = remainingSpace / lines.length; + } + + let currentPos = spacingBefore; + for (let i = 0, n = lines.length; i < n; i++) { + const crossAxisLayoutOffset = currentPos; + const aligner = lines[i].createItemAligner(); + + let finalCrossAxisLayoutSize = lines[i].crossAxisLayoutSize + growSize; + + aligner.setCrossAxisLayoutSize(finalCrossAxisLayoutSize); + aligner.setCrossAxisLayoutOffset(crossAxisLayoutOffset); + + aligner.align(); + + if (aligner.recursiveResizeOccured) { + lines[i].setItemPositions(); + } + + currentPos += finalCrossAxisLayoutSize; + currentPos += spacingBetween; + } + } + + get totalCrossAxisSize() { + return this._totalCrossAxisSize; + } + + _getTotalCrossAxisSize() { + const lines = this._lines; + let total = 0; + for (let i = 0, n = lines.length; i < n; i++) { + const line = lines[i]; + total += line.crossAxisLayoutSize; + } + return total; + } + + _getSpacing(remainingSpace) { + const mode = this._layout._flexContainer.alignContent; + const numberOfItems = this._lines.length; + return SpacingCalculator.getSpacing(mode, numberOfItems, remainingSpace); + } + +} + +class FlexUtils { + + static getParentAxisSizeWithPadding(item, horizontal) { + const target = item.target; + const parent = target.getParent(); + if (!parent) { + return 0; + } else { + const flexParent = item.flexParent; + if (flexParent) { + // Use pending layout size. + return this.getAxisLayoutSize(flexParent, horizontal) + this.getTotalPadding(flexParent, horizontal); + } else { + // Use 'absolute' size. + return horizontal ? parent.w : parent.h; + } + } + } + + static getRelAxisSize(item, horizontal) { + if (horizontal) { + if (item.funcW) { + if (this._allowRelAxisSizeFunction(item, true)) { + return item.funcW(this.getParentAxisSizeWithPadding(item, true)); + } else { + return 0; + } + } else { + return item.originalWidth; + } + } else { + if (item.funcH) { + if (this._allowRelAxisSizeFunction(item, false)) { + return item.funcH(this.getParentAxisSizeWithPadding(item, false)); + } else { + return 0; + } + } else { + return item.originalHeight; + } + } + } + + static _allowRelAxisSizeFunction(item, horizontal) { + const flexParent = item.flexParent; + if (flexParent && flexParent._flex._layout.isAxisFitToContents(horizontal)) { + // We don't allow relative width on fit-to-contents because it leads to conflicts. + return false; + } + return true; + } + + static isZeroAxisSize(item, horizontal) { + if (horizontal) { + return !item.originalWidth && !item.funcW; + } else { + return !item.originalHeight && !item.funcH; + } + } + + static getAxisLayoutPos(item, horizontal) { + return horizontal ? item.x : item.y; + } + + static getAxisLayoutSize(item, horizontal) { + return horizontal ? item.w : item.h; + } + + static setAxisLayoutPos(item, horizontal, pos) { + if (horizontal) { + item.x = pos; + } else { + item.y = pos; + } + } + + static setAxisLayoutSize(item, horizontal, size) { + if (horizontal) { + item.w = size; + } else { + item.h = size; + } + } + + static getAxisMinSize(item, horizontal) { + let minSize = this.getPlainAxisMinSize(item, horizontal); + + let flexItemMinSize = 0; + if (item.isFlexItemEnabled()) { + flexItemMinSize = item._flexItem._getMinSizeSetting(horizontal); + } + + const hasLimitedMinSize = (flexItemMinSize > 0); + if (hasLimitedMinSize) { + minSize = Math.max(minSize, flexItemMinSize); + } + return minSize; + } + + static getPlainAxisMinSize(item, horizontal) { + if (item.isFlexEnabled()) { + return item._flex._layout.getAxisMinSize(horizontal); + } else { + const isShrinkable = (item.flexItem.shrink !== 0); + if (isShrinkable) { + return 0; + } else { + return this.getRelAxisSize(item, horizontal); + } + } + } + + static resizeAxis(item, horizontal, size) { + if (item.isFlexEnabled()) { + const isMainAxis = (item._flex._horizontal === horizontal); + if (isMainAxis) { + item._flex._layout.resizeMainAxis(size); + } else { + item._flex._layout.resizeCrossAxis(size); + } + } else { + this.setAxisLayoutSize(item, horizontal, size); + } + } + + + static getPaddingOffset(item, horizontal) { + if (item.isFlexEnabled()) { + const flex = item._flex; + if (horizontal) { + return flex.paddingLeft; + } else { + return flex.paddingTop; + } + } else { + return 0; + } + } + + static getTotalPadding(item, horizontal) { + if (item.isFlexEnabled()) { + const flex = item._flex; + if (horizontal) { + return flex.paddingRight + flex.paddingLeft; + } else { + return flex.paddingTop + flex.paddingBottom; + } + } else { + return 0; + } + } + + static getMarginOffset(item, horizontal) { + const flexItem = item.flexItem; + if (flexItem) { + if (horizontal) { + return flexItem.marginLeft; + } else { + return flexItem.marginTop; + } + } else { + return 0; + } + } + + static getTotalMargin(item, horizontal) { + const flexItem = item.flexItem; + if (flexItem) { + if (horizontal) { + return flexItem.marginRight + flexItem.marginLeft; + } else { + return flexItem.marginTop + flexItem.marginBottom; + } + } else { + return 0; + } + } + +} + +class SizeShrinker { + + constructor(line) { + this._line = line; + this._amountRemaining = 0; + this._shrunkSize = 0; + } + + shrink(amount) { + this._shrunkSize = 0; + + this._amountRemaining = amount; + let totalShrinkAmount = this._getTotalShrinkAmount(); + if (totalShrinkAmount) { + const items = this._line.items; + do { + let amountPerShrink = this._amountRemaining / totalShrinkAmount; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + const shrinkAmount = flexItem.shrink; + const isShrinkableItem = (shrinkAmount > 0); + if (isShrinkableItem) { + let shrink = shrinkAmount * amountPerShrink; + const minSize = flexItem._getMainAxisMinSize(); + const size = flexItem._getMainAxisLayoutSize(); + if (size > minSize) { + const maxShrink = size - minSize; + const isFullyShrunk = (shrink >= maxShrink); + if (isFullyShrunk) { + shrink = maxShrink; + + // Destribute remaining amount over the other flex items. + totalShrinkAmount -= shrinkAmount; + } + + const finalSize = size - shrink; + flexItem._resizeMainAxis(finalSize); + + this._shrunkSize += shrink; + this._amountRemaining -= shrink; + + if (Math.abs(this._amountRemaining) < 10e-6) { + return; + } + } + } + } + } while(totalShrinkAmount && (Math.abs(this._amountRemaining) > 10e-6)); + } + } + + _getTotalShrinkAmount() { + let total = 0; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + + if (flexItem.shrink) { + const minSize = flexItem._getMainAxisMinSize(); + const size = flexItem._getMainAxisLayoutSize(); + + // Exclude those already fully shrunk. + if (size > minSize) { + total += flexItem.shrink; + } + } + } + return total; + } + + getShrunkSize() { + return this._shrunkSize; + } + +} + +class SizeGrower { + + constructor(line) { + this._line = line; + this._amountRemaining = 0; + this._grownSize = 0; + } + + grow(amount) { + this._grownSize = 0; + + this._amountRemaining = amount; + let totalGrowAmount = this._getTotalGrowAmount(); + if (totalGrowAmount) { + const items = this._line.items; + do { + let amountPerGrow = this._amountRemaining / totalGrowAmount; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + const growAmount = flexItem.grow; + const isGrowableItem = (growAmount > 0); + if (isGrowableItem) { + let grow = growAmount * amountPerGrow; + const maxSize = flexItem._getMainAxisMaxSizeSetting(); + const size = flexItem._getMainAxisLayoutSize(); + if (maxSize > 0) { + if (size >= maxSize) { + // Already fully grown. + grow = 0; + } else { + const maxGrow = maxSize - size; + const isFullyGrown = (grow >= maxGrow); + if (isFullyGrown) { + grow = maxGrow; + + // Destribute remaining amount over the other flex items. + totalGrowAmount -= growAmount; + } + } + } + + if (grow > 0) { + const finalSize = size + grow; + flexItem._resizeMainAxis(finalSize); + + this._grownSize += grow; + this._amountRemaining -= grow; + + if (Math.abs(this._amountRemaining) < 10e-6) { + return; + } + } + } + } + } while(totalGrowAmount && (Math.abs(this._amountRemaining) > 10e-6)); + } + } + + _getTotalGrowAmount() { + let total = 0; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + + if (flexItem.grow) { + const maxSize = flexItem._getMainAxisMaxSizeSetting(); + const size = flexItem._getMainAxisLayoutSize(); + + // Exclude those already fully grown. + if (maxSize === 0 || size < maxSize) { + total += flexItem.grow; + } + } + } + return total; + } + + getGrownSize() { + return this._grownSize; + } + +} + +class ItemPositioner { + + constructor(lineLayout) { + this._line = lineLayout; + } + + get _layout() { + return this._line._layout; + } + + position() { + const {spacingBefore, spacingBetween} = this._getSpacing(); + + let currentPos = spacingBefore; + + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + + item.flexItem._setMainAxisLayoutPos(currentPos); + currentPos += item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin(); + currentPos += spacingBetween; + } + } + + _getSpacing() { + const remainingSpace = this._line._availableSpace; + let mode = this._layout._flexContainer.justifyContent; + const numberOfItems = this._line.numberOfItems; + + return SpacingCalculator.getSpacing(mode, numberOfItems, remainingSpace); + } + +} + +class ItemAligner { + + constructor(line) { + this._line = line; + this._crossAxisLayoutSize = 0; + this._crossAxisLayoutOffset = 0; + this._alignItemsSetting = null; + this._recursiveResizeOccured = false; + this._isCrossAxisFitToContents = false; + } + + get _layout() { + return this._line._layout; + } + + get _flexContainer() { + return this._layout._flexContainer; + } + + setCrossAxisLayoutSize(size) { + this._crossAxisLayoutSize = size; + } + + setCrossAxisLayoutOffset(offset) { + this._crossAxisLayoutOffset = offset; + } + + align() { + this._alignItemsSetting = this._flexContainer.alignItems; + + this._isCrossAxisFitToContents = this._layout.isAxisFitToContents(!this._flexContainer._horizontal); + + this._recursiveResizeOccured = false; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + this._alignItem(item); + } + } + + get recursiveResizeOccured() { + return this._recursiveResizeOccured; + } + + _alignItem(item) { + const flexItem = item.flexItem; + let align = flexItem.alignSelf || this._alignItemsSetting; + + if (align === "stretch" && this._preventStretch(flexItem)) { + align = "flex-start"; + } + + if (align !== "stretch" && !this._isCrossAxisFitToContents) { + if (flexItem._hasRelCrossAxisSize()) { + // As cross axis size might have changed, we need to recalc the relative flex item's size. + flexItem._resetCrossAxisLayoutSize(); + } + } + + switch(align) { + case "flex-start": + this._alignItemFlexStart(flexItem); + break; + case "flex-end": + this._alignItemFlexEnd(flexItem); + break; + case "center": + this._alignItemFlexCenter(flexItem); + break; + case "stretch": + this._alignItemStretch(flexItem); + break; + } + } + + _alignItemFlexStart(flexItem) { + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset); + } + + _alignItemFlexEnd(flexItem) { + const itemCrossAxisSize = flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin(); + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset + (this._crossAxisLayoutSize - itemCrossAxisSize)); + } + + _alignItemFlexCenter(flexItem) { + const itemCrossAxisSize = flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin(); + const center = (this._crossAxisLayoutSize - itemCrossAxisSize) / 2; + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset + center); + } + + _alignItemStretch(flexItem) { + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset); + + const mainAxisLayoutSizeBeforeResize = flexItem._getMainAxisLayoutSize(); + let size = this._crossAxisLayoutSize - flexItem._getCrossAxisMargin() - flexItem._getCrossAxisPadding(); + + const crossAxisMinSizeSetting = flexItem._getCrossAxisMinSizeSetting(); + if (crossAxisMinSizeSetting > 0) { + size = Math.max(size, crossAxisMinSizeSetting); + } + + const crossAxisMaxSizeSetting = flexItem._getCrossAxisMaxSizeSetting(); + const crossAxisMaxSizeSettingEnabled = (crossAxisMaxSizeSetting > 0); + if (crossAxisMaxSizeSettingEnabled) { + size = Math.min(size, crossAxisMaxSizeSetting); + } + + flexItem._resizeCrossAxis(size); + const mainAxisLayoutSizeAfterResize = flexItem._getMainAxisLayoutSize(); + + const recursiveResize = (mainAxisLayoutSizeAfterResize !== mainAxisLayoutSizeBeforeResize); + if (recursiveResize) { + // Recursive resize can happen when this flex item has the opposite direction than the container + // and is wrapping and auto-sizing. Due to item/content stretching the main axis size of the flex + // item may decrease. If it does so, we must re-justify-content the complete line. + // Notice that we don't account for changes to the (if autosized) main axis size caused by recursive + // resize, which may cause the container's main axis to not shrink to the contents properly. + // This is by design, because if we had re-run the main axis layout, we could run into issues such + // as slow layout or endless loops. + this._recursiveResizeOccured = true; + } + } + + _preventStretch(flexItem) { + const hasFixedCrossAxisSize = flexItem._hasFixedCrossAxisSize(); + const forceStretch = (flexItem.alignSelf === "stretch"); + return hasFixedCrossAxisSize && !forceStretch; + } + +} + +class LineLayout { + + constructor(layout, startIndex, endIndex, availableSpace) { + this._layout = layout; + this.items = layout.items; + this.startIndex = startIndex; + this.endIndex = endIndex; + this._availableSpace = availableSpace; + } + + performLayout() { + this._setItemSizes(); + this.setItemPositions(); + this._calcLayoutInfo(); + } + + _setItemSizes() { + if (this._availableSpace > 0) { + this._growItemSizes(this._availableSpace); + } else if (this._availableSpace < 0) { + this._shrinkItemSizes(-this._availableSpace); + } + } + + _growItemSizes(amount) { + const grower = new SizeGrower(this); + grower.grow(amount); + this._availableSpace -= grower.getGrownSize(); + } + + _shrinkItemSizes(amount) { + const shrinker = new SizeShrinker(this); + shrinker.shrink(amount); + this._availableSpace += shrinker.getShrunkSize(); + } + + setItemPositions() { + const positioner = new ItemPositioner(this); + positioner.position(); + } + + createItemAligner() { + return new ItemAligner(this); + } + + _calcLayoutInfo() { + this._calcCrossAxisMaxLayoutSize(); + } + + getMainAxisMinSize() { + let mainAxisMinSize = 0; + for (let i = this.startIndex; i <= this.endIndex; i++) { + const item = this.items[i]; + mainAxisMinSize += item.flexItem._getMainAxisMinSizeWithPaddingAndMargin(); + } + return mainAxisMinSize; + } + + get numberOfItems() { + return this.endIndex - this.startIndex + 1; + } + + get crossAxisLayoutSize() { + const noSpecifiedCrossAxisSize = (this._layout.isCrossAxisFitToContents() && !this._layout.resizingCrossAxis); + const shouldFitToContents = (this._layout.isWrapping() || noSpecifiedCrossAxisSize); + if (shouldFitToContents) { + return this._crossAxisMaxLayoutSize; + } else { + return this._layout.crossAxisSize; + } + } + + _calcCrossAxisMaxLayoutSize() { + this._crossAxisMaxLayoutSize = this._getCrossAxisMaxLayoutSize(); + } + + _getCrossAxisMaxLayoutSize() { + let crossAxisMaxSize = 0; + for (let i = this.startIndex; i <= this.endIndex; i++) { + const item = this.items[i]; + crossAxisMaxSize = Math.max(crossAxisMaxSize, item.flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin()); + } + return crossAxisMaxSize; + } + + +} + +/** + * Distributes items over layout lines. + */ +class LineLayouter { + + constructor(layout) { + this._layout = layout; + this._mainAxisMinSize = -1; + this._crossAxisMinSize = -1; + this._mainAxisContentSize = 0; + } + + get lines() { + return this._lines; + } + + get mainAxisMinSize() { + if (this._mainAxisMinSize === -1) { + this._mainAxisMinSize = this._getMainAxisMinSize(); + } + return this._mainAxisMinSize; + } + + get crossAxisMinSize() { + if (this._crossAxisMinSize === -1) { + this._crossAxisMinSize = this._getCrossAxisMinSize(); + } + return this._crossAxisMinSize; + } + + get mainAxisContentSize() { + return this._mainAxisContentSize; + } + + layoutLines() { + this._setup(); + const items = this._layout.items; + const wrap = this._layout.isWrapping(); + + let startIndex = 0; + let i; + const n = items.length; + for (i = 0; i < n; i++) { + const item = items[i]; + + this._layoutFlexItem(item); + + // Get predicted main axis size. + const itemMainAxisSize = item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin(); + + if (wrap && (i > startIndex)) { + const isOverflowing = (this._curMainAxisPos + itemMainAxisSize > this._mainAxisSize); + if (isOverflowing) { + this._layoutLine(startIndex, i - 1); + this._curMainAxisPos = 0; + startIndex = i; + } + } + + this._addToMainAxisPos(itemMainAxisSize); + } + + if (startIndex < i) { + this._layoutLine(startIndex, i - 1); + } + } + + _layoutFlexItem(item) { + if (item.isFlexEnabled()) { + item.flexLayout.updateTreeLayout(); + } else { + item.flexItem._resetLayoutSize(); + } + } + + _setup() { + this._mainAxisSize = this._layout.mainAxisSize; + this._curMainAxisPos = 0; + this._maxMainAxisPos = 0; + this._lines = []; + + this._mainAxisMinSize = -1; + this._crossAxisMinSize = -1; + this._mainAxisContentSize = 0; + } + + _addToMainAxisPos(itemMainAxisSize) { + this._curMainAxisPos += itemMainAxisSize; + if (this._curMainAxisPos > this._maxMainAxisPos) { + this._maxMainAxisPos = this._curMainAxisPos; + } + } + + _layoutLine(startIndex, endIndex) { + const availableSpace = this._getAvailableMainAxisLayoutSpace(); + const line = new LineLayout(this._layout, startIndex, endIndex, availableSpace); + line.performLayout(); + this._lines.push(line); + + if (this._mainAxisContentSize === 0 || (this._curMainAxisPos > this._mainAxisContentSize)) { + this._mainAxisContentSize = this._curMainAxisPos; + } + } + + _getAvailableMainAxisLayoutSpace() { + if (!this._layout.resizingMainAxis && this._layout.isMainAxisFitToContents()) { + return 0; + } else { + return this._mainAxisSize - this._curMainAxisPos; + } + } + + _getCrossAxisMinSize() { + let crossAxisMinSize = 0; + const items = this._layout.items; + for (let i = 0, n = items.length; i < n; i++) { + const item = items[i]; + const itemCrossAxisMinSize = item.flexItem._getCrossAxisMinSizeWithPaddingAndMargin(); + crossAxisMinSize = Math.max(crossAxisMinSize, itemCrossAxisMinSize); + } + return crossAxisMinSize; + } + + _getMainAxisMinSize() { + if (this._lines.length === 1) { + return this._lines[0].getMainAxisMinSize(); + } else { + // Wrapping lines: specified width is used as min width (in accordance to W3C flexbox). + return this._layout.mainAxisSize; + } + } + +} + +class ItemCoordinatesUpdater { + + constructor(layout) { + this._layout = layout; + this._isReverse = this._flexContainer._reverse; + this._horizontalPaddingOffset = this._layout._getHorizontalPaddingOffset(); + this._verticalPaddingOffset = this._layout._getVerticalPaddingOffset(); + } + + get _flexContainer() { + return this._layout._flexContainer; + } + + finalize() { + const parentFlex = this._layout.getParentFlexContainer(); + if (parentFlex) { + // We must update it from the parent to set padding offsets and reverse position. + const updater = new ItemCoordinatesUpdater(parentFlex._layout); + updater._finalizeItemAndChildren(this._flexContainer.item); + } else { + this._finalizeRoot(); + this._finalizeItems(); + } + } + + _finalizeRoot() { + const item = this._flexContainer.item; + let x = FlexUtils.getAxisLayoutPos(item, true); + let y = FlexUtils.getAxisLayoutPos(item, false); + let w = FlexUtils.getAxisLayoutSize(item, true); + let h = FlexUtils.getAxisLayoutSize(item, false); + + w += this._layout._getHorizontalPadding(); + h += this._layout._getVerticalPadding(); + + item.clearRecalcFlag(); + + item.setLayout(x, y, w, h); + } + + _finalizeItems() { + const items = this._layout.items; + for (let i = 0, n = items.length; i < n; i++) { + const item = items[i]; + const validCache = this._validateItemCache(item); + + // Notice that we must also finalize a cached items, as it's coordinates may have changed. + this._finalizeItem(item); + + if (!validCache) { + this._finalizeItemChildren(item); + } + } + } + + _validateItemCache(item) { + if (item.recalc === 0) { + if (item.isFlexEnabled()) { + const layout = item._flex._layout; + + const dimensionsMatchPreviousResult = (item.w === item.target.w && item.h === item.target.h); + if (dimensionsMatchPreviousResult) { + // Cache is valid. + return true; + } else { + const crossAxisSize = layout.crossAxisSize; + layout.performResizeMainAxis(layout.mainAxisSize); + layout.performResizeCrossAxis(crossAxisSize); + } + } + } + return false; + } + + _finalizeItemAndChildren(item) { + this._finalizeItem(item); + this._finalizeItemChildren(item); + } + + _finalizeItem(item) { + if (this._isReverse) { + this._reverseMainAxisLayoutPos(item); + } + + let x = FlexUtils.getAxisLayoutPos(item, true); + let y = FlexUtils.getAxisLayoutPos(item, false); + let w = FlexUtils.getAxisLayoutSize(item, true); + let h = FlexUtils.getAxisLayoutSize(item, false); + + x += this._horizontalPaddingOffset; + y += this._verticalPaddingOffset; + + const flex = item.flex; + if (flex) { + w += item._flex._layout._getHorizontalPadding(); + h += item._flex._layout._getVerticalPadding(); + } + + const flexItem = item.flexItem; + if (flexItem) { + x += flexItem._getHorizontalMarginOffset(); + y += flexItem._getVerticalMarginOffset(); + } + + item.clearRecalcFlag(); + item.setLayout(x, y, w, h); + } + + _finalizeItemChildren(item) { + const flex = item._flex; + if (flex) { + const updater = new ItemCoordinatesUpdater(flex._layout); + updater._finalizeItems(); + } + } + + _reverseMainAxisLayoutPos(item) { + const endPos = (item.flexItem._getMainAxisLayoutPos() + item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin()); + const reversedPos = this._layout.mainAxisSize - endPos; + item.flexItem._setMainAxisLayoutPos(reversedPos); + } + +} + +/** + * Layouts a flex container (and descendants). + */ +class FlexLayout { + + constructor(flexContainer) { + this._flexContainer = flexContainer; + + this._lineLayouter = new LineLayouter(this); + + this._resizingMainAxis = false; + this._resizingCrossAxis = false; + + this._cachedMainAxisSizeAfterLayout = 0; + this._cachedCrossAxisSizeAfterLayout = 0; + + this._shrunk = false; + } + + get shrunk() { + return this._shrunk; + } + + get recalc() { + return this.item.recalc; + } + + layoutTree() { + const isSubTree = (this.item.flexParent !== null); + if (isSubTree) { + // Use the dimensions set by the parent flex tree. + this._updateSubTreeLayout(); + } else { + this.updateTreeLayout(); + } + this.updateItemCoords(); + } + + updateTreeLayout() { + if (this.recalc) { + this._performUpdateLayoutTree(); + } else { + this._performUpdateLayoutTreeFromCache(); + } + } + + _performUpdateLayoutTree() { + this._setInitialAxisSizes(); + this._layoutAxes(); + this._refreshLayoutCache(); + } + + _refreshLayoutCache() { + this._cachedMainAxisSizeAfterLayout = this.mainAxisSize; + this._cachedCrossAxisSizeAfterLayout = this.crossAxisSize; + } + + _performUpdateLayoutTreeFromCache() { + const sizeMightHaveChanged = (this.item.funcW || this.item.funcH); + if (sizeMightHaveChanged) { + // Update after all. + this.item.enableLocalRecalcFlag(); + this._performUpdateLayoutTree(); + } else { + this.mainAxisSize = this._cachedMainAxisSizeAfterLayout; + this.crossAxisSize = this._cachedCrossAxisSizeAfterLayout; + } + } + + updateItemCoords() { + const updater = new ItemCoordinatesUpdater(this); + updater.finalize(); + } + + _updateSubTreeLayout() { + // The dimensions of this container are guaranteed not to have changed. + // That's why we can safely 'reuse' those and re-layout the contents. + const crossAxisSize = this.crossAxisSize; + this._layoutMainAxis(); + this.performResizeCrossAxis(crossAxisSize); + } + + _setInitialAxisSizes() { + if (this.item.isFlexItemEnabled()) { + this.item.flexItem._resetLayoutSize(); + } else { + this.mainAxisSize = this._getMainAxisBasis(); + this.crossAxisSize = this._getCrossAxisBasis(); + } + this._resizingMainAxis = false; + this._resizingCrossAxis = false; + this._shrunk = false; + } + + _layoutAxes() { + this._layoutMainAxis(); + this._layoutCrossAxis(); + } + + /** + * @pre mainAxisSize should exclude padding. + */ + _layoutMainAxis() { + this._layoutLines(); + this._fitMainAxisSizeToContents(); + } + + _layoutLines() { + this._lineLayouter.layoutLines(); + } + + get _lines() { + return this._lineLayouter.lines; + } + + _fitMainAxisSizeToContents() { + if (!this._resizingMainAxis) { + if (this.isMainAxisFitToContents()) { + this.mainAxisSize = this._lineLayouter.mainAxisContentSize; + } + } + } + + /** + * @pre crossAxisSize should exclude padding. + */ + _layoutCrossAxis() { + const aligner = new ContentAligner(this); + aligner.init(); + this._totalCrossAxisSize = aligner.totalCrossAxisSize; + this._fitCrossAxisSizeToContents(); + aligner.align(); + } + + _fitCrossAxisSizeToContents() { + if (!this._resizingCrossAxis) { + if (this.isCrossAxisFitToContents()) { + this.crossAxisSize = this._totalCrossAxisSize; + } + } + } + + isWrapping() { + return this._flexContainer.wrap; + } + + isAxisFitToContents(horizontal) { + if (this._horizontal === horizontal) { + return this.isMainAxisFitToContents(); + } else { + return this.isCrossAxisFitToContents(); + } + } + + isMainAxisFitToContents() { + return !this.isWrapping() && !this._hasFixedMainAxisBasis(); + } + + isCrossAxisFitToContents() { + return !this._hasFixedCrossAxisBasis(); + } + + _hasFixedMainAxisBasis() { + return !FlexUtils.isZeroAxisSize(this.item, this._horizontal); + } + + _hasFixedCrossAxisBasis() { + return !FlexUtils.isZeroAxisSize(this.item, !this._horizontal); + } + + getAxisMinSize(horizontal) { + if (this._horizontal === horizontal) { + return this._getMainAxisMinSize(); + } else { + return this._getCrossAxisMinSize(); + } + } + + _getMainAxisMinSize() { + return this._lineLayouter.mainAxisMinSize; + } + + _getCrossAxisMinSize() { + return this._lineLayouter.crossAxisMinSize; + } + + resizeMainAxis(size) { + if (this.mainAxisSize !== size) { + if (this.recalc > 0) { + this.performResizeMainAxis(size); + } else { + if (this._checkValidCacheMainAxisResize()) { + this.mainAxisSize = size; + this._fitCrossAxisSizeToContents(); + } else { + // Cache miss. + this.item.enableLocalRecalcFlag(); + this.performResizeMainAxis(size); + } + } + } + } + + _checkValidCacheMainAxisResize(size) { + const isFinalMainAxisSize = (size === this.targetMainAxisSize); + if (isFinalMainAxisSize) { + return true; + } + const canIgnoreCacheMiss = !this.isCrossAxisFitToContents(); + if (canIgnoreCacheMiss) { + // Allow other main axis resizes and check if final resize matches the target main axis size + // (ItemCoordinatesUpdater). + return true; + } + return false; + } + + performResizeMainAxis(size) { + const isShrinking = (size < this.mainAxisSize); + this._shrunk = isShrinking; + + this.mainAxisSize = size; + + this._resizingMainAxis = true; + this._layoutAxes(); + this._resizingMainAxis = false; + } + + resizeCrossAxis(size) { + if (this.crossAxisSize !== size) { + if (this.recalc > 0) { + this.performResizeCrossAxis(size); + } else { + this.crossAxisSize = size; + } + } + } + + performResizeCrossAxis(size) { + this.crossAxisSize = size; + + this._resizingCrossAxis = true; + this._layoutCrossAxis(); + this._resizingCrossAxis = false; + } + + get targetMainAxisSize() { + return this._horizontal ? this.item.target.w : this.item.target.h; + } + + get targetCrossAxisSize() { + return this._horizontal ? this.item.target.h : this.item.target.w; + } + + getParentFlexContainer() { + return this.item.isFlexItemEnabled() ? this.item.flexItem.ctr : null; + } + + _getHorizontalPadding() { + return FlexUtils.getTotalPadding(this.item, true); + } + + _getVerticalPadding() { + return FlexUtils.getTotalPadding(this.item, false); + } + + _getHorizontalPaddingOffset() { + return FlexUtils.getPaddingOffset(this.item, true); + } + + _getVerticalPaddingOffset() { + return FlexUtils.getPaddingOffset(this.item, false); + } + + _getMainAxisBasis() { + return FlexUtils.getRelAxisSize(this.item, this._horizontal); + } + + _getCrossAxisBasis() { + return FlexUtils.getRelAxisSize(this.item, !this._horizontal); + } + + get _horizontal() { + return this._flexContainer._horizontal; + } + + get _reverse() { + return this._flexContainer._reverse; + } + + get item() { + return this._flexContainer.item; + } + + get items() { + return this.item.items; + } + + get resizingMainAxis() { + return this._resizingMainAxis; + } + + get resizingCrossAxis() { + return this._resizingCrossAxis; + } + + get numberOfItems() { + return this.items.length; + } + + get mainAxisSize() { + return FlexUtils.getAxisLayoutSize(this.item, this._horizontal); + } + + get crossAxisSize() { + return FlexUtils.getAxisLayoutSize(this.item, !this._horizontal); + } + + set mainAxisSize(v) { + FlexUtils.setAxisLayoutSize(this.item, this._horizontal, v); + } + + set crossAxisSize(v) { + FlexUtils.setAxisLayoutSize(this.item, !this._horizontal, v); + } + +} + +class FlexContainer { + + + constructor(item) { + this._item = item; + + this._layout = new FlexLayout(this); + this._horizontal = true; + this._reverse = false; + this._wrap = false; + this._alignItems = 'stretch'; + this._justifyContent = 'flex-start'; + this._alignContent = 'flex-start'; + + this._paddingLeft = 0; + this._paddingTop = 0; + this._paddingRight = 0; + this._paddingBottom = 0; + } + + get item() { + return this._item; + } + + _changedDimensions() { + this._item.changedDimensions(); + } + + _changedContents() { + this._item.changedContents(); + } + + get direction() { + return (this._horizontal ? "row" : "column") + (this._reverse ? "-reverse" : ""); + } + + set direction(f) { + if (this.direction === f) return; + + this._horizontal = (f === 'row' || f === 'row-reverse'); + this._reverse = (f === 'row-reverse' || f === 'column-reverse'); + + this._changedContents(); + } + + set wrap(v) { + this._wrap = v; + this._changedContents(); + } + + get wrap() { + return this._wrap; + } + + get alignItems() { + return this._alignItems; + } + + set alignItems(v) { + if (this._alignItems === v) return; + if (FlexContainer.ALIGN_ITEMS.indexOf(v) === -1) { + throw new Error("Unknown alignItems, options: " + FlexContainer.ALIGN_ITEMS.join(",")); + } + this._alignItems = v; + + this._changedContents(); + } + + get alignContent() { + return this._alignContent; + } + + set alignContent(v) { + if (this._alignContent === v) return; + if (FlexContainer.ALIGN_CONTENT.indexOf(v) === -1) { + throw new Error("Unknown alignContent, options: " + FlexContainer.ALIGN_CONTENT.join(",")); + } + this._alignContent = v; + + this._changedContents(); + } + + get justifyContent() { + return this._justifyContent; + } + + set justifyContent(v) { + if (this._justifyContent === v) return; + + if (FlexContainer.JUSTIFY_CONTENT.indexOf(v) === -1) { + throw new Error("Unknown justifyContent, options: " + FlexContainer.JUSTIFY_CONTENT.join(",")); + } + this._justifyContent = v; + + this._changedContents(); + } + + set padding(v) { + this.paddingLeft = v; + this.paddingTop = v; + this.paddingRight = v; + this.paddingBottom = v; + } + + get padding() { + return this.paddingLeft; + } + + set paddingLeft(v) { + this._paddingLeft = v; + this._changedDimensions(); + } + + get paddingLeft() { + return this._paddingLeft; + } + + set paddingTop(v) { + this._paddingTop = v; + this._changedDimensions(); + } + + get paddingTop() { + return this._paddingTop; + } + + set paddingRight(v) { + this._paddingRight = v; + this._changedDimensions(); + } + + get paddingRight() { + return this._paddingRight; + } + + set paddingBottom(v) { + this._paddingBottom = v; + this._changedDimensions(); + } + + get paddingBottom() { + return this._paddingBottom; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +FlexContainer.ALIGN_ITEMS = ["flex-start", "flex-end", "center", "stretch"]; +FlexContainer.ALIGN_CONTENT = ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly", "stretch"]; +FlexContainer.JUSTIFY_CONTENT = ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]; + +class FlexItem { + + constructor(item) { + this._ctr = null; + this._item = item; + this._grow = 0; + this._shrink = FlexItem.SHRINK_AUTO; + this._alignSelf = undefined; + this._minWidth = 0; + this._minHeight = 0; + this._maxWidth = 0; + this._maxHeight = 0; + + this._marginLeft = 0; + this._marginTop = 0; + this._marginRight = 0; + this._marginBottom = 0; + } + + get item() { + return this._item; + } + + get grow() { + return this._grow; + } + + set grow(v) { + if (this._grow === v) return; + + this._grow = parseInt(v) || 0; + + this._changed(); + } + + get shrink() { + if (this._shrink === FlexItem.SHRINK_AUTO) { + return this._getDefaultShrink(); + } + return this._shrink; + } + + _getDefaultShrink() { + if (this.item.isFlexEnabled()) { + return 1; + } else { + // All non-flex containers are absolutely positioned items with fixed dimensions, and by default not shrinkable. + return 0; + } + } + + set shrink(v) { + if (this._shrink === v) return; + + this._shrink = parseInt(v) || 0; + + this._changed(); + } + + get alignSelf() { + return this._alignSelf; + } + + set alignSelf(v) { + if (this._alignSelf === v) return; + + if (v === undefined) { + this._alignSelf = undefined; + } else { + if (FlexContainer.ALIGN_ITEMS.indexOf(v) === -1) { + throw new Error("Unknown alignSelf, options: " + FlexContainer.ALIGN_ITEMS.join(",")); + } + this._alignSelf = v; + } + + this._changed(); + } + + get minWidth() { + return this._minWidth; + } + + set minWidth(v) { + this._minWidth = Math.max(0, v); + this._item.changedDimensions(true, false); + } + + get minHeight() { + return this._minHeight; + } + + set minHeight(v) { + this._minHeight = Math.max(0, v); + this._item.changedDimensions(false, true); + } + + get maxWidth() { + return this._maxWidth; + } + + set maxWidth(v) { + this._maxWidth = Math.max(0, v); + this._item.changedDimensions(true, false); + } + + get maxHeight() { + return this._maxHeight; + } + + set maxHeight(v) { + this._maxHeight = Math.max(0, v); + this._item.changedDimensions(false, true); + } + + /** + * @note margins behave slightly different than in HTML with regard to shrinking. + * In HTML, (outer) margins can be removed when shrinking. In this engine, they will not shrink at all. + */ + set margin(v) { + this.marginLeft = v; + this.marginTop = v; + this.marginRight = v; + this.marginBottom = v; + } + + get margin() { + return this.marginLeft; + } + + set marginLeft(v) { + this._marginLeft = v; + this._changed(); + } + + get marginLeft() { + return this._marginLeft; + } + + set marginTop(v) { + this._marginTop = v; + this._changed(); + } + + get marginTop() { + return this._marginTop; + } + + set marginRight(v) { + this._marginRight = v; + this._changed(); + } + + get marginRight() { + return this._marginRight; + } + + set marginBottom(v) { + this._marginBottom = v; + this._changed(); + } + + get marginBottom() { + return this._marginBottom; + } + + _changed() { + if (this.ctr) this.ctr._changedContents(); + } + + set ctr(v) { + this._ctr = v; + } + + get ctr() { + return this._ctr; + } + + patch(settings) { + Base.patchObject(this, settings); + } + + _resetLayoutSize() { + this._resetHorizontalAxisLayoutSize(); + this._resetVerticalAxisLayoutSize(); + } + + _resetCrossAxisLayoutSize() { + if (this.ctr._horizontal) { + this._resetVerticalAxisLayoutSize(); + } else { + this._resetHorizontalAxisLayoutSize(); + } + } + + _resetHorizontalAxisLayoutSize() { + let w = FlexUtils.getRelAxisSize(this.item, true); + if (this._minWidth) { + w = Math.max(this._minWidth, w); + } + if (this._maxWidth) { + w = Math.min(this._maxWidth, w); + } + FlexUtils.setAxisLayoutSize(this.item, true, w); + } + + _resetVerticalAxisLayoutSize() { + let h = FlexUtils.getRelAxisSize(this.item, false); + if (this._minHeight) { + h = Math.max(this._minHeight, h); + } + if (this._maxHeight) { + h = Math.min(this._maxHeight, h); + } + FlexUtils.setAxisLayoutSize(this.item, false, h); + } + + _getCrossAxisMinSizeSetting() { + return this._getMinSizeSetting(!this.ctr._horizontal); + } + + _getCrossAxisMaxSizeSetting() { + return this._getMaxSizeSetting(!this.ctr._horizontal); + } + + _getMainAxisMaxSizeSetting() { + return this._getMaxSizeSetting(this.ctr._horizontal); + } + + _getMinSizeSetting(horizontal) { + if (horizontal) { + return this._minWidth; + } else { + return this._minHeight; + } + } + + _getMaxSizeSetting(horizontal) { + if (horizontal) { + return this._maxWidth; + } else { + return this._maxHeight; + } + } + + _getMainAxisMinSize() { + return FlexUtils.getAxisMinSize(this.item, this.ctr._horizontal); + } + + _getCrossAxisMinSize() { + return FlexUtils.getAxisMinSize(this.item, !this.ctr._horizontal); + } + + _getMainAxisLayoutSize() { + return FlexUtils.getAxisLayoutSize(this.item, this.ctr._horizontal); + } + + _getMainAxisLayoutPos() { + return FlexUtils.getAxisLayoutPos(this.item, this.ctr._horizontal); + } + + _setMainAxisLayoutPos(pos) { + return FlexUtils.setAxisLayoutPos(this.item, this.ctr._horizontal, pos); + } + + _setCrossAxisLayoutPos(pos) { + return FlexUtils.setAxisLayoutPos(this.item, !this.ctr._horizontal, pos); + } + + _getCrossAxisLayoutSize() { + return FlexUtils.getAxisLayoutSize(this.item, !this.ctr._horizontal); + } + + _resizeCrossAxis(size) { + return FlexUtils.resizeAxis(this.item, !this.ctr._horizontal, size); + } + + _resizeMainAxis(size) { + return FlexUtils.resizeAxis(this.item, this.ctr._horizontal, size); + } + + _getMainAxisPadding() { + return FlexUtils.getTotalPadding(this.item, this.ctr._horizontal); + } + + _getCrossAxisPadding() { + return FlexUtils.getTotalPadding(this.item, !this.ctr._horizontal); + } + + _getMainAxisMargin() { + return FlexUtils.getTotalMargin(this.item, this.ctr._horizontal); + } + + _getCrossAxisMargin() { + return FlexUtils.getTotalMargin(this.item, !this.ctr._horizontal); + } + + _getHorizontalMarginOffset() { + return FlexUtils.getMarginOffset(this.item, true); + } + + _getVerticalMarginOffset() { + return FlexUtils.getMarginOffset(this.item, false); + } + + _getMainAxisMinSizeWithPaddingAndMargin() { + return this._getMainAxisMinSize() + this._getMainAxisPadding() + this._getMainAxisMargin(); + } + + _getCrossAxisMinSizeWithPaddingAndMargin() { + return this._getCrossAxisMinSize() + this._getCrossAxisPadding() + this._getCrossAxisMargin(); + } + + _getMainAxisLayoutSizeWithPaddingAndMargin() { + return this._getMainAxisLayoutSize() + this._getMainAxisPadding() + this._getMainAxisMargin(); + } + + _getCrossAxisLayoutSizeWithPaddingAndMargin() { + return this._getCrossAxisLayoutSize() + this._getCrossAxisPadding() + this._getCrossAxisMargin(); + } + + _hasFixedCrossAxisSize() { + return !FlexUtils.isZeroAxisSize(this.item, !this.ctr._horizontal); + } + + _hasRelCrossAxisSize() { + return !!(this.ctr._horizontal ? this.item.funcH : this.item.funcW); + } + +} + + +FlexItem.SHRINK_AUTO = -1; + +/** + * This is the connection between the render tree with the layout tree of this flex container/item. + */ +class FlexTarget { + + constructor(target) { + this._target = target; + + /** + * Possible values (only in case of container): + * bit 0: has changed or contains items with changes + * bit 1: width changed + * bit 2: height changed + */ + this._recalc = 0; + + this._enabled = false; + + this.x = 0; + this.y = 0; + this.w = 0; + this.h = 0; + + this._originalX = 0; + this._originalY = 0; + this._originalWidth = 0; + this._originalHeight = 0; + + this._flex = null; + this._flexItem = null; + this._flexItemDisabled = false; + + this._items = null; + } + + get flexLayout() { + return this.flex ? this.flex._layout : null; + } + + layoutFlexTree() { + if (this.isFlexEnabled() && this.isChanged()) { + this.flexLayout.layoutTree(); + } + } + + get target() { + return this._target; + } + + get flex() { + return this._flex; + } + + set flex(v) { + if (!v) { + if (this.isFlexEnabled()) { + this._disableFlex(); + } + } else { + if (!this.isFlexEnabled()) { + this._enableFlex(); + } + this._flex.patch(v); + } + } + + get flexItem() { + if (this._flexItemDisabled) { + return false; + } + this._ensureFlexItem(); + return this._flexItem; + } + + set flexItem(v) { + if (v === false) { + if (!this._flexItemDisabled) { + const parent = this.flexParent; + this._flexItemDisabled = true; + this._checkEnabled(); + if (parent) { + parent._clearFlexItemsCache(); + parent.changedContents(); + } + } + } else { + this._ensureFlexItem(); + + this._flexItem.patch(v); + + if (this._flexItemDisabled) { + this._flexItemDisabled = false; + this._checkEnabled(); + const parent = this.flexParent; + if (parent) { + parent._clearFlexItemsCache(); + parent.changedContents(); + } + } + } + } + + _enableFlex() { + this._flex = new FlexContainer(this); + this._checkEnabled(); + this.changedDimensions(); + this._enableChildrenAsFlexItems(); + } + + _disableFlex() { + this.changedDimensions(); + this._flex = null; + this._checkEnabled(); + this._disableChildrenAsFlexItems(); + } + + _enableChildrenAsFlexItems() { + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + child.layout._enableFlexItem(); + } + } + } + + _disableChildrenAsFlexItems() { + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + child.layout._disableFlexItem(); + } + } + } + + _enableFlexItem() { + this._ensureFlexItem(); + const flexParent = this._target._parent._layout; + this._flexItem.ctr = flexParent._flex; + flexParent.changedContents(); + this._checkEnabled(); + } + + _disableFlexItem() { + if (this._flexItem) { + this._flexItem.ctr = null; + } + + // We keep the flexItem object because it may contain custom settings. + this._checkEnabled(); + + // Offsets have been changed. We can't recover them, so we'll just clear them instead. + this._resetOffsets(); + } + + _resetOffsets() { + this.x = 0; + this.y = 0; + } + + _ensureFlexItem() { + if (!this._flexItem) { + this._flexItem = new FlexItem(this); + } + } + + _checkEnabled() { + const enabled = this.isEnabled(); + if (this._enabled !== enabled) { + if (enabled) { + this._enable(); + } else { + this._disable(); + } + this._enabled = enabled; + } + } + + _enable() { + this._setupTargetForFlex(); + this._target.enableFlexLayout(); + } + + _disable() { + this._restoreTargetToNonFlex(); + this._target.disableFlexLayout(); + } + + isEnabled() { + return this.isFlexEnabled() || this.isFlexItemEnabled(); + } + + isFlexEnabled() { + return this._flex !== null; + } + + isFlexItemEnabled() { + return this.flexParent !== null; + } + + _restoreTargetToNonFlex() { + const target = this._target; + target.x = this._originalX; + target.y = this._originalY; + target.setDimensions(this._originalWidth, this._originalHeight); + } + + _setupTargetForFlex() { + const target = this._target; + this._originalX = target._x; + this._originalY = target._y; + this._originalWidth = target._w; + this._originalHeight = target._h; + } + + setParent(from, to) { + if (from && from.isFlexContainer()) { + from._layout._changedChildren(); + } + + if (to && to.isFlexContainer()) { + this._enableFlexItem(); + to._layout._changedChildren(); + } + this._checkEnabled(); + } + + get flexParent() { + if (this._flexItemDisabled) { + return null; + } + + const parent = this._target._parent; + if (parent && parent.isFlexContainer()) { + return parent._layout; + } + return null; + } + + setVisible(v) { + const parent = this.flexParent; + if (parent) { + parent._changedChildren(); + } + } + + get items() { + if (!this._items) { + this._items = this._getFlexItems(); + } + return this._items; + } + + _getFlexItems() { + const items = []; + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const item = children[i]; + if (item.visible) { + if (item.isFlexItem()) { + items.push(item.layout); + } + } + } + } + return items; + } + + _changedChildren() { + this._clearFlexItemsCache(); + this.changedContents(); + } + + _clearFlexItemsCache() { + this._items = null; + } + + setLayout(x, y, w, h) { + let originalX = this._originalX; + let originalY = this._originalY; + if (this.funcX) { + originalX = this.funcX(FlexUtils.getParentAxisSizeWithPadding(this, true)); + } + if (this.funcY) { + originalY = this.funcY(FlexUtils.getParentAxisSizeWithPadding(this, false)); + } + + if (this.isFlexItemEnabled()) { + this.target.setLayout(x + originalX, y + originalY, w, h); + } else { + // Reuse the x,y 'settings'. + this.target.setLayout(originalX, originalY, w, h); + } + } + + changedDimensions(changeWidth = true, changeHeight = true) { + this._updateRecalc(changeWidth, changeHeight); + } + + changedContents() { + this._updateRecalc(); + } + + forceLayout() { + this._updateRecalc(); + } + + isChanged() { + return this._recalc > 0; + } + + _updateRecalc(changeExternalWidth = false, changeExternalHeight = false) { + if (this.isFlexEnabled()) { + const layout = this._flex._layout; + + // When something internal changes, it can have effect on the external dimensions. + changeExternalWidth = changeExternalWidth || layout.isAxisFitToContents(true); + changeExternalHeight = changeExternalHeight || layout.isAxisFitToContents(false); + } + + const recalc = 1 + (changeExternalWidth ? 2 : 0) + (changeExternalHeight ? 4 : 0); + const newRecalcFlags = this.getNewRecalcFlags(recalc); + this._recalc |= recalc; + if (newRecalcFlags > 1) { + if (this.flexParent) { + this.flexParent._updateRecalcBottomUp(recalc); + } else { + this._target.triggerLayout(); + } + } else { + this._target.triggerLayout(); + } + } + + getNewRecalcFlags(flags) { + return (7 - this._recalc) & flags; + } + + _updateRecalcBottomUp(childRecalc) { + const newRecalc = this._getRecalcFromChangedChildRecalc(childRecalc); + const newRecalcFlags = this.getNewRecalcFlags(newRecalc); + this._recalc |= newRecalc; + if (newRecalcFlags > 1) { + const flexParent = this.flexParent; + if (flexParent) { + flexParent._updateRecalcBottomUp(newRecalc); + } else { + this._target.triggerLayout(); + } + } else { + this._target.triggerLayout(); + } + } + + _getRecalcFromChangedChildRecalc(childRecalc) { + const layout = this._flex._layout; + + const mainAxisRecalcFlag = layout._horizontal ? 1 : 2; + const crossAxisRecalcFlag = layout._horizontal ? 2 : 1; + + const crossAxisDimensionsChangedInChild = (childRecalc & crossAxisRecalcFlag); + if (!crossAxisDimensionsChangedInChild) { + const mainAxisDimensionsChangedInChild = (childRecalc & mainAxisRecalcFlag); + if (mainAxisDimensionsChangedInChild) { + const mainAxisIsWrapping = layout.isWrapping(); + if (mainAxisIsWrapping) { + const crossAxisIsFitToContents = layout.isCrossAxisFitToContents(); + if (crossAxisIsFitToContents) { + // Special case: due to wrapping, the cross axis size may be changed. + childRecalc += crossAxisRecalcFlag; + } + } + } + } + + let isWidthDynamic = layout.isAxisFitToContents(true); + let isHeightDynamic = layout.isAxisFitToContents(false); + + if (layout.shrunk) { + // If during previous layout this container was 'shrunk', any changes may change the 'min axis size' of the + // contents, leading to a different axis size on this container even when it was not 'fit to contents'. + if (layout._horizontal) { + isWidthDynamic = true; + } else { + isHeightDynamic = true; + } + } + + const localRecalc = 1 + (isWidthDynamic ? 2 : 0) + (isHeightDynamic ? 4 : 0); + + const combinedRecalc = childRecalc & localRecalc; + return combinedRecalc; + } + + get recalc() { + return this._recalc; + } + + clearRecalcFlag() { + this._recalc = 0; + } + + enableLocalRecalcFlag() { + this._recalc = 1; + } + + get originalX() { + return this._originalX; + } + + setOriginalXWithoutUpdatingLayout(v) { + this._originalX = v; + } + + get originalY() { + return this._originalY; + } + + setOriginalYWithoutUpdatingLayout(v) { + this._originalY = v; + } + + get originalWidth() { + return this._originalWidth; + } + + set originalWidth(v) { + if (this._originalWidth !== v) { + this._originalWidth = v; + this.changedDimensions(true, false); + } + } + + get originalHeight() { + return this._originalHeight; + } + + set originalHeight(v) { + if (this._originalHeight !== v) { + this._originalHeight = v; + this.changedDimensions(false, true); + } + } + + get funcX() { + return this._target.funcX; + } + + get funcY() { + return this._target.funcY; + } + + get funcW() { + return this._target.funcW; + } + + get funcH() { + return this._target.funcH; + } +} + +class TextureSource { + + constructor(manager, loader = null) { + this.id = TextureSource.id++; + + this.manager = manager; + + this.stage = manager.stage; + + /** + * All enabled textures (textures that are used by visible elements). + * @type {Set} + */ + this.textures = new Set(); + + /** + * The number of active textures (textures that have at least one active element). + * @type {number} + * @private + */ + this._activeTextureCount = 0; + + /** + * The factory for the source of this texture. + * @type {Function} + */ + this.loader = loader; + + /** + * Identifier for reuse. + * @type {String} + */ + this.lookupId = null; + + /** + * If set, this.is called when the texture source is no longer displayed (this.components.size becomes 0). + * @type {Function} + */ + this._cancelCb = null; + + /** + * Loading since timestamp in millis. + * @type {number} + */ + this.loadingSince = 0; + + this.w = 0; + this.h = 0; + + this._nativeTexture = null; + + /** + * If true, then this.texture source is never freed from memory during garbage collection. + * @type {boolean} + */ + this.permanent = false; + + /** + * Sub-object with texture-specific rendering information. + * For images, contains the src property, for texts, contains handy rendering information. + * @type {Object} + */ + this.renderInfo = null; + + /** + * Generated for 'renderToTexture'. + * @type {boolean} + * @private + */ + this._isResultTexture = !this.loader; + + /** + * Contains the load error, if the texture source could previously not be loaded. + * @type {object} + * @private + */ + this._loadError = null; + + /** + * Hold a reference to the javascript variable which contains the texture, this is not required for WebGL in WebBrowsers but is required for Spark runtime. + * @type {object} + * @private + */ + this._imageRef = null; + + } + + get loadError() { + return this._loadError; + } + + addTexture(v) { + if (!this.textures.has(v)) { + this.textures.add(v); + } + } + + removeTexture(v) { + this.textures.delete(v); + } + + incActiveTextureCount() { + this._activeTextureCount++; + if (this._activeTextureCount === 1) { + this.becomesUsed(); + } + } + + decActiveTextureCount() { + this._activeTextureCount--; + if (this._activeTextureCount === 0) { + this.becomesUnused(); + } + } + + get isResultTexture() { + return this._isResultTexture; + } + + set isResultTexture(v) { + this._isResultTexture = v; + } + + forEachEnabledElement(cb) { + this.textures.forEach(texture => { + texture.elements.forEach(cb); + }); + } + + hasEnabledElements() { + return this.textures.size > 0; + } + + forEachActiveElement(cb) { + this.textures.forEach(texture => { + texture.elements.forEach(element => { + if (element.active) { + cb(element); + } + }); + }); + } + + getRenderWidth() { + return this.w; + } + + getRenderHeight() { + return this.h; + } + + allowCleanup() { + return !this.permanent && !this.isUsed(); + } + + becomesUsed() { + // Even while the texture is being loaded, make sure it is on the lookup map so that others can reuse it. + this.load(); + } + + becomesUnused() { + this.cancel(); + } + + cancel() { + if (this.isLoading()) { + if (this._cancelCb) { + this._cancelCb(this); + + // Clear callback to avoid memory leaks. + this._cancelCb = null; + } + this.loadingSince = 0; + } + } + + isLoaded() { + return !!this._nativeTexture; + } + + isLoading() { + return (this.loadingSince > 0); + } + + isError() { + return !!this._loadError; + } + + reload() { + this.free(); + if (this.isUsed()) { + this.load(); + } + } + + load(forceSync = false) { + // From the moment of loading (when a texture source becomes used by active elements) + if (this.isResultTexture) { + // Element result texture source, for which the loading is managed by the core. + return; + } + + if (!this._nativeTexture && !this.isLoading()) { + this.loadingSince = (new Date()).getTime(); + this._cancelCb = this.loader((err, options) => { + // Ignore loads that come in after a cancel. + if (this.isLoading()) { + // Clear callback to avoid memory leaks. + this._cancelCb = null; + + if (this.manager.stage.destroyed) { + // Ignore async load when stage is destroyed. + return; + } + if (err) { + // Emit txError. + this.onError(err); + } else if (options && options.source) { + if (!this.stage.isUpdatingFrame() && !forceSync && (options.throttle !== false)) { + const textureThrottler = this.stage.textureThrottler; + this._cancelCb = textureThrottler.genericCancelCb; + textureThrottler.add(this, options); + } else { + this.processLoadedSource(options); + } + } + } + }, this); + } + } + + processLoadedSource(options) { + this.loadingSince = 0; + this.setSource(options); + } + + setSource(options) { + const source = options.source; + + this.w = source.width || (options && options.w) || 0; + this.h = source.height || (options && options.h) || 0; + + if (options && options.renderInfo) { + // Assign to id in cache so that it can be reused. + this.renderInfo = options.renderInfo; + } + + this.permanent = !!options.permanent; + + if (options && options.imageRef) + this._imageRef = options.imageRef; + if (options && options.flipTextureY) { + this._flipTextureY = options.flipTextureY; + } else { + this._flipTextureY = false; + } + + if (this._isNativeTexture(source)) { + // Texture managed by caller. + this._nativeTexture = source; + + this.w = this.w || source.w; + this.h = this.h || source.h; + + // WebGLTexture objects are by default; + this.permanent = options.hasOwnProperty('permanent') ? options.permanent : true; + } else { + this.manager.uploadTextureSource(this, options); + } + + // Must be cleared when reload is succesful. + this._loadError = null; + + this.onLoad(); + } + + isUsed() { + return this._activeTextureCount > 0; + } + + onLoad() { + if (this.isUsed()) { + this.textures.forEach(texture => { + texture.onLoad(); + }); + } + } + + forceRenderUpdate() { + // Userland should call this method after changing the nativeTexture manually outside of the framework + // (using tex[Sub]Image2d for example). + + if (this._nativeTexture) { + // Change 'update' flag. This is currently not used by the framework but is handy in userland. + this._nativeTexture.update = this.stage.frameCounter; + } + + this.forEachActiveElement(function (element) { + element.forceRenderUpdate(); + }); + + } + + forceUpdateRenderCoords() { + this.forEachActiveElement(function (element) { + element._updateTextureCoords(); + }); + } + + get nativeTexture() { + return this._nativeTexture; + } + + clearNativeTexture() { + this._nativeTexture = null; + //also clear the reference to the texture variable. + this._imageRef = null; + } + + /** + * Used for result textures. + */ + replaceNativeTexture(newNativeTexture, w, h) { + let prevNativeTexture = this._nativeTexture; + // Loaded by core. + this._nativeTexture = newNativeTexture; + this.w = w; + this.h = h; + + if (!prevNativeTexture && this._nativeTexture) { + this.forEachActiveElement(element => element.onTextureSourceLoaded()); + } + + if (!this._nativeTexture) { + this.forEachActiveElement(element => element._setDisplayedTexture(null)); + } + + // Dimensions must be updated also on enabled elements, as it may force it to go within bounds. + this.forEachEnabledElement(element => element._updateDimensions()); + + // Notice that the sprite map must never contain render textures. + } + + onError(e) { + this._loadError = e; + this.loadingSince = 0; + console.error('texture load error', e, this.lookupId); + this.forEachActiveElement(element => element.onTextureSourceLoadError(e)); + } + + free() { + if (this.isLoaded()) { + this.manager.freeTextureSource(this); + } + } + + _isNativeTexture(source) { + return ((Utils.isNode ? source.constructor.name === "WebGLTexture" : source instanceof WebGLTexture)); + } + +} + +TextureSource.prototype.isTextureSource = true; + +TextureSource.id = 1; + +class ElementTexturizer { + + constructor(elementCore) { + + this._element = elementCore.element; + this._core = elementCore; + + this.ctx = this._core.ctx; + + this._enabled = false; + this.lazy = false; + this._colorize = false; + + this._renderTexture = null; + + this._renderTextureReused = false; + + this._resultTextureSource = null; + + this._renderOffscreen = false; + + this.empty = false; + } + + get enabled() { + return this._enabled; + } + + set enabled(v) { + this._enabled = v; + this._core.updateRenderToTextureEnabled(); + } + + get renderOffscreen() { + return this._renderOffscreen; + } + + set renderOffscreen(v) { + this._renderOffscreen = v; + this._core.setHasRenderUpdates(1); + + // This enforces rechecking the 'within bounds'. + this._core._setRecalc(6); + } + + get colorize() { + return this._colorize; + } + + set colorize(v) { + if (this._colorize !== v) { + this._colorize = v; + + // Only affects the finally drawn quad. + this._core.setHasRenderUpdates(1); + } + } + + _getTextureSource() { + if (!this._resultTextureSource) { + this._resultTextureSource = new TextureSource(this._element.stage.textureManager); + this.updateResultTexture(); + } + return this._resultTextureSource; + } + + hasResultTexture() { + return !!this._resultTextureSource; + } + + resultTextureInUse() { + return this._resultTextureSource && this._resultTextureSource.hasEnabledElements(); + } + + updateResultTexture() { + let resultTexture = this.getResultTexture(); + if (this._resultTextureSource) { + if (this._resultTextureSource.nativeTexture !== resultTexture) { + let w = resultTexture ? resultTexture.w : 0; + let h = resultTexture ? resultTexture.h : 0; + this._resultTextureSource.replaceNativeTexture(resultTexture, w, h); + } + + // Texture will be updated: all elements using the source need to be updated as well. + this._resultTextureSource.forEachEnabledElement(element => { + element._updateDimensions(); + element.core.setHasRenderUpdates(3); + }); + } + } + + mustRenderToTexture() { + // Check if we must really render as texture. + if (this._enabled && !this.lazy) { + return true; + } else if (this._enabled && this.lazy && this._core._hasRenderUpdates < 3) { + // Static-only: if renderToTexture did not need to update during last drawn frame, generate it as a cache. + return true; + } + return false; + } + + deactivate() { + this.release(); + } + + get renderTextureReused() { + return this._renderTextureReused; + } + + release() { + this.releaseRenderTexture(); + } + + releaseRenderTexture() { + if (this._renderTexture) { + if (!this._renderTextureReused) { + this.ctx.releaseRenderTexture(this._renderTexture); + } + this._renderTexture = null; + this._renderTextureReused = false; + this.updateResultTexture(); + } + } + + // Reuses the specified texture as the render texture (in ancestor). + reuseTextureAsRenderTexture(nativeTexture) { + if (this._renderTexture !== nativeTexture) { + this.releaseRenderTexture(); + this._renderTexture = nativeTexture; + this._renderTextureReused = true; + } + } + + hasRenderTexture() { + return !!this._renderTexture; + } + + getRenderTexture() { + if (!this._renderTexture) { + this._renderTexture = this.ctx.allocateRenderTexture(this._core._w, this._core._h); + this._renderTextureReused = false; + } + return this._renderTexture; + } + + getResultTexture() { + return this._renderTexture; + } + +} + +class ElementCore { + + constructor(element) { + this._element = element; + + this.ctx = element.stage.ctx; + + // The memory layout of the internal variables is affected by their position in the constructor. + // It boosts performance to order them by usage of cpu-heavy functions (renderSimple and update). + + this._recalc = 0; + + this._parent = null; + + this._onUpdate = null; + + this._pRecalc = 0; + + this._worldContext = new ElementCoreContext(); + + this._hasUpdates = false; + + this._localAlpha = 1; + + this._onAfterCalcs = null; + + this._onAfterUpdate = null; + + // All local translation/transform updates: directly propagated from x/y/w/h/scale/whatever. + this._localPx = 0; + this._localPy = 0; + + this._localTa = 1; + this._localTb = 0; + this._localTc = 0; + this._localTd = 1; + + this._isComplex = false; + + this._dimsUnknown = false; + + this._clipping = false; + + // Used by both update and render. + this._zSort = false; + + this._outOfBounds = 0; + + /** + * The texture source to be displayed. + * @type {TextureSource} + */ + this._displayedTextureSource = null; + + this._zContextUsage = 0; + + this._children = null; + + this._hasRenderUpdates = 0; + + this._zIndexedChildren = null; + + this._renderContext = this._worldContext; + + this.renderState = this.ctx.renderState; + + this._scissor = null; + + // The ancestor ElementCore that owns the inherited shader. Null if none is active (default shader). + this._shaderOwner = null; + + + this._updateTreeOrder = 0; + + this._colorUl = this._colorUr = this._colorBl = this._colorBr = 0xFFFFFFFF; + + this._x = 0; + this._y = 0; + this._w = 0; + this._h = 0; + + this._optFlags = 0; + this._funcX = null; + this._funcY = null; + this._funcW = null; + this._funcH = null; + + this._scaleX = 1; + this._scaleY = 1; + this._pivotX = 0.5; + this._pivotY = 0.5; + this._mountX = 0; + this._mountY = 0; + this._rotation = 0; + + this._alpha = 1; + this._visible = true; + + this._ulx = 0; + this._uly = 0; + this._brx = 1; + this._bry = 1; + + this._zIndex = 0; + this._forceZIndexContext = false; + this._zParent = null; + + this._isRoot = false; + + /** + * Iff true, during zSort, this element should be 're-sorted' because either: + * - zIndex did chang + * - zParent did change + * - element was moved in the render tree + * @type {boolean} + */ + this._zIndexResort = false; + + this._shader = null; + + // Element is rendered on another texture. + this._renderToTextureEnabled = false; + + this._texturizer = null; + + this._useRenderToTexture = false; + + this._boundsMargin = null; + + this._recBoundsMargin = null; + + this._withinBoundsMargin = false; + + this._viewport = null; + + this._clipbox = true; + + this.render = this._renderSimple; + + this._layout = null; + } + + get offsetX() { + if (this._funcX) { + return this._funcX; + } else { + if (this.hasFlexLayout()) { + return this._layout.originalX; + } else { + return this._x; + } + } + } + + set offsetX(v) { + if (Utils.isFunction(v)) { + this.funcX = v; + } else { + this._disableFuncX(); + if (this.hasFlexLayout()) { + this.x += (v - this._layout.originalX); + this._layout.setOriginalXWithoutUpdatingLayout(v); + } else { + this.x = v; + } + } + } + + get x() { + return this._x; + } + + set x(v) { + if (v !== this._x) { + this._updateLocalTranslateDelta(v - this._x, 0); + this._x = v; + } + } + + get funcX() { + return (this._optFlags & 1 ? this._funcX : null); + } + + set funcX(v) { + if (this._funcX !== v) { + this._optFlags |= 1; + this._funcX = v; + if (this.hasFlexLayout()) { + this._layout.setOriginalXWithoutUpdatingLayout(0); + this.layout.forceLayout(); + } else { + this._x = 0; + this._triggerRecalcTranslate(); + } + } + } + + _disableFuncX() { + this._optFlags = this._optFlags & (0xFFFF - 1); + this._funcX = null; + } + + get offsetY() { + if (this._funcY) { + return this._funcY; + } else { + if (this.hasFlexLayout()) { + return this._layout.originalY; + } else { + return this._y; + } + } + } + + set offsetY(v) { + if (Utils.isFunction(v)) { + this.funcY = v; + } else { + this._disableFuncY(); + if (this.hasFlexLayout()) { + this.y += (v - this._layout.originalY); + this._layout.setOriginalYWithoutUpdatingLayout(v); + } else { + this.y = v; + } + } + } + + get y() { + return this._y; + } + + set y(v) { + if (v !== this._y) { + this._updateLocalTranslateDelta(0, v - this._y); + this._y = v; + } + } + + get funcY() { + return (this._optFlags & 2 ? this._funcY : null); + } + + set funcY(v) { + if (this._funcY !== v) { + this._optFlags |= 2; + this._funcY = v; + if (this.hasFlexLayout()) { + this._layout.setOriginalYWithoutUpdatingLayout(0); + this.layout.forceLayout(); + } else { + this._y = 0; + this._triggerRecalcTranslate(); + } + } + } + + _disableFuncY() { + this._optFlags = this._optFlags & (0xFFFF - 2); + this._funcY = null; + } + + get funcW() { + return (this._optFlags & 4 ? this._funcW : null); + } + + set funcW(v) { + if (this._funcW !== v) { + this._optFlags |= 4; + this._funcW = v; + if (this.hasFlexLayout()) { + this._layout._originalWidth = 0; + this.layout.changedDimensions(true, false); + } else { + this._w = 0; + this._triggerRecalcTranslate(); + } + } + } + + disableFuncW() { + this._optFlags = this._optFlags & (0xFFFF - 4); + this._funcW = null; + } + + get funcH() { + return (this._optFlags & 8 ? this._funcH : null); + } + + set funcH(v) { + if (this._funcH !== v) { + this._optFlags |= 8; + this._funcH = v; + if (this.hasFlexLayout()) { + this._layout._originalHeight = 0; + this.layout.changedDimensions(false, true); + } else { + this._h = 0; + this._triggerRecalcTranslate(); + } + } + } + + disableFuncH() { + this._optFlags = this._optFlags & (0xFFFF - 8); + this._funcH = null; + } + + get w() { + return this._w; + } + + getRenderWidth() { + if (this.hasFlexLayout()) { + return this._layout.originalWidth; + } else { + return this._w; + } + } + + get h() { + return this._h; + } + + getRenderHeight() { + if (this.hasFlexLayout()) { + return this._layout.originalHeight; + } else { + return this._h; + } + } + + get scaleX() { + return this._scaleX; + } + + set scaleX(v) { + if (this._scaleX !== v) { + this._scaleX = v; + this._updateLocalTransform(); + } + } + + get scaleY() { + return this._scaleY; + } + + set scaleY(v) { + if (this._scaleY !== v) { + this._scaleY = v; + this._updateLocalTransform(); + } + } + + get scale() { + return this.scaleX; + } + + set scale(v) { + if (this._scaleX !== v || this._scaleY !== v) { + this._scaleX = v; + this._scaleY = v; + this._updateLocalTransform(); + } + } + + get pivotX() { + return this._pivotX; + } + + set pivotX(v) { + if (this._pivotX !== v) { + this._pivotX = v; + this._updateLocalTranslate(); + } + } + + get pivotY() { + return this._pivotY; + } + + set pivotY(v) { + if (this._pivotY !== v) { + this._pivotY = v; + this._updateLocalTranslate(); + } + } + + get pivot() { + return this._pivotX; + } + + set pivot(v) { + if (this._pivotX !== v || this._pivotY !== v) { + this._pivotX = v; + this._pivotY = v; + this._updateLocalTranslate(); + } + } + + get mountX() { + return this._mountX; + } + + set mountX(v) { + if (this._mountX !== v) { + this._mountX = v; + this._updateLocalTranslate(); + } + } + + get mountY() { + return this._mountY; + } + + set mountY(v) { + if (this._mountY !== v) { + this._mountY = v; + this._updateLocalTranslate(); + } + } + + get mount() { + return this._mountX; + } + + set mount(v) { + if (this._mountX !== v || this._mountY !== v) { + this._mountX = v; + this._mountY = v; + this._updateLocalTranslate(); + } + } + + get rotation() { + return this._rotation; + } + + set rotation(v) { + if (this._rotation !== v) { + this._rotation = v; + this._updateLocalTransform(); + } + } + + get alpha() { + return this._alpha; + } + + set alpha(v) { + // Account for rounding errors. + v = (v > 1 ? 1 : (v < 1e-14 ? 0 : v)); + if (this._alpha !== v) { + let prev = this._alpha; + this._alpha = v; + this._updateLocalAlpha(); + if ((prev === 0) !== (v === 0)) { + this._element._updateEnabledFlag(); + } + } + } + + get visible() { + return this._visible; + } + + set visible(v) { + if (this._visible !== v) { + this._visible = v; + this._updateLocalAlpha(); + this._element._updateEnabledFlag(); + + if (this.hasFlexLayout()) { + this.layout.setVisible(v); + } + } + } + + _updateLocalTransform() { + if (this._rotation !== 0 && this._rotation % (2 * Math.PI)) { + // check to see if the rotation is the same as the previous render. This means we only need to use sin and cos when rotation actually changes + let _sr = Math.sin(this._rotation); + let _cr = Math.cos(this._rotation); + + this._setLocalTransform( + _cr * this._scaleX, + -_sr * this._scaleY, + _sr * this._scaleX, + _cr * this._scaleY + ); + } else { + this._setLocalTransform( + this._scaleX, + 0, + 0, + this._scaleY + ); + } + this._updateLocalTranslate(); + }; + + _updateLocalTranslate() { + this._recalcLocalTranslate(); + this._triggerRecalcTranslate(); + }; + + _recalcLocalTranslate() { + let pivotXMul = this._pivotX * this._w; + let pivotYMul = this._pivotY * this._h; + let px = this._x - (pivotXMul * this._localTa + pivotYMul * this._localTb) + pivotXMul; + let py = this._y - (pivotXMul * this._localTc + pivotYMul * this._localTd) + pivotYMul; + px -= this._mountX * this._w; + py -= this._mountY * this._h; + this._localPx = px; + this._localPy = py; + } + + _updateLocalTranslateDelta(dx, dy) { + this._addLocalTranslate(dx, dy); + }; + + _updateLocalAlpha() { + this._setLocalAlpha(this._visible ? this._alpha : 0); + }; + + /** + * @param {number} type + * 0: no updates + * 1: re-invoke shader + * 3: re-create render texture and re-invoke shader + */ + setHasRenderUpdates(type) { + if (this._worldContext.alpha) { + // Ignore if 'world invisible'. Render updates will be reset to 3 for every element that becomes visible. + let p = this; + p._hasRenderUpdates = Math.max(type, p._hasRenderUpdates); + while ((p = p._parent) && (p._hasRenderUpdates !== 3)) { + p._hasRenderUpdates = 3; + } + } + } + + /** + * @param {Number} type + * 1: alpha + * 2: translate + * 4: transform + * 128: becomes visible + * 256: flex layout updated + */ + _setRecalc(type) { + this._recalc |= type; + + this._setHasUpdates(); + + // Any changes in descendants should trigger texture updates. + if (this._parent) { + this._parent.setHasRenderUpdates(3); + } + } + + _setHasUpdates() { + let p = this; + while(p && !p._hasUpdates) { + p._hasUpdates = true; + p = p._parent; + } + } + + getParent() { + return this._parent; + } + + setParent(parent) { + if (parent !== this._parent) { + let prevIsZContext = this.isZContext(); + let prevParent = this._parent; + this._parent = parent; + + // Notify flex layout engine. + if (this._layout || (parent && parent.isFlexContainer())) { + this.layout.setParent(prevParent, parent); + } + + if (prevParent) { + // When elements are deleted, the render texture must be re-rendered. + prevParent.setHasRenderUpdates(3); + } + + this._setRecalc(1 + 2 + 4); + + if (this._parent) { + // Force parent to propagate hasUpdates flag. + this._parent._setHasUpdates(); + } + + if (this._zIndex === 0) { + this.setZParent(parent); + } else { + this.setZParent(parent ? parent.findZContext() : null); + } + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(prevParent.findZContext()); + } + } + + // Tree order did change: even if zParent stays the same, we must resort. + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + + if (!this._shader) { + let newShaderOwner = parent && !parent._renderToTextureEnabled ? parent._shaderOwner : null; + if (newShaderOwner !== this._shaderOwner) { + this.setHasRenderUpdates(1); + this._setShaderOwnerRecursive(newShaderOwner); + } + } + } + }; + + enableZSort(force = false) { + if (!this._zSort && this._zContextUsage > 0) { + this._zSort = true; + if (force) { + // ZSort must be done, even if this element is invisible. + // This is done to prevent memory leaks when removing element from inactive render branches. + this.ctx.forceZSort(this); + } + } + } + + addChildAt(index, child) { + if (!this._children) this._children = []; + this._children.splice(index, 0, child); + child.setParent(this); + }; + + setChildAt(index, child) { + if (!this._children) this._children = []; + this._children[index].setParent(null); + this._children[index] = child; + child.setParent(this); + } + + removeChildAt(index) { + let child = this._children[index]; + this._children.splice(index, 1); + child.setParent(null); + }; + + removeChildren() { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].setParent(null); + } + + this._children.splice(0); + + if (this._zIndexedChildren) { + this._zIndexedChildren.splice(0); + } + } + }; + + syncChildren(removed, added, order) { + this._children = order; + for (let i = 0, n = removed.length; i < n; i++) { + removed[i].setParent(null); + } + for (let i = 0, n = added.length; i < n; i++) { + added[i].setParent(this); + } + } + + moveChild(fromIndex, toIndex) { + let c = this._children[fromIndex]; + this._children.splice(fromIndex, 1); + this._children.splice(toIndex, 0, c); + + // Tree order changed: must resort!; + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + } + + _setLocalTransform(a, b, c, d) { + this._setRecalc(4); + this._localTa = a; + this._localTb = b; + this._localTc = c; + this._localTd = d; + + // We also regard negative scaling as a complex case, so that we can optimize the non-complex case better. + this._isComplex = (b !== 0) || (c !== 0) || (a < 0) || (d < 0); + }; + + _addLocalTranslate(dx, dy) { + this._localPx += dx; + this._localPy += dy; + this._triggerRecalcTranslate(); + } + + _setLocalAlpha(a) { + if (!this._worldContext.alpha && ((this._parent && this._parent._worldContext.alpha) && a)) { + // Element is becoming visible. We need to force update. + this._setRecalc(1 + 128); + } else { + this._setRecalc(1); + } + + if (a < 1e-14) { + // Tiny rounding errors may cause failing visibility tests. + a = 0; + } + + this._localAlpha = a; + }; + + setDimensions(w, h, isEstimate = this._dimsUnknown) { + // In case of an estimation, the update loop should perform different bound checks. + this._dimsUnknown = isEstimate; + + if (this.hasFlexLayout()) { + this._layout.originalWidth = w; + this._layout.originalHeight = h; + } else { + if (this._w !== w || this._h !== h) { + this._updateDimensions(w, h); + return true; + } + } + return false; + }; + + _updateDimensions(w, h) { + if (this._w !== w || this._h !== h) { + this._w = w; + this._h = h; + + this._triggerRecalcTranslate(); + + if (this._texturizer) { + this._texturizer.releaseRenderTexture(); + this._texturizer.updateResultTexture(); + } + // Due to width/height change: update the translation vector. + this._updateLocalTranslate(); + } + } + + setTextureCoords(ulx, uly, brx, bry) { + this.setHasRenderUpdates(3); + + this._ulx = ulx; + this._uly = uly; + this._brx = brx; + this._bry = bry; + }; + + get displayedTextureSource() { + return this._displayedTextureSource; + } + + setDisplayedTextureSource(textureSource) { + this.setHasRenderUpdates(3); + this._displayedTextureSource = textureSource; + }; + + get isRoot() { + return this._isRoot; + } + + setAsRoot() { + // Use parent dummy. + this._parent = new ElementCore(this._element); + + // After setting root, make sure it's updated. + this._parent._hasRenderUpdates = 3; + this._parent._hasUpdates = true; + + // Root is, and will always be, the primary zContext. + this._isRoot = true; + + this.ctx.root = this; + + // Set scissor area of 'fake parent' to stage's viewport. + this._parent._viewport = [0, 0, this.ctx.stage.coordsWidth, this.ctx.stage.coordsHeight]; + this._parent._scissor = this._parent._viewport; + + // When recBoundsMargin is null, the defaults are used (100 for all sides). + this._parent._recBoundsMargin = null; + + this._setRecalc(1 + 2 + 4); + }; + + isAncestorOf(c) { + let p = c; + while (p = p._parent) { + if (this === p) { + return true; + } + } + return false; + }; + + isZContext() { + return (this._forceZIndexContext || this._renderToTextureEnabled || this._zIndex !== 0 || this._isRoot || !this._parent); + }; + + findZContext() { + if (this.isZContext()) { + return this; + } else { + return this._parent.findZContext(); + } + }; + + setZParent(newZParent) { + if (this._zParent !== newZParent) { + if (this._zParent !== null) { + if (this._zIndex !== 0) { + this._zParent.decZContextUsage(); + } + + // We must filter out this item upon the next resort. + this._zParent.enableZSort(); + } + + if (newZParent !== null) { + let hadZContextUsage = (newZParent._zContextUsage > 0); + + // @pre: new parent's children array has already been modified. + if (this._zIndex !== 0) { + newZParent.incZContextUsage(); + } + + if (newZParent._zContextUsage > 0) { + if (!hadZContextUsage && (this._parent === newZParent)) ; else { + // Add new child to array. + newZParent._zIndexedChildren.push(this); + } + + // Order should be checked. + newZParent.enableZSort(); + } + } + + this._zParent = newZParent; + + // Newly added element must be marked for resort. + this._zIndexResort = true; + } + }; + + incZContextUsage() { + this._zContextUsage++; + if (this._zContextUsage === 1) { + if (!this._zIndexedChildren) { + this._zIndexedChildren = []; + } + if (this._children) { + // Copy. + for (let i = 0, n = this._children.length; i < n; i++) { + this._zIndexedChildren.push(this._children[i]); + } + // Initially, children are already sorted properly (tree order). + this._zSort = false; + } + } + }; + + decZContextUsage() { + this._zContextUsage--; + if (this._zContextUsage === 0) { + this._zSort = false; + this._zIndexedChildren.splice(0); + } + }; + + get zIndex() { + return this._zIndex; + } + + set zIndex(zIndex) { + if (this._zIndex !== zIndex) { + this.setHasRenderUpdates(1); + + let newZParent = this._zParent; + + let prevIsZContext = this.isZContext(); + if (zIndex === 0 && this._zIndex !== 0) { + if (this._parent === this._zParent) { + if (this._zParent) { + this._zParent.decZContextUsage(); + } + } else { + newZParent = this._parent; + } + } else if (zIndex !== 0 && this._zIndex === 0) { + newZParent = this._parent ? this._parent.findZContext() : null; + if (newZParent === this._zParent) { + if (this._zParent) { + this._zParent.incZContextUsage(); + this._zParent.enableZSort(); + } + } + } else if (zIndex !== this._zIndex) { + if (this._zParent && this._zParent._zContextUsage) { + this._zParent.enableZSort(); + } + } + + if (newZParent !== this._zParent) { + this.setZParent(null); + } + + this._zIndex = zIndex; + + if (newZParent !== this._zParent) { + this.setZParent(newZParent); + } + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(this._parent.findZContext()); + } + } + + // Make sure that resort is done. + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + } + }; + + get forceZIndexContext() { + return this._forceZIndexContext; + } + + set forceZIndexContext(v) { + this.setHasRenderUpdates(1); + + let prevIsZContext = this.isZContext(); + this._forceZIndexContext = v; + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(this._parent.findZContext()); + } + } + }; + + enableZContext(prevZContext) { + if (prevZContext && prevZContext._zContextUsage > 0) { + // Transfer from upper z context to this z context. + const results = this._getZIndexedDescs(); + results.forEach((c) => { + if (this.isAncestorOf(c) && c._zIndex !== 0) { + c.setZParent(this); + } + }); + } + } + + _getZIndexedDescs() { + const results = []; + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i]._getZIndexedDescsRec(results); + } + } + return results; + } + + _getZIndexedDescsRec(results) { + if (this._zIndex) { + results.push(this); + } else if (this._children && !this.isZContext()) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i]._getZIndexedDescsRec(results); + } + } + } + + disableZContext() { + // Transfer from this z context to upper z context. + if (this._zContextUsage > 0) { + let newZParent = this._parent.findZContext(); + + // Make sure that z-indexed children are up to date (old ones removed). + if (this._zSort) { + this.sortZIndexedChildren(); + } + + this._zIndexedChildren.slice().forEach(function (c) { + if (c._zIndex !== 0) { + c.setZParent(newZParent); + } + }); + } + }; + + get colorUl() { + return this._colorUl; + } + + set colorUl(color) { + if (this._colorUl !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorUl = color; + } + } + + get colorUr() { + return this._colorUr; + } + + set colorUr(color) { + if (this._colorUr !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorUr = color; + } + }; + + get colorBl() { + return this._colorBl; + } + + set colorBl(color) { + if (this._colorBl !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorBl = color; + } + }; + + get colorBr() { + return this._colorBr; + } + + set colorBr(color) { + if (this._colorBr !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorBr = color; + } + }; + + + set onUpdate(f) { + this._onUpdate = f; + this._setRecalc(7); + } + + set onAfterUpdate(f) { + this._onAfterUpdate = f; + this._setRecalc(7); + } + + set onAfterCalcs(f) { + this._onAfterCalcs = f; + this._setRecalc(7); + } + + get shader() { + return this._shader; + } + + set shader(v) { + this.setHasRenderUpdates(1); + + let prevShader = this._shader; + this._shader = v; + if (!v && prevShader) { + // Disabled shader. + let newShaderOwner = (this._parent && !this._parent._renderToTextureEnabled ? this._parent._shaderOwner : null); + this._setShaderOwnerRecursive(newShaderOwner); + } else if (v) { + // Enabled shader. + this._setShaderOwnerRecursive(this); + } + } + + get activeShader() { + return this._shaderOwner ? this._shaderOwner.shader : this.renderState.defaultShader; + } + + get activeShaderOwner() { + return this._shaderOwner; + } + + get clipping() { + return this._clipping; + } + + set clipping(v) { + if (this._clipping !== v) { + this._clipping = v; + + // Force update of scissor by updating translate. + // Alpha must also be updated because the scissor area may have been empty. + this._setRecalc(1 + 2); + } + } + + get clipbox() { + return this._clipbox; + } + + set clipbox(v) { + // In case of out-of-bounds element, all children will also be ignored. + // It will save us from executing the update/render loops for those. + // The optimization will be used immediately during the next frame. + this._clipbox = v; + } + + _setShaderOwnerRecursive(elementCore) { + this._shaderOwner = elementCore; + + if (this._children && !this._renderToTextureEnabled) { + for (let i = 0, n = this._children.length; i < n; i++) { + let c = this._children[i]; + if (!c._shader) { + c._setShaderOwnerRecursive(elementCore); + c._hasRenderUpdates = 3; + } + } + } + }; + + _setShaderOwnerChildrenRecursive(elementCore) { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + let c = this._children[i]; + if (!c._shader) { + c._setShaderOwnerRecursive(elementCore); + c._hasRenderUpdates = 3; + } + } + } + }; + + _hasRenderContext() { + return this._renderContext !== this._worldContext; + } + + get renderContext() { + return this._renderContext; + } + + updateRenderToTextureEnabled() { + // Enforce texturizer initialisation. + let v = this.texturizer._enabled; + + if (v) { + this._enableRenderToTexture(); + } else { + this._disableRenderToTexture(); + this._texturizer.releaseRenderTexture(); + } + } + + _enableRenderToTexture() { + if (!this._renderToTextureEnabled) { + let prevIsZContext = this.isZContext(); + + this._renderToTextureEnabled = true; + + this._renderContext = new ElementCoreContext(); + + // If render to texture is active, a new shader context is started. + this._setShaderOwnerChildrenRecursive(null); + + if (!prevIsZContext) { + // Render context forces z context. + this.enableZContext(this._parent ? this._parent.findZContext() : null); + } + + this.setHasRenderUpdates(3); + + // Make sure that the render coordinates get updated. + this._setRecalc(7); + + this.render = this._renderAdvanced; + } + } + + _disableRenderToTexture() { + if (this._renderToTextureEnabled) { + this._renderToTextureEnabled = false; + + this._setShaderOwnerChildrenRecursive(this._shaderOwner); + + this._renderContext = this._worldContext; + + if (!this.isZContext()) { + this.disableZContext(); + } + + // Make sure that the render coordinates get updated. + this._setRecalc(7); + + this.setHasRenderUpdates(3); + + this.render = this._renderSimple; + } + } + + isWhite() { + return (this._colorUl === 0xFFFFFFFF) && (this._colorUr === 0xFFFFFFFF) && (this._colorBl === 0xFFFFFFFF) && (this._colorBr === 0xFFFFFFFF); + } + + hasSimpleTexCoords() { + return (this._ulx === 0) && (this._uly === 0) && (this._brx === 1) && (this._bry === 1); + } + + _stashTexCoords() { + this._stashedTexCoords = [this._ulx, this._uly, this._brx, this._bry]; + this._ulx = 0; + this._uly = 0; + this._brx = 1; + this._bry = 1; + } + + _unstashTexCoords() { + this._ulx = this._stashedTexCoords[0]; + this._uly = this._stashedTexCoords[1]; + this._brx = this._stashedTexCoords[2]; + this._bry = this._stashedTexCoords[3]; + this._stashedTexCoords = null; + } + + _stashColors() { + this._stashedColors = [this._colorUl, this._colorUr, this._colorBr, this._colorBl]; + this._colorUl = 0xFFFFFFFF; + this._colorUr = 0xFFFFFFFF; + this._colorBr = 0xFFFFFFFF; + this._colorBl = 0xFFFFFFFF; + } + + _unstashColors() { + this._colorUl = this._stashedColors[0]; + this._colorUr = this._stashedColors[1]; + this._colorBr = this._stashedColors[2]; + this._colorBl = this._stashedColors[3]; + this._stashedColors = null; + } + + isVisible() { + return (this._localAlpha > 1e-14); + }; + + get outOfBounds() { + return this._outOfBounds; + } + + set boundsMargin(v) { + + /** + * null: inherit from parent. + * number[4]: specific margins: left, top, right, bottom. + */ + this._boundsMargin = v ? v.slice() : null; + + // We force recalc in order to set all boundsMargin recursively during the next update. + this._triggerRecalcTranslate(); + } + + get boundsMargin() { + return this._boundsMargin; + } + + update() { + this._recalc |= this._parent._pRecalc; + + if (this._layout && this._layout.isEnabled()) { + if (this._recalc & 256) { + this._layout.layoutFlexTree(); + } + } else if ((this._recalc & 2) && this._optFlags) { + this._applyRelativeDimFuncs(); + } + + if (this._onUpdate) { + // Block all 'upwards' updates when changing things in this branch. + this._hasUpdates = true; + this._onUpdate(this.element, this); + } + + const pw = this._parent._worldContext; + let w = this._worldContext; + const visible = (pw.alpha && this._localAlpha); + + /** + * We must update if: + * - branch contains updates (even when invisible because it may contain z-indexed descendants) + * - there are (inherited) updates and this branch is visible + * - this branch becomes invisible (descs may be z-indexed so we must update all alpha values) + */ + if (this._hasUpdates || (this._recalc && visible) || (w.alpha && !visible)) { + let recalc = this._recalc; + + // Update world coords/alpha. + if (recalc & 1) { + if (!w.alpha && visible) { + // Becomes visible. + this._hasRenderUpdates = 3; + } + w.alpha = pw.alpha * this._localAlpha; + + if (w.alpha < 1e-14) { + // Tiny rounding errors may cause failing visibility tests. + w.alpha = 0; + } + } + + if (recalc & 6) { + w.px = pw.px + this._localPx * pw.ta; + w.py = pw.py + this._localPy * pw.td; + if (pw.tb !== 0) w.px += this._localPy * pw.tb; + if (pw.tc !== 0) w.py += this._localPx * pw.tc; + } + + if (recalc & 4) { + w.ta = this._localTa * pw.ta; + w.tb = this._localTd * pw.tb; + w.tc = this._localTa * pw.tc; + w.td = this._localTd * pw.td; + + if (this._isComplex) { + w.ta += this._localTc * pw.tb; + w.tb += this._localTb * pw.ta; + w.tc += this._localTc * pw.td; + w.td += this._localTb * pw.tc; + } + } + + // Update render coords/alpha. + const pr = this._parent._renderContext; + if (this._parent._hasRenderContext()) { + const init = this._renderContext === this._worldContext; + if (init) { + // First render context build: make sure that it is fully initialized correctly. + // Otherwise, if we get into bounds later, the render context would not be initialized correctly. + this._renderContext = new ElementCoreContext(); + } + + const r = this._renderContext; + + // Update world coords/alpha. + if (init || (recalc & 1)) { + r.alpha = pr.alpha * this._localAlpha; + + if (r.alpha < 1e-14) { + r.alpha = 0; + } + } + + if (init || (recalc & 6)) { + r.px = pr.px + this._localPx * pr.ta; + r.py = pr.py + this._localPy * pr.td; + if (pr.tb !== 0) r.px += this._localPy * pr.tb; + if (pr.tc !== 0) r.py += this._localPx * pr.tc; + } + + if (init) { + // We set the recalc toggle, because we must make sure that the scissor is updated. + recalc |= 2; + } + + if (init || (recalc & 4)) { + r.ta = this._localTa * pr.ta; + r.tb = this._localTd * pr.tb; + r.tc = this._localTa * pr.tc; + r.td = this._localTd * pr.td; + + if (this._isComplex) { + r.ta += this._localTc * pr.tb; + r.tb += this._localTb * pr.ta; + r.tc += this._localTc * pr.td; + r.td += this._localTb * pr.tc; + } + } + } else { + this._renderContext = this._worldContext; + } + + if (this.ctx.updateTreeOrder === -1) { + this.ctx.updateTreeOrder = this._updateTreeOrder + 1; + } else { + this._updateTreeOrder = this.ctx.updateTreeOrder++; + } + + // Determine whether we must use a 'renderTexture'. + const useRenderToTexture = this._renderToTextureEnabled && this._texturizer.mustRenderToTexture(); + if (this._useRenderToTexture !== useRenderToTexture) { + // Coords must be changed. + this._recalc |= 2 + 4; + + // Scissor may change: force update. + recalc |= 2; + + if (!this._useRenderToTexture) { + // We must release the texture. + this._texturizer.release(); + } + } + this._useRenderToTexture = useRenderToTexture; + + const r = this._renderContext; + + const bboxW = this._dimsUnknown ? 2048 : this._w; + const bboxH = this._dimsUnknown ? 2048 : this._h; + + // Calculate a bbox for this element. + let sx, sy, ex, ey; + const rComplex = (r.tb !== 0) || (r.tc !== 0) || (r.ta < 0) || (r.td < 0); + if (rComplex) { + sx = Math.min(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + ex = Math.max(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + sy = Math.min(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + ey = Math.max(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + } else { + sx = r.px; + ex = r.px + r.ta * bboxW; + sy = r.py; + ey = r.py + r.td * bboxH; + } + + if (this._dimsUnknown && (rComplex || this._localTa < 1 || this._localTb < 1)) { + // If we are dealing with a non-identity matrix, we must extend the bbox so that withinBounds and + // scissors will include the complete range of (positive) dimensions up to ,lh. + const nx = this._x * pr.ta + this._y * pr.tb + pr.px; + const ny = this._x * pr.tc + this._y * pr.td + pr.py; + if (nx < sx) sx = nx; + if (ny < sy) sy = ny; + if (nx > ex) ex = nx; + if (ny > ey) ey = ny; + } + + if (recalc & 6 || !this._scissor /* initial */) { + // Determine whether we must 'clip'. + if (this._clipping && r.isSquare()) { + // If the parent renders to a texture, it's scissor should be ignored; + const area = this._parent._useRenderToTexture ? this._parent._viewport : this._parent._scissor; + if (area) { + // Merge scissor areas. + const lx = Math.max(area[0], sx); + const ly = Math.max(area[1], sy); + this._scissor = [ + lx, + ly, + Math.min(area[2] + area[0], ex) - lx, + Math.min(area[3] + area[1], ey) - ly + ]; + } else { + this._scissor = [sx, sy, ex - sx, ey - sy]; + } + } else { + // No clipping: reuse parent scissor. + this._scissor = this._parent._useRenderToTexture ? this._parent._viewport : this._parent._scissor; + } + } + + // Calculate the outOfBounds margin. + if (this._boundsMargin) { + this._recBoundsMargin = this._boundsMargin; + } else { + this._recBoundsMargin = this._parent._recBoundsMargin; + } + + if (this._onAfterCalcs) { + // After calcs may change render coords, scissor and/or recBoundsMargin. + if (this._onAfterCalcs(this.element)) { + // Recalculate bbox. + if (rComplex) { + sx = Math.min(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + ex = Math.max(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + sy = Math.min(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + ey = Math.max(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + } else { + sx = r.px; + ex = r.px + r.ta * bboxW; + sy = r.py; + ey = r.py + r.td * bboxH; + } + + if (this._dimsUnknown && (rComplex || this._localTa < 1 || this._localTb < 1)) { + const nx = this._x * pr.ta + this._y * pr.tb + pr.px; + const ny = this._x * pr.tc + this._y * pr.td + pr.py; + if (nx < sx) sx = nx; + if (ny < sy) sy = ny; + if (nx > ex) ex = nx; + if (ny > ey) ey = ny; + } + } + } + + if (this._parent._outOfBounds === 2) { + // Inherit parent out of boundsness. + this._outOfBounds = 2; + + if (this._withinBoundsMargin) { + this._withinBoundsMargin = false; + this.element._disableWithinBoundsMargin(); + } + } else { + if (recalc & 6) { + // Recheck if element is out-of-bounds (all settings that affect this should enable recalc bit 2 or 4). + this._outOfBounds = 0; + let withinMargin = true; + + // Offscreens are always rendered as long as the parent is within bounds. + if (!this._renderToTextureEnabled || !this._texturizer || !this._texturizer.renderOffscreen) { + if (this._scissor && (this._scissor[2] <= 0 || this._scissor[3] <= 0)) { + // Empty scissor area. + this._outOfBounds = 2; + } else { + // Use bbox to check out-of-boundness. + if ((this._scissor[0] > ex) || + (this._scissor[1] > ey) || + (sx > (this._scissor[0] + this._scissor[2])) || + (sy > (this._scissor[1] + this._scissor[3])) + ) { + this._outOfBounds = 1; + } + + if (this._outOfBounds) { + if (this._clipping || this._useRenderToTexture || (this._clipbox && (bboxW && bboxH))) { + this._outOfBounds = 2; + } + } + } + + withinMargin = (this._outOfBounds === 0); + if (!withinMargin) { + // Re-test, now with margins. + if (this._recBoundsMargin) { + withinMargin = !((ex < this._scissor[0] - this._recBoundsMargin[2]) || + (ey < this._scissor[1] - this._recBoundsMargin[3]) || + (sx > this._scissor[0] + this._scissor[2] + this._recBoundsMargin[0]) || + (sy > this._scissor[1] + this._scissor[3] + this._recBoundsMargin[1])); + } else { + withinMargin = !((ex < this._scissor[0] - 100) || + (ey < this._scissor[1] - 100) || + (sx > this._scissor[0] + this._scissor[2] + 100) || + (sy > this._scissor[1] + this._scissor[3] + 100)); + } + if (withinMargin && this._outOfBounds === 2) { + // Children must be visited because they may contain elements that are within margin, so must be visible. + this._outOfBounds = 1; + } + } + } + + if (this._withinBoundsMargin !== withinMargin) { + this._withinBoundsMargin = withinMargin; + + if (this._withinBoundsMargin) { + // This may update things (txLoaded events) in the element itself, but also in descendants and ancestors. + + // Changes in ancestors should be executed during the next call of the stage update. But we must + // take care that the _recalc and _hasUpdates flags are properly registered. That's why we clear + // both before entering the children, and use _pRecalc to transfer inherited updates instead of + // _recalc directly. + + // Changes in descendants are automatically executed within the current update loop, though we must + // take care to not update the hasUpdates flag unnecessarily in ancestors. We achieve this by making + // sure that the hasUpdates flag of this element is turned on, which blocks it for ancestors. + this._hasUpdates = true; + + const recalc = this._recalc; + this._recalc = 0; + this.element._enableWithinBoundsMargin(); + + if (this._recalc) { + // This element needs to be re-updated now, because we want the dimensions (and other changes) to be updated. + return this.update(); + } + + this._recalc = recalc; + } else { + this.element._disableWithinBoundsMargin(); + } + } + } + } + + if (this._useRenderToTexture) { + // Set viewport necessary for children scissor calculation. + if (this._viewport) { + this._viewport[2] = bboxW; + this._viewport[3] = bboxH; + } else { + this._viewport = [0, 0, bboxW, bboxH]; + } + } + + // Filter out bits that should not be copied to the children (currently all are). + this._pRecalc = (this._recalc & 135); + + // Clear flags so that future updates are properly detected. + this._recalc = 0; + this._hasUpdates = false; + + if (this._outOfBounds < 2) { + if (this._useRenderToTexture) { + if (this._worldContext.isIdentity()) { + // Optimization. + // The world context is already identity: use the world context as render context to prevents the + // ancestors from having to update the render context. + this._renderContext = this._worldContext; + } else { + // Temporarily replace the render coord attribs by the identity matrix. + // This allows the children to calculate the render context. + this._renderContext = ElementCoreContext.IDENTITY; + } + } + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].update(); + } + } + + if (this._useRenderToTexture) { + this._renderContext = r; + } + } else { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._hasUpdates) { + this._children[i].update(); + } else { + // Make sure we don't lose the 'inherited' updates. + this._children[i]._recalc |= this._pRecalc; + this._children[i].updateOutOfBounds(); + } + } + } + } + + if (this._onAfterUpdate) { + this._onAfterUpdate(this.element); + } + } else { + if (this.ctx.updateTreeOrder === -1 || this._updateTreeOrder >= this.ctx.updateTreeOrder) { + // If new tree order does not interfere with the current (gaps allowed) there's no need to traverse the branch. + this.ctx.updateTreeOrder = -1; + } else { + this.updateTreeOrder(); + } + } + } + + _applyRelativeDimFuncs() { + if (this._optFlags & 1) { + const x = this._funcX(this._parent.w); + if (x !== this._x) { + this._localPx += (x - this._x); + this._x = x; + } + } + if (this._optFlags & 2) { + const y = this._funcY(this._parent.h); + if (y !== this._y) { + this._localPy += (y - this._y); + this._y = y; + } + } + + let changedDims = false; + if (this._optFlags & 4) { + const w = this._funcW(this._parent.w); + if (w !== this._w) { + this._w = w; + changedDims = true; + } + } + if (this._optFlags & 8) { + const h = this._funcH(this._parent.h); + if (h !== this._h) { + this._h = h; + changedDims = true; + } + } + + if (changedDims) { + // Recalc mount, scale position. + this._recalcLocalTranslate(); + + this.element.onDimensionsChanged(this._w, this._h); + } + } + + updateOutOfBounds() { + // Propagate outOfBounds flag to descendants (necessary because of z-indexing). + // Invisible elements are not drawn anyway. When alpha is updated, so will _outOfBounds. + if (this._outOfBounds !== 2 && this._renderContext.alpha > 0) { + + // Inherit parent out of boundsness. + this._outOfBounds = 2; + + if (this._withinBoundsMargin) { + this._withinBoundsMargin = false; + this.element._disableWithinBoundsMargin(); + } + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].updateOutOfBounds(); + } + } + } + } + + updateTreeOrder() { + if (this._localAlpha && (this._outOfBounds !== 2)) { + this._updateTreeOrder = this.ctx.updateTreeOrder++; + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].updateTreeOrder(); + } + } + } + } + + _renderSimple() { + this._hasRenderUpdates = 0; + + if (this._zSort) { + this.sortZIndexedChildren(); + } + + if (this._outOfBounds < 2 && this._renderContext.alpha) { + let renderState = this.renderState; + + if ((this._outOfBounds === 0) && this._displayedTextureSource) { + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + this.renderState.addQuad(this); + } + + // Also add children to the VBO. + if (this._children) { + if (this._zContextUsage) { + for (let i = 0, n = this._zIndexedChildren.length; i < n; i++) { + this._zIndexedChildren[i].render(); + } + } else { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._zIndex === 0) { + // If zIndex is set, this item already belongs to a zIndexedChildren array in one of the ancestors. + this._children[i].render(); + } + + } + } + } + } + } + + _renderAdvanced() { + const hasRenderUpdates = this._hasRenderUpdates; + + // We must clear the hasRenderUpdates flag before rendering, because updating result textures in combination + // with z-indexing may trigger render updates on a render branch that is 'half done'. + // We need to ensure that the full render branch is marked for render updates, not only half (leading to freeze). + this._hasRenderUpdates = 0; + + if (this._zSort) { + this.sortZIndexedChildren(); + } + + if (this._outOfBounds < 2 && this._renderContext.alpha) { + let renderState = this.renderState; + + let mustRenderChildren = true; + let renderTextureInfo; + let prevRenderTextureInfo; + if (this._useRenderToTexture) { + if (this._w === 0 || this._h === 0) { + // Ignore this branch and don't draw anything. + return; + } else if (!this._texturizer.hasRenderTexture() || (hasRenderUpdates >= 3)) { + // Switch to default shader for building up the render texture. + renderState.setShader(renderState.defaultShader, this); + + prevRenderTextureInfo = renderState.renderTextureInfo; + + renderTextureInfo = { + nativeTexture: null, + offset: 0, // Set by CoreRenderState. + w: this._w, + h: this._h, + empty: true, + cleared: false, + ignore: false, + cache: false + }; + + if (this._texturizer.hasResultTexture() || (!renderState.isCachingTexturizer && (hasRenderUpdates < 3))) { + /** + * We don't always cache render textures. + * + * The rule is, that caching for a specific render texture is only enabled if: + * - There is a result texture to be updated. + * - There were no render updates -within the contents- since last frame (ElementCore.hasRenderUpdates < 3) + * - AND there are no ancestors that are being cached during this frame (CoreRenderState.isCachingTexturizer) + * If an ancestor is cached anyway, it's probably not necessary to keep deeper caches. If the top level is to + * change while a lower one is not, that lower level will be cached instead. + * + * In case of the fast blur element, this prevents having to cache all blur levels and stages, saving a huge amount + * of GPU memory! + * + * Especially when using multiple stacked layers of the same dimensions that are RTT this will have a very + * noticable effect on performance as less render textures need to be allocated. + */ + renderTextureInfo.cache = true; + renderState.isCachingTexturizer = true; + } + + if (!this._texturizer.hasResultTexture()) { + // We can already release the current texture to the pool, as it will be rebuild anyway. + // In case of multiple layers of 'filtering', this may save us from having to create one + // render-to-texture layer. + // Notice that we don't do this when there is a result texture, as any other element may rely on + // that result texture being filled. + this._texturizer.releaseRenderTexture(); + } + + renderState.setRenderTextureInfo(renderTextureInfo); + renderState.setScissor(null); + + if (this._displayedTextureSource) { + let r = this._renderContext; + + // Use an identity context for drawing the displayed texture to the render texture. + this._renderContext = ElementCoreContext.IDENTITY; + + // Add displayed texture source in local coordinates. + this.renderState.addQuad(this); + + this._renderContext = r; + } + } else { + mustRenderChildren = false; + } + } else { + if ((this._outOfBounds === 0) && this._displayedTextureSource) { + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + this.renderState.addQuad(this); + } + } + + // Also add children to the VBO. + if (mustRenderChildren && this._children) { + if (this._zContextUsage) { + for (let i = 0, n = this._zIndexedChildren.length; i < n; i++) { + this._zIndexedChildren[i].render(); + } + } else { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._zIndex === 0) { + // If zIndex is set, this item already belongs to a zIndexedChildren array in one of the ancestors. + this._children[i].render(); + } + } + } + } + + if (this._useRenderToTexture) { + let updateResultTexture = false; + if (mustRenderChildren) { + // Finished refreshing renderTexture. + renderState.finishedRenderTexture(); + + // If nothing was rendered, we store a flag in the texturizer and prevent unnecessary + // render-to-texture and filtering. + this._texturizer.empty = renderTextureInfo.empty; + + if (renderTextureInfo.empty) { + // We ignore empty render textures and do not draw the final quad. + + // The following cleans up memory and enforces that the result texture is also cleared. + this._texturizer.releaseRenderTexture(); + } else if (renderTextureInfo.nativeTexture) { + // If nativeTexture is set, we can reuse that directly instead of creating a new render texture. + this._texturizer.reuseTextureAsRenderTexture(renderTextureInfo.nativeTexture); + + renderTextureInfo.ignore = true; + } else { + if (this._texturizer.renderTextureReused) { + // Quad operations must be written to a render texture actually owned. + this._texturizer.releaseRenderTexture(); + } + // Just create the render texture. + renderTextureInfo.nativeTexture = this._texturizer.getRenderTexture(); + } + + // Restore the parent's render texture. + renderState.setRenderTextureInfo(prevRenderTextureInfo); + + updateResultTexture = true; + } + + if (!this._texturizer.empty) { + let resultTexture = this._texturizer.getResultTexture(); + if (updateResultTexture) { + if (resultTexture) { + // Logging the update frame can be handy for userland. + resultTexture.update = renderState.stage.frameCounter; + } + this._texturizer.updateResultTexture(); + } + + if (!this._texturizer.renderOffscreen) { + // Render result texture to the actual render target. + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + + // If no render texture info is set, the cache can be reused. + const cache = !renderTextureInfo || renderTextureInfo.cache; + + renderState.setTexturizer(this._texturizer, cache); + this._stashTexCoords(); + if (!this._texturizer.colorize) this._stashColors(); + this.renderState.addQuad(this, true); + if (!this._texturizer.colorize) this._unstashColors(); + this._unstashTexCoords(); + renderState.setTexturizer(null); + } + } + } + + if (renderTextureInfo && renderTextureInfo.cache) { + // Allow siblings to cache. + renderState.isCachingTexturizer = false; + } + } + } + + get zSort() { + return this._zSort; + } + + sortZIndexedChildren() { + /** + * We want to avoid resorting everything. Instead, we do a single pass of the full array: + * - filtering out elements with a different zParent than this (were removed) + * - filtering out, but also gathering (in a temporary array) the elements that have zIndexResort flag + * - then, finally, we merge-sort both the new array and the 'old' one + * - element may have been added 'double', so when merge-sorting also check for doubles. + * - if the old one is larger (in size) than it should be, splice off the end of the array. + */ + + const n = this._zIndexedChildren.length; + let ptr = 0; + const a = this._zIndexedChildren; + + // Notice that items may occur multiple times due to z-index changing. + const b = []; + for (let i = 0; i < n; i++) { + if (a[i]._zParent === this) { + if (a[i]._zIndexResort) { + b.push(a[i]); + } else { + if (ptr !== i) { + a[ptr] = a[i]; + } + ptr++; + } + } + } + + const m = b.length; + if (m) { + for (let j = 0; j < m; j++) { + b[j]._zIndexResort = false; + } + + b.sort(ElementCore.sortZIndexedChildren); + const n = ptr; + if (!n) { + ptr = 0; + let j = 0; + do { + a[ptr++] = b[j++]; + } while(j < m); + + if (a.length > ptr) { + // Slice old (unnecessary) part off array. + a.splice(ptr); + } + } else { + // Merge-sort arrays; + ptr = 0; + let i = 0; + let j = 0; + const mergeResult = []; + do { + const v = (a[i]._zIndex === b[j]._zIndex ? a[i]._updateTreeOrder - b[j]._updateTreeOrder : a[i]._zIndex - b[j]._zIndex); + + const add = v > 0 ? b[j++] : a[i++]; + + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + + if (i >= n) { + do { + const add = b[j++]; + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + } while(j < m); + break; + } else if (j >= m) { + do { + const add = a[i++]; + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + } while(i < n); + break; + } + } while(true); + + this._zIndexedChildren = mergeResult; + } + } else { + if (a.length > ptr) { + // Slice old (unnecessary) part off array. + a.splice(ptr); + } + } + + this._zSort = false; + }; + + get localTa() { + return this._localTa; + }; + + get localTb() { + return this._localTb; + }; + + get localTc() { + return this._localTc; + }; + + get localTd() { + return this._localTd; + }; + + get element() { + return this._element; + } + + get renderUpdates() { + return this._hasRenderUpdates; + } + + get texturizer() { + if (!this._texturizer) { + this._texturizer = new ElementTexturizer(this); + } + return this._texturizer; + } + + getCornerPoints() { + let w = this._worldContext; + + return [ + w.px, + w.py, + w.px + this._w * w.ta, + w.py + this._w * w.tc, + w.px + this._w * w.ta + this._h * w.tb, + w.py + this._w * w.tc + this._h * w.td, + w.px + this._h * w.tb, + w.py + this._h * w.td + ] + }; + + getRenderTextureCoords(relX, relY) { + let r = this._renderContext; + return [ + r.px + r.ta * relX + r.tb * relY, + r.py + r.tc * relX + r.td * relY + ] + } + + getAbsoluteCoords(relX, relY) { + let w = this._renderContext; + return [ + w.px + w.ta * relX + w.tb * relY, + w.py + w.tc * relX + w.td * relY + ] + } + + + get layout() { + this._ensureLayout(); + return this._layout; + } + + get flex() { + return this._layout ? this._layout.flex : null; + } + + set flex(v) { + this.layout.flex = v; + } + + get flexItem() { + return this._layout ? this._layout.flexItem : null; + } + + set flexItem(v) { + this.layout.flexItem = v; + } + + isFlexItem() { + return !!this._layout && this._layout.isFlexItemEnabled(); + } + + isFlexContainer() { + return !!this._layout && this._layout.isFlexEnabled(); + } + + enableFlexLayout() { + this._ensureLayout(); + } + + _ensureLayout() { + if (!this._layout) { + this._layout = new FlexTarget(this); + } + } + + disableFlexLayout() { + this._triggerRecalcTranslate(); + } + + hasFlexLayout() { + return (this._layout && this._layout.isEnabled()); + } + + setLayout(x, y, w, h) { + this.x = x; + this.y = y; + this._updateDimensions(w, h); + } + + triggerLayout() { + this._setRecalc(256); + } + + _triggerRecalcTranslate() { + this._setRecalc(2); + } + +} + +class ElementCoreContext { + + constructor() { + this.alpha = 1; + + this.px = 0; + this.py = 0; + + this.ta = 1; + this.tb = 0; + this.tc = 0; + this.td = 1; + } + + isIdentity() { + return this.alpha === 1 && + this.px === 0 && + this.py === 0 && + this.ta === 1 && + this.tb === 0 && + this.tc === 0 && + this.td === 1; + } + + isSquare() { + return this.tb === 0 && this.tc === 0; + } + +} + +ElementCoreContext.IDENTITY = new ElementCoreContext(); +ElementCore.sortZIndexedChildren = function(a, b) { + return (a._zIndex === b._zIndex ? a._updateTreeOrder - b._updateTreeOrder : a._zIndex - b._zIndex); +}; + +/** + * This is a partial (and more efficient) implementation of the event emitter. + * It attempts to maintain a one-to-one mapping between events and listeners, skipping an array lookup. + * Only if there are multiple listeners, they are combined in an array. + */ +class EventEmitter { + + constructor() { + // This is set (and kept) to true when events are used at all. + this._hasEventListeners = false; + } + + on(name, listener) { + if (!this._hasEventListeners) { + this._eventFunction = {}; + this._eventListeners = {}; + this._hasEventListeners = true; + } + + const current = this._eventFunction[name]; + if (!current) { + this._eventFunction[name] = listener; + } else { + if (this._eventFunction[name] !== EventEmitter.combiner) { + this._eventListeners[name] = [this._eventFunction[name], listener]; + this._eventFunction[name] = EventEmitter.combiner; + } else { + this._eventListeners[name].push(listener); + } + } + } + + has(name, listener) { + if (this._hasEventListeners) { + const current = this._eventFunction[name]; + if (current) { + if (current === EventEmitter.combiner) { + const listeners = this._eventListeners[name]; + let index = listeners.indexOf(listener); + return (index >= 0); + } else if (this._eventFunction[name] === listener) { + return true; + } + } + } + return false; + } + + off(name, listener) { + if (this._hasEventListeners) { + const current = this._eventFunction[name]; + if (current) { + if (current === EventEmitter.combiner) { + const listeners = this._eventListeners[name]; + let index = listeners.indexOf(listener); + if (index >= 0) { + listeners.splice(index, 1); + } + if (listeners.length === 1) { + this._eventFunction[name] = listeners[0]; + this._eventListeners[name] = undefined; + } + } else if (this._eventFunction[name] === listener) { + this._eventFunction[name] = undefined; + } + } + } + } + + removeListener(name, listener) { + this.off(name, listener); + } + + emit(name, arg1, arg2, arg3) { + if (this._hasEventListeners) { + const func = this._eventFunction[name]; + if (func) { + if (func === EventEmitter.combiner) { + func(this, name, arg1, arg2, arg3); + } else { + func(arg1, arg2, arg3); + } + } + } + } + + listenerCount(name) { + if (this._hasEventListeners) { + const func = this._eventFunction[name]; + if (func) { + if (func === EventEmitter.combiner) { + return this._eventListeners[name].length; + } else { + return 1; + } + } + } else { + return 0; + } + } + + removeAllListeners(name) { + if (this._hasEventListeners) { + delete this._eventFunction[name]; + delete this._eventListeners[name]; + } + } + +} + +EventEmitter.combiner = function(object, name, arg1, arg2, arg3) { + const listeners = object._eventListeners[name]; + if (listeners) { + // Because listener may detach itself while being invoked, we use a forEach instead of for loop. + listeners.forEach((listener) => { + listener(arg1, arg2, arg3); + }); + } +}; + +EventEmitter.addAsMixin = function(cls) { + cls.prototype.on = EventEmitter.prototype.on; + cls.prototype.has = EventEmitter.prototype.has; + cls.prototype.off = EventEmitter.prototype.off; + cls.prototype.removeListener = EventEmitter.prototype.removeListener; + cls.prototype.emit = EventEmitter.prototype.emit; + cls.prototype.listenerCount = EventEmitter.prototype.listenerCount; + cls.prototype.removeAllListeners = EventEmitter.prototype.removeAllListeners; +}; + +class Shader { + + constructor(coreContext) { + this._initialized = false; + + this.ctx = coreContext; + + /** + * The (enabled) elements that use this shader. + * @type {Set} + */ + this._elements = new Set(); + } + + static create(stage, v) { + let shader; + if (Utils.isObjectLiteral(v)) { + if (v.type) { + shader = stage.renderer.createShader(stage.ctx, v); + } else { + shader = this.shader; + } + + if (shader) { + Base.patchObject(shader, v); + } + } else if (v === null) { + shader = stage.ctx.renderState.defaultShader; + } else if (v === undefined) { + shader = null; + } else { + if (v.isShader) { + if (!stage.renderer.isValidShaderType(v.constructor)) { + console.error("Invalid shader type"); + v = null; + } + shader = v; + } else { + console.error("Please specify a shader type."); + return; + } + } + + return shader; + } + + static getWebGL() { + return undefined; + } + + static getC2d() { + return undefined; + } + + addElement(elementCore) { + this._elements.add(elementCore); + } + + removeElement(elementCore) { + this._elements.delete(elementCore); + if (!this._elements) { + this.cleanup(); + } + } + + redraw() { + this._elements.forEach(elementCore => { + elementCore.setHasRenderUpdates(2); + }); + } + + patch(settings) { + Base.patchObject(this, settings); + } + + useDefault() { + // Should return true if this shader is configured (using it's properties) to not have any effect. + // This may allow the render engine to avoid unnecessary shader program switches or even texture copies. + return false; + } + + addEmpty() { + // Draws this shader even if there are no quads to be added. + // This is handy for custom shaders. + return false; + } + + cleanup() { + // Called when no more enabled elements have this shader. + } + + get isShader() { + return true; + } +} + +class Texture { + + /** + * @param {Stage} stage + */ + constructor(stage) { + this.stage = stage; + + this.manager = this.stage.textureManager; + + this.id = Texture.id++; + + /** + * All enabled elements that use this texture object (either as texture or displayedTexture). + * @type {Set} + */ + this.elements = new Set(); + + /** + * The number of enabled elements that are active. + * @type {number} + */ + this._activeCount = 0; + + /** + * The associated texture source. + * Should not be changed. + * @type {TextureSource} + */ + this._source = null; + + /** + * A resize mode can be set to cover or contain a certain area. + * It will reset the texture clipping settings. + * When manual texture clipping is performed, the resizeMode is reset. + * @type {{type: string, width: number, height: number}} + * @private + */ + this._resizeMode = null; + + /** + * The texture clipping x-offset. + * @type {number} + */ + this._x = 0; + + /** + * The texture clipping y-offset. + * @type {number} + */ + this._y = 0; + + /** + * The texture clipping width. If 0 then full width. + * @type {number} + */ + this._w = 0; + + /** + * The texture clipping height. If 0 then full height. + * @type {number} + */ + this._h = 0; + + /** + * Render precision (0.5 = fuzzy, 1 = normal, 2 = sharp even when scaled twice, etc.). + * @type {number} + * @private + */ + this._precision = 1; + + /** + * The (maximum) expected texture source width. Used for within bounds determination while texture is not yet loaded. + * If not set, 2048 is used by ElementCore.update. + * @type {number} + */ + this.mw = 0; + + /** + * The (maximum) expected texture source height. Used for within bounds determination while texture is not yet loaded. + * If not set, 2048 is used by ElementCore.update. + * @type {number} + */ + this.mh = 0; + + /** + * Indicates if Texture.prototype.texture uses clipping. + * @type {boolean} + */ + this.clipping = false; + + /** + * Indicates whether this texture must update (when it becomes used again). + * @type {boolean} + * @private + */ + this._mustUpdate = true; + + } + + get source() { + if (this._mustUpdate || this.stage.hasUpdateSourceTexture(this)) { + this._performUpdateSource(true); + this.stage.removeUpdateSourceTexture(this); + } + return this._source; + } + + addElement(v) { + if (!this.elements.has(v)) { + this.elements.add(v); + + if (this.elements.size === 1) { + if (this._source) { + this._source.addTexture(this); + } + } + + if (v.active) { + this.incActiveCount(); + } + } + } + + removeElement(v) { + if (this.elements.delete(v)) { + if (this.elements.size === 0) { + if (this._source) { + this._source.removeTexture(this); + } + } + + if (v.active) { + this.decActiveCount(); + } + } + } + + incActiveCount() { + // Ensure that texture source's activeCount has transferred ownership. + const source = this.source; + + if (source) { + this._checkForNewerReusableTextureSource(); + } + + this._activeCount++; + if (this._activeCount === 1) { + this.becomesUsed(); + } + } + + decActiveCount() { + const source = this.source; // Force updating the source. + this._activeCount--; + if (!this._activeCount) { + this.becomesUnused(); + } + } + + becomesUsed() { + if (this.source) { + this.source.incActiveTextureCount(); + } + } + + onLoad() { + if (this._resizeMode) { + this._applyResizeMode(); + } + + this.elements.forEach(element => { + if (element.active) { + element.onTextureSourceLoaded(); + } + }); + } + + _checkForNewerReusableTextureSource() { + // When this source became unused and cleaned up, it may have disappeared from the reusable texture map. + // In the meantime another texture may have been generated loaded with the same lookup id. + // If this is the case, use that one instead to make sure only one active texture source per lookup id exists. + const source = this.source; + if (!source.isLoaded()) { + const reusable = this._getReusableTextureSource(); + if (reusable && reusable.isLoaded() && (reusable !== source)) { + this._replaceTextureSource(reusable); + } + } else { + if (this._resizeMode) { + this._applyResizeMode(); + } + } + } + + becomesUnused() { + if (this.source) { + this.source.decActiveTextureCount(); + } + } + + isUsed() { + return this._activeCount > 0; + } + + /** + * Returns the lookup id for the current texture settings, to be able to reuse it. + * @returns {string|null} + */ + _getLookupId() { + // Default: do not reuse texture. + return null; + } + + /** + * Generates a loader function that is able to generate the texture for the current settings of this texture. + * It should return a function that receives a single callback argument. + * That callback should be called with the following arguments: + * - err + * - options: object + * - source: ArrayBuffer|WebGlTexture|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap + * - w: Number + * - h: Number + * - permanent: Boolean + * - hasAlpha: boolean + * - permultiplyAlpha: boolean + * - flipBlueRed: boolean + * - renderInfo: object + * The loader itself may return a Function that is called when loading of the texture is cancelled. This can be used + * to stop fetching an image when it is no longer in element, for example. + */ + _getSourceLoader() { + throw new Error("Texture.generate must be implemented."); + } + + get isValid() { + return this._getIsValid(); + } + + /** + * If texture is not 'valid', no source can be created for it. + * @returns {boolean} + */ + _getIsValid() { + return true; + } + + /** + * This must be called when the texture source must be re-generated. + */ + _changed() { + // If no element is actively using this texture, ignore it altogether. + if (this.isUsed()) { + this._updateSource(); + } else { + this._mustUpdate = true; + } + } + + _updateSource() { + // We delay all updateSource calls to the next drawFrame, so that we can bundle them. + // Otherwise we may reload a texture more often than necessary, when, for example, patching multiple text + // properties. + this.stage.addUpdateSourceTexture(this); + } + + _performUpdateSource(force = false) { + // If, in the meantime, the texture was no longer used, just remember that it must update until it becomes used + // again. + if (force || this.isUsed()) { + this._mustUpdate = false; + let source = this._getTextureSource(); + this._replaceTextureSource(source); + } + } + + _getTextureSource() { + let source = null; + if (this._getIsValid()) { + const lookupId = this._getLookupId(); + source = this._getReusableTextureSource(lookupId); + if (!source) { + source = this.manager.getTextureSource(this._getSourceLoader(), lookupId); + } + } + return source; + } + + _getReusableTextureSource(lookupId = this._getLookupId()) { + if (this._getIsValid()) { + if (lookupId) { + return this.manager.getReusableTextureSource(lookupId); + } + } + return null; + } + + _replaceTextureSource(newSource = null) { + let oldSource = this._source; + + this._source = newSource; + + if (this.elements.size) { + if (oldSource) { + if (this._activeCount) { + oldSource.decActiveTextureCount(); + } + + oldSource.removeTexture(this); + } + + if (newSource) { + // Must happen before setDisplayedTexture to ensure sprite map texcoords are used. + newSource.addTexture(this); + if (this._activeCount) { + newSource.incActiveTextureCount(); + } + } + } + + if (this.isUsed()) { + if (newSource) { + if (newSource.isLoaded()) { + this.elements.forEach(element => { + if (element.active) { + element._setDisplayedTexture(this); + } + }); + } else { + const loadError = newSource.loadError; + if (loadError) { + this.elements.forEach(element => { + if (element.active) { + element.onTextureSourceLoadError(loadError); + } + }); + } + } + } else { + this.elements.forEach(element => { + if (element.active) { + element._setDisplayedTexture(null); + } + }); + } + } + } + + load() { + // Make sure that source is up to date. + if (this.source) { + if (!this.isLoaded()) { + this.source.load(true); + } + } + } + + isLoaded() { + return this._source && this._source.isLoaded(); + } + + get loadError() { + return this._source && this._source.loadError; + } + + free() { + if (this._source) { + this._source.free(); + } + } + + set resizeMode({type = "cover", w = 0, h = 0, clipX = 0.5, clipY = 0.5}) { + this._resizeMode = {type, w, h, clipX, clipY}; + if (this.isLoaded()) { + this._applyResizeMode(); + } + } + + get resizeMode() { + return this._resizeMode; + } + + _clearResizeMode() { + this._resizeMode = null; + } + + _applyResizeMode() { + if (this._resizeMode.type === "cover") { + this._applyResizeCover(); + } else if (this._resizeMode.type === "contain") { + this._applyResizeContain(); + } + this._updatePrecision(); + this._updateClipping(); + } + + _applyResizeCover() { + const scaleX = this._resizeMode.w / this._source.w; + const scaleY = this._resizeMode.h / this._source.h; + let scale = Math.max(scaleX, scaleY); + if (!scale) return; + this._precision = 1/scale; + if (scaleX && scaleX < scale) { + const desiredSize = this._precision * this._resizeMode.w; + const choppedOffPixels = this._source.w - desiredSize; + this._x = choppedOffPixels * this._resizeMode.clipX; + this._w = this._source.w - choppedOffPixels; + } + if (scaleY && scaleY < scale) { + const desiredSize = this._precision * this._resizeMode.h; + const choppedOffPixels = this._source.h - desiredSize; + this._y = choppedOffPixels * this._resizeMode.clipY; + this._h = this._source.h - choppedOffPixels; + } + } + + _applyResizeContain() { + const scaleX = this._resizeMode.w / this._source.w; + const scaleY = this._resizeMode.h / this._source.h; + let scale = scaleX; + if (!scale || scaleY < scale) { + scale = scaleY; + } + if (!scale) return; + this._precision = 1/scale; + } + + enableClipping(x, y, w, h) { + this._clearResizeMode(); + + x *= this._precision; + y *= this._precision; + w *= this._precision; + h *= this._precision; + if (this._x !== x || this._y !== y || this._w !== w || this._h !== h) { + this._x = x; + this._y = y; + this._w = w; + this._h = h; + + this._updateClipping(true); + } + } + + disableClipping() { + this._clearResizeMode(); + + if (this._x || this._y || this._w || this._h) { + this._x = 0; + this._y = 0; + this._w = 0; + this._h = 0; + + this._updateClipping(); + } + } + + _updateClipping() { + this.clipping = !!(this._x || this._y || this._w || this._h); + + let self = this; + this.elements.forEach(function(element) { + // Ignore if not the currently displayed texture. + if (element.displayedTexture === self) { + element.onDisplayedTextureClippingChanged(); + } + }); + } + + _updatePrecision() { + let self = this; + this.elements.forEach(function(element) { + // Ignore if not the currently displayed texture. + if (element.displayedTexture === self) { + element.onPrecisionChanged(); + } + }); + } + + getNonDefaults() { + let nonDefaults = {}; + nonDefaults['type'] = this.constructor.name; + if (this.x !== 0) nonDefaults['x'] = this.x; + if (this.y !== 0) nonDefaults['y'] = this.y; + if (this.w !== 0) nonDefaults['w'] = this.w; + if (this.h !== 0) nonDefaults['h'] = this.h; + if (this.precision !== 1) nonDefaults['precision'] = this.precision; + return nonDefaults; + } + + get px() { + return this._x; + } + + get py() { + return this._y; + } + + get pw() { + return this._w; + } + + get ph() { + return this._h; + } + + get x() { + return this._x / this._precision; + } + set x(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._x !== v) { + this._x = v; + this._updateClipping(); + } + } + + get y() { + return this._y / this._precision; + } + set y(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._y !== v) { + this._y = v; + this._updateClipping(); + } + } + + get w() { + return this._w / this._precision; + } + + set w(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._w !== v) { + this._w = v; + this._updateClipping(); + } + } + + get h() { + return this._h / this._precision; + } + + set h(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._h !== v) { + this._h = v; + this._updateClipping(); + } + } + + get precision() { + return this._precision; + } + + set precision(v) { + this._clearResizeMode(); + if (this._precision !== v) { + this._precision = v; + this._updatePrecision(); + } + } + + isAutosizeTexture() { + return true; + } + + getRenderWidth() { + if (!this.isAutosizeTexture()) { + // In case of the rectangle texture, we'd prefer to not cause a 1x1 w,h as it would interfere with flex layout fit-to-contents. + return 0; + } + + // If dimensions are unknown (texture not yet loaded), use maximum width as a fallback as render width to allow proper bounds checking. + return (this._w || (this._source ? this._source.getRenderWidth() - this._x : 0)) / this._precision; + } + + getRenderHeight() { + if (!this.isAutosizeTexture()) { + // In case of the rectangle texture, we'd prefer to not cause a 1x1 w,h as it would interfere with flex layout fit-to-contents. + return 0; + } + + return (this._h || (this._source ? this._source.getRenderHeight() - this._y : 0)) / this._precision; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +Texture.prototype.isTexture = true; + +Texture.id = 0; + +class ImageTexture extends Texture { + + constructor(stage) { + super(stage); + + this._src = undefined; + this._hasAlpha = false; + } + + get src() { + return this._src; + } + + set src(v) { + if (this._src !== v) { + this._src = v; + this._changed(); + } + } + + get hasAlpha() { + return this._hasAlpha; + } + + set hasAlpha(v) { + if (this._hasAlpha !== v) { + this._hasAlpha = v; + this._changed(); + } + } + + _getIsValid() { + return !!this._src; + } + + _getLookupId() { + return this._src; + } + + _getSourceLoader() { + let src = this._src; + let hasAlpha = this._hasAlpha; + if (this.stage.getOption('srcBasePath')) { + var fc = src.charCodeAt(0); + if ((src.indexOf("//") === -1) && ((fc >= 65 && fc <= 90) || (fc >= 97 && fc <= 122) || fc == 46)) { + // Alphabetical or dot: prepend base path. + src = this.stage.getOption('srcBasePath') + src; + } + } + + return (cb) => { + return this.stage.platform.loadSrcTexture({src: src, hasAlpha: hasAlpha}, cb); + } + } + + getNonDefaults() { + const obj = super.getNonDefaults(); + if (this._src) { + obj.src = this._src; + } + return obj; + } + +} + +class TextTextureRenderer { + + constructor(stage, canvas, settings) { + this._stage = stage; + this._canvas = canvas; + this._context = this._canvas.getContext('2d'); + this._settings = settings; + } + + getPrecision() { + return this._settings.precision; + }; + + setFontProperties() { + this._context.font = Utils.isSpark ? this._stage.platform.getFontSetting(this) : this._getFontSetting(); + this._context.textBaseline = this._settings.textBaseline; + }; + + _getFontSetting() { + let ff = this._settings.fontFace; + + if (!Array.isArray(ff)) { + ff = [ff]; + } + + let ffs = []; + for (let i = 0, n = ff.length; i < n; i++) { + if (ff[i] === "serif" || ff[i] === "sans-serif") { + ffs.push(ff[i]); + } else { + ffs.push(`"${ff[i]}"`); + } + } + + return `${this._settings.fontStyle} ${this._settings.fontSize * this.getPrecision()}px ${ffs.join(",")}` + } + + _load() { + if (Utils.isWeb && document.fonts) { + const fontSetting = this._getFontSetting(); + try { + if (!document.fonts.check(fontSetting, this._settings.text)) { + // Use a promise that waits for loading. + return document.fonts.load(fontSetting, this._settings.text).catch(err => { + // Just load the fallback font. + console.warn('Font load error', err, fontSetting); + }).then(() => { + if (!document.fonts.check(fontSetting, this._settings.text)) { + console.warn('Font not found', fontSetting); + } + }); + } + } catch(e) { + console.warn("Can't check font loading for " + fontSetting); + } + } + } + + draw() { + // We do not use a promise so that loading is performed syncronous when possible. + const loadPromise = this._load(); + if (!loadPromise) { + return Utils.isSpark ? this._stage.platform.drawText(this) : this._draw(); + } else { + return loadPromise.then(() => { + return Utils.isSpark ? this._stage.platform.drawText(this) : this._draw(); + }); + } + } + + _draw() { + let renderInfo = {}; + + const precision = this.getPrecision(); + + let paddingLeft = this._settings.paddingLeft * precision; + let paddingRight = this._settings.paddingRight * precision; + const fontSize = this._settings.fontSize * precision; + let offsetY = this._settings.offsetY === null ? null : (this._settings.offsetY * precision); + let lineHeight = this._settings.lineHeight * precision; + const w = this._settings.w * precision; + const h = this._settings.h * precision; + let wordWrapWidth = this._settings.wordWrapWidth * precision; + const cutSx = this._settings.cutSx * precision; + const cutEx = this._settings.cutEx * precision; + const cutSy = this._settings.cutSy * precision; + const cutEy = this._settings.cutEy * precision; + + // Set font properties. + this.setFontProperties(); + + // Total width. + let width = w || (2048 / this.getPrecision()); + + // Inner width. + let innerWidth = width - (paddingLeft); + if (innerWidth < 10) { + width += (10 - innerWidth); + innerWidth += (10 - innerWidth); + } + + if (!wordWrapWidth) { + wordWrapWidth = innerWidth; + } + + // word wrap + // preserve original text + let linesInfo; + if (this._settings.wordWrap) { + linesInfo = this.wrapText(this._settings.text, wordWrapWidth); + } else { + linesInfo = {l: this._settings.text.split(/(?:\r\n|\r|\n)/), n: []}; + let n = linesInfo.l.length; + for (let i = 0; i < n - 1; i++) { + linesInfo.n.push(i); + } + } + let lines = linesInfo.l; + + if (this._settings.maxLines && lines.length > this._settings.maxLines) { + let usedLines = lines.slice(0, this._settings.maxLines); + + let otherLines = null; + if (this._settings.maxLinesSuffix) { + // Wrap again with max lines suffix enabled. + let w = this._settings.maxLinesSuffix ? this._context.measureText(this._settings.maxLinesSuffix).width : 0; + let al = this.wrapText(usedLines[usedLines.length - 1], wordWrapWidth - w); + usedLines[usedLines.length - 1] = al.l[0] + this._settings.maxLinesSuffix; + otherLines = [al.l.length > 1 ? al.l[1] : '']; + } else { + otherLines = ['']; + } + + // Re-assemble the remaining text. + let i, n = lines.length; + let j = 0; + let m = linesInfo.n.length; + for (i = this._settings.maxLines; i < n; i++) { + otherLines[j] += (otherLines[j] ? " " : "") + lines[i]; + if (i + 1 < m && linesInfo.n[i + 1]) { + j++; + } + } + + renderInfo.remainingText = otherLines.join("\n"); + + renderInfo.moreTextLines = true; + + lines = usedLines; + } else { + renderInfo.moreTextLines = false; + renderInfo.remainingText = ""; + } + + // calculate text width + let maxLineWidth = 0; + let lineWidths = []; + for (let i = 0; i < lines.length; i++) { + let lineWidth = this._context.measureText(lines[i]).width; + lineWidths.push(lineWidth); + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + + renderInfo.lineWidths = lineWidths; + + if (!w) { + // Auto-set width to max text length. + width = maxLineWidth + paddingLeft + paddingRight; + innerWidth = maxLineWidth; + } + + // calculate text height + lineHeight = lineHeight || fontSize; + + let height; + if (h) { + height = h; + } else { + height = lineHeight * (lines.length - 1) + 0.5 * fontSize + Math.max(lineHeight, fontSize) + offsetY; + } + + if (offsetY === null) { + offsetY = fontSize; + } + + renderInfo.w = width; + renderInfo.h = height; + renderInfo.lines = lines; + renderInfo.precision = precision; + + if (!width) { + // To prevent canvas errors. + width = 1; + } + + if (!height) { + // To prevent canvas errors. + height = 1; + } + + if (cutSx || cutEx) { + width = Math.min(width, cutEx - cutSx); + } + + if (cutSy || cutEy) { + height = Math.min(height, cutEy - cutSy); + } + + // Add extra margin to prevent issue with clipped text when scaling. + this._canvas.width = Math.ceil(width + this._stage.getOption('textRenderIssueMargin')); + this._canvas.height = Math.ceil(height); + + // Canvas context has been reset. + this.setFontProperties(); + + if (fontSize >= 128) { + // WpeWebKit bug: must force compositing because cairo-traps-compositor will not work with text first. + this._context.globalAlpha = 0.01; + this._context.fillRect(0, 0, 0.01, 0.01); + this._context.globalAlpha = 1.0; + } + + if (cutSx || cutSy) { + this._context.translate(-cutSx, -cutSy); + } + + let linePositionX; + let linePositionY; + + let drawLines = []; + + // Draw lines line by line. + for (let i = 0, n = lines.length; i < n; i++) { + linePositionX = 0; + linePositionY = (i * lineHeight) + offsetY; + + if (this._settings.textAlign === 'right') { + linePositionX += (innerWidth - lineWidths[i]); + } else if (this._settings.textAlign === 'center') { + linePositionX += ((innerWidth - lineWidths[i]) / 2); + } + linePositionX += paddingLeft; + + drawLines.push({text: lines[i], x: linePositionX, y: linePositionY, w: lineWidths[i]}); + } + + // Highlight. + if (this._settings.highlight) { + let color = this._settings.highlightColor || 0x00000000; + + let hlHeight = (this._settings.highlightHeight * precision || fontSize * 1.5); + let offset = (this._settings.highlightOffset !== null ? this._settings.highlightOffset * precision : -0.5 * fontSize); + const hlPaddingLeft = (this._settings.highlightPaddingLeft !== null ? this._settings.highlightPaddingLeft * precision : paddingLeft); + const hlPaddingRight = (this._settings.highlightPaddingRight !== null ? this._settings.highlightPaddingRight * precision : paddingRight); + + this._context.fillStyle = StageUtils.getRgbaString(color); + for (let i = 0; i < drawLines.length; i++) { + let drawLine = drawLines[i]; + this._context.fillRect((drawLine.x - hlPaddingLeft), (drawLine.y + offset), (drawLine.w + hlPaddingRight + hlPaddingLeft), hlHeight); + } + } + + // Text shadow. + let prevShadowSettings = null; + if (this._settings.shadow) { + prevShadowSettings = [this._context.shadowColor, this._context.shadowOffsetX, this._context.shadowOffsetY, this._context.shadowBlur]; + + this._context.shadowColor = StageUtils.getRgbaString(this._settings.shadowColor); + this._context.shadowOffsetX = this._settings.shadowOffsetX * precision; + this._context.shadowOffsetY = this._settings.shadowOffsetY * precision; + this._context.shadowBlur = this._settings.shadowBlur * precision; + } + + this._context.fillStyle = StageUtils.getRgbaString(this._settings.textColor); + for (let i = 0, n = drawLines.length; i < n; i++) { + let drawLine = drawLines[i]; + this._context.fillText(drawLine.text, drawLine.x, drawLine.y); + } + + if (prevShadowSettings) { + this._context.shadowColor = prevShadowSettings[0]; + this._context.shadowOffsetX = prevShadowSettings[1]; + this._context.shadowOffsetY = prevShadowSettings[2]; + this._context.shadowBlur = prevShadowSettings[3]; + } + + if (cutSx || cutSy) { + this._context.translate(cutSx, cutSy); + } + + this.renderInfo = renderInfo; + }; + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + */ + wrapText(text, wordWrapWidth) { + // Greedy wrapping algorithm that will wrap words as the line grows longer. + // than its horizontal bounds. + let lines = text.split(/\r?\n/g); + let allLines = []; + let realNewlines = []; + for (let i = 0; i < lines.length; i++) { + let resultLines = []; + let result = ''; + let spaceLeft = wordWrapWidth; + let words = lines[i].split(' '); + for (let j = 0; j < words.length; j++) { + let wordWidth = this._context.measureText(words[j]).width; + let wordWidthWithSpace = wordWidth + this._context.measureText(' ').width; + if (j === 0 || wordWidthWithSpace > spaceLeft) { + // Skip printing the newline if it's the first word of the line that is. + // greater than the word wrap width. + if (j > 0) { + resultLines.push(result); + result = ''; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else { + spaceLeft -= wordWidthWithSpace; + result += ' ' + words[j]; + } + } + + if (result) { + resultLines.push(result); + result = ''; + } + + allLines = allLines.concat(resultLines); + + if (i < lines.length - 1) { + realNewlines.push(allLines.length); + } + } + + return {l: allLines, n: realNewlines}; + }; + +} + +class TextTexture extends Texture { + + constructor(stage) { + super(stage); + + // We use the stage precision as the default precision in case of a text texture. + this._precision = this.stage.getOption('precision'); + } + + get text() { + return this._text; + } + + set text(v) { + if (this._text !== v) { + this._text = "" + v; + this._changed(); + } + } + + get w() { + return this._w; + } + + set w(v) { + if (this._w !== v) { + this._w = v; + this._changed(); + } + } + + get h() { + return this._h; + } + + set h(v) { + if (this._h !== v) { + this._h = v; + this._changed(); + } + } + + get fontStyle() { + return this._fontStyle; + } + + set fontStyle(v) { + if (this._fontStyle !== v) { + this._fontStyle = v; + this._changed(); + } + } + + get fontSize() { + return this._fontSize; + } + + set fontSize(v) { + if (this._fontSize !== v) { + this._fontSize = v; + this._changed(); + } + } + + get fontFace() { + return this._fontFace; + } + + set fontFace(v) { + if (this._fontFace !== v) { + this._fontFace = v; + this._changed(); + } + } + + get wordWrap() { + return this._wordWrap; + } + + set wordWrap(v) { + if (this._wordWrap !== v) { + this._wordWrap = v; + this._changed(); + } + } + + get wordWrapWidth() { + return this._wordWrapWidth; + } + + set wordWrapWidth(v) { + if (this._wordWrapWidth !== v) { + this._wordWrapWidth = v; + this._changed(); + } + } + + get lineHeight() { + return this._lineHeight; + } + + set lineHeight(v) { + if (this._lineHeight !== v) { + this._lineHeight = v; + this._changed(); + } + } + + get textBaseline() { + return this._textBaseline; + } + + set textBaseline(v) { + if (this._textBaseline !== v) { + this._textBaseline = v; + this._changed(); + } + } + + get textAlign() { + return this._textAlign; + } + + set textAlign(v) { + if (this._textAlign !== v) { + this._textAlign = v; + this._changed(); + } + } + + get offsetY() { + return this._offsetY; + } + + set offsetY(v) { + if (this._offsetY !== v) { + this._offsetY = v; + this._changed(); + } + } + + get maxLines() { + return this._maxLines; + } + + set maxLines(v) { + if (this._maxLines !== v) { + this._maxLines = v; + this._changed(); + } + } + + get maxLinesSuffix() { + return this._maxLinesSuffix; + } + + set maxLinesSuffix(v) { + if (this._maxLinesSuffix !== v) { + this._maxLinesSuffix = v; + this._changed(); + } + } + + get textColor() { + return this._textColor; + } + + set textColor(v) { + if (this._textColor !== v) { + this._textColor = v; + this._changed(); + } + } + + get paddingLeft() { + return this._paddingLeft; + } + + set paddingLeft(v) { + if (this._paddingLeft !== v) { + this._paddingLeft = v; + this._changed(); + } + } + + get paddingRight() { + return this._paddingRight; + } + + set paddingRight(v) { + if (this._paddingRight !== v) { + this._paddingRight = v; + this._changed(); + } + } + + get shadow() { + return this._shadow; + } + + set shadow(v) { + if (this._shadow !== v) { + this._shadow = v; + this._changed(); + } + } + + get shadowColor() { + return this._shadowColor; + } + + set shadowColor(v) { + if (this._shadowColor !== v) { + this._shadowColor = v; + this._changed(); + } + } + + get shadowOffsetX() { + return this._shadowOffsetX; + } + + set shadowOffsetX(v) { + if (this._shadowOffsetX !== v) { + this._shadowOffsetX = v; + this._changed(); + } + } + + get shadowOffsetY() { + return this._shadowOffsetY; + } + + set shadowOffsetY(v) { + if (this._shadowOffsetY !== v) { + this._shadowOffsetY = v; + this._changed(); + } + } + + get shadowBlur() { + return this._shadowBlur; + } + + set shadowBlur(v) { + if (this._shadowBlur !== v) { + this._shadowBlur = v; + this._changed(); + } + } + + get highlight() { + return this._highlight; + } + + set highlight(v) { + if (this._highlight !== v) { + this._highlight = v; + this._changed(); + } + } + + get highlightHeight() { + return this._highlightHeight; + } + + set highlightHeight(v) { + if (this._highlightHeight !== v) { + this._highlightHeight = v; + this._changed(); + } + } + + get highlightColor() { + return this._highlightColor; + } + + set highlightColor(v) { + if (this._highlightColor !== v) { + this._highlightColor = v; + this._changed(); + } + } + + get highlightOffset() { + return this._highlightOffset; + } + + set highlightOffset(v) { + if (this._highlightOffset !== v) { + this._highlightOffset = v; + this._changed(); + } + } + + get highlightPaddingLeft() { + return this._highlightPaddingLeft; + } + + set highlightPaddingLeft(v) { + if (this._highlightPaddingLeft !== v) { + this._highlightPaddingLeft = v; + this._changed(); + } + } + + get highlightPaddingRight() { + return this._highlightPaddingRight; + } + + set highlightPaddingRight(v) { + if (this._highlightPaddingRight !== v) { + this._highlightPaddingRight = v; + this._changed(); + } + } + + get cutSx() { + return this._cutSx; + } + + set cutSx(v) { + if (this._cutSx !== v) { + this._cutSx = v; + this._changed(); + } + } + + get cutEx() { + return this._cutEx; + } + + set cutEx(v) { + if (this._cutEx !== v) { + this._cutEx = v; + this._changed(); + } + } + + get cutSy() { + return this._cutSy; + } + + set cutSy(v) { + if (this._cutSy !== v) { + this._cutSy = v; + this._changed(); + } + } + + get cutEy() { + return this._cutEy; + } + + set cutEy(v) { + if (this._cutEy !== v) { + this._cutEy = v; + this._changed(); + } + } + + get precision() { + return super.precision; + } + + set precision(v) { + // We actually draw differently when the precision changes. + if (this.precision !== v) { + super.precision = v; + this._changed(); + } + } + + _getIsValid() { + return !!this.text; + } + + _getLookupId() { + let parts = []; + + if (this.w !== 0) parts.push("w " + this.w); + if (this.h !== 0) parts.push("h " + this.h); + if (this.fontStyle !== "normal") parts.push("fS" + this.fontStyle); + if (this.fontSize !== 40) parts.push("fs" + this.fontSize); + if (this.fontFace !== null) parts.push("ff" + (Array.isArray(this.fontFace) ? this.fontFace.join(",") : this.fontFace)); + if (this.wordWrap !== true) parts.push("wr" + (this.wordWrap ? 1 : 0)); + if (this.wordWrapWidth !== 0) parts.push("ww" + this.wordWrapWidth); + if (this.lineHeight !== null) parts.push("lh" + this.lineHeight); + if (this.textBaseline !== "alphabetic") parts.push("tb" + this.textBaseline); + if (this.textAlign !== "left") parts.push("ta" + this.textAlign); + if (this.offsetY !== null) parts.push("oy" + this.offsetY); + if (this.maxLines !== 0) parts.push("ml" + this.maxLines); + if (this.maxLinesSuffix !== "..") parts.push("ms" + this.maxLinesSuffix); + parts.push("pc" + this.precision); + if (this.textColor !== 0xffffffff) parts.push("co" + this.textColor.toString(16)); + if (this.paddingLeft !== 0) parts.push("pl" + this.paddingLeft); + if (this.paddingRight !== 0) parts.push("pr" + this.paddingRight); + if (this.shadow !== false) parts.push("sh" + (this.shadow ? 1 : 0)); + if (this.shadowColor !== 0xff000000) parts.push("sc" + this.shadowColor.toString(16)); + if (this.shadowOffsetX !== 0) parts.push("sx" + this.shadowOffsetX); + if (this.shadowOffsetY !== 0) parts.push("sy" + this.shadowOffsetY); + if (this.shadowBlur !== 5) parts.push("sb" + this.shadowBlur); + if (this.highlight !== false) parts.push("hL" + (this.highlight ? 1 : 0)); + if (this.highlightHeight !== 0) parts.push("hh" + this.highlightHeight); + if (this.highlightColor !== 0xff000000) parts.push("hc" + this.highlightColor.toString(16)); + if (this.highlightOffset !== null) parts.push("ho" + this.highlightOffset); + if (this.highlightPaddingLeft !== null) parts.push("hl" + this.highlightPaddingLeft); + if (this.highlightPaddingRight !== null) parts.push("hr" + this.highlightPaddingRight); + + if (this.cutSx) parts.push("csx" + this.cutSx); + if (this.cutEx) parts.push("cex" + this.cutEx); + if (this.cutSy) parts.push("csy" + this.cutSy); + if (this.cutEy) parts.push("cey" + this.cutEy); + + let id = "TX$" + parts.join("|") + ":" + this.text; + return id; + } + + _getSourceLoader() { + const args = this.cloneArgs(); + + // Inherit font face from stage. + if (args.fontFace === null) { + args.fontFace = this.stage.getOption('defaultFontFace'); + } + + return function(cb) { + const canvas = this.stage.platform.getDrawingCanvas(); + const renderer = new TextTextureRenderer(this.stage, canvas, args); + const p = renderer.draw(); + + if (p) { + p.then(() => { + cb(null, Object.assign({renderInfo: renderer.renderInfo}, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas))); + }).catch((err) => { + cb(err); + }); + } else { + cb(null, Object.assign({renderInfo: renderer.renderInfo}, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas))); + } + } + } + + getNonDefaults() { + const nonDefaults = super.getNonDefaults(); + if (this.text !== "") nonDefaults['text'] = this.text; + if (this.w !== 0) nonDefaults['w'] = this.w; + if (this.h !== 0) nonDefaults['h'] = this.h; + if (this.fontStyle !== "normal") nonDefaults['fontStyle'] = this.fontStyle; + if (this.fontSize !== 40) nonDefaults["fontSize"] = this.fontSize; + if (this.fontFace !== null) nonDefaults["fontFace"] = this.fontFace; + if (this.wordWrap !== true) nonDefaults["wordWrap"] = this.wordWrap; + if (this.wordWrapWidth !== 0) nonDefaults["wordWrapWidth"] = this.wordWrapWidth; + if (this.lineHeight !== null) nonDefaults["lineHeight"] = this.lineHeight; + if (this.textBaseline !== "alphabetic") nonDefaults["textBaseline"] = this.textBaseline; + if (this.textAlign !== "left") nonDefaults["textAlign"] = this.textAlign; + if (this.offsetY !== null) nonDefaults["offsetY"] = this.offsetY; + if (this.maxLines !== 0) nonDefaults["maxLines"] = this.maxLines; + if (this.maxLinesSuffix !== "..") nonDefaults["maxLinesSuffix"] = this.maxLinesSuffix; + if (this.precision !== this.stage.getOption('precision')) nonDefaults["precision"] = this.precision; + if (this.textColor !== 0xffffffff) nonDefaults["textColor"] = this.textColor; + if (this.paddingLeft !== 0) nonDefaults["paddingLeft"] = this.paddingLeft; + if (this.paddingRight !== 0) nonDefaults["paddingRight"] = this.paddingRight; + if (this.shadow !== false) nonDefaults["shadow"] = this.shadow; + if (this.shadowColor !== 0xff000000) nonDefaults["shadowColor"] = this.shadowColor; + if (this.shadowOffsetX !== 0) nonDefaults["shadowOffsetX"] = this.shadowOffsetX; + if (this.shadowOffsetY !== 0) nonDefaults["shadowOffsetY"] = this.shadowOffsetY; + if (this.shadowBlur !== 5) nonDefaults["shadowBlur"] = this.shadowBlur; + if (this.highlight !== false) nonDefaults["highlight"] = this.highlight; + if (this.highlightHeight !== 0) nonDefaults["highlightHeight"] = this.highlightHeight; + if (this.highlightColor !== 0xff000000) nonDefaults["highlightColor"] = this.highlightColor; + if (this.highlightOffset !== 0) nonDefaults["highlightOffset"] = this.highlightOffset; + if (this.highlightPaddingLeft !== 0) nonDefaults["highlightPaddingLeft"] = this.highlightPaddingLeft; + if (this.highlightPaddingRight !== 0) nonDefaults["highlightPaddingRight"] = this.highlightPaddingRight; + + if (this.cutSx) nonDefaults["cutSx"] = this.cutSx; + if (this.cutEx) nonDefaults["cutEx"] = this.cutEx; + if (this.cutSy) nonDefaults["cutSy"] = this.cutSy; + if (this.cutEy) nonDefaults["cutEy"] = this.cutEy; + return nonDefaults; + } + + cloneArgs() { + let obj = {}; + obj.text = this._text; + obj.w = this._w; + obj.h = this._h; + obj.fontStyle = this._fontStyle; + obj.fontSize = this._fontSize; + obj.fontFace = this._fontFace; + obj.wordWrap = this._wordWrap; + obj.wordWrapWidth = this._wordWrapWidth; + obj.lineHeight = this._lineHeight; + obj.textBaseline = this._textBaseline; + obj.textAlign = this._textAlign; + obj.offsetY = this._offsetY; + obj.maxLines = this._maxLines; + obj.maxLinesSuffix = this._maxLinesSuffix; + obj.precision = this._precision; + obj.textColor = this._textColor; + obj.paddingLeft = this._paddingLeft; + obj.paddingRight = this._paddingRight; + obj.shadow = this._shadow; + obj.shadowColor = this._shadowColor; + obj.shadowOffsetX = this._shadowOffsetX; + obj.shadowOffsetY = this._shadowOffsetY; + obj.shadowBlur = this._shadowBlur; + obj.highlight = this._highlight; + obj.highlightHeight = this._highlightHeight; + obj.highlightColor = this._highlightColor; + obj.highlightOffset = this._highlightOffset; + obj.highlightPaddingLeft = this._highlightPaddingLeft; + obj.highlightPaddingRight = this._highlightPaddingRight; + obj.cutSx = this._cutSx; + obj.cutEx = this._cutEx; + obj.cutSy = this._cutSy; + obj.cutEy = this._cutEy; + return obj; + } + + +} + +// Because there are so many properties, we prefer to use the prototype for default values. +// This causes a decrease in performance, but also a decrease in memory usage. +let proto = TextTexture.prototype; +proto._text = ""; +proto._w = 0; +proto._h = 0; +proto._fontStyle = "normal"; +proto._fontSize = 40; +proto._fontFace = null; +proto._wordWrap = true; +proto._wordWrapWidth = 0; +proto._lineHeight = null; +proto._textBaseline = "alphabetic"; +proto._textAlign = "left"; +proto._offsetY = null; +proto._maxLines = 0; +proto._maxLinesSuffix = ".."; +proto._textColor = 0xFFFFFFFF; +proto._paddingLeft = 0; +proto._paddingRight = 0; +proto._shadow = false; +proto._shadowColor = 0xFF000000; +proto._shadowOffsetX = 0; +proto._shadowOffsetY = 0; +proto._shadowBlur = 5; +proto._highlight = false; +proto._highlightHeight = 0; +proto._highlightColor = 0xFF000000; +proto._highlightOffset = 0; +proto._highlightPaddingLeft = 0; +proto._highlightPaddingRight = 0; +proto._cutSx = 0; +proto._cutEx = 0; +proto._cutSy = 0; +proto._cutEy = 0; + +class SourceTexture extends Texture { + + constructor(stage) { + super(stage); + + this._textureSource = undefined; + } + + get textureSource() { + return this._textureSource; + } + + set textureSource(v) { + if (v !== this._textureSource) { + if (v.isResultTexture) { + // In case of a result texture, automatically inherit the precision. + this._precision = this.stage.getRenderPrecision(); + } + this._textureSource = v; + this._changed(); + } + } + + _getTextureSource() { + return this._textureSource; + } + +} + +class Transition extends EventEmitter { + + constructor(manager, settings, element, property) { + super(); + + this.manager = manager; + + this._settings = settings; + + this._element = element; + this._getter = Element.getGetter(property); + this._setter = Element.getSetter(property); + + this._merger = settings.merger; + + if (!this._merger) { + this._merger = Element.getMerger(property); + } + + this._startValue = this._getter(this._element); + this._targetValue = this._startValue; + + this._p = 1; + this._delayLeft = 0; + } + + start(targetValue) { + this._startValue = this._getter(this._element); + + if (!this.isAttached()) { + // We don't support transitions on non-attached elements. Just set value without invoking listeners. + this._targetValue = targetValue; + this._p = 1; + this._updateDrawValue(); + } else { + if (targetValue === this._startValue) { + this.reset(targetValue, 1); + } else { + this._targetValue = targetValue; + this._p = 0; + this._delayLeft = this._settings.delay; + this.emit('start'); + this.add(); + } + } + } + + finish() { + if (this._p < 1) { + // Value setting and will must be invoked (async) upon next transition cycle. + this._p = 1; + } + } + + stop() { + // Just stop where the transition is at. + this.emit('stop'); + this.manager.removeActive(this); + } + + pause() { + this.stop(); + } + + play() { + this.manager.addActive(this); + } + + reset(targetValue, p) { + if (!this.isAttached()) { + // We don't support transitions on non-attached elements. Just set value without invoking listeners. + this._startValue = this._getter(this._element); + this._targetValue = targetValue; + this._p = 1; + this._updateDrawValue(); + } else { + this._startValue = this._getter(this._element); + this._targetValue = targetValue; + this._p = p; + this.add(); + } + } + + _updateDrawValue() { + this._setter(this._element, this.getDrawValue()); + } + + add() { + this.manager.addActive(this); + } + + isAttached() { + return this._element.attached; + } + + isRunning() { + return (this._p < 1.0); + } + + progress(dt) { + if (!this.isAttached()) { + // Skip to end of transition so that it is removed. + this._p = 1; + } + + if (this.p < 1) { + if (this.delayLeft > 0) { + this._delayLeft -= dt; + + if (this.delayLeft < 0) { + dt = -this.delayLeft; + this._delayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + if (this._settings.duration == 0) { + this._p = 1; + } else { + this._p += dt / this._settings.duration; + } + if (this._p >= 1) { + // Finished!; + this._p = 1; + } + } + + this._updateDrawValue(); + + this.invokeListeners(); + } + + invokeListeners() { + this.emit('progress', this.p); + if (this.p === 1) { + this.emit('finish'); + } + } + + updateTargetValue(targetValue) { + let t = this._settings.timingFunctionImpl(this.p); + if (t === 1) { + this._targetValue = targetValue; + } else if (t === 0) { + this._startValue = this._targetValue; + this._targetValue = targetValue; + } else { + this._startValue = targetValue - ((targetValue - this._targetValue) / (1 - t)); + this._targetValue = targetValue; + } + } + + getDrawValue() { + if (this.p >= 1) { + return this.targetValue; + } else { + let v = this._settings._timingFunctionImpl(this.p); + return this._merger(this.targetValue, this.startValue, v); + } + } + + skipDelay() { + this._delayLeft = 0; + } + + get startValue() { + return this._startValue; + } + + get targetValue() { + return this._targetValue; + } + + get p() { + return this._p; + } + + get delayLeft() { + return this._delayLeft; + } + + get element() { + return this._element; + } + + get settings() { + return this._settings; + } + + set settings(v) { + this._settings = v; + } + +} + +Transition.prototype.isTransition = true; + +/** + * Manages a list of objects. + * Objects may be patched. Then, they can be referenced using the 'ref' (string) property. + */ +class ObjectList { + + constructor() { + this._items = []; + this._refs = {}; + } + + get() { + return this._items; + } + + get first() { + return this._items[0]; + } + + get last() { + return this._items.length ? this._items[this._items.length - 1] : undefined; + } + + add(item) { + this.addAt(item, this._items.length); + } + + addAt(item, index) { + if (index >= 0 && index <= this._items.length) { + let currentIndex = this._items.indexOf(item); + if (currentIndex === index) { + return item; + } + + if (currentIndex != -1) { + this.setAt(item, index); + } else { + if (item.ref) { + this._refs[item.ref] = item; + } + this._items.splice(index, 0, item); + this.onAdd(item, index); + } + } else { + throw new Error('addAt: The index ' + index + ' is out of bounds ' + this._items.length); + } + } + + replaceByRef(item) { + if (item.ref) { + const existingItem = this.getByRef(item.ref); + if (!existingItem) { + throw new Error('replaceByRef: no item found with reference: ' + item.ref); + } + this.replace(item, existingItem); + } else { + throw new Error('replaceByRef: no ref specified in item'); + } + this.addAt(item, this._items.length); + + } + + replace(item, prevItem) { + const index = this.getIndex(prevItem); + if (index === -1) { + throw new Error('replace: The previous item does not exist'); + } + this.setAt(item, index); + } + + setAt(item, index) { + if (index >= 0 && index <= this._items.length) { + let currentIndex = this._items.indexOf(item); + if (currentIndex != -1) { + if (currentIndex !== index) { + const fromIndex = currentIndex; + if (fromIndex !== index) { + this._items.splice(fromIndex, 1); + this._items.splice(index, 0, item); + this.onMove(item, fromIndex, index); + } + } + } else { + if (index < this._items.length) { + if (this._items[index].ref) { + this._refs[this._items[index].ref] = undefined; + } + } + + const prevItem = this._items[index]; + + // Doesn't exist yet: overwrite current. + this._items[index] = item; + + if (item.ref) { + this._refs[item.ref] = item; + } + + this.onSet(item, index, prevItem); + } + } else { + throw new Error('setAt: The index ' + index + ' is out of bounds ' + this._items.length); + } + } + + getAt(index) { + return this._items[index]; + } + + getIndex(item) { + return this._items.indexOf(item); + } + + remove(item) { + let index = this._items.indexOf(item); + + if (index !== -1) { + this.removeAt(index); + } + }; + + removeAt(index) { + let item = this._items[index]; + + if (item.ref) { + this._refs[item.ref] = undefined; + } + + this._items.splice(index, 1); + + this.onRemove(item, index); + + return item; + }; + + clear() { + let n = this._items.length; + if (n) { + let prev = this._items; + this._items = []; + this._refs = {}; + this.onSync(prev, [], []); + } + }; + + a(o) { + if (Utils.isObjectLiteral(o)) { + let c = this.createItem(o); + c.patch(o); + this.add(c); + return c; + } else if (Array.isArray(o)) { + for (let i = 0, n = o.length; i < n; i++) { + this.a(o[i]); + } + return null; + } else if (this.isItem(o)) { + this.add(o); + return o; + } + }; + + get length() { + return this._items.length; + } + + _getRefs() { + return this._refs; + } + + getByRef(ref) { + return this._refs[ref]; + } + + clearRef(ref) { + delete this._refs[ref]; + } + + setRef(ref, child) { + this._refs[ref] = child; + } + + patch(settings) { + if (Utils.isObjectLiteral(settings)) { + this._setByObject(settings); + } else if (Array.isArray(settings)) { + this._setByArray(settings); + } + } + + _setByObject(settings) { + // Overrule settings of known referenced items. + let refs = this._getRefs(); + let crefs = Object.keys(settings); + for (let i = 0, n = crefs.length; i < n; i++) { + let cref = crefs[i]; + let s = settings[cref]; + + let c = refs[cref]; + if (!c) { + if (this.isItem(s)) { + // Replace previous item; + s.ref = cref; + this.add(s); + } else { + // Create new item. + c = this.createItem(s); + c.ref = cref; + c.patch(s); + this.add(c); + } + } else { + if (this.isItem(s)) { + if (c !== s) { + // Replace previous item; + let idx = this.getIndex(c); + s.ref = cref; + this.setAt(s, idx); + } + } else { + c.patch(s); + } + } + } + } + + _equalsArray(array) { + let same = true; + if (array.length === this._items.length) { + for (let i = 0, n = this._items.length; (i < n) && same; i++) { + same = same && (this._items[i] === array[i]); + } + } else { + same = false; + } + return same; + } + + _setByArray(array) { + // For performance reasons, first check if the arrays match exactly and bail out if they do. + if (this._equalsArray(array)) { + return; + } + + for (let i = 0, n = this._items.length; i < n; i++) { + this._items[i].marker = true; + } + + let refs; + let newItems = []; + for (let i = 0, n = array.length; i < n; i++) { + let s = array[i]; + if (this.isItem(s)) { + s.marker = false; + newItems.push(s); + } else { + let cref = s.ref; + let c; + if (cref) { + if (!refs) refs = this._getRefs(); + c = refs[cref]; + } + + if (!c) { + // Create new item. + c = this.createItem(s); + } else { + c.marker = false; + } + + if (Utils.isObjectLiteral(s)) { + c.patch(s); + } + + newItems.push(c); + } + } + + this._setItems(newItems); + } + + _setItems(newItems) { + let prevItems = this._items; + this._items = newItems; + + // Remove the items. + let removed = prevItems.filter(item => {let m = item.marker; delete item.marker; return m}); + let added = newItems.filter(item => (prevItems.indexOf(item) === -1)); + + if (removed.length || added.length) { + // Recalculate refs. + this._refs = {}; + for (let i = 0, n = this._items.length; i < n; i++) { + let ref = this._items[i].ref; + if (ref) { + this._refs[ref] = this._items[i]; + } + } + } + + this.onSync(removed, added, newItems); + } + + sort(f) { + const items = this._items.slice(); + items.sort(f); + this._setByArray(items); + } + + onAdd(item, index) { + } + + onRemove(item, index) { + } + + onSync(removed, added, order) { + } + + onSet(item, index, prevItem) { + } + + onMove(item, fromIndex, toIndex) { + } + + createItem(object) { + throw new Error("ObjectList.createItem must create and return a new object"); + } + + isItem(object) { + return false; + } + + forEach(f) { + this.get().forEach(f); + } + +} + +/** + * Manages the list of children for an element. + */ + +class ElementChildList extends ObjectList { + + constructor(element) { + super(); + this._element = element; + } + + _connectParent(item) { + const prevParent = item.parent; + if (prevParent && prevParent !== this._element) { + // Cleanup in previous child list, without + const prevChildList = item.parent.childList; + const index = prevChildList.getIndex(item); + + if (item.ref) { + prevChildList._refs[item.ref] = undefined; + } + prevChildList._items.splice(index, 1); + + // Also clean up element core. + prevParent.core.removeChildAt(index); + + } + + item._setParent(this._element); + + // We are expecting the caller to sync it to the core. + } + + onAdd(item, index) { + this._connectParent(item); + this._element.core.addChildAt(index, item.core); + } + + onRemove(item, index) { + item._setParent(null); + this._element.core.removeChildAt(index); + } + + onSync(removed, added, order) { + for (let i = 0, n = removed.length; i < n; i++) { + removed[i]._setParent(null); + } + for (let i = 0, n = added.length; i < n; i++) { + this._connectParent(added[i]); + } + let gc = i => i.core; + this._element.core.syncChildren(removed.map(gc), added.map(gc), order.map(gc)); + } + + onSet(item, index, prevItem) { + prevItem._setParent(null); + + this._connectParent(item); + this._element.core.setChildAt(index, item.core); + } + + onMove(item, fromIndex, toIndex) { + this._element.core.moveChild(fromIndex, toIndex); + } + + createItem(object) { + if (object.type) { + return new object.type(this._element.stage); + } else { + return this._element.stage.createElement(); + } + } + + isItem(object) { + return object.isElement; + } + +} + +/** + * Render tree node. + * Copyright Metrological, 2017 + */ + +class Element { + + constructor(stage) { + this.stage = stage; + + this.__id = Element.id++; + + this.__start(); + + // EventEmitter constructor. + this._hasEventListeners = false; + + this.__core = new ElementCore(this); + + /** + * A reference that can be used while merging trees. + * @type {string} + */ + this.__ref = null; + + /** + * An element is attached if it is a descendant of the stage root. + * @type {boolean} + */ + this.__attached = false; + + /** + * An element is enabled when it is attached and it is visible (worldAlpha > 0). + * @type {boolean} + */ + this.__enabled = false; + + /** + * An element is active when it is enabled and it is within bounds. + * @type {boolean} + */ + this.__active = false; + + /** + * @type {Element} + */ + this.__parent = null; + + /** + * The texture that is currently set. + * @type {Texture} + */ + this.__texture = null; + + /** + * The currently displayed texture. While this.texture is loading, this one may be different. + * @type {Texture} + */ + this.__displayedTexture = null; + + /** + * Tags that can be used to identify/search for a specific element. + * @type {String[]} + */ + this.__tags = null; + + /** + * The tree's tags mapping. + * This contains all elements for all known tags, at all times. + * @type {Map} + */ + this.__treeTags = null; + + /** + * Creates a tag context: tagged elements in this branch will not be reachable from ancestors of this elements. + * @type {boolean} + */ + this.__tagRoot = false; + + /** + * (Lazy-initialised) list of children owned by this elements. + * @type {ElementChildList} + */ + this.__childList = null; + + this._w = 0; + + this._h = 0; + } + + __start() { + } + + get id() { + return this.__id; + } + + set ref(ref) { + if (this.__ref !== ref) { + const charcode = ref.charCodeAt(0); + if (!Utils.isUcChar(charcode)) { + this._throwError("Ref must start with an upper case character: " + ref); + } + if (this.__ref !== null) { + this.removeTag(this.__ref); + if (this.__parent) { + this.__parent.__childList.clearRef(this.__ref); + } + } + + this.__ref = ref; + + if (this.__ref) { + this._addTag(this.__ref); + if (this.__parent) { + this.__parent.__childList.setRef(this.__ref, this); + } + } + } + } + + get ref() { + return this.__ref; + } + + get core() { + return this.__core; + } + + setAsRoot() { + this.__core.setAsRoot(); + this._updateAttachedFlag(); + this._updateEnabledFlag(); + } + + get isRoot() { + return this.__core.isRoot; + } + + _setParent(parent) { + if (this.__parent === parent) return; + + if (this.__parent) { + this._unsetTagsParent(); + } + + this.__parent = parent; + + if (parent) { + this._setTagsParent(); + } + + this._updateAttachedFlag(); + this._updateEnabledFlag(); + + if (this.isRoot && parent) { + this._throwError("Root should not be added as a child! Results are unspecified!"); + } + }; + + getDepth() { + let depth = 0; + + let p = this.__parent; + while(p) { + depth++; + p = p.__parent; + } + + return depth; + }; + + getAncestor(l) { + let p = this; + while (l > 0 && p.__parent) { + p = p.__parent; + l--; + } + return p; + }; + + getAncestorAtDepth(depth) { + let levels = this.getDepth() - depth; + if (levels < 0) { + return null; + } + return this.getAncestor(levels); + }; + + isAncestorOf(c) { + let p = c; + while(p = p.parent) { + if (this === p) { + return true; + } + } + return false; + }; + + getSharedAncestor(c) { + let o1 = this; + let o2 = c; + let l1 = o1.getDepth(); + let l2 = o2.getDepth(); + if (l1 > l2) { + o1 = o1.getAncestor(l1 - l2); + } else if (l2 > l1) { + o2 = o2.getAncestor(l2 - l1); + } + + do { + if (o1 === o2) { + return o1; + } + + o1 = o1.__parent; + o2 = o2.__parent; + } while (o1 && o2); + + return null; + }; + + get attached() { + return this.__attached; + } + + get enabled() { + return this.__enabled; + } + + get active() { + return this.__active; + } + + _isAttached() { + return (this.__parent ? this.__parent.__attached : (this.stage.root === this)); + }; + + _isEnabled() { + return this.__core.visible && (this.__core.alpha > 0) && (this.__parent ? this.__parent.__enabled : (this.stage.root === this)); + }; + + _isActive() { + return this._isEnabled() && this.withinBoundsMargin; + }; + + /** + * Updates the 'attached' flag for this branch. + */ + _updateAttachedFlag() { + let newAttached = this._isAttached(); + if (this.__attached !== newAttached) { + this.__attached = newAttached; + + if (newAttached) { + this._onSetup(); + } + + let children = this._children.get(); + if (children) { + let m = children.length; + if (m > 0) { + for (let i = 0; i < m; i++) { + children[i]._updateAttachedFlag(); + } + } + } + + if (newAttached) { + this._onAttach(); + } else { + this._onDetach(); + } + } + }; + + /** + * Updates the 'enabled' flag for this branch. + */ + _updateEnabledFlag() { + let newEnabled = this._isEnabled(); + if (this.__enabled !== newEnabled) { + if (newEnabled) { + this._onEnabled(); + this._setEnabledFlag(); + } else { + this._onDisabled(); + this._unsetEnabledFlag(); + } + + let children = this._children.get(); + if (children) { + let m = children.length; + if (m > 0) { + for (let i = 0; i < m; i++) { + children[i]._updateEnabledFlag(); + } + } + } + } + }; + + _setEnabledFlag() { + this.__enabled = true; + + // Force re-check of texture because dimensions might have changed (cutting). + this._updateDimensions(); + this._updateTextureCoords(); + + if (this.__texture) { + this.__texture.addElement(this); + } + + if (this.withinBoundsMargin) { + this._setActiveFlag(); + } + + if (this.__core.shader) { + this.__core.shader.addElement(this.__core); + } + + } + + _unsetEnabledFlag() { + if (this.__active) { + this._unsetActiveFlag(); + } + + if (this.__texture) { + this.__texture.removeElement(this); + } + + if (this.__core.shader) { + this.__core.shader.removeElement(this.__core); + } + + if (this._texturizer) { + this.texturizer.filters.forEach(filter => filter.removeElement(this.__core)); + } + + this.__enabled = false; + } + + _setActiveFlag() { + this.__active = true; + + // This must happen before enabling the texture, because it may already be loaded or load directly. + if (this.__texture) { + this.__texture.incActiveCount(); + } + + if (this.__texture) { + this._enableTexture(); + } + this._onActive(); + } + + _unsetActiveFlag() { + if (this.__texture) { + this.__texture.decActiveCount(); + } + + this.__active = false; + if (this.__texture) { + this._disableTexture(); + } + + if (this._hasTexturizer()) { + this.texturizer.deactivate(); + } + + this._onInactive(); + } + + _onSetup() { + } + + _onAttach() { + } + + _onDetach() { + } + + _onEnabled() { + } + + _onDisabled() { + } + + _onActive() { + } + + _onInactive() { + } + + _onResize() { + } + + _getRenderWidth() { + if (this._w) { + return this._w; + } else if (this.__displayedTexture) { + return this.__displayedTexture.getRenderWidth(); + } else if (this.__texture) { + // Texture already loaded, but not yet updated (probably because this element is not active). + return this.__texture.getRenderWidth(); + } else { + return 0; + } + }; + + _getRenderHeight() { + if (this._h) { + return this._h; + } else if (this.__displayedTexture) { + return this.__displayedTexture.getRenderHeight(); + } else if (this.__texture) { + // Texture already loaded, but not yet updated (probably because this element is not active). + return this.__texture.getRenderHeight(); + } else { + return 0; + } + }; + + get renderWidth() { + if (this.__enabled) { + // Render width is only maintained if this element is enabled. + return this.__core.getRenderWidth(); + } else { + return this._getRenderWidth(); + } + } + + get renderHeight() { + if (this.__enabled) { + return this.__core.getRenderHeight(); + } else { + return this._getRenderHeight(); + } + } + + get finalX() { + return this.__core.x; + } + + get finalY() { + return this.__core.y; + } + + get finalW() { + return this.__core.w; + } + + get finalH() { + return this.__core.h; + } + + textureIsLoaded() { + return this.__texture && this.__texture.isLoaded(); + } + + loadTexture() { + if (this.__texture) { + this.__texture.load(); + + if (!this.__texture.isUsed() || !this._isEnabled()) { + // Loading the texture will have no effect on the dimensions of this element. + // Manually update them, so that calcs can be performed immediately in userland. + this._updateDimensions(); + } + } + } + + _enableTextureError() { + // txError event should automatically be re-triggered when a element becomes active. + const loadError = this.__texture.loadError; + if (loadError) { + this.emit('txError', loadError, this.__texture._source); + } + } + + _enableTexture() { + if (this.__texture.isLoaded()) { + this._setDisplayedTexture(this.__texture); + } else { + // We don't want to retain the old 'ghost' image as it wasn't visible anyway. + this._setDisplayedTexture(null); + + this._enableTextureError(); + } + } + + _disableTexture() { + // We disable the displayed texture because, when the texture changes while invisible, we should use that w, h, + // mw, mh for checking within bounds. + this._setDisplayedTexture(null); + } + + get texture() { + return this.__texture; + } + + set texture(v) { + let texture; + if (Utils.isObjectLiteral(v)) { + if (v.type) { + texture = new v.type(this.stage); + } else { + texture = this.texture; + } + + if (texture) { + Base.patchObject(texture, v); + } + } else if (!v) { + texture = null; + } else { + if (v.isTexture) { + texture = v; + } else if (v.isTextureSource) { + texture = new SourceTexture(this.stage); + texture.textureSource = v; + } else { + console.error("Please specify a texture type."); + return; + } + } + + const prevTexture = this.__texture; + if (texture !== prevTexture) { + this.__texture = texture; + + if (this.__texture) { + if (this.__enabled) { + this.__texture.addElement(this); + + if (this.withinBoundsMargin) { + if (this.__texture.isLoaded()) { + this._setDisplayedTexture(this.__texture); + } else { + this._enableTextureError(); + } + } + } + } else { + // Make sure that current texture is cleared when the texture is explicitly set to null. + this._setDisplayedTexture(null); + } + + if (prevTexture && prevTexture !== this.__displayedTexture) { + prevTexture.removeElement(this); + } + + this._updateDimensions(); + } + } + + get displayedTexture() { + return this.__displayedTexture; + } + + _setDisplayedTexture(v) { + let prevTexture = this.__displayedTexture; + + if (prevTexture && (v !== prevTexture)) { + if (this.__texture !== prevTexture) { + // The old displayed texture is deprecated. + prevTexture.removeElement(this); + } + } + + const prevSource = this.__core.displayedTextureSource ? this.__core.displayedTextureSource._source : null; + const sourceChanged = (v ? v._source : null) !== prevSource; + + this.__displayedTexture = v; + this._updateDimensions(); + + if (this.__displayedTexture) { + if (sourceChanged) { + // We don't need to reference the displayed texture because it was already referenced (this.texture === this.displayedTexture). + this._updateTextureCoords(); + this.__core.setDisplayedTextureSource(this.__displayedTexture._source); + } + } else { + this.__core.setDisplayedTextureSource(null); + } + + if (sourceChanged) { + if (this.__displayedTexture) { + this.emit('txLoaded', this.__displayedTexture); + } else { + this.emit('txUnloaded', this.__displayedTexture); + } + } + } + + onTextureSourceLoaded() { + // This function is called when element is enabled, but we only want to set displayed texture for active elements. + if (this.active) { + // We may be dealing with a texture reloading, so we must force update. + this._setDisplayedTexture(this.__texture); + } + }; + + onTextureSourceLoadError(e) { + this.emit('txError', e, this.__texture._source); + }; + + forceRenderUpdate() { + this.__core.setHasRenderUpdates(3); + } + + onDisplayedTextureClippingChanged() { + this._updateDimensions(); + this._updateTextureCoords(); + }; + + onPrecisionChanged() { + this._updateDimensions(); + }; + + onDimensionsChanged(w, h) { + if (this.texture instanceof TextTexture) { + this.texture.w = w; + this.texture.h = h; + this.w = w; + this.h = h; + } + } + + _updateDimensions() { + let w = this._getRenderWidth(); + let h = this._getRenderHeight(); + + let unknownSize = false; + if (!w || !h) { + if (!this.__displayedTexture && this.__texture) { + // We use a 'max width' replacement instead in the ElementCore calcs. + // This makes sure that it is able to determine withinBounds. + w = w || this.__texture.mw; + h = h || this.__texture.mh; + + if ((!w || !h) && this.__texture.isAutosizeTexture()) { + unknownSize = true; + } + } + } + + if (this.__core.setDimensions(w, h, unknownSize)) { + this._onResize(); + } + } + + _updateTextureCoords() { + if (this.displayedTexture && this.displayedTexture._source) { + let displayedTexture = this.displayedTexture; + let displayedTextureSource = this.displayedTexture._source; + + let tx1 = 0, ty1 = 0, tx2 = 1.0, ty2 = 1.0; + if (displayedTexture.clipping) { + // Apply texture clipping. + let w = displayedTextureSource.getRenderWidth(); + let h = displayedTextureSource.getRenderHeight(); + let iw, ih, rw, rh; + iw = 1 / w; + ih = 1 / h; + + if (displayedTexture.pw) { + rw = (displayedTexture.pw) * iw; + } else { + rw = (w - displayedTexture.px) * iw; + } + + if (displayedTexture.ph) { + rh = displayedTexture.ph * ih; + } else { + rh = (h - displayedTexture.py) * ih; + } + + iw *= (displayedTexture.px); + ih *= (displayedTexture.py); + + tx1 = iw; + ty1 = ih; + tx2 = tx2 * rw + iw; + ty2 = ty2 * rh + ih; + + tx1 = Math.max(0, tx1); + ty1 = Math.max(0, ty1); + tx2 = Math.min(1, tx2); + ty2 = Math.min(1, ty2); + } + + if (displayedTextureSource._flipTextureY) { + let tempty = ty2; + ty2 = ty1; + ty1 = tempty; + } + this.__core.setTextureCoords(tx1, ty1, tx2, ty2); + } + } + + getCornerPoints() { + return this.__core.getCornerPoints(); + } + + _unsetTagsParent() { + if (this.__tags) { + this.__tags.forEach((tag) => { + // Remove from treeTags. + let p = this; + while (p = p.__parent) { + let parentTreeTags = p.__treeTags.get(tag); + parentTreeTags.delete(this); + + if (p.__tagRoot) { + break; + } + } + }); + } + + let tags = null; + let n = 0; + if (this.__treeTags) { + if (!this.__tagRoot) { + tags = Utils.iteratorToArray(this.__treeTags.keys()); + n = tags.length; + + if (n > 0) { + for (let i = 0; i < n; i++) { + let tagSet = this.__treeTags.get(tags[i]); + + // Remove from treeTags. + let p = this; + while ((p = p.__parent)) { + let parentTreeTags = p.__treeTags.get(tags[i]); + + tagSet.forEach(function (comp) { + parentTreeTags.delete(comp); + }); + + if (p.__tagRoot) { + break; + } + } + } + } + } + } + }; + + _setTagsParent() { + // Just copy over the 'local' tags. + if (this.__tags) { + this.__tags.forEach((tag) => { + let p = this; + while (p = p.__parent) { + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + s.add(this); + + if (p.__tagRoot) { + break; + } + } + }); + } + + if (this.__treeTags && this.__treeTags.size) { + if (!this.__tagRoot) { + this.__treeTags.forEach((tagSet, tag) => { + let p = this; + while (!p.__tagRoot && (p = p.__parent)) { + if (p.__tagRoot) ; + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + tagSet.forEach(function (comp) { + s.add(comp); + }); + } + }); + } + } + }; + + + _getByTag(tag) { + if (!this.__treeTags) { + return []; + } + let t = this.__treeTags.get(tag); + return t ? Utils.setToArray(t) : []; + }; + + getTags() { + return this.__tags ? this.__tags : []; + }; + + setTags(tags) { + tags = tags.reduce((acc, tag) => { + return acc.concat(tag.split(' ')); + }, []); + + if (this.__ref) { + tags.push(this.__ref); + } + + let i, n = tags.length; + let removes = []; + let adds = []; + for (i = 0; i < n; i++) { + if (!this.hasTag(tags[i])) { + adds.push(tags[i]); + } + } + + let currentTags = this.tags || []; + n = currentTags.length; + for (i = 0; i < n; i++) { + if (tags.indexOf(currentTags[i]) == -1) { + removes.push(currentTags[i]); + } + } + + for (i = 0; i < removes.length; i++) { + this.removeTag(removes[i]); + } + + for (i = 0; i < adds.length; i++) { + this.addTag(adds[i]); + } + } + + addTag(tag) { + if (tag.indexOf(' ') === -1) { + if (Utils.isUcChar(tag.charCodeAt(0))) { + this._throwError("Tag may not start with an upper case character."); + } + + this._addTag(tag); + } else { + const tags = tag.split(' '); + for (let i = 0, m = tags.length; i < m; i++) { + const tag = tags[i]; + + if (Utils.isUcChar(tag.charCodeAt(0))) { + this._throwError("Tag may not start with an upper case character."); + } + + this._addTag(tag); + } + } + } + + _addTag(tag) { + if (!this.__tags) { + this.__tags = []; + } + if (this.__tags.indexOf(tag) === -1) { + this.__tags.push(tag); + + // Add to treeTags hierarchy. + let p = this.__parent; + if (p) { + do { + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + s.add(this); + + } while (!p.__tagRoot && (p = p.__parent)); + } + } + } + + removeTag(tag) { + let i = this.__tags.indexOf(tag); + if (i !== -1) { + this.__tags.splice(i, 1); + + // Remove from treeTags hierarchy. + let p = this.__parent; + if (p) { + do { + let list = p.__treeTags.get(tag); + if (list) { + list.delete(this); + } + } while (!p.__tagRoot && (p = p.__parent)); + } + } + } + + hasTag(tag) { + return (this.__tags && (this.__tags.indexOf(tag) !== -1)); + } + + /** + * Returns one of the elements from the subtree that have this tag. + * @param {string} tag + * @returns {Element} + */ + _tag(tag) { + if (tag.indexOf(".") !== -1) { + return this.mtag(tag)[0]; + } else { + if (this.__treeTags) { + let t = this.__treeTags.get(tag); + if (t) { + const item = t.values().next(); + return item ? item.value : undefined; + } + } + } + }; + + get tag() { + return this._tag; + } + + set tag(t) { + this.tags = t; + } + + /** + * Returns all elements from the subtree that have this tag. + * @param {string} tag + * @returns {Element[]} + */ + mtag(tag) { + let idx = tag.indexOf("."); + if (idx >= 0) { + let parts = tag.split('.'); + let res = this._getByTag(parts[0]); + let level = 1; + let c = parts.length; + while (res.length && level < c) { + let resn = []; + for (let j = 0, n = res.length; j < n; j++) { + resn = resn.concat(res[j]._getByTag(parts[level])); + } + + res = resn; + level++; + } + return res; + } else { + return this._getByTag(tag); + } + }; + + stag(tag, settings) { + let t = this.mtag(tag); + let n = t.length; + for (let i = 0; i < n; i++) { + Base.patchObject(t[i], settings); + } + } + + get tagRoot() { + return this.__tagRoot; + } + + set tagRoot(v) { + if (this.__tagRoot !== v) { + if (!v) { + this._setTagsParent(); + } else { + this._unsetTagsParent(); + } + + this.__tagRoot = v; + } + } + + sel(path) { + const results = this.select(path); + if (results.length) { + return results[0]; + } else { + return undefined; + } + } + + select(path) { + if (path.indexOf(",") !== -1) { + let selectors = path.split(','); + let res = []; + for (let i = 0; i < selectors.length; i++) { + res = res.concat(this._select(selectors[i])); + } + return res; + } else { + return this._select(path); + } + } + + _select(path) { + if (path === "") return [this]; + + + let pointIdx = path.indexOf("."); + let arrowIdx = path.indexOf(">"); + if (pointIdx === -1 && arrowIdx === -1) { + // Quick case. + return this.mtag(path); + } + + // Detect by first char. + let isRef; + if (arrowIdx === 0) { + isRef = true; + path = path.substr(1); + } else if (pointIdx === 0) { + isRef = false; + path = path.substr(1); + } else { + isRef = false; + } + + return this._selectChilds(path, isRef); + } + + _selectChilds(path, isRef) { + const pointIdx = path.indexOf("."); + const arrowIdx = path.indexOf(">"); + + if (pointIdx === -1 && arrowIdx === -1) { + if (isRef) { + const ref = this.getByRef(path); + return ref ? [ref] : []; + } else { + return this.mtag(path); + } + } + + if ((arrowIdx === -1) || (pointIdx !== -1 && pointIdx < arrowIdx)) { + let next; + const str = path.substr(0, pointIdx); + if (isRef) { + const ref = this.getByRef(str); + next = ref ? [ref] : []; + } else { + next = this.mtag(str); + } + let total = []; + const subPath = path.substr(pointIdx + 1); + for (let i = 0, n = next.length; i < n; i++) { + total = total.concat(next[i]._selectChilds(subPath, false)); + } + return total; + } else { + let next; + const str = path.substr(0, arrowIdx); + if (isRef) { + const ref = this.getByRef(str); + next = ref ? [ref] : []; + } else { + next = this.mtag(str); + } + let total = []; + const subPath = path.substr(arrowIdx + 1); + for (let i = 0, n = next.length; i < n; i++) { + total = total.concat(next[i]._selectChilds(subPath, true)); + } + return total; + } + } + + getByRef(ref) { + return this.childList.getByRef(ref); + } + + getLocationString() { + let i; + i = this.__parent ? this.__parent._children.getIndex(this) : "R"; + let localTags = this.getTags(); + let str = this.__parent ? this.__parent.getLocationString(): ""; + if (this.ref) { + str += ":[" + i + "]" + this.ref; + } else if (localTags.length) { + str += ":[" + i + "]" + localTags.join(","); + } else { + str += ":[" + i + "]#" + this.id; + } + return str; + } + + toString() { + let obj = this.getSettings(); + return Element.getPrettyString(obj, ""); + }; + + static getPrettyString(obj, indent) { + let children = obj.children; + delete obj.children; + + + // Convert singular json settings object. + let colorKeys = ["color", "colorUl", "colorUr", "colorBl", "colorBr"]; + let str = JSON.stringify(obj, function (k, v) { + if (colorKeys.indexOf(k) !== -1) { + return "COLOR[" + v.toString(16) + "]"; + } + return v; + }); + str = str.replace(/"COLOR\[([a-f0-9]{1,8})\]"/g, "0x$1"); + + if (children) { + let childStr = ""; + if (Utils.isObjectLiteral(children)) { + let refs = Object.keys(children); + childStr = ""; + for (let i = 0, n = refs.length; i < n; i++) { + childStr += `\n${indent} "${refs[i]}":`; + delete children[refs[i]].ref; + childStr += Element.getPrettyString(children[refs[i]], indent + " ") + (i < n - 1 ? "," : ""); + } + let isEmpty = (str === "{}"); + str = str.substr(0, str.length - 1) + (isEmpty ? "" : ",") + childStr + "\n" + indent + "}"; + } else { + let n = children.length; + childStr = "["; + for (let i = 0; i < n; i++) { + childStr += Element.getPrettyString(children[i], indent + " ") + (i < n - 1 ? "," : "") + "\n"; + } + childStr += indent + "]}"; + let isEmpty = (str === "{}"); + str = str.substr(0, str.length - 1) + (isEmpty ? "" : ",") + "\"children\":\n" + indent + childStr + "}"; + } + + } + + return str; + } + + getSettings() { + let settings = this.getNonDefaults(); + + let children = this._children.get(); + if (children) { + let n = children.length; + if (n) { + const childArray = []; + let missing = false; + for (let i = 0; i < n; i++) { + childArray.push(children[i].getSettings()); + missing = missing || !children[i].ref; + } + + if (!missing) { + settings.children = {}; + childArray.forEach(child => { + settings.children[child.ref] = child; + }); + } else { + settings.children = childArray; + } + } + } + + settings.id = this.id; + + return settings; + } + + getNonDefaults() { + let settings = {}; + + if (this.constructor !== Element) { + settings.type = this.constructor.name; + } + + if (this.__ref) { + settings.ref = this.__ref; + } + + if (this.__tags && this.__tags.length) { + settings.tags = this.__tags; + } + + if (this.x !== 0) settings.x = this.x; + if (this.y !== 0) settings.y = this.y; + if (this.w !== 0) settings.w = this.w; + if (this.h !== 0) settings.h = this.h; + + if (this.scaleX === this.scaleY) { + if (this.scaleX !== 1) settings.scale = this.scaleX; + } else { + if (this.scaleX !== 1) settings.scaleX = this.scaleX; + if (this.scaleY !== 1) settings.scaleY = this.scaleY; + } + + if (this.pivotX === this.pivotY) { + if (this.pivotX !== 0.5) settings.pivot = this.pivotX; + } else { + if (this.pivotX !== 0.5) settings.pivotX = this.pivotX; + if (this.pivotY !== 0.5) settings.pivotY = this.pivotY; + } + + if (this.mountX === this.mountY) { + if (this.mountX !== 0) settings.mount = this.mountX; + } else { + if (this.mountX !== 0) settings.mountX = this.mountX; + if (this.mountY !== 0) settings.mountY = this.mountY; + } + + if (this.alpha !== 1) settings.alpha = this.alpha; + + if (!this.visible) settings.visible = false; + + if (this.rotation !== 0) settings.rotation = this.rotation; + + if (this.colorUl === this.colorUr && this.colorBl === this.colorBr && this.colorUl === this.colorBl) { + if (this.colorUl !== 0xFFFFFFFF) settings.color = this.colorUl.toString(16); + } else { + if (this.colorUl !== 0xFFFFFFFF) settings.colorUl = this.colorUl.toString(16); + if (this.colorUr !== 0xFFFFFFFF) settings.colorUr = this.colorUr.toString(16); + if (this.colorBl !== 0xFFFFFFFF) settings.colorBl = this.colorBl.toString(16); + if (this.colorBr !== 0xFFFFFFFF) settings.colorBr = this.colorBr.toString(16); + } + + if (this.zIndex) settings.zIndex = this.zIndex; + + if (this.forceZIndexContext) settings.forceZIndexContext = true; + + if (this.clipping) settings.clipping = this.clipping; + + if (!this.clipbox) settings.clipbox = this.clipbox; + + if (this.__texture) { + let tnd = this.__texture.getNonDefaults(); + if (Object.keys(tnd).length) { + settings.texture = tnd; + } + } + + if (this.shader) { + let tnd = this.shader.getNonDefaults(); + if (Object.keys(tnd).length) { + settings.shader = tnd; + } + } + + if (this._hasTexturizer()) { + if (this.texturizer.enabled) { + settings.renderToTexture = this.texturizer.enabled; + } + if (this.texturizer.lazy) { + settings.renderToTextureLazy = this.texturizer.lazy; + } + if (this.texturizer.colorize) { + settings.colorizeResultTexture = this.texturizer.colorize; + } + if (this.texturizer.renderOffscreen) { + settings.renderOffscreen = this.texturizer.renderOffscreen; + } + } + + return settings; + }; + + static getGetter(propertyPath) { + let getter = Element.PROP_GETTERS.get(propertyPath); + if (!getter) { + getter = new Function('obj', 'return obj.' + propertyPath); + Element.PROP_GETTERS.set(propertyPath, getter); + } + return getter; + } + + static getSetter(propertyPath) { + let setter = Element.PROP_SETTERS.get(propertyPath); + if (!setter) { + setter = new Function('obj', 'v', 'obj.' + propertyPath + ' = v'); + Element.PROP_SETTERS.set(propertyPath, setter); + } + return setter; + } + + get withinBoundsMargin() { + return this.__core._withinBoundsMargin; + } + + _enableWithinBoundsMargin() { + // Iff enabled, this toggles the active flag. + if (this.__enabled) { + this._setActiveFlag(); + } + } + + _disableWithinBoundsMargin() { + // Iff active, this toggles the active flag. + if (this.__active) { + this._unsetActiveFlag(); + } + } + + set boundsMargin(v) { + if (!Array.isArray(v) && v !== null) { + throw new Error("boundsMargin should be an array of left-top-right-bottom values or null (inherit margin)"); + } + this.__core.boundsMargin = v; + } + + get boundsMargin() { + return this.__core.boundsMargin; + } + + get x() { + return this.__core.offsetX; + } + + set x(v) { + this.__core.offsetX = v; + } + + get y() { + return this.__core.offsetY; + } + + set y(v) { + this.__core.offsetY = v; + } + + get w() { + return this._w; + } + + set w(v) { + if (Utils.isFunction(v)) { + this._w = 0; + this.__core.funcW = v; + } else { + v = Math.max(v, 0); + if (this._w !== v) { + this.__core.disableFuncW(); + this._w = v; + this._updateDimensions(); + } + } + } + + get h() { + return this._h; + } + + set h(v) { + if (Utils.isFunction(v)) { + this._h = 0; + this.__core.funcH = v; + } else { + v = Math.max(v, 0); + if (this._h !== v) { + this.__core.disableFuncH(); + this._h = v; + this._updateDimensions(); + } + } + } + + get scaleX() { + return this.__core.scaleX; + } + + set scaleX(v) { + this.__core.scaleX = v; + } + + get scaleY() { + return this.__core.scaleY; + } + + set scaleY(v) { + this.__core.scaleY = v; + } + + get scale() { + return this.__core.scale; + } + + set scale(v) { + this.__core.scale = v; + } + + get pivotX() { + return this.__core.pivotX; + } + + set pivotX(v) { + this.__core.pivotX = v; + } + + get pivotY() { + return this.__core.pivotY; + } + + set pivotY(v) { + this.__core.pivotY = v; + } + + get pivot() { + return this.__core.pivot; + } + + set pivot(v) { + this.__core.pivot = v; + } + + get mountX() { + return this.__core.mountX; + } + + set mountX(v) { + this.__core.mountX = v; + } + + get mountY() { + return this.__core.mountY; + } + + set mountY(v) { + this.__core.mountY = v; + } + + get mount() { + return this.__core.mount; + } + + set mount(v) { + this.__core.mount = v; + } + + get rotation() { + return this.__core.rotation; + } + + set rotation(v) { + this.__core.rotation = v; + } + + get alpha() { + return this.__core.alpha; + } + + set alpha(v) { + this.__core.alpha = v; + } + + get visible() { + return this.__core.visible; + } + + set visible(v) { + this.__core.visible = v; + } + + get colorUl() { + return this.__core.colorUl; + } + + set colorUl(v) { + this.__core.colorUl = v; + } + + get colorUr() { + return this.__core.colorUr; + } + + set colorUr(v) { + this.__core.colorUr = v; + } + + get colorBl() { + return this.__core.colorBl; + } + + set colorBl(v) { + this.__core.colorBl = v; + } + + get colorBr() { + return this.__core.colorBr; + } + + set colorBr(v) { + this.__core.colorBr = v; + } + + get color() { + return this.__core.colorUl; + } + + set color(v) { + if (this.colorUl !== v || this.colorUr !== v || this.colorBl !== v || this.colorBr !== v) { + this.colorUl = v; + this.colorUr = v; + this.colorBl = v; + this.colorBr = v; + } + } + + get colorTop() { + return this.colorUl; + } + + set colorTop(v) { + if (this.colorUl !== v || this.colorUr !== v) { + this.colorUl = v; + this.colorUr = v; + } + } + + get colorBottom() { + return this.colorBl; + } + + set colorBottom(v) { + if (this.colorBl !== v || this.colorBr !== v) { + this.colorBl = v; + this.colorBr = v; + } + } + + get colorLeft() { + return this.colorUl; + } + + set colorLeft(v) { + if (this.colorUl !== v || this.colorBl !== v) { + this.colorUl = v; + this.colorBl = v; + } + } + + get colorRight() { + return this.colorUr; + } + + set colorRight(v) { + if (this.colorUr !== v || this.colorBr !== v) { + this.colorUr = v; + this.colorBr = v; + } + } + + get zIndex() {return this.__core.zIndex} + set zIndex(v) { + this.__core.zIndex = v; + } + + get forceZIndexContext() {return this.__core.forceZIndexContext} + set forceZIndexContext(v) { + this.__core.forceZIndexContext = v; + } + + get clipping() {return this.__core.clipping} + set clipping(v) { + this.__core.clipping = v; + } + + get clipbox() {return this.__core.clipbox} + set clipbox(v) { + this.__core.clipbox = v; + } + + get tags() { + return this.getTags(); + } + + set tags(v) { + if (!Array.isArray(v)) v = [v]; + this.setTags(v); + } + + set t(v) { + this.tags = v; + } + + get _children() { + if (!this.__childList) { + this.__childList = new ElementChildList(this, false); + } + return this.__childList; + } + + get childList() { + if (!this._allowChildrenAccess()) { + this._throwError("Direct access to children is not allowed in " + this.getLocationString()); + } + return this._children; + } + + hasChildren() { + return this._allowChildrenAccess() && this.__childList && (this.__childList.length > 0); + } + + _allowChildrenAccess() { + return true; + } + + get children() { + return this.childList.get(); + } + + set children(children) { + this.childList.patch(children); + } + + add(o) { + return this.childList.a(o); + } + + get p() { + return this.__parent; + } + + get parent() { + return this.__parent; + } + + get src() { + if (this.texture && this.texture instanceof ImageTexture) { + return this.texture._src; + } else { + return undefined; + } + } + + set src(v) { + const texture = new ImageTexture(this.stage); + texture.src = v; + this.texture = texture; + } + + set mw(v) { + if (this.texture) { + this.texture.mw = v; + this._updateDimensions(); + } else { + this._throwError('Please set mw after setting a texture.'); + } + } + + set mh(v) { + if (this.texture) { + this.texture.mh = v; + this._updateDimensions(); + } else { + this._throwError('Please set mh after setting a texture.'); + } + } + + get rect() { + return (this.texture === this.stage.rectangleTexture); + } + + set rect(v) { + if (v) { + this.texture = this.stage.rectangleTexture; + } else { + this.texture = null; + } + } + + enableTextTexture() { + if (!this.texture || !(this.texture instanceof TextTexture)) { + this.texture = new TextTexture(this.stage); + + if (!this.texture.w && !this.texture.h) { + // Inherit dimensions from element. + // This allows userland to set dimensions of the Element and then later specify the text. + this.texture.w = this.w; + this.texture.h = this.h; + } + } + return this.texture; + } + + get text() { + if (this.texture && (this.texture instanceof TextTexture)) { + return this.texture; + } else { + return null; + } + } + + set text(v) { + if (!this.texture || !(this.texture instanceof TextTexture)) { + this.enableTextTexture(); + } + if (Utils.isString(v)) { + this.texture.text = v; + } else { + this.texture.patch(v); + } + } + + set onUpdate(f) { + this.__core.onUpdate = f; + } + + set onAfterCalcs(f) { + this.__core.onAfterCalcs = f; + } + + set onAfterUpdate(f) { + this.__core.onAfterUpdate = f; + } + + forceUpdate() { + // Make sure that the update loop is run. + this.__core._setHasUpdates(); + } + + get shader() { + return this.__core.shader; + } + + set shader(v) { + if (Utils.isObjectLiteral(v) && !v.type) { + // Setting properties on an existing shader. + if (this.shader) { + this.shader.patch(v); + } + } else { + const shader = Shader.create(this.stage, v); + + if (this.__enabled && this.__core.shader) { + this.__core.shader.removeElement(this.__core); + } + + this.__core.shader = shader; + + if (this.__enabled && this.__core.shader) { + this.__core.shader.addElement(this.__core); + } + } + } + + _hasTexturizer() { + return !!this.__core._texturizer; + } + + get renderToTexture() { + return this.rtt + } + + set renderToTexture(v) { + this.rtt = v; + } + + get rtt() { + return this._hasTexturizer() && this.texturizer.enabled; + } + + set rtt(v) { + this.texturizer.enabled = v; + } + + get rttLazy() { + return this._hasTexturizer() && this.texturizer.lazy; + } + + set rttLazy(v) { + this.texturizer.lazy = v; + } + + get renderOffscreen() { + return this._hasTexturizer() && this.texturizer.renderOffscreen; + } + + set renderOffscreen(v) { + this.texturizer.renderOffscreen = v; + } + + get colorizeResultTexture() { + return this._hasTexturizer() && this.texturizer.colorize; + } + + set colorizeResultTexture(v) { + this.texturizer.colorize = v; + } + + getTexture() { + return this.texturizer._getTextureSource(); + } + + get texturizer() { + return this.__core.texturizer; + } + + patch(settings) { + let paths = Object.keys(settings); + + for (let i = 0, n = paths.length; i < n; i++) { + let path = paths[i]; + const v = settings[path]; + + const firstCharCode = path.charCodeAt(0); + if (Utils.isUcChar(firstCharCode)) { + // Ref. + const child = this.getByRef(path); + if (!child) { + if (v !== undefined) { + // Add to list immediately. + let c; + if (Utils.isObjectLiteral(v)) { + // Catch this case to capture createMode flag. + c = this.childList.createItem(v); + c.patch(v); + } else if (Utils.isObject(v)) { + c = v; + } + if (c.isElement) { + c.ref = path; + } + + this.childList.a(c); + } + } else { + if (v === undefined) { + if (child.parent) { + child.parent.childList.remove(child); + } + } else if (Utils.isObjectLiteral(v)) { + child.patch(v); + } else if (v.isElement) { + // Replace element by new element. + v.ref = path; + this.childList.replace(v, child); + } else { + this._throwError("Unexpected value for path: " + path); + } + } + } else { + // Property. + Base.patchObjectProperty(this, path, v); + } + } + } + + _throwError(message) { + throw new Error(this.constructor.name + " (" + this.getLocationString() + "): " + message); + } + + animation(settings) { + return this.stage.animations.createAnimation(this, settings); + } + + transition(property, settings = null) { + if (settings === null) { + return this._getTransition(property); + } else { + this._setTransition(property, settings); + // We do not create/return the transition, because it would undo the 'lazy transition creation' optimization. + return null; + } + } + + set transitions(object) { + let keys = Object.keys(object); + keys.forEach(property => { + this.transition(property, object[property]); + }); + } + + set smooth(object) { + let keys = Object.keys(object); + keys.forEach(property => { + let value = object[property]; + if (Array.isArray(value)) { + this.setSmooth(property, value[0], value[1]); + } else { + this.setSmooth(property, value); + } + }); + } + + fastForward(property) { + if (this._transitions) { + let t = this._transitions[property]; + if (t && t.isTransition) { + t.finish(); + } + } + } + + _getTransition(property) { + if (!this._transitions) { + this._transitions = {}; + } + let t = this._transitions[property]; + if (!t) { + // Create default transition. + t = new Transition(this.stage.transitions, this.stage.transitions.defaultTransitionSettings, this, property); + } else if (t.isTransitionSettings) { + // Upgrade to 'real' transition. + t = new Transition( + this.stage.transitions, + t, + this, + property + ); + } + this._transitions[property] = t; + return t; + } + + _setTransition(property, settings) { + if (!settings) { + this._removeTransition(property); + } else { + if (Utils.isObjectLiteral(settings)) { + // Convert plain object to proper settings object. + settings = this.stage.transitions.createSettings(settings); + } + + if (!this._transitions) { + this._transitions = {}; + } + + let current = this._transitions[property]; + if (current && current.isTransition) { + // Runtime settings change. + current.settings = settings; + return current; + } else { + // Initially, only set the settings and upgrade to a 'real' transition when it is used. + this._transitions[property] = settings; + } + } + } + + _removeTransition(property) { + if (this._transitions) { + delete this._transitions[property]; + } + } + + getSmooth(property, v) { + let t = this._getTransition(property); + if (t && t.isAttached()) { + return t.targetValue; + } else { + return v; + } + } + + setSmooth(property, v, settings) { + if (settings) { + this._setTransition(property, settings); + } + let t = this._getTransition(property); + t.start(v); + return t; + } + + get flex() { + return this.__core.flex; + } + + set flex(v) { + this.__core.flex = v; + } + + get flexItem() { + return this.__core.flexItem; + } + + set flexItem(v) { + this.__core.flexItem = v; + } + + static isColorProperty(property) { + return property.toLowerCase().indexOf("color") >= 0; + } + + static getMerger(property) { + if (Element.isColorProperty(property)) { + return StageUtils.mergeColors; + } else { + return StageUtils.mergeNumbers; + } + } +} + +// This gives a slight performance benefit compared to extending EventEmitter. +EventEmitter.addAsMixin(Element); + +Element.prototype.isElement = 1; + +Element.id = 1; + +// Getters reused when referencing element (subobject) properties by a property path, as used in a transition or animation ('x', 'texture.x', etc). +Element.PROP_GETTERS = new Map(); + +// Setters reused when referencing element (subobject) properties by a property path, as used in a transition or animation ('x', 'texture.x', etc). +Element.PROP_SETTERS = new Map(); + +class StateMachine { + + constructor() { + StateMachine.setupStateMachine(this); + } + + static setupStateMachine(target) { + const targetConstructor = target.constructor; + const router = StateMachine.create(targetConstructor); + Object.setPrototypeOf(target, router.prototype); + target.constructor = targetConstructor; + target._initStateMachine(); + } + + /** + * Creates a state machine implementation. + * It extends the original type and should be used when creating new instances. + * The original type is available as static property 'original', and it must be used when subclassing as follows: + * const type = StateMachine.create(class YourNewStateMachineClass extends YourBaseStateMachineClass.original { }) + * @param {Class} type + * @returns {StateMachine} + */ + static create(type) { + if (!type.hasOwnProperty('_sm')) { + // Only need to run once. + const stateMachineType = new StateMachineType(type); + type._sm = stateMachineType; + } + + return type._sm.router; + } + + /** + * Calls the specified method if it exists. + * @param {string} event + * @param {*...} args + */ + fire(event, ...args) { + if (this._hasMethod(event)) { + return this[event](...args); + } + } + + /** + * Returns the current state path (for example "Initialized.Loading"). + * @returns {string} + * @protected + */ + _getState() { + return this._state.__path; + } + + /** + * Returns true iff statePath is (an ancestor of) currentStatePath. + * @param {string} statePath + * @param {string} currentStatePath + * @returns {Boolean} + * @protected + */ + _inState(statePath, currentStatePath = this._state.__path) { + const state = this._sm.getStateByPath(statePath); + const currentState = this._sm.getStateByPath(currentStatePath); + const level = state.__level; + const stateAtLevel = StateMachine._getStateAtLevel(currentState, level); + return (stateAtLevel === state); + } + + /** + * Returns true if the specified class member is defined for the currently set state. + * @param {string} name + * @returns {boolean} + * @protected + */ + _hasMember(name) { + return !!this.constructor.prototype[name]; + } + + /** + * Returns true if the specified class member is a method for the currently set state. + * @param {string} name + * @returns {boolean} + * @protected + */ + _hasMethod(name) { + const member = this.constructor.prototype[name]; + return !!member && (typeof member === "function") + } + + /** + * Switches to the specified state. + * @param {string} statePath + * Substates are seperated by a underscores (for example "Initialized.Loading"). + * @param {*[]} [args] + * Args that are supplied in $enter and $exit events. + * @protected + */ + _setState(statePath, args) { + const setStateId = ++this._setStateCounter; + this._setStateId = setStateId; + + if (this._state.__path !== statePath) { + // Performance optimization. + let newState = this._sm._stateMap[statePath]; + if (!newState) { + // Check for super state. + newState = this._sm.getStateByPath(statePath); + } + + const prevState = this._state; + + const hasDifferentEnterMethod = (newState.prototype.$enter !== this._state.prototype.$enter); + const hasDifferentExitMethod = (newState.prototype.$exit !== this._state.prototype.$exit); + if (hasDifferentEnterMethod || hasDifferentExitMethod) { + const sharedState = StateMachine._getSharedState(this._state, newState); + const context = { + newState: newState.__path, + prevState: prevState.__path, + sharedState: sharedState.__path + }; + const sharedLevel = sharedState.__level; + + if (hasDifferentExitMethod) { + const exitStates = StateMachine._getStatesUntilLevel(this._state, sharedLevel); + for (let i = 0, n = exitStates.length; i < n; i++) { + this.__setState(exitStates[i]); + this._callExit(this._state, args, context); + const stateChangeOverridden = (this._setStateId !== setStateId); + if (stateChangeOverridden) { + return; + } + } + } + + if (hasDifferentEnterMethod) { + const enterStates = StateMachine._getStatesUntilLevel(newState, sharedLevel).reverse(); + for (let i = 0, n = enterStates.length; i < n; i++) { + this.__setState(enterStates[i]); + this._callEnter(this._state, args, context); + const stateChangeOverridden = (this._setStateId !== setStateId); + if (stateChangeOverridden) { + return; + } + } + } + + } + + this.__setState(newState); + + if (this._changedState) { + const context = { + newState: newState.__path, + prevState: prevState.__path + }; + + if (args) { + this._changedState(context, ...args); + } else { + this._changedState(context); + } + } + + if (this._onStateChange) { + const context = { + newState: newState.__path, + prevState: prevState.__path + }; + this._onStateChange(context); + } + + } + } + + _callEnter(state, args = [], context) { + const hasParent = !!state.__parent; + if (state.prototype.$enter) { + if (!hasParent || (state.__parent.prototype.$enter !== state.prototype.$enter)) { + state.prototype.$enter.apply(this, [context, ...args]); + } + } + } + + _callExit(state, args = [], context) { + const hasParent = !!state.__parent; + if (state.prototype.$exit) { + if (!hasParent || (state.__parent.prototype.$exit !== state.prototype.$exit)) { + state.prototype.$exit.apply(this, [context, ...args]); + } + } + } + + __setState(state) { + this._state = state; + this._stateIndex = state.__index; + this.constructor = state; + } + + _initStateMachine() { + this._state = null; + this._stateIndex = 0; + this._setStateCounter = 0; + this._sm = this._routedType._sm; + this.__setState(this._sm.getStateByPath("")); + const context = {newState: "", prevState: undefined, sharedState: undefined}; + this._callEnter(this._state, [], context); + this._onStateChange = undefined; + } + + /** + * Between multiple member names, select the one specified in the deepest state. + * If multiple member names are specified in the same deepest state, the first one in the array is returned. + * @param {string[]} memberNames + * @returns {string|undefined} + * @protected + */ + _getMostSpecificHandledMember(memberNames) { + let cur = this._state; + do { + for (let i = 0, n = memberNames.length; i < n; i++) { + const memberName = memberNames[i]; + if (!cur.__parent) { + if (cur.prototype[memberName]) { + return memberName; + } + } else { + const alias = StateMachineType.getStateMemberAlias(cur.__path, memberName); + if (this[alias]) { + return memberName; + } + } + } + cur = cur.__parent; + } while (cur); + } + + static _getStatesUntilLevel(state, level) { + const states = []; + while (state.__level > level) { + states.push(state); + state = state.__parent; + } + return states; + } + + static _getSharedState(state1, state2) { + const state1Array = StateMachine._getAncestorStates(state1); + const state2Array = StateMachine._getAncestorStates(state2); + const n = Math.min(state1Array.length, state2Array.length); + for (let i = 0; i < n; i++) { + if (state1Array[i] !== state2Array[i]) { + return state1Array[i - 1]; + } + } + return state1Array[n - 1]; + } + + static _getAncestorStates(state) { + const result = []; + do { + result.push(state); + } while(state = state.__parent); + return result.reverse(); + } + + static _getStateAtLevel(state, level) { + if (level > state.__level) { + return undefined; + } + + while(level < state.__level) { + state = state.__parent; + } + return state; + } +} + +class StateMachineType { + + constructor(type) { + this._type = type; + this._router = null; + + this.init(); + } + + get router() { + return this._router; + } + + init() { + this._router = this._createRouter(); + + this._stateMap = this._getStateMap(); + + this._addStateMemberDelegatorsToRouter(); + + } + + _createRouter() { + const type = this._type; + + const router = class StateMachineRouter extends type { + constructor() { + super(...arguments); + if (!this.constructor.hasOwnProperty('_isRouter')) { + throw new Error(`You need to extend ${type.name}.original instead of ${type.name}.`); + } + } + }; + router._isRouter = true; + router.prototype._routedType = type; + router.original = type; + + this._mixinStateMachineMethods(router); + + return router; + } + + _mixinStateMachineMethods(router) { + // Mixin the state machine methods, so that we reuse the methods instead of re-creating them. + const names = Object.getOwnPropertyNames(StateMachine.prototype); + for (let i = 0, n = names.length; i < n; i++) { + const name = names[i]; + if (name !== "constructor") { + const descriptor = Object.getOwnPropertyDescriptor(StateMachine.prototype, name); + Object.defineProperty(router.prototype, name, descriptor); + } + } + } + + _addStateMemberDelegatorsToRouter() { + const members = this._getAllMemberNames(); + + members.forEach(member => { + this._addMemberRouter(member); + }); + } + + /** + * @note We are generating code because it yields much better performance. + */ + _addMemberRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + let type = undefined; + descriptors.forEach(descriptor => { + if (descriptor) { + const descType = this._getDescriptorType(descriptor); + if (type && (type !== descType)) { + console.warn(`Member ${member} in ${this._type.name} has inconsistent types.`); + return; + } + type = descType; + } + }); + + switch(type) { + case "method": + this._addMethodRouter(member, descriptors, aliases); + break; + case "getter": + this._addGetterSetterRouters(member); + break; + case "property": + console.warn("Fixed properties are not supported; please use a getter instead!"); + break; + } + } + + _getDescriptor(state, member, isValid = () => true) { + let type = state; + let curState = state; + + do { + const descriptor = Object.getOwnPropertyDescriptor(type.prototype, member); + if (descriptor) { + if (isValid(descriptor)) { + descriptor._source = curState; + return descriptor; + } + } + type = Object.getPrototypeOf(type); + if (type && type.hasOwnProperty('__state')) { + curState = type; + } + } while(type && type.prototype); + return undefined; + } + + _getDescriptorType(descriptor) { + if (descriptor.get || descriptor.set) { + return 'getter'; + } else { + if (typeof descriptor.value === "function") { + return 'method'; + } else { + return 'property'; + } + } + } + + static _supportsSpread() { + if (this.__supportsSpread === undefined) { + this.__supportsSpread = false; + try { + const func = new Function("return [].concat(...arguments);"); + func(); + this.__supportsSpread = true; + } catch(e) {} + } + return this.__supportsSpread; + } + + _addMethodRouter(member, descriptors, aliases) { + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + const supportsSpread = StateMachineType._supportsSpread(); + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + if (supportsSpread) { + code.push(`if (i < ${i}) return this["${cur}"](...arguments); else`); + } else { + code.push(`if (i < ${i}) return this["${cur}"].apply(this, arguments); else`); + } + } else { + code.push(`if (i < ${i}) return ; else`); + } + } + cur = alias; + } + if (cur) { + if (supportsSpread) { + code.push(`return this["${cur}"](...arguments);`); + } else { + code.push(`return this["${cur}"].apply(this, arguments);`); + } + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function([], functionBody); + + const descriptor = {value: router}; + Object.defineProperty(this._router.prototype, member, descriptor); + } + + _addGetterSetterRouters(member) { + const getter = this._getGetterRouter(member); + const setter = this._getSetterRouter(member); + const descriptor = { + get: getter, + set: setter + }; + Object.defineProperty(this._router.prototype, member, descriptor); + } + + _getGetterRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member, (descriptor => descriptor.get)); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + code.push(`if (i < ${i}) return this["${cur}"]; else`); + } else { + code.push(`if (i < ${i}) return ; else`); + } + } + cur = alias; + } + if (cur) { + code.push(`return this["${cur}"];`); + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function([], functionBody); + return router; + } + + _getSetterRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member, (descriptor => descriptor.set)); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + code.push(`if (i < ${i}) this["${cur}"] = arg; else`); + } else { + code.push(`if (i < ${i}) ; else`); + } + } + cur = alias; + } + if (cur) { + code.push(`this["${cur}"] = arg;`); + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function(["arg"], functionBody); + return router; + } + + static getStateMemberAlias(path, member) { + return "$" + (path ? path + "." : "") + member; + } + + _getAllMemberNames() { + const stateMap = this._stateMap; + const map = Object.keys(stateMap); + let members = new Set(); + map.forEach(statePath => { + if (statePath === "") { + // Root state can be skipped: if the method only occurs in the root state, we don't need to re-delegate it based on state. + return; + } + const state = stateMap[statePath]; + const names = this._getStateMemberNames(state); + names.forEach(name => { + members.add(name); + }); + }); + return [...members]; + } + + _getStateMemberNames(state) { + let type = state; + let members = new Set(); + const isRoot = this._type === state; + do { + const names = this._getStateMemberNamesForType(type); + names.forEach(name => { + members.add(name); + }); + + type = Object.getPrototypeOf(type); + } while(type && type.prototype && (!type.hasOwnProperty("__state") || isRoot)); + + return members; + } + + _getStateMemberNamesForType(type) { + const memberNames = Object.getOwnPropertyNames(type.prototype); + return memberNames.filter(memberName => { + return (memberName !== "constructor") && !StateMachineType._isStateLocalMember(memberName); + }); + } + + static _isStateLocalMember(memberName) { + return (memberName === "$enter") || (memberName === "$exit"); + } + + getStateByPath(statePath) { + if (this._stateMap[statePath]) { + return this._stateMap[statePath]; + } + + // Search for closest match. + const parts = statePath.split("."); + while(parts.pop()) { + const statePath = parts.join("."); + if (this._stateMap[statePath]) { + return this._stateMap[statePath]; + } + } + } + + _getStateMap() { + if (!this._stateMap) { + this._stateMap = this._createStateMap(); + } + return this._stateMap; + } + + _createStateMap() { + const stateMap = {}; + this._addState(this._type, null, "", stateMap); + return stateMap; + } + + _addState(state, parentState, name, stateMap) { + state.__state = true; + state.__name = name; + + this._addStaticStateProperty(state, parentState); + + const parentPath = (parentState ? parentState.__path : ""); + let path = (parentPath ? parentPath + "." : "") + name; + state.__path = path; + state.__level = parentState ? parentState.__level + 1 : 0; + state.__parent = parentState; + state.__index = Object.keys(stateMap).length; + stateMap[path] = state; + + const states = state._states; + if (states) { + const isInheritedFromParent = (parentState && parentState._states === states); + if (!isInheritedFromParent) { + const subStates = state._states(); + subStates.forEach(subState => { + const stateName = StateMachineType._getStateName(subState); + this._addState(subState, state, stateName, stateMap); + }); + } + } + } + + static _getStateName(state) { + const name = state.name; + + const index = name.indexOf('$'); + if (index > 0) { + // Strip off rollup name suffix. + return name.substr(0, index); + } + + return name; + } + + _addStaticStateProperty(state, parentState) { + if (parentState) { + const isClassStateLevel = parentState && !parentState.__parent; + if (isClassStateLevel) { + this._router[state.__name] = state; + } else { + parentState[state.__name] = state; + } + } + } + +} + +/** + * @extends StateMachine + */ +class Component extends Element { + + constructor(stage, properties) { + super(stage); + + // Encapsulate tags to prevent leaking. + this.tagRoot = true; + + if (Utils.isObjectLiteral(properties)) { + Object.assign(this, properties); + } + + this.__initialized = false; + this.__firstActive = false; + this.__firstEnable = false; + + this.__signals = undefined; + + this.__passSignals = undefined; + + this.__construct(); + + // Quick-apply template. + const func = this.constructor.getTemplateFunc(); + func.f(this, func.a); + + this._build(); + } + + __start() { + StateMachine.setupStateMachine(this); + this._onStateChange = Component.prototype.__onStateChange; + } + + get state() { + return this._getState(); + } + + __onStateChange() { + /* FIXME: Workaround for case, where application was shut but component still lives */ + if (this.application) { + this.application.updateFocusPath(); + } + } + + _refocus() { + /* FIXME: Workaround for case, where application was shut but component still lives */ + if (this.application) { + this.application.updateFocusPath(); + } + } + + /** + * Returns a high-performance template patcher. + */ + static getTemplateFunc() { + // We need a different template function per patch id. + const name = "_templateFunc"; + + // Be careful with class-based static inheritance. + const hasName = '__has' + name; + if (this[hasName] !== this) { + this[hasName] = this; + this[name] = this.parseTemplate(this._template()); + } + return this[name]; + } + + static parseTemplate(obj) { + const context = { + loc: [], + store: [], + rid: 0 + }; + + this.parseTemplateRec(obj, context, "element"); + + const code = context.loc.join(";\n"); + const f = new Function("element", "store", code); + return {f:f, a:context.store} + } + + static parseTemplateRec(obj, context, cursor) { + const store = context.store; + const loc = context.loc; + const keys = Object.keys(obj); + keys.forEach(key => { + let value = obj[key]; + if (Utils.isUcChar(key.charCodeAt(0))) { + // Value must be expanded as well. + if (Utils.isObjectLiteral(value)) { + // Ref. + const childCursor = `r${key.replace(/[^a-z0-9]/gi, "") + context.rid}`; + let type = value.type ? value.type : Element; + if (type === Element) { + loc.push(`const ${childCursor} = element.stage.createElement()`); + } else { + store.push(type); + loc.push(`const ${childCursor} = new store[${store.length - 1}](${cursor}.stage)`); + } + loc.push(`${childCursor}.ref = "${key}"`); + context.rid++; + + // Enter sub. + this.parseTemplateRec(value, context, childCursor); + + loc.push(`${cursor}.childList.add(${childCursor})`); + } else if (Utils.isObject(value)) { + // Dynamic assignment. + store.push(value); + loc.push(`${cursor}.childList.add(store[${store.length - 1}])`); + } + } else { + if (key === "text") { + const propKey = cursor + "__text"; + loc.push(`const ${propKey} = ${cursor}.enableTextTexture()`); + this.parseTemplatePropRec(value, context, propKey); + } else if (key === "texture" && Utils.isObjectLiteral(value)) { + const propKey = cursor + "__texture"; + const type = value.type; + if (type) { + store.push(type); + loc.push(`const ${propKey} = new store[${store.length - 1}](${cursor}.stage)`); + this.parseTemplatePropRec(value, context, propKey); + loc.push(`${cursor}["${key}"] = ${propKey}`); + } else { + loc.push(`${propKey} = ${cursor}.texture`); + this.parseTemplatePropRec(value, context, propKey); + } + } else { + // Property; + if (Utils.isNumber(value)) { + loc.push(`${cursor}["${key}"] = ${value}`); + } else if (Utils.isBoolean(value)) { + loc.push(`${cursor}["${key}"] = ${value ? "true" : "false"}`); + } else if (Utils.isObject(value) || Array.isArray(value)) { + // Dynamic assignment. + // Because literal objects may contain dynamics, we store the full object. + store.push(value); + loc.push(`${cursor}["${key}"] = store[${store.length - 1}]`); + } else { + // String etc. + loc.push(`${cursor}["${key}"] = ${JSON.stringify(value)}`); + } + } + } + }); + } + + static parseTemplatePropRec(obj, context, cursor) { + const store = context.store; + const loc = context.loc; + const keys = Object.keys(obj); + keys.forEach(key => { + if (key !== "type") { + const value = obj[key]; + if (Utils.isNumber(value)) { + loc.push(`${cursor}["${key}"] = ${value}`); + } else if (Utils.isBoolean(value)) { + loc.push(`${cursor}["${key}"] = ${value ? "true" : "false"}`); + } else if (Utils.isObject(value) || Array.isArray(value)) { + // Dynamic assignment. + // Because literal objects may contain dynamics, we store the full object. + store.push(value); + loc.push(`${cursor}["${key}"] = store[${store.length - 1}]`); + } else { + // String etc. + loc.push(`${cursor}["${key}"] = ${JSON.stringify(value)}`); + } + } + }); + } + + _onSetup() { + if (!this.__initialized) { + this._setup(); + } + } + + _setup() { + } + + _onAttach() { + if (!this.__initialized) { + this.__init(); + this.__initialized = true; + } + + this._attach(); + } + + _attach() { + } + + _onDetach() { + this._detach(); + } + + _detach() { + } + + _onEnabled() { + if (!this.__firstEnable) { + this._firstEnable(); + this.__firstEnable = true; + } + + this._enable(); + } + + _firstEnable() { + } + + _enable() { + } + + _onDisabled() { + this._disable(); + } + + _disable() { + } + + _onActive() { + if (!this.__firstActive) { + this._firstActive(); + this.__firstActive = true; + } + + this._active(); + } + + _firstActive() { + } + + _active() { + } + + _onInactive() { + this._inactive(); + } + + _inactive() { + } + + get application() { + return this.stage.application; + } + + __construct() { + this._construct(); + } + + _construct() { + } + + _build() { + } + + __init() { + this._init(); + } + + _init() { + } + + _focus(newTarget, prevTarget) { + } + + _unfocus(newTarget) { + } + + _focusChange(target, newTarget) { + } + + _getFocused() { + // Override to delegate focus to child components. + return this; + } + + _setFocusSettings(settings) { + // Override to add custom settings. See Application._handleFocusSettings(). + } + + _handleFocusSettings(settings) { + // Override to react on custom settings. See Application._handleFocusSettings(). + } + + static _template() { + return {} + } + + hasFinalFocus() { + let path = this.application._focusPath; + return path && path.length && path[path.length - 1] === this; + } + + hasFocus() { + let path = this.application._focusPath; + return path && (path.indexOf(this) >= 0); + } + + get cparent() { + return Component.getParent(this); + } + + seekAncestorByType(type) { + let c = this.cparent; + while(c) { + if (c.constructor === type) { + return c; + } + c = c.cparent; + } + } + + getSharedAncestorComponent(element) { + let ancestor = this.getSharedAncestor(element); + while(ancestor && !ancestor.isComponent) { + ancestor = ancestor.parent; + } + return ancestor; + } + + get signals() { + return this.__signals; + } + + set signals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + this.__signals = v; + } + + set alterSignals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + if (!this.__signals) { + this.__signals = {}; + } + for (let key in v) { + const d = v[key]; + if (d === undefined) { + delete this.__signals[key]; + } else { + this.__signals[key] = v; + } + } + } + + get passSignals() { + return this.__passSignals || {}; + } + + set passSignals(v) { + this.__passSignals = Object.assign(this.__passSignals || {}, v); + } + + set alterPassSignals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + if (!this.__passSignals) { + this.__passSignals = {}; + } + for (let key in v) { + const d = v[key]; + if (d === undefined) { + delete this.__passSignals[key]; + } else { + this.__passSignals[key] = v; + } + } + } + + /** + * Signals the parent of the specified event. + * A parent/ancestor that wishes to handle the signal should set the 'signals' property on this component. + * @param {string} event + * @param {...*} args + */ + signal(event, ...args) { + return this._signal(event, args); + } + + _signal(event, args) { + const signalParent = this._getParentSignalHandler(); + if (signalParent) { + if (this.__signals) { + let fireEvent = this.__signals[event]; + if (fireEvent === false) { + // Ignore event. + return; + } + if (fireEvent) { + if (fireEvent === true) { + fireEvent = event; + } + + if (signalParent._hasMethod(fireEvent)) { + return signalParent[fireEvent](...args); + } + } + } + + let passSignal = (this.__passSignals && this.__passSignals[event]); + if (passSignal) { + // Bubble up. + if (passSignal && passSignal !== true) { + // Replace signal name. + event = passSignal; + } + + return signalParent._signal(event, args); + } + } + } + + _getParentSignalHandler() { + return this.cparent ? this.cparent._getSignalHandler() : null; + } + + _getSignalHandler() { + if (this._signalProxy) { + return this.cparent ? this.cparent._getSignalHandler() : null; + } + return this; + } + + get _signalProxy() { + return false; + } + + fireAncestors(name, ...args) { + if (!name.startsWith('$')) { + throw new Error("Ancestor event name must be prefixed by dollar sign."); + } + + return this._doFireAncestors(name, args); + } + + _doFireAncestors(name, args) { + if (this._hasMethod(name)) { + return this.fire(name, ...args); + } else { + const signalParent = this._getParentSignalHandler(); + if (signalParent) { + return signalParent._doFireAncestors(name, args); + } + } + } + + static collectSubComponents(subs, element) { + if (element.hasChildren()) { + const childList = element.__childList; + for (let i = 0, n = childList.length; i < n; i++) { + const child = childList.getAt(i); + if (child.isComponent) { + subs.push(child); + } else { + Component.collectSubComponents(subs, child); + } + } + } + } + + static getComponent(element) { + let parent = element; + while (parent && !parent.isComponent) { + parent = parent.parent; + } + return parent; + } + + static getParent(element) { + return Component.getComponent(element.parent); + } + +} + +Component.prototype.isComponent = true; + +class CoreQuadList { + + constructor(ctx) { + + this.ctx = ctx; + + this.quadTextures = []; + + this.quadElements = []; + } + + get length() { + return this.quadTextures.length; + } + + reset() { + this.quadTextures = []; + this.quadElements = []; + this.dataLength = 0; + } + + getElement(index) { + return this.quadElements[index]._element; + } + + getElementCore(index) { + return this.quadElements[index]; + } + + getTexture(index) { + return this.quadTextures[index]; + } + + getTextureWidth(index) { + let nativeTexture = this.quadTextures[index]; + if (nativeTexture.w) { + // Render texture; + return nativeTexture.w; + } else { + return this.quadElements[index]._displayedTextureSource.w; + } + } + + getTextureHeight(index) { + let nativeTexture = this.quadTextures[index]; + if (nativeTexture.h) { + // Render texture; + return nativeTexture.h; + } else { + return this.quadElements[index]._displayedTextureSource.h; + } + } + +} + +class WebGLCoreQuadList extends CoreQuadList { + + constructor(ctx) { + super(ctx); + + // Allocate a fairly big chunk of memory that should be enough to support ~100000 (default) quads. + // We do not (want to) handle memory overflow. + const byteSize = ctx.stage.getOption('bufferMemory'); + + this.dataLength = 0; + + this.data = new ArrayBuffer(byteSize); + this.floats = new Float32Array(this.data); + this.uints = new Uint32Array(this.data); + } + + getAttribsDataByteOffset(index) { + // Where this quad can be found in the attribs buffer. + return index * 80; + } + + getQuadContents() { + // Debug: log contents of quad buffer. + let floats = this.floats; + let uints = this.uints; + let lines = []; + for (let i = 1; i <= this.length; i++) { + let str = 'entry ' + i + ': '; + for (let j = 0; j < 4; j++) { + let b = i * 20 + j * 4; + str += floats[b] + ',' + floats[b+1] + ':' + floats[b+2] + ',' + floats[b+3] + '[' + uints[b+4].toString(16) + '] '; + } + lines.push(str); + } + + return lines; + } + + +} + +class CoreQuadOperation { + + constructor(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + + this.ctx = ctx; + this.shader = shader; + this.shaderOwner = shaderOwner; + this.renderTextureInfo = renderTextureInfo; + this.scissor = scissor; + this.index = index; + this.length = 0; + + } + + get quads() { + return this.ctx.renderState.quads; + } + + getTexture(index) { + return this.quads.getTexture(this.index + index); + } + + getElementCore(index) { + return this.quads.getElementCore(this.index + index); + } + + getElement(index) { + return this.quads.getElement(this.index + index); + } + + getElementWidth(index) { + return this.getElement(index).renderWidth; + } + + getElementHeight(index) { + return this.getElement(index).renderHeight; + } + + getTextureWidth(index) { + return this.quads.getTextureWidth(this.index + index); + } + + getTextureHeight(index) { + return this.quads.getTextureHeight(this.index + index); + } + + getRenderWidth() { + if (this.renderTextureInfo) { + return this.renderTextureInfo.w; + } else { + return this.ctx.stage.w; + } + } + + getRenderHeight() { + if (this.renderTextureInfo) { + return this.renderTextureInfo.h; + } else { + return this.ctx.stage.h; + } + } + +} + +class WebGLCoreQuadOperation extends CoreQuadOperation { + + constructor(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + super(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + + this.extraAttribsDataByteOffset = 0; + } + + getAttribsDataByteOffset(index) { + // Where this quad can be found in the attribs buffer. + return this.quads.getAttribsDataByteOffset(this.index + index); + } + + /** + * Returns the relative pixel coordinates in the shader owner to gl position coordinates in the render texture. + * @param x + * @param y + * @return {number[]} + */ + getNormalRenderTextureCoords(x, y) { + let coords = this.shaderOwner.getRenderTextureCoords(x, y); + coords[0] /= this.getRenderWidth(); + coords[1] /= this.getRenderHeight(); + coords[0] = coords[0] * 2 - 1; + coords[1] = 1 - coords[1] * 2; + return coords; + } + + getProjection() { + if (this.renderTextureInfo === null) { + return this.ctx.renderExec._projection; + } else { + return this.renderTextureInfo.nativeTexture.projection; + } + } + +} + +class CoreRenderExecutor { + + constructor(ctx) { + this.ctx = ctx; + + this.renderState = ctx.renderState; + + this.gl = this.ctx.stage.gl; + } + + destroy() { + } + + _reset() { + this._bindRenderTexture(null); + this._setScissor(null); + this._clearRenderTexture(); + } + + execute() { + this._reset(); + + let qops = this.renderState.quadOperations; + + let i = 0, n = qops.length; + while (i < n) { + this._processQuadOperation(qops[i]); + i++; + } + } + + _processQuadOperation(quadOperation) { + if (quadOperation.renderTextureInfo && quadOperation.renderTextureInfo.ignore) { + // Ignore quad operations when we are 're-using' another texture as the render texture result. + return; + } + + this._setupQuadOperation(quadOperation); + this._execQuadOperation(quadOperation); + + } + + _setupQuadOperation(quadOperation) { + } + + _execQuadOperation(op) { + // Set render texture. + let nativeTexture = op.renderTextureInfo ? op.renderTextureInfo.nativeTexture : null; + + if (this._renderTexture !== nativeTexture) { + this._bindRenderTexture(nativeTexture); + } + + if (op.renderTextureInfo && !op.renderTextureInfo.cleared) { + this._setScissor(null); + this._clearRenderTexture(); + op.renderTextureInfo.cleared = true; + this._setScissor(op.scissor); + } else { + this._setScissor(op.scissor); + } + + this._renderQuadOperation(op); + } + + _renderQuadOperation(op) { + } + + _bindRenderTexture(renderTexture) { + this._renderTexture = renderTexture; + } + + _clearRenderTexture(renderTexture) { + } + + _setScissor(area) { + } + +} + +class WebGLCoreRenderExecutor extends CoreRenderExecutor { + + constructor(ctx) { + super(ctx); + + this.gl = this.ctx.stage.gl; + + this.init(); + } + + init() { + let gl = this.gl; + + // Create new sharable buffer for params. + this._attribsBuffer = gl.createBuffer(); + + let maxQuads = Math.floor(this.renderState.quads.data.byteLength / 80); + + // Init webgl arrays. + let allIndices = new Uint16Array(maxQuads * 6); + + // fill the indices with the quads to draw. + for (let i = 0, j = 0; i < maxQuads; i += 6, j += 4) { + allIndices[i] = j; + allIndices[i + 1] = j + 1; + allIndices[i + 2] = j + 2; + allIndices[i + 3] = j; + allIndices[i + 4] = j + 2; + allIndices[i + 5] = j + 3; + } + + // The quads buffer can be (re)used to draw a range of quads. + this._quadsBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._quadsBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, allIndices, gl.STATIC_DRAW); + + // The matrix that causes the [0,0 - W,H] box to map to [-1,-1 - 1,1] in the end results. + this._projection = new Float32Array([2/this.ctx.stage.coordsWidth, -2/this.ctx.stage.coordsHeight]); + + } + + destroy() { + super.destroy(); + this.gl.deleteBuffer(this._attribsBuffer); + this.gl.deleteBuffer(this._quadsBuffer); + } + + _reset() { + super._reset(); + + let gl = this.gl; + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.enable(gl.BLEND); + gl.disable(gl.DEPTH_TEST); + + this._stopShaderProgram(); + this._setupBuffers(); + } + + _setupBuffers() { + let gl = this.gl; + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._quadsBuffer); + let element = new Float32Array(this.renderState.quads.data, 0, this.renderState.quads.dataLength); + gl.bindBuffer(gl.ARRAY_BUFFER, this._attribsBuffer); + gl.bufferData(gl.ARRAY_BUFFER, element, gl.DYNAMIC_DRAW); + } + + _setupQuadOperation(quadOperation) { + super._setupQuadOperation(quadOperation); + this._useShaderProgram(quadOperation.shader, quadOperation); + } + + _renderQuadOperation(op) { + let shader = op.shader; + + if (op.length || op.shader.addEmpty()) { + shader.beforeDraw(op); + shader.draw(op); + shader.afterDraw(op); + } + } + + /** + * @param {WebGLShader} shader; + * @param {CoreQuadOperation} operation; + */ + _useShaderProgram(shader, operation) { + if (!shader.hasSameProgram(this._currentShaderProgram)) { + if (this._currentShaderProgram) { + this._currentShaderProgram.stopProgram(); + } + shader.useProgram(); + this._currentShaderProgram = shader; + } + shader.setupUniforms(operation); + } + + _stopShaderProgram() { + if (this._currentShaderProgram) { + // The currently used shader program should be stopped gracefully. + this._currentShaderProgram.stopProgram(); + this._currentShaderProgram = null; + } + } + + _bindRenderTexture(renderTexture) { + super._bindRenderTexture(renderTexture); + + let gl = this.gl; + if (!this._renderTexture) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0,0,this.ctx.stage.w,this.ctx.stage.h); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderTexture.framebuffer); + gl.viewport(0,0,this._renderTexture.w, this._renderTexture.h); + } + } + + _clearRenderTexture() { + super._clearRenderTexture(); + let gl = this.gl; + if (!this._renderTexture) { + let glClearColor = this.ctx.stage.getClearColor(); + if (glClearColor) { + gl.clearColor(glClearColor[0] * glClearColor[3], glClearColor[1] * glClearColor[3], glClearColor[2] * glClearColor[3], glClearColor[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } else { + // Clear texture. + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } + + _setScissor(area) { + super._setScissor(area); + + if (this._scissor === area) { + return; + } + this._scissor = area; + + let gl = this.gl; + if (!area) { + gl.disable(gl.SCISSOR_TEST); + } else { + gl.enable(gl.SCISSOR_TEST); + let precision = this.ctx.stage.getRenderPrecision(); + let y = area[1]; + if (this._renderTexture === null) { + // Flip. + y = (this.ctx.stage.h / precision - (area[1] + area[3])); + } + gl.scissor(Math.round(area[0] * precision), Math.round(y * precision), Math.round(area[2] * precision), Math.round(area[3] * precision)); + } + } + +} + +class CoreRenderState { + + constructor(ctx) { + this.ctx = ctx; + + this.stage = ctx.stage; + + this.defaultShader = this.stage.renderer.getDefaultShader(ctx); + + this.renderer = ctx.stage.renderer; + + this.quads = this.renderer.createCoreQuadList(ctx); + + } + + reset() { + this._renderTextureInfo = null; + + this._scissor = null; + + this._shader = null; + + this._shaderOwner = null; + + this._realShader = null; + + this._check = false; + + this.quadOperations = []; + + this._texturizer = null; + + this._texturizerTemporary = false; + + this._quadOperation = null; + + this.quads.reset(); + + this._temporaryTexturizers = []; + + this._isCachingTexturizer = false; + + } + + get length() { + return this.quads.quadTextures.length; + } + + setShader(shader, owner) { + if (this._shaderOwner !== owner || this._realShader !== shader) { + // Same shader owner: active shader is also the same. + // Prevent any shader usage to save performance. + + this._realShader = shader; + + if (shader.useDefault()) { + // Use the default shader when possible to prevent unnecessary program changes. + shader = this.defaultShader; + } + if (this._shader !== shader || this._shaderOwner !== owner) { + this._shader = shader; + this._shaderOwner = owner; + this._check = true; + } + } + } + + get renderTextureInfo() { + return this._renderTextureInfo; + } + + setScissor(area) { + if (this._scissor !== area) { + if (area) { + this._scissor = area; + } else { + this._scissor = null; + } + this._check = true; + } + } + + getScissor() { + return this._scissor; + } + + setRenderTextureInfo(renderTextureInfo) { + if (this._renderTextureInfo !== renderTextureInfo) { + this._renderTextureInfo = renderTextureInfo; + this._scissor = null; + this._check = true; + } + } + + /** + * Sets the texturizer to be drawn during subsequent addQuads. + * @param {ElementTexturizer} texturizer + */ + setTexturizer(texturizer, cache = false) { + this._texturizer = texturizer; + this._cacheTexturizer = cache; + } + + set isCachingTexturizer(v) { + this._isCachingTexturizer = v; + } + + get isCachingTexturizer() { + return this._isCachingTexturizer; + } + + addQuad(elementCore) { + if (!this._quadOperation) { + this._createQuadOperation(); + } else if (this._check && this._hasChanges()) { + this._finishQuadOperation(); + this._check = false; + } + + let nativeTexture = null; + if (this._texturizer) { + nativeTexture = this._texturizer.getResultTexture(); + + if (!this._cacheTexturizer) { + // We can release the temporary texture immediately after finalizing this quad operation. + this._temporaryTexturizers.push(this._texturizer); + } + } + + if (!nativeTexture) { + nativeTexture = elementCore._displayedTextureSource.nativeTexture; + } + + if (this._renderTextureInfo) { + if (this._shader === this.defaultShader && this._renderTextureInfo.empty) { + // The texture might be reusable under some conditions. We will check them in ElementCore.renderer. + this._renderTextureInfo.nativeTexture = nativeTexture; + this._renderTextureInfo.offset = this.length; + } else { + // It is not possible to reuse another texture when there is more than one quad. + this._renderTextureInfo.nativeTexture = null; + } + this._renderTextureInfo.empty = false; + } + + this.quads.quadTextures.push(nativeTexture); + this.quads.quadElements.push(elementCore); + + this._quadOperation.length++; + + this.renderer.addQuad(this, this.quads, this.length - 1); + } + + finishedRenderTexture() { + if (this._renderTextureInfo.nativeTexture) { + // There was only one texture drawn in this render texture. + // Check if we can reuse it so that we can optimize out an unnecessary render texture operation. + // (it should exactly span this render texture). + if (!this._isRenderTextureReusable()) { + this._renderTextureInfo.nativeTexture = null; + } + } + } + + _isRenderTextureReusable() { + const offset = this._renderTextureInfo.offset; + return (this.quads.quadTextures[offset].w === this._renderTextureInfo.w) && + (this.quads.quadTextures[offset].h === this._renderTextureInfo.h) && + this.renderer.isRenderTextureReusable(this, this._renderTextureInfo) + } + + _hasChanges() { + let q = this._quadOperation; + if (this._shader !== q.shader) return true; + if (this._shaderOwner !== q.shaderOwner) return true; + if (this._renderTextureInfo !== q.renderTextureInfo) return true; + if (this._scissor !== q.scissor) { + if ((this._scissor[0] !== q.scissor[0]) || (this._scissor[1] !== q.scissor[1]) || (this._scissor[2] !== q.scissor[2]) || (this._scissor[3] !== q.scissor[3])) { + return true; + } + } + + return false; + } + + _finishQuadOperation(create = true) { + if (this._quadOperation) { + if (this._quadOperation.length || this._shader.addEmpty()) { + if (!this._quadOperation.scissor || ((this._quadOperation.scissor[2] > 0) && (this._quadOperation.scissor[3] > 0))) { + // Ignore empty clipping regions. + this.quadOperations.push(this._quadOperation); + } + } + + if (this._temporaryTexturizers.length) { + for (let i = 0, n = this._temporaryTexturizers.length; i < n; i++) { + // We can now reuse these render-to-textures in subsequent stages. + // Huge performance benefit when filtering (fast blur). + this._temporaryTexturizers[i].releaseRenderTexture(); + } + this._temporaryTexturizers = []; + } + + this._quadOperation = null; + } + + if (create) { + this._createQuadOperation(); + } + } + + _createQuadOperation() { + this._quadOperation = this.renderer.createCoreQuadOperation( + this.ctx, + this._shader, + this._shaderOwner, + this._renderTextureInfo, + this._scissor, + this.length + ); + this._check = false; + } + + finish() { + if (this._quadOperation) { + // Add remaining. + this._finishQuadOperation(false); + } + + this.renderer.finishRenderState(this); + } + +} + +/** + * Base functionality for shader setup/destroy. + */ +class WebGLShaderProgram { + + constructor(vertexShaderSource, fragmentShaderSource) { + + this.vertexShaderSource = vertexShaderSource; + this.fragmentShaderSource = fragmentShaderSource; + + this._program = null; + + this._uniformLocations = new Map(); + this._attributeLocations = new Map(); + + this._currentUniformValues = {}; + } + + compile(gl) { + if (this._program) return; + + this.gl = gl; + + this._program = gl.createProgram(); + + let glVertShader = this._glCompile(gl.VERTEX_SHADER, this.vertexShaderSource); + let glFragShader = this._glCompile(gl.FRAGMENT_SHADER, this.fragmentShaderSource); + + gl.attachShader(this._program, glVertShader); + gl.attachShader(this._program, glFragShader); + gl.linkProgram(this._program); + + // if linking fails, then log and cleanup + if (!gl.getProgramParameter(this._program, gl.LINK_STATUS)) { + console.error('Error: Could not initialize shader.'); + console.error('gl.VALIDATE_STATUS', gl.getProgramParameter(this._program, gl.VALIDATE_STATUS)); + console.error('gl.getError()', gl.getError()); + + // if there is a program info log, log it + if (gl.getProgramInfoLog(this._program) !== '') { + console.warn('Warning: gl.getProgramInfoLog()', gl.getProgramInfoLog(this._program)); + } + + gl.deleteProgram(this._program); + this._program = null; + } + + // clean up some shaders + gl.deleteShader(glVertShader); + gl.deleteShader(glFragShader); + } + + _glCompile(type, src) { + let shader = this.gl.createShader(type); + + this.gl.shaderSource(shader, src); + this.gl.compileShader(shader); + + if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { + console.log(this.constructor.name, 'Type: ' + (type === this.gl.VERTEX_SHADER ? 'vertex shader' : 'fragment shader') ); + console.log(this.gl.getShaderInfoLog(shader)); + let idx = 0; + console.log("========== source ==========\n" + src.split("\n").map(line => "" + (++idx) + ": " + line).join("\n")); + return null; + } + + return shader; + } + + getUniformLocation(name) { + let location = this._uniformLocations.get(name); + if (location === undefined) { + location = this.gl.getUniformLocation(this._program, name); + this._uniformLocations.set(name, location); + } + + return location; + } + + getAttribLocation(name) { + let location = this._attributeLocations.get(name); + if (location === undefined) { + location = this.gl.getAttribLocation(this._program, name); + this._attributeLocations.set(name, location); + } + + return location; + } + + destroy() { + if (this._program) { + this.gl.deleteProgram(this._program); + this._program = null; + } + } + + get glProgram() { + return this._program; + } + + get compiled() { + return !!this._program; + } + + _valueEquals(v1, v2) { + // Uniform value is either a typed array or a numeric value. + if (v1.length && v2.length) { + for (let i = 0, n = v1.length; i < n; i++) { + if (v1[i] !== v2[i]) return false; + } + return true; + } else { + return (v1 === v2); + } + } + + _valueClone(v) { + if (v.length) { + return v.slice(0); + } else { + return v; + } + } + + setUniformValue(name, value, glFunction) { + let v = this._currentUniformValues[name]; + if (v === undefined || !this._valueEquals(v, value)) { + let clonedValue = this._valueClone(value); + this._currentUniformValues[name] = clonedValue; + + let loc = this.getUniformLocation(name); + if (loc) { + let isMatrix = (glFunction === this.gl.uniformMatrix2fv || glFunction === this.gl.uniformMatrix3fv || glFunction === this.gl.uniformMatrix4fv); + if (isMatrix) { + glFunction.call(this.gl, loc, false, clonedValue); + } else { + glFunction.call(this.gl, loc, clonedValue); + } + } + } + } + +} + +class WebGLShader extends Shader { + + constructor(ctx) { + super(ctx); + + const stage = ctx.stage; + + this._program = stage.renderer.shaderPrograms.get(this.constructor); + if (!this._program) { + this._program = new WebGLShaderProgram(this.constructor.vertexShaderSource, this.constructor.fragmentShaderSource); + + // Let the vbo context perform garbage collection. + stage.renderer.shaderPrograms.set(this.constructor, this._program); + } + + this.gl = stage.gl; + } + + get glProgram() { + return this._program.glProgram; + } + + _init() { + if (!this._initialized) { + this.initialize(); + this._initialized = true; + } + } + + initialize() { + this._program.compile(this.gl); + } + + get initialized() { + return this._initialized; + } + + _uniform(name) { + return this._program.getUniformLocation(name); + } + + _attrib(name) { + return this._program.getAttribLocation(name); + } + + _setUniform(name, value, glFunction) { + this._program.setUniformValue(name, value, glFunction); + } + + useProgram() { + this._init(); + this.gl.useProgram(this.glProgram); + this.beforeUsage(); + this.enableAttribs(); + } + + stopProgram() { + this.afterUsage(); + this.disableAttribs(); + } + + hasSameProgram(other) { + // For performance reasons, we first check for identical references. + return (other && ((other === this) || (other._program === this._program))); + } + + beforeUsage() { + // Override to set settings other than the default settings (blend mode etc). + } + + afterUsage() { + // All settings changed in beforeUsage should be reset here. + } + + enableAttribs() { + + } + + disableAttribs() { + + } + + getExtraAttribBytesPerVertex() { + return 0; + } + + getVertexAttribPointerOffset(operation) { + return operation.extraAttribsDataByteOffset - (operation.index + 1) * 4 * this.getExtraAttribBytesPerVertex(); + } + + setExtraAttribsInBuffer(operation) { + // Set extra attrib data in in operation.quads.data/floats/uints, starting from + // operation.extraAttribsBufferByteOffset. + } + + setupUniforms(operation) { + // Set all shader-specific uniforms. + // Notice that all uniforms should be set, even if they have not been changed within this shader instance. + // The uniforms are shared by all shaders that have the same type (and shader program). + } + + _getProjection(operation) { + return operation.getProjection(); + } + + getFlipY(operation) { + return this._getProjection(operation)[1] < 0; + } + + beforeDraw(operation) { + } + + draw(operation) { + } + + afterDraw(operation) { + } + + cleanup() { + this._initialized = false; + // Program takes little resources, so it is only destroyed when the full stage is destroyed. + } + +} + +class DefaultShader extends WebGLShader { + + enableAttribs() { + // Enables the attribs in the shader program. + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aVertexPosition"), 2, gl.FLOAT, false, 20, 0); + gl.enableVertexAttribArray(this._attrib("aVertexPosition")); + + if (this._attrib("aTextureCoord") !== -1) { + gl.vertexAttribPointer(this._attrib("aTextureCoord"), 2, gl.FLOAT, false, 20, 2 * 4); + gl.enableVertexAttribArray(this._attrib("aTextureCoord")); + } + + if (this._attrib("aColor") !== -1) { + // Some shaders may ignore the color. + gl.vertexAttribPointer(this._attrib("aColor"), 4, gl.UNSIGNED_BYTE, true, 20, 4 * 4); + gl.enableVertexAttribArray(this._attrib("aColor")); + } + } + + disableAttribs() { + // Disables the attribs in the shader program. + let gl = this.gl; + gl.disableVertexAttribArray(this._attrib("aVertexPosition")); + + if (this._attrib("aTextureCoord") !== -1) { + gl.disableVertexAttribArray(this._attrib("aTextureCoord")); + } + + if (this._attrib("aColor") !== -1) { + gl.disableVertexAttribArray(this._attrib("aColor")); + } + } + + setupUniforms(operation) { + this._setUniform("projection", this._getProjection(operation), this.gl.uniform2fv, false); + } + + draw(operation) { + let gl = this.gl; + + let length = operation.length; + + if (length) { + let glTexture = operation.getTexture(0); + let pos = 0; + for (let i = 0; i < length; i++) { + let tx = operation.getTexture(i); + if (glTexture !== tx) { + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.drawElements(gl.TRIANGLES, 6 * (i - pos), gl.UNSIGNED_SHORT, (pos + operation.index) * 6 * 2); + glTexture = tx; + pos = i; + } + } + if (pos < length) { + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.drawElements(gl.TRIANGLES, 6 * (length - pos), gl.UNSIGNED_SHORT, (pos + operation.index) * 6 * 2); + } + } + } + +} + +DefaultShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +DefaultShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor; + } +`; + +class Renderer { + + constructor(stage) { + this.stage = stage; + this._defaultShader = undefined; + } + + gc(aggressive) { + } + + destroy() { + } + + getDefaultShader(ctx = this.stage.ctx) { + if (!this._defaultShader) { + this._defaultShader = this._createDefaultShader(ctx); + } + return this._defaultShader; + } + + _createDefaultShader(ctx) { + } + + isValidShaderType(shaderType) { + return (shaderType.prototype instanceof this._getShaderBaseType()); + } + + createShader(ctx, settings) { + const shaderType = settings.type; + // If shader type is not correct, use a different platform. + if (!this.isValidShaderType(shaderType)) { + const convertedShaderType = this._getShaderAlternative(shaderType); + if (!convertedShaderType) { + console.warn("Shader has no implementation for render target: " + shaderType.name); + return this._createDefaultShader(ctx); + } + return new convertedShaderType(ctx); + } else { + const shader = new shaderType(ctx); + Base.patchObject(this, settings); + return shader; + } + } + + _getShaderBaseType() { + } + + _getShaderAlternative(shaderType) { + return this.getDefaultShader(); + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + console.warn('copyRenderTexture not supported by renderer'); + } +} + +class WebGLRenderer extends Renderer { + + constructor(stage) { + super(stage); + this.shaderPrograms = new Map(); + } + + destroy() { + this.shaderPrograms.forEach(shaderProgram => shaderProgram.destroy()); + } + + _createDefaultShader(ctx) { + return new DefaultShader(ctx); + } + + _getShaderBaseType() { + return WebGLShader + } + + _getShaderAlternative(shaderType) { + return shaderType.getWebGL && shaderType.getWebGL(); + } + + createCoreQuadList(ctx) { + return new WebGLCoreQuadList(ctx); + } + + createCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + return new WebGLCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + } + + createCoreRenderExecutor(ctx) { + return new WebGLCoreRenderExecutor(ctx); + } + + createCoreRenderState(ctx) { + return new CoreRenderState(ctx); + } + + createRenderTexture(w, h, pw, ph) { + const gl = this.stage.gl; + const glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, pw, ph, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + glTexture.params = {}; + glTexture.params[gl.TEXTURE_MAG_FILTER] = gl.LINEAR; + glTexture.params[gl.TEXTURE_MIN_FILTER] = gl.LINEAR; + glTexture.params[gl.TEXTURE_WRAP_S] = gl.CLAMP_TO_EDGE; + glTexture.params[gl.TEXTURE_WRAP_T] = gl.CLAMP_TO_EDGE; + glTexture.options = {format: gl.RGBA, internalFormat: gl.RGBA, type: gl.UNSIGNED_BYTE}; + + // We need a specific framebuffer for every render texture. + glTexture.framebuffer = gl.createFramebuffer(); + glTexture.projection = new Float32Array([2/w, 2/h]); + + gl.bindFramebuffer(gl.FRAMEBUFFER, glTexture.framebuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, glTexture, 0); + + return glTexture; + } + + freeRenderTexture(glTexture) { + let gl = this.stage.gl; + gl.deleteFramebuffer(glTexture.framebuffer); + gl.deleteTexture(glTexture); + } + + uploadTextureSource(textureSource, options) { + const gl = this.stage.gl; + + const source = options.source; + + const format = { + premultiplyAlpha: true, + hasAlpha: true + }; + + if (options && options.hasOwnProperty('premultiplyAlpha')) { + format.premultiplyAlpha = options.premultiplyAlpha; + } + + if (options && options.hasOwnProperty('flipBlueRed')) { + format.flipBlueRed = options.flipBlueRed; + } + + if (options && options.hasOwnProperty('hasAlpha')) { + format.hasAlpha = options.hasAlpha; + } + + if (!format.hasAlpha) { + format.premultiplyAlpha = false; + } + + format.texParams = options.texParams || {}; + format.texOptions = options.texOptions || {}; + + let glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, format.premultiplyAlpha); + + if (Utils.isNode) { + gl.pixelStorei(gl.UNPACK_FLIP_BLUE_RED, !!format.flipBlueRed); + } + + const texParams = format.texParams; + if (!texParams[gl.TEXTURE_MAG_FILTER]) texParams[gl.TEXTURE_MAG_FILTER] = gl.LINEAR; + if (!texParams[gl.TEXTURE_MIN_FILTER]) texParams[gl.TEXTURE_MIN_FILTER] = gl.LINEAR; + if (!texParams[gl.TEXTURE_WRAP_S]) texParams[gl.TEXTURE_WRAP_S] = gl.CLAMP_TO_EDGE; + if (!texParams[gl.TEXTURE_WRAP_T]) texParams[gl.TEXTURE_WRAP_T] = gl.CLAMP_TO_EDGE; + + Object.keys(texParams).forEach(key => { + const value = texParams[key]; + gl.texParameteri(gl.TEXTURE_2D, parseInt(key), value); + }); + + const texOptions = format.texOptions; + texOptions.format = texOptions.format || (format.hasAlpha ? gl.RGBA : gl.RGB); + texOptions.type = texOptions.type || gl.UNSIGNED_BYTE; + texOptions.internalFormat = texOptions.internalFormat || texOptions.format; + + this.stage.platform.uploadGlTexture(gl, textureSource, source, texOptions); + + glTexture.params = Utils.cloneObjShallow(texParams); + glTexture.options = Utils.cloneObjShallow(texOptions); + + return glTexture; + } + + freeTextureSource(textureSource) { + this.stage.gl.deleteTexture(textureSource.nativeTexture); + } + + addQuad(renderState, quads, index) { + let offset = (index * 20); + const elementCore = quads.quadElements[index]; + + let r = elementCore._renderContext; + + let floats = renderState.quads.floats; + let uints = renderState.quads.uints; + const mca = StageUtils.mergeColorAlpha; + + if (r.tb !== 0 || r.tc !== 0) { + floats[offset++] = r.px; + floats[offset++] = r.py; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUl, r.alpha); + floats[offset++] = r.px + elementCore._w * r.ta; + floats[offset++] = r.py + elementCore._w * r.tc; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUr, r.alpha); + floats[offset++] = r.px + elementCore._w * r.ta + elementCore._h * r.tb; + floats[offset++] = r.py + elementCore._w * r.tc + elementCore._h * r.td; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._bry; + uints[offset++] = mca(elementCore._colorBr, r.alpha); + floats[offset++] = r.px + elementCore._h * r.tb; + floats[offset++] = r.py + elementCore._h * r.td; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._bry; + uints[offset] = mca(elementCore._colorBl, r.alpha); + } else { + // Simple. + let cx = r.px + elementCore._w * r.ta; + let cy = r.py + elementCore._h * r.td; + + floats[offset++] = r.px; + floats[offset++] = r.py; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUl, r.alpha); + floats[offset++] = cx; + floats[offset++] = r.py; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUr, r.alpha); + floats[offset++] = cx; + floats[offset++] = cy; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._bry; + uints[offset++] = mca(elementCore._colorBr, r.alpha); + floats[offset++] = r.px; + floats[offset++] = cy; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._bry; + uints[offset] = mca(elementCore._colorBl, r.alpha); + } + } + + isRenderTextureReusable(renderState, renderTextureInfo) { + let offset = (renderState._renderTextureInfo.offset * 80) / 4; + let floats = renderState.quads.floats; + let uints = renderState.quads.uints; + return ((floats[offset] === 0) && + (floats[offset + 1] === 0) && + (floats[offset + 2] === 0) && + (floats[offset + 3] === 0) && + (uints[offset + 4] === 0xFFFFFFFF) && + (floats[offset + 5] === renderTextureInfo.w) && + (floats[offset + 6] === 0) && + (floats[offset + 7] === 1) && + (floats[offset + 8] === 0) && + (uints[offset + 9] === 0xFFFFFFFF) && + (floats[offset + 10] === renderTextureInfo.w) && + (floats[offset + 11] === renderTextureInfo.h) && + (floats[offset + 12] === 1) && + (floats[offset + 13] === 1) && + (uints[offset + 14] === 0xFFFFFFFF) && + (floats[offset + 15] === 0) && + (floats[offset + 16] === renderTextureInfo.h) && + (floats[offset + 17] === 0) && + (floats[offset + 18] === 1) && + (uints[offset + 19] === 0xFFFFFFFF)); + } + + finishRenderState(renderState) { + // Set extra shader attribute data. + let offset = renderState.length * 80; + for (let i = 0, n = renderState.quadOperations.length; i < n; i++) { + renderState.quadOperations[i].extraAttribsDataByteOffset = offset; + let extra = renderState.quadOperations[i].shader.getExtraAttribBytesPerVertex() * 4 * renderState.quadOperations[i].length; + offset += extra; + if (extra) { + renderState.quadOperations[i].shader.setExtraAttribsInBuffer(renderState.quadOperations[i], renderState.quads); + } + } + renderState.quads.dataLength = offset; + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + const gl = this.stage.gl; + gl.bindTexture(gl.TEXTURE_2D, nativeTexture); + gl.bindFramebuffer(gl.FRAMEBUFFER, renderTexture.framebuffer); + const precision = renderTexture.precision; + gl.copyTexSubImage2D( + gl.TEXTURE_2D, + 0, + precision * (options.sx || 0), + precision * (options.sy || 0), + precision * (options.x || 0), + precision * (options.y || 0), + precision * (options.w || renderTexture.ow), + precision * (options.h || renderTexture.oh)); + } + +} + +class C2dCoreQuadList extends CoreQuadList { + + constructor(ctx) { + super(ctx); + + this.renderContexts = []; + this.modes = []; + } + + setRenderContext(index, v) { + this.renderContexts[index] = v; + } + + setSimpleTc(index, v) { + if (v) { + this.modes[index] |= 1; + } else { + this.modes[index] -= (this.modes[index] & 1); + } + } + + setWhite(index, v) { + if (v) { + this.modes[index] |= 2; + } else { + this.modes[index] -= (this.modes[index] & 2); + } + } + + getRenderContext(index) { + return this.renderContexts[index]; + } + + getSimpleTc(index) { + return (this.modes[index] & 1); + } + + getWhite(index) { + return (this.modes[index] & 2); + } + +} + +class C2dCoreQuadOperation extends CoreQuadOperation { + + getRenderContext(index) { + return this.quads.getRenderContext(this.index + index); + } + + getSimpleTc(index) { + return this.quads.getSimpleTc(this.index + index); + } + + getWhite(index) { + return this.quads.getWhite(this.index + index); + } + +} + +class C2dCoreRenderExecutor extends CoreRenderExecutor { + + init() { + this._mainRenderTexture = this.ctx.stage.getCanvas(); + } + + _renderQuadOperation(op) { + let shader = op.shader; + + if (op.length || op.shader.addEmpty()) { + const target = this._renderTexture || this._mainRenderTexture; + shader.beforeDraw(op, target); + shader.draw(op, target); + shader.afterDraw(op, target); + } + } + + _clearRenderTexture() { + const ctx = this._getContext(); + + let clearColor = [0, 0, 0, 0]; + if (this._mainRenderTexture.ctx === ctx) { + clearColor = this.ctx.stage.getClearColor(); + } + + const renderTexture = ctx.canvas; + ctx.setTransform(1, 0, 0, 1, 0, 0); + if (!clearColor[0] && !clearColor[1] && !clearColor[2] && !clearColor[3]) { + ctx.clearRect(0, 0, renderTexture.width, renderTexture.height); + } else { + ctx.fillStyle = StageUtils.getRgbaStringFromArray(clearColor); + // Do not use fillRect because it produces artifacts. + ctx.globalCompositeOperation = 'copy'; + ctx.beginPath(); + ctx.rect(0, 0, renderTexture.width, renderTexture.height); + ctx.closePath(); + ctx.fill(); + ctx.globalCompositeOperation = 'source-over'; + } + } + + _getContext() { + if (this._renderTexture) { + return this._renderTexture.ctx; + } else { + return this._mainRenderTexture.ctx; + } + } + + _restoreContext() { + const ctx = this._getContext(); + ctx.restore(); + ctx.save(); + ctx._scissor = null; + } + + _setScissor(area) { + const ctx = this._getContext(); + + if (!C2dCoreRenderExecutor._equalScissorAreas(ctx.canvas, ctx._scissor, area)) { + // Clipping is stored in the canvas context state. + // We can't reset clipping alone so we need to restore the full context. + this._restoreContext(); + + let precision = this.ctx.stage.getRenderPrecision(); + if (area) { + ctx.beginPath(); + ctx.rect(Math.round(area[0] * precision), Math.round(area[1] * precision), Math.round(area[2] * precision), Math.round(area[3] * precision)); + ctx.closePath(); + ctx.clip(); + } + ctx._scissor = area; + } + } + + static _equalScissorAreas(canvas, area, current) { + if (!area) { + area = [0, 0, canvas.width, canvas.height]; + } + if (!current) { + current = [0, 0, canvas.width, canvas.height]; + } + return Utils.equalValues(area, current) + } + +} + +class C2dShader extends Shader { + + beforeDraw(operation) { + } + + draw(operation) { + } + + afterDraw(operation) { + } + +} + +class DefaultShader$1 extends C2dShader { + + constructor(ctx) { + super(ctx); + this._rectangleTexture = ctx.stage.rectangleTexture.source.nativeTexture; + this._tintManager = this.ctx.stage.renderer.tintManager; + } + + draw(operation, target) { + const ctx = target.ctx; + let length = operation.length; + for (let i = 0; i < length; i++) { + const tx = operation.getTexture(i); + const vc = operation.getElementCore(i); + const rc = operation.getRenderContext(i); + const white = operation.getWhite(i); + const stc = operation.getSimpleTc(i); + + //@todo: try to optimize out per-draw transform setting. split translate, transform. + const precision = this.ctx.stage.getRenderPrecision(); + ctx.setTransform(rc.ta * precision, rc.tc * precision, rc.tb * precision, rc.td * precision, rc.px * precision, rc.py * precision); + + const rect = (tx === this._rectangleTexture); + const info = {operation, target, index: i, rect}; + + if (rect) { + // Check for gradient. + if (white) { + ctx.fillStyle = 'white'; + } else { + this._setColorGradient(ctx, vc); + } + + ctx.globalAlpha = rc.alpha; + this._beforeDrawEl(info); + ctx.fillRect(0, 0, vc.w, vc.h); + this._afterDrawEl(info); + ctx.globalAlpha = 1.0; + } else { + // @todo: set image smoothing based on the texture. + + // @todo: optimize by registering whether identity texcoords are used. + ctx.globalAlpha = rc.alpha; + this._beforeDrawEl(info); + + // @todo: test if rounding yields better performance. + + // Notice that simple texture coords can be turned on even though vc._ulx etc are not simple, because + // we are rendering a render-to-texture (texcoords were stashed). Same is true for 'white' color btw. + const sourceX = stc ? 0 : (vc._ulx * tx.w); + const sourceY = stc ? 0 : (vc._uly * tx.h); + const sourceW = (stc ? 1 : (vc._brx - vc._ulx)) * tx.w; + const sourceH = (stc ? 1 : (vc._bry - vc._uly)) * tx.h; + + let colorize = !white; + if (colorize) { + // @todo: cache the tint texture for better performance. + + // Draw to intermediate texture with background color/gradient. + // This prevents us from having to create a lot of render texture canvases. + + // Notice that we don't support (non-rect) gradients, only color tinting for c2d. We'll just take the average color. + let color = vc._colorUl; + if (vc._colorUl !== vc._colorUr || vc._colorUr !== vc._colorBl || vc._colorBr !== vc._colorBl) { + color = StageUtils.mergeMultiColorsEqual([vc._colorUl, vc._colorUr, vc._colorBl, vc._colorBr]); + } + + const alpha = ((color / 16777216) | 0) / 255.0; + ctx.globalAlpha *= alpha; + + const rgb = color & 0x00FFFFFF; + const tintTexture = this._tintManager.getTintTexture(tx, rgb); + + // Actually draw result. + ctx.fillStyle = 'white'; + ctx.drawImage(tintTexture, sourceX, sourceY, sourceW, sourceH, 0, 0, vc.w, vc.h); + } else { + ctx.fillStyle = 'white'; + ctx.drawImage(tx, sourceX, sourceY, sourceW, sourceH, 0, 0, vc.w, vc.h); + } + this._afterDrawEl(info); + ctx.globalAlpha = 1.0; + } + } + } + + _setColorGradient(ctx, vc, w = vc.w, h = vc.h, transparency = true) { + let color = vc._colorUl; + let gradient; + //@todo: quick single color check. + //@todo: cache gradient/fill style (if possible, probably context-specific). + + if (vc._colorUl === vc._colorUr) { + if (vc._colorBl === vc._colorBr) { + if (vc._colorUl === vc.colorBl) ; else { + // Vertical gradient. + gradient = ctx.createLinearGradient(0, 0, 0, h); + if (transparency) { + gradient.addColorStop(0, StageUtils.getRgbaString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbaString(vc._colorBl)); + } else { + gradient.addColorStop(0, StageUtils.getRgbString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbString(vc._colorBl)); + + } + } + } + } else { + if (vc._colorUl === vc._colorBl && vc._colorUr === vc._colorBr) { + // Horizontal gradient. + gradient = ctx.createLinearGradient(0, 0, w, 0); + if (transparency) { + gradient.addColorStop(0, StageUtils.getRgbaString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbaString(vc._colorBr)); + } else { + gradient.addColorStop(0, StageUtils.getRgbString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbString(vc._colorBr)); + } + } + } + + if (gradient) { + ctx.fillStyle = gradient; + } else { + ctx.fillStyle = transparency ? StageUtils.getRgbaString(color) : StageUtils.getRgbString(color); + } + } + + _beforeDrawEl(info) { + } + + _afterDrawEl(info) { + } + +} + +class C2dTextureTintManager { + + constructor(stage) { + this.stage = stage; + this._usedMemory = 0; + this._cachedNativeTextures = new Set(); + } + + destroy() { + this.gc(true); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + + this.stage.addMemoryUsage(delta); + } + + delete(nativeTexture) { + // Should be called when native texture is cleaned up. + if (this._hasCache(nativeTexture)) { + const cache = this._getCache(nativeTexture); + const prevMemUsage = cache.memoryUsage; + cache.clear(); + this._cachedNativeTextures.delete(nativeTexture); + this._addMemoryUsage(cache.memoryUsage - prevMemUsage); + } + } + + getTintTexture(nativeTexture, color) { + const frame = this.stage.frameCounter; + + this._cachedNativeTextures.add(nativeTexture); + + const cache = this._getCache(nativeTexture); + + const item = cache.get(color); + item.lf = frame; + + if (item.tx) { + if (nativeTexture.update > item.u) { + // Native texture was updated in the mean time: renew. + this._tintTexture(item.tx, nativeTexture, color); + } + + return item.tx; + } else { + const before = cache.memoryUsage; + + // Find blanco tint texture. + let target = cache.reuseTexture(frame); + if (target) { + target.ctx.clearRect(0, 0, target.width, target.height); + } else { + // Allocate new. + target = document.createElement('canvas'); + target.width = nativeTexture.w; + target.height = nativeTexture.h; + target.ctx = target.getContext('2d'); + } + + this._tintTexture(target, nativeTexture, color); + cache.set(color, target, frame); + + const after = cache.memoryUsage; + + if (after !== before) { + this._addMemoryUsage(after - before); + } + + return target; + } + } + + _tintTexture(target, source, color) { + let col = color.toString(16); + while (col.length < 6) { + col = "0" + col; + } + target.ctx.fillStyle = '#' + col; + target.ctx.globalCompositeOperation = 'copy'; + target.ctx.fillRect(0, 0, source.w, source.h); + target.ctx.globalCompositeOperation = 'multiply'; + target.ctx.drawImage(source, 0, 0, source.w, source.h, 0, 0, target.width, target.height); + + // Alpha-mix the texture. + target.ctx.globalCompositeOperation = 'destination-in'; + target.ctx.drawImage(source, 0, 0, source.w, source.h, 0, 0, target.width, target.height); + } + + _hasCache(nativeTexture) { + return !!nativeTexture._tintCache; + } + + _getCache(nativeTexture) { + if (!nativeTexture._tintCache) { + nativeTexture._tintCache = new C2dTintCache(nativeTexture); + } + return nativeTexture._tintCache; + } + + gc(aggressive = false) { + const frame = this.stage.frameCounter; + let delta = 0; + this._cachedNativeTextures.forEach(texture => { + const cache = this._getCache(texture); + if (aggressive) { + delta += cache.memoryUsage; + texture.clear(); + } else { + const before = cache.memoryUsage; + cache.cleanup(frame); + cache.releaseBlancoTextures(); + delta += (cache.memoryUsage - before); + } + }); + + if (aggressive) { + this._cachedNativeTextures.clear(); + } + + if (delta) { + this._addMemoryUsage(delta); + } + } + +} + +class C2dTintCache { + + constructor(nativeTexture) { + this._tx = nativeTexture; + this._colors = new Map(); + this._blancoTextures = null; + this._lastCleanupFrame = 0; + this._memTextures = 0; + } + + get memoryUsage() { + return this._memTextures * this._tx.w * this._tx.h; + } + + releaseBlancoTextures() { + this._memTextures -= this._blancoTextures.length; + this._blancoTextures = []; + } + + clear() { + // Dereference the textures. + this._blancoTextures = null; + this._colors.clear(); + this._memTextures = 0; + } + + get(color) { + let item = this._colors.get(color); + if (!item) { + item = {lf: -1, tx: undefined, u: -1}; + this._colors.set(color, item); + } + return item; + } + + set(color, texture, frame) { + const item = this.get(color); + item.lf = frame; + item.tx = texture; + item.u = frame; + this._memTextures++; + } + + cleanup(frame) { + // We only need to clean up once per frame. + if (this._lastCleanupFrame !== frame) { + + // We limit blanco textures reuse to one frame only to prevent memory usage growth. + this._blancoTextures = []; + + this._colors.forEach((item, color) => { + // Clean up entries that were not used last frame. + if (item.lf < frame - 1) { + if (item.tx) { + // Keep as reusable blanco texture. + this._blancoTextures.push(item.tx); + } + this._colors.delete(color); + } + }); + + this._lastCleanupFrame = frame; + } + } + + reuseTexture(frame) { + // Try to reuse textures, because creating them every frame is expensive. + this.cleanup(frame); + if (this._blancoTextures && this._blancoTextures.length) { + this._memTextures--; + return this._blancoTextures.pop(); + } + } + +} + +class C2dRenderer extends Renderer { + + constructor(stage) { + super(stage); + + this.tintManager = new C2dTextureTintManager(stage); + + this.setupC2d(this.stage.c2d.canvas); + } + + destroy() { + this.tintManager.destroy(); + } + + _createDefaultShader(ctx) { + return new DefaultShader$1(ctx); + } + + _getShaderBaseType() { + return C2dShader + } + + _getShaderAlternative(shaderType) { + return shaderType.getC2d && shaderType.getC2d(); + } + + createCoreQuadList(ctx) { + return new C2dCoreQuadList(ctx); + } + + createCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + return new C2dCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + } + + createCoreRenderExecutor(ctx) { + return new C2dCoreRenderExecutor(ctx); + } + + createCoreRenderState(ctx) { + return new CoreRenderState(ctx); + } + + createRenderTexture(w, h, pw, ph) { + const canvas = document.createElement('canvas'); + canvas.width = pw; + canvas.height = ph; + this.setupC2d(canvas); + return canvas; + } + + freeRenderTexture(nativeTexture) { + this.tintManager.delete(nativeTexture); + } + + gc(aggressive) { + this.tintManager.gc(aggressive); + } + + uploadTextureSource(textureSource, options) { + // For canvas, we do not need to upload. + if (options.source.buffer) { + // Convert RGBA buffer to canvas. + const canvas = document.createElement('canvas'); + canvas.width = options.w; + canvas.height = options.h; + + const imageData = new ImageData(new Uint8ClampedArray(options.source.buffer), options.w, options.h); + canvas.getContext('2d').putImageData(imageData, 0, 0); + return canvas; + } + + return options.source; + } + + freeTextureSource(textureSource) { + this.tintManager.delete(textureSource.nativeTexture); + } + + addQuad(renderState, quads, index) { + // Render context changes while traversing so we save it by ref. + const elementCore = quads.quadElements[index]; + quads.setRenderContext(index, elementCore._renderContext); + quads.setWhite(index, elementCore.isWhite()); + quads.setSimpleTc(index, elementCore.hasSimpleTexCoords()); + } + + isRenderTextureReusable(renderState, renderTextureInfo) { + // @todo: check render coords/matrix, maybe move this to core? + return false; + } + + finishRenderState(renderState) { + } + + setupC2d(canvas) { + const ctx = canvas.getContext('2d'); + canvas.ctx = ctx; + + ctx._scissor = null; + + // Save base state so we can restore the defaults later. + canvas.ctx.save(); + } + +} + +class ImageWorker { + + constructor(options = {}) { + this._items = new Map(); + this._id = 0; + + this._initWorker(); + } + + destroy() { + if (this._worker) { + this._worker.terminate(); + } + } + + _initWorker() { + const code = `(${createWorker.toString()})()`; + const blob = new Blob([code.replace('"use strict";', '')]); // firefox adds "use strict"; to any function which might block worker execution so knock it off + const blobURL = (window.URL ? URL : webkitURL).createObjectURL(blob, { + type: 'application/javascript; charset=utf-8' + }); + this._worker = new Worker(blobURL); + + this._worker.postMessage({type: 'config', config: {path: window.location.href}}); + + this._worker.onmessage = (e) => { + if (e.data && e.data.id) { + const id = e.data.id; + const item = this._items.get(id); + if (item) { + if (e.data.type == 'data') { + this.finish(item, e.data.info); + } else { + this.error(item, e.data.info); + } + } + } + }; + } + + create(src) { + const id = ++this._id; + const item = new ImageWorkerImage(this, id, src); + this._items.set(id, item); + this._worker.postMessage({type: "add", id: id, src: src}); + return item; + } + + cancel(image) { + this._worker.postMessage({type: "cancel", id: image.id}); + this._items.delete(image.id); + } + + error(image, info) { + image.error(info); + this._items.delete(image.id); + } + + finish(image, info) { + image.load(info); + this._items.delete(image.id); + } + +} + +class ImageWorkerImage { + + constructor(manager, id, src) { + this._manager = manager; + this._id = id; + this._src = src; + this._onError = null; + this._onLoad = null; + } + + get id() { + return this._id; + } + + get src() { + return this._src; + } + + set onError(f) { + this._onError = f; + } + + set onLoad(f) { + this._onLoad = f; + } + + cancel() { + this._manager.cancel(this); + } + + load(info) { + if (this._onLoad) { + this._onLoad(info); + } + } + + error(info) { + if (this._onError) { + this._onError(info); + } + } + +} + +/** + * Notice that, within the createWorker function, we must only use ES5 code to keep it ES5-valid after babelifying, as + * the converted code of this section is converted to a blob and used as the js of the web worker thread. + */ +const createWorker = function() { + + function ImageWorkerServer() { + + this.items = new Map(); + + var t = this; + onmessage = function(e) { + t._receiveMessage(e); + }; + + } + + ImageWorkerServer.isPathAbsolute = function(path) { + return /^(?:\/|[a-z]+:\/\/)/.test(path); + }; + + ImageWorkerServer.prototype._receiveMessage = function(e) { + if (e.data.type === 'config') { + this.config = e.data.config; + + var base = this.config.path; + var parts = base.split("/"); + parts.pop(); + this._relativeBase = parts.join("/") + "/"; + + } else if (e.data.type === 'add') { + this.add(e.data.id, e.data.src); + } else if (e.data.type === 'cancel') { + this.cancel(e.data.id); + } + }; + + ImageWorkerServer.prototype.add = function(id, src) { + // Convert relative URLs. + if (!ImageWorkerServer.isPathAbsolute(src)) { + src = this._relativeBase + src; + } + + if (src.substr(0,2) === "//") { + // This doesn't work for image workers. + src = "http:" + src; + } + + var item = new ImageWorkerServerItem(id, src); + const t = this; + item.onFinish = function(result) { + t.finish(item, result); + }; + item.onError = function(info) { + t.error(item, info); + }; + this.items.set(id, item); + item.start(); + }; + + ImageWorkerServer.prototype.cancel = function(id) { + var item = this.items.get(id); + if (item) { + item.cancel(); + this.items.delete(id); + } + }; + + ImageWorkerServer.prototype.finish = function(item, {imageBitmap, hasAlphaChannel}) { + postMessage({ + type: "data", + id: item.id, + info: { + imageBitmap, + hasAlphaChannel + } + }, [imageBitmap]); + this.items.delete(item.id); + }; + + ImageWorkerServer.prototype.error = function(item, {type, message}) { + postMessage({ + type: "error", + id: item.id, + info: { + type, + message + } + }); + this.items.delete(item.id); + }; + + ImageWorkerServer.isWPEBrowser = function() { + return (navigator.userAgent.indexOf("WPE") !== -1); + }; + + function ImageWorkerServerItem(id, src) { + + this._onError = undefined; + this._onFinish = undefined; + this._id = id; + this._src = src; + this._xhr = undefined; + this._mimeType = undefined; + this._canceled = false; + + } + + Object.defineProperty(ImageWorkerServerItem.prototype, 'id', { + get: function() { + return this._id; + } + }); + + Object.defineProperty(ImageWorkerServerItem.prototype, 'onFinish', { + get: function() { + return this._onFinish; + }, + set: function(f) { + this._onFinish = f; + } + }); + + Object.defineProperty(ImageWorkerServerItem.prototype, 'onError', { + get: function() { + return this._onError; + }, + set: function(f) { + this._onError = f; + } + }); + + ImageWorkerServerItem.prototype.start = function() { + this._xhr = new XMLHttpRequest(); + this._xhr.open("GET", this._src, true); + this._xhr.responseType = "blob"; + + var t = this; + this._xhr.onerror = function(oEvent) { + t.error({type: "connection", message: "Connection error"}); + }; + + this._xhr.onload = function(oEvent) { + var blob = t._xhr.response; + t._mimeType = blob.type; + + t._createImageBitmap(blob); + }; + + this._xhr.send(); + }; + + ImageWorkerServerItem.prototype._createImageBitmap = function(blob) { + var t = this; + createImageBitmap(blob, {premultiplyAlpha: 'premultiply', colorSpaceConversion: 'none', imageOrientation: 'none'}).then(function(imageBitmap) { + t.finish({ + imageBitmap, + hasAlphaChannel: t._hasAlphaChannel() + }); + }).catch(function(e) { + t.error({type: "parse", message: "Error parsing image data"}); + }); + }; + + ImageWorkerServerItem.prototype._hasAlphaChannel = function() { + if (ImageWorkerServer.isWPEBrowser()) { + // When using unaccelerated rendering image (https://github.com/WebPlatformForEmbedded/WPEWebKit/blob/wpe-20170728/Source/WebCore/html/ImageBitmap.cpp#L52), + // everything including JPG images are in RGBA format. Upload is way faster when using an alpha channel. + // @todo: after hardware acceleration is fixed and re-enabled, JPG should be uploaded in RGB to get the best possible performance and memory usage. + return true; + } else { + return (this._mimeType.indexOf("image/png") !== -1); + } + }; + + ImageWorkerServerItem.prototype.cancel = function() { + if (this._canceled) return; + if (this._xhr) { + this._xhr.abort(); + } + this._canceled = true; + }; + + ImageWorkerServerItem.prototype.error = function(type, message) { + if (!this._canceled && this._onError) { + this._onError({type, message}); + } + }; + + ImageWorkerServerItem.prototype.finish = function(info) { + if (!this._canceled && this._onFinish) { + this._onFinish(info); + } + }; + + var worker = new ImageWorkerServer(); +}; + +/** + * Platform-specific functionality. + * Copyright Metrological, 2017; + */ +class WebPlatform { + + init(stage) { + this.stage = stage; + this._looping = false; + this._awaitingLoop = false; + + if (this.stage.getOption("useImageWorker")) { + if (!window.createImageBitmap || !window.Worker) { + console.warn("Can't use image worker because browser does not have createImageBitmap and Web Worker support"); + } else { + console.log('Using image worker!'); + this._imageWorker = new ImageWorker(); + } + } + } + + destroy() { + if (this._imageWorker) { + this._imageWorker.destroy(); + } + this._removeKeyHandler(); + } + + startLoop() { + this._looping = true; + if (!this._awaitingLoop) { + this.loop(); + } + } + + stopLoop() { + this._looping = false; + } + + loop() { + let self = this; + let lp = function() { + self._awaitingLoop = false; + if (self._looping) { + self.stage.drawFrame(); + requestAnimationFrame(lp); + self._awaitingLoop = true; + } + }; + requestAnimationFrame(lp); + } + + uploadGlTexture(gl, textureSource, source, options) { + if (source instanceof ImageData || source instanceof HTMLImageElement || source instanceof HTMLCanvasElement || source instanceof HTMLVideoElement || (window.ImageBitmap && source instanceof ImageBitmap)) { + // Web-specific data types. + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, options.format, options.type, source); + } else { + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, textureSource.w, textureSource.h, 0, options.format, options.type, source); + } + } + + loadSrcTexture({src, hasAlpha}, cb) { + let cancelCb = undefined; + let isPng = (src.indexOf(".png") >= 0); + if (this._imageWorker) { + // WPE-specific image parser. + const image = this._imageWorker.create(src); + image.onError = function(err) { + return cb("Image load error"); + }; + image.onLoad = function({imageBitmap, hasAlphaChannel}) { + cb(null, { + source: imageBitmap, + renderInfo: {src: src}, + hasAlpha: hasAlphaChannel, + premultiplyAlpha: true + }); + }; + cancelCb = function() { + image.cancel(); + }; + } else { + let image = new Image(); + if (!(src.substr(0,5) == "data:")) { + // Base64. + image.crossOrigin = "Anonymous"; + } + image.onerror = function(err) { + // Ignore error message when cancelled. + if (image.src) { + return cb("Image load error"); + } + }; + image.onload = function() { + cb(null, { + source: image, + renderInfo: {src: src}, + hasAlpha: isPng || hasAlpha + }); + }; + image.src = src; + + cancelCb = function() { + image.onerror = null; + image.onload = null; + image.removeAttribute('src'); + }; + } + + return cancelCb; + } + + createWebGLContext(w, h) { + let canvas = this.stage.getOption('canvas') || document.createElement('canvas'); + + if (w && h) { + canvas.width = w; + canvas.height = h; + } + + let opts = { + alpha: true, + antialias: false, + premultipliedAlpha: true, + stencil: true, + preserveDrawingBuffer: false + }; + + let gl = canvas.getContext('webgl', opts) || canvas.getContext('experimental-webgl', opts); + if (!gl) { + throw new Error('This browser does not support webGL.'); + } + + return gl; + } + + createCanvasContext(w, h) { + let canvas = this.stage.getOption('canvas') || document.createElement('canvas'); + + if (w && h) { + canvas.width = w; + canvas.height = h; + } + + let c2d = canvas.getContext('2d'); + if (!c2d) { + throw new Error('This browser does not support 2d canvas.'); + } + + return c2d; + } + + getHrTime() { + return window.performance ? window.performance.now() : (new Date()).getTime(); + } + + getDrawingCanvas() { + // We can't reuse this canvas because textures may load async. + return document.createElement('canvas'); + } + + getTextureOptionsForDrawingCanvas(canvas) { + let options = {}; + options.source = canvas; + return options; + } + + nextFrame(changes) { + /* WebGL blits automatically */ + } + + registerKeyHandler(keyhandler) { + this._keyListener = e => { + keyhandler(e); + }; + window.addEventListener('keydown', this._keyListener); + } + + _removeKeyHandler() { + if (this._keyListener) { + window.removeEventListener('keydown', this._keyListener); + } + } + +} + +class PlatformLoader { + static load(options) { + if (options.platform) { + return options.platform; + } else { + if (Utils.isWeb) { + return WebPlatform; + } else { + throw new Error("You must specify the platform class to be used."); + } + } + } +} + +class Utils$1 { + + static isFunction(value) { + return typeof value === 'function'; + } + + static isNumber(value) { + return typeof value === 'number'; + } + + static isInteger(value) { + return (typeof value === 'number' && (value % 1) === 0); + } + + static isBoolean(value) { + return value === true || value === false; + } + + static isString(value) { + return typeof value == 'string'; + } + + static isObject(value) { + let type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + static isPlainObject(value) { + let type = typeof value; + return !!value && (type == 'object'); + } + + static isObjectLiteral(value){ + return typeof value === 'object' && value && value.constructor === Object + } + + static getArrayIndex(index, arr) { + return Utils$1.getModuloIndex(index, arr.length); + } + + static equalValues(v1, v2) { + if ((typeof v1) !== (typeof v2)) return false + if (Utils$1.isObjectLiteral(v1)) { + return Utils$1.isObjectLiteral(v2) && Utils$1.equalObjectLiterals(v1, v2) + } else if (Array.isArray(v1)) { + return Array.isArray(v2) && Utils$1.equalArrays(v1, v2) + } else { + return v1 === v2 + } + } + + static equalObjectLiterals(obj1, obj2) { + let keys1 = Object.keys(obj1); + let keys2 = Object.keys(obj2); + if (keys1.length !== keys2.length) { + return false + } + + for (let i = 0, n = keys1.length; i < n; i++) { + const k1 = keys1[i]; + const k2 = keys2[i]; + if (k1 !== k2) { + return false + } + + const v1 = obj1[k1]; + const v2 = obj2[k2]; + + if (!Utils$1.equalValues(v1, v2)) { + return false + } + } + + return true; + } + + static equalArrays(v1, v2) { + if (v1.length !== v2.length) { + return false + } + for (let i = 0, n = v1.length; i < n; i++) { + if (!this.equalValues(v1[i], v2[i])) { + return false + } + } + + return true + } + +} + +/** + * Maintains the state of a WebGLRenderingContext. + */ +class WebGLState { + + constructor(id, gl) { + this._id = id; + this._gl = gl; + this._program = undefined; + this._buffers = new Map(); + this._framebuffers = new Map(); + this._renderbuffers = new Map(); + + // Contains vertex attribute definition arrays (enabled, size, type, normalized, stride, offset). + this._vertexAttribs = new Array(16); + this._nonDefaultFlags = new Set(); + this._settings = new Map(); + this._textures = new Array(8); + this._maxTexture = 0; + this._activeTexture = gl.TEXTURE0; + this._pixelStorei = new Array(5); + } + + _getDefaultFlag(cap) { + return (cap === this._gl.DITHER); + } + + setFlag(cap, v) { + const def = this._getDefaultFlag(cap); + if (v === def) { + return this._nonDefaultFlags.delete(cap); + } else { + if (!this._nonDefaultFlags.has(cap)) { + this._nonDefaultFlags.add(cap); + return true; + } else { + return false; + } + } + } + + setBuffer(target, buffer) { + const change = this._buffers.get(target) !== buffer; + this._buffers.set(target, buffer); + + if (change && (target === this._gl.ARRAY_BUFFER)) { + // When the array buffer is changed all attributes are cleared. + this._vertexAttribs = []; + } + + return change; + } + + setFramebuffer(target, buffer) { + const change = this._framebuffers.get(target) !== buffer; + this._framebuffers.set(target, buffer); + return change; + } + + setRenderbuffer(target, buffer) { + const change = this._renderbuffers.get(target) !== buffer; + this._renderbuffers.set(target, buffer); + return change; + } + + setProgram(program) { + const change = this._program !== program; + this._program = program; + return change + } + + setSetting(func, v) { + const s = this._settings.get(func); + const change = !s || !Utils$1.equalValues(s, v); + this._settings.set(func, v); + return change + } + + disableVertexAttribArray(index) { + const va = this._vertexAttribs[index]; + if (va && va[5]) { + va[5] = false; + return true; + } + return false; + } + + enableVertexAttribArray(index) { + const va = this._vertexAttribs[index]; + if (va) { + if (!va[0]) { + va[0] = true; + return true; + } + } else { + this._vertexAttribs[index] = [0, 0, 0, 0, 0, true]; + return true; + } + return false; + } + + vertexAttribPointer(index, props) { + let va = this._vertexAttribs[index]; + let equal = false; + if (va) { + equal = va[0] === props[0] && + va[1] === props[1] && + va[2] === props[2] && + va[3] === props[3] && + va[4] === props[4]; + } + + if (equal) { + return false; + } else { + props[5] = va ? va[5] : false; + return true; + } + } + + setActiveTexture(texture) { + const changed = this._activeTexture !== texture; + this._activeTexture = texture; + return changed; + } + + bindTexture(target, texture) { + const activeIndex = WebGLState._getTextureIndex(this._activeTexture); + this._maxTexture = Math.max(this._maxTexture, activeIndex + 1); + const current = this._textures[activeIndex]; + const targetIndex = WebGLState._getTextureTargetIndex(target); + if (current) { + if (current[targetIndex] === texture) { + return false; + } + current[targetIndex] = texture; + return true; + } else { + if (texture) { + this._textures[activeIndex] = []; + this._textures[activeIndex][targetIndex] = texture; + return true + } else { + return false + } + } + } + + setPixelStorei(pname, param) { + const i = WebGLState._getPixelStoreiIndex(pname); + const change = !Utils$1.equalValues(this._pixelStorei[i], param); + this._pixelStorei[i] = param; + return change; + } + + migrate(s) { + const t = this; + + // Warning: migrate should call the original prototype methods directly. + + this._migrateFlags(t, s); + + // useProgram + if (s._program !== t._program) { + this._gl._useProgram(s._program); + } + + this._migrateFramebuffers(t, s); + this._migrateRenderbuffers(t, s); + + const buffersChanged = this._migrateBuffers(t, s); + this._migrateAttributes(t, s, buffersChanged); + + this._migrateFlags(t, s); + + this._migrateSettings(t, s); + + this._migratePixelStorei(t, s); + + this._migrateTextures(t, s); + + } + + _migratePixelStorei(t, s) { + for (let i = 0, n = t._pixelStorei.length; i < n; i++) { + if (t._pixelStorei[i] !== s._pixelStorei[i]) { + const value = s._pixelStorei[i] !== undefined ? s._pixelStorei[i] : WebGLState._getDefaultPixelStoreiByIndex(i); + this._gl._pixelStorei(WebGLState._getPixelStoreiByIndex(i), value); + } + } + } + + _migrateTextures(t, s) { + const max = Math.max(t._maxTexture, s._maxTexture); + + let activeTexture = t._activeTexture; + + for (let i = 0; i < max; i++) { + const sTargets = s._textures[i]; + const tTargets = t._textures[i]; + const textureNumb = WebGLState._getTextureByIndex(i); + + const targetMax = Math.max(tTargets ? tTargets.length : 0, sTargets ? sTargets.length : 0); + for (let j = 0, n = targetMax; j < n; j++) { + const target = WebGLState._getTextureTargetByIndex(j); + if (activeTexture !== textureNumb) { + this._gl._activeTexture(textureNumb); + activeTexture = textureNumb; + } + + const texture = (sTargets && sTargets[j]) || null; + this._gl._bindTexture(target, texture); + } + } + + if (s._activeTexture !== activeTexture) { + this._gl._activeTexture(s._activeTexture); + } + } + + _migrateBuffers(t, s) { + s._buffers.forEach((framebuffer, target) => { + if (t._buffers.get(target) !== framebuffer) { + this._gl._bindBuffer(target, framebuffer); + } + }); + + t._buffers.forEach((buffer, target) => { + const b = s._buffers.get(target); + if (b === undefined) { + this._gl._bindBuffer(target, null); + } + }); + return (s._buffers.get(this._gl.ARRAY_BUFFER) !== t._buffers.get(this._gl.ARRAY_BUFFER)) + } + + _migrateFramebuffers(t, s) { + s._framebuffers.forEach((framebuffer, target) => { + if (t._framebuffers.get(target) !== framebuffer) { + this._gl._bindFramebuffer(target, framebuffer); + } + }); + + t._framebuffers.forEach((framebuffer, target) => { + const fb = s._framebuffers.get(target); + if (fb === undefined) { + this._gl._bindFramebuffer(target, null); + } + }); + } + + _migrateRenderbuffers(t, s) { + s._renderbuffers.forEach((renderbuffer, target) => { + if (t._renderbuffers.get(target) !== renderbuffer) { + this._gl._bindRenderbuffer(target, renderbuffer); + } + }); + + t._renderbuffers.forEach((renderbuffer, target) => { + const fb = s._renderbuffers.get(target); + if (fb === undefined) { + this._gl._bindRenderbuffer(target, null); + } + }); + } + + _migrateAttributes(t, s, buffersChanged) { + + if (!buffersChanged) { + t._vertexAttribs.forEach((attrib, index) => { + if (!s._vertexAttribs[index]) { + // We can't 'delete' a vertex attrib so we'll disable it. + this._gl._disableVertexAttribArray(index); + } + }); + + s._vertexAttribs.forEach((attrib, index) => { + this._gl._vertexAttribPointer(index, attrib[0], attrib[1], attrib[2], attrib[4]); + if (attrib[5]) { + this._gl._enableVertexAttribArray(index); + } else { + this._gl._disableVertexAttribArray(index); + } + }); + } else { + // When buffers are changed, previous attributes were reset automatically. + s._vertexAttribs.forEach((attrib, index) => { + if (attrib[0]) { + // Do not set vertex attrib pointer when it was just the default value. + this._gl._vertexAttribPointer(index, attrib[0], attrib[1], attrib[2], attrib[3], attrib[4]); + } + if (attrib[5]) { + this._gl._enableVertexAttribArray(index); + } + }); + } + } + + _migrateSettings(t, s) { + const defaults = this.constructor.getDefaultSettings(); + t._settings.forEach((value, func) => { + const name = func.name || func.xname; + if (!s._settings.has(func)) { + let args = defaults.get(name); + if (Utils$1.isFunction(args)) { + args = args(this._gl); + } + // We are actually setting the setting for optimization purposes. + s._settings.set(func, args); + func.apply(this._gl, args); + } + }); + s._settings.forEach((value, func) => { + const tValue = t._settings.get(func); + if (!tValue || !Utils$1.equalValues(tValue, value)) { + func.apply(this._gl, value); + } + }); + } + + _migrateFlags(t, s) { + t._nonDefaultFlags.forEach(setting => { + if (!s._nonDefaultFlags.has(setting)) { + if (this._getDefaultFlag(setting)) { + this._gl._enable(setting); + } else { + this._gl._disable(setting); + } + } + }); + s._nonDefaultFlags.forEach(setting => { + if (!t._nonDefaultFlags.has(setting)) { + if (this._getDefaultFlag(setting)) { + this._gl._disable(setting); + } else { + this._gl._enable(setting); + } + } + }); + } + + static getDefaultSettings() { + if (!this._defaultSettings) { + this._defaultSettings = new Map(); + const d = this._defaultSettings; + const g = WebGLRenderingContext.prototype; + d.set("viewport", function(gl) {return [0,0,gl.canvas.width, gl.canvas.height]}); + d.set("scissor", function(gl) {return [0,0,gl.canvas.width, gl.canvas.height]}); + d.set("blendColor", [0, 0, 0, 0]); + d.set("blendEquation", [g.FUNC_ADD]); + d.set("blendEquationSeparate", [g.FUNC_ADD, g.FUNC_ADD]); + d.set("blendFunc", [g.ONE, g.ZERO]); + d.set("blendFuncSeparate", [g.ONE, g.ZERO, g.ONE, g.ZERO]); + d.set("clearColor", [0, 0, 0, 0]); + d.set("clearDepth", [1]); + d.set("clearStencil", [0]); + d.set("colorMask", [true, true, true, true]); + d.set("cullFace", [g.BACK]); + d.set("depthFunc", [g.LESS]); + d.set("depthMask", [true]); + d.set("depthRange", [0, 1]); + d.set("frontFace", [g.CCW]); + d.set("lineWidth", [1]); + d.set("polygonOffset", [0, 0]); + d.set("sampleCoverage", [1, false]); + d.set("stencilFunc", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateFront", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateBack", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateFrontAndBack", [g.ALWAYS, 0, 1]); + d.set("stencilMask", [1]); + d.set("_stencilMaskSeparateFront", [1]); + d.set("_stencilMaskSeparateBack", [1]); + d.set("_stencilMaskSeparateFrontAndBack", [1]); + d.set("stencilOp", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateFront", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateBack", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateFrontAndBack", [g.KEEP, g.KEEP, g.KEEP]); + d.set("vertexAttrib1f", []); + d.set("vertexAttrib1fv", []); + d.set("vertexAttrib2f", []); + d.set("vertexAttrib2fv", []); + d.set("vertexAttrib3f", []); + d.set("vertexAttrib3fv", []); + d.set("vertexAttrib4f", []); + d.set("vertexAttrib4fv", []); + } + return this._defaultSettings + } + + static _getTextureTargetIndex(target) { + switch(target) { + case 0x0DE1: + /* TEXTURE_2D */ + return 0; + case 0x8513: + /* TEXTURE_CUBE_MAP */ + return 1; + default: + // Shouldn't happen. + throw new Error('Unknown texture target: ' + target); + } + } + + static _getTextureTargetByIndex(index) { + if (!this._textureTargetIndices) { + this._textureTargetIndices = [0x0DE1, 0x8513]; + } + return this._textureTargetIndices[index] + } + + static _getTextureIndex(index) { + return index - 0x84C0 /* GL_TEXTURE0 */; + } + + static _getTextureByIndex(index) { + return index + 0x84C0; + } + + static _getPixelStoreiIndex(pname) { + switch(pname) { + case 0x0D05: + /* PACK_ALIGNMENT */ + return 0; + case 0x0CF5: + /* UNPACK_ALIGNMENT */ + return 1; + case 0x9240: + /* UNPACK_FLIP_Y_WEBGL */ + return 2; + case 0x9241: + /* UNPACK_PREMULTIPLY_ALPHA_WEBGL */ + return 3; + case 0x9243: + /* UNPACK_COLORSPACE_CONVERSION_WEBGL */ + return 4; + //@todo: support WebGL2 properties, see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/pixelStorei + case 0x9245: + /* UNPACK_FLIP_BLUE_RED */ + return 5; + default: + // Shouldn't happen. + throw new Error('Unknown pixelstorei: ' + pname); + } + } + + static _getPixelStoreiByIndex(index) { + if (!this._pixelStoreiIndices) { + this._pixelStoreiIndices = [0x0D05, 0x0CF5, 0x9240, 0x9241, 0x9243]; + } + return this._pixelStoreiIndices[index] + } + + static _getDefaultPixelStoreiByIndex(index) { + if (!this._pixelStoreiDefaults) { + this._pixelStoreiDefaults = [4, 4, false, false, WebGLRenderingContext.prototype.BROWSER_DEFAULT_WEBGL]; + } + return this._pixelStoreiDefaults[index] + } +} + +class WebGLStateManager { + + _initStateManager(id = "default") { + this._states = {}; + this._state = this._getState(id); + } + + _getState(id) { + if (!this._states[id]) { + this._states[id] = new WebGLState(id, this); + } + return this._states[id]; + } + + switchState(id = "default") { + if (this._state._id !== id) { + const newState = this._getState(id); + this._state.migrate(newState); + this._state = newState; + } + } + + $useProgram(program) { + if (this._state.setProgram(program)) + this._useProgram(program); + } + + $bindBuffer(target, fb) { + if (this._state.setBuffer(target, fb)) + this._bindBuffer(target, fb); + } + + $bindFramebuffer(target, fb) { + if (this._state.setFramebuffer(target, fb)) + this._bindFramebuffer(target, fb); + } + + $bindRenderbuffer(target, fb) { + if (this._state.setRenderbuffer(target, fb)) + this._bindRenderbuffer(target, fb); + } + + $enable(cap) { + if (this._state.setFlag(cap, true)) + this._enable(cap); + } + + $disable(cap) { + if (this._state.setFlag(cap, false)) + this._disable(cap); + } + + $viewport(x, y, w, h) { + if (this._state.setSetting(this._viewport, [x, y, w, h])) + this._viewport(x, y, w, h); + } + + $scissor(x, y, w, h) { + if (this._state.setSetting(this._scissor, [x, y, w, h])) + this._scissor(x, y, w, h); + } + + $disableVertexAttribArray(index) { + if (this._state.disableVertexAttribArray(index)) + this._disableVertexAttribArray(index); + } + + $enableVertexAttribArray(index) { + if (this._state.enableVertexAttribArray(index)) + this._enableVertexAttribArray(index); + } + + $vertexAttribPointer(index, size, type, normalized, stride, offset) { + if (this._state.vertexAttribPointer(index, [size, type, normalized, stride, offset])) + this._vertexAttribPointer(index, size, type, normalized, stride, offset); + } + + $activeTexture(texture) { + if (this._state.setActiveTexture(texture)) + this._activeTexture(texture); + } + + $bindTexture(target, texture) { + if (this._state.bindTexture(target, texture)) + this._bindTexture(target, texture); + } + + $pixelStorei(pname, param) { + if (this._state.setPixelStorei(pname, param)) { + this._pixelStorei(pname, param); + } + } + + $stencilFuncSeparate(face, func, ref, mask) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilFuncSeparateFront; + break; + case this.BACK: + f = this._stencilFuncSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilFuncSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [func, ref, mask])) + f.apply(this, [func, ref, mask]); + } + + _stencilFuncSeparateFront(func, ref, mask) { + this._stencilFuncSeparate(this.FRONT, func, ref, mask); + } + + _stencilFuncSeparateBack(func, ref, mask) { + this._stencilFuncSeparate(this.BACK, func, ref, mask); + } + + _stencilFuncSeparateFrontAndBack(func, ref, mask) { + this._stencilFuncSeparate(this.FRONT_AND_BACK, func, ref, mask); + } + + $stencilMaskSeparate(face, mask) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilMaskSeparateFront; + break; + case this.BACK: + f = this._stencilMaskSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilMaskSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [mask])) + f.apply(this, [mask]); + } + + _stencilMaskSeparateFront(mask) { + this._stencilMaskSeparate(this.FRONT, mask); + } + + _stencilMaskSeparateBack(mask) { + this._stencilMaskSeparate(this.BACK, mask); + } + + _stencilMaskSeparateFrontAndBack(mask) { + this._stencilMaskSeparate(this.FRONT_AND_BACK, mask); + } + + $stencilOpSeparate(face, fail, zfail, zpass) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilOpSeparateFront; + break; + case this.BACK: + f = this._stencilOpSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilOpSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [fail, zfail, zpass])) + f.apply(this, [fail, zfail, zpass]); + } + + _stencilOpSeparateFront(fail, zfail, zpass) { + this._stencilOpSeparate(this.FRONT, fail, zfail, zpass); + } + + _stencilOpSeparateBack(fail, zfail, zpass) { + this._stencilOpSeparate(this.BACK, fail, zfail, zpass); + } + + _stencilOpSeparateFrontAndBack(fail, zfail, zpass) { + this._stencilOpSeparate(this.FRONT_AND_BACK, fail, zfail, zpass); + } + + $blendColor(red, green, blue, alpha) { + if (this._state.setSetting(this._blendColor, [red, green, blue, alpha])) + this._blendColor(red, green, blue, alpha); + } + + $blendEquation(mode) { + if (this._state.setSetting(this._blendEquation, [mode])) + this._blendEquation(mode); + } + + $blendEquationSeparate(modeRGB, modeAlpha) { + if (this._state.setSetting(this._blendEquationSeparate, [modeRGB, modeAlpha])) + this._blendEquationSeparate(modeRGB, modeAlpha); + } + + $blendFunc(sfactor, dfactor) { + if (this._state.setSetting(this._blendFunc, [sfactor, dfactor])) + this._blendFunc(sfactor, dfactor); + } + + $blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha) { + if (this._state.setSetting(this._blendFuncSeparate, [srcRGB, dstRGB, srcAlpha, dstAlpha])) + this._blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + } + + $clearColor(red, green, blue, alpha) { + if (this._state.setSetting(this._clearColor, [red, green, blue, alpha])) + this._clearColor(red, green, blue, alpha); + } + + $clearDepth(depth) { + if (this._state.setSetting(this._clearDepth, [depth])) + this._clearDepth(depth); + } + + $clearStencil(s) { + if (this._state.setSetting(this._clearStencil, [s])) + this._clearStencil(s); + } + + $colorMask(red, green, blue, alpha) { + if (this._state.setSetting(this._colorMask, [red, green, blue, alpha])) + this._colorMask(red, green, blue, alpha); + } + + $cullFace(mode) { + if (this._state.setSetting(this._cullFace, [mode])) + this._cullFace(mode); + } + + $depthFunc(func) { + if (this._state.setSetting(this._depthFunc, [func])) + this._depthFunc(func); + } + + $depthMask(flag) { + if (this._state.setSetting(this._depthMask, [flag])) + this._depthMask(flag); + } + + $depthRange(zNear, zFar) { + if (this._state.setSetting(this._depthRange, [zNear, zFar])) + this._depthRange(zNear, zFar); + } + + $frontFace(mode) { + if (this._state.setSetting(this._frontFace, [mode])) + this._frontFace(mode); + } + + $lineWidth(width) { + if (this._state.setSetting(this._lineWidth, [width])) + this._lineWidth(width); + } + + $polygonOffset(factor, units) { + if (this._state.setSetting(this._polygonOffset, [factor, units])) + this._polygonOffset(factor, units); + } + + $sampleCoverage(value, invert) { + if (this._state.setSetting(this._sampleCoverage, [value, invert])) + this._sampleCoverage(value, invert); + } + + $stencilFunc(func, ref, mask) { + if (this._state.setSetting(this._stencilFunc, [func, ref, mask])) + this._stencilFunc(func, ref, mask); + } + + $stencilMask(mask) { + if (this._state.setSetting(this._stencilMask, [mask])) + this._stencilMask(mask); + } + + $stencilOp(fail, zfail, zpass) { + if (this._state.setSetting(this._stencilOp, [fail, zfail, zpass])) + this._stencilOp(fail, zfail, zpass); + } + + $vertexAttrib1f(indx, x) { + if (this._state.setSetting(this._vertexAttrib1f, [indx, x])) + this._vertexAttrib1f(indx, x); + } + + $vertexAttrib1fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib1fv, [indx, values])) + this._vertexAttrib1fv(indx, values); + } + + $vertexAttrib2f(indx, x, y) { + if (this._state.setSetting(this._vertexAttrib2f, [indx, x, y])) + this._vertexAttrib2f(indx, x, y); + } + + $vertexAttrib2fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib2fv, [indx, values])) + this._vertexAttrib2fv(indx, values); + } + + $vertexAttrib3f(indx, x, y, z) { + if (this._state.setSetting(this._vertexAttrib3f, [indx, x, y, z])) + this._vertexAttrib3f(indx, x, y, z); + } + + $vertexAttrib3fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib3fv, [indx, values])) + this._vertexAttrib3fv(indx, values); + } + + $vertexAttrib4f(indx, x, y, z, w) { + if (this._state.setSetting(this._vertexAttrib4f, [indx, x, y, z, w])) + this._vertexAttrib4f(indx, x, y, z, w); + } + + $vertexAttrib4fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib4fv, [indx, values])) + this._vertexAttrib4fv(indx, values); + } + + /** + * Sets up the rendering context for context sharing. + * @param {WebGLRenderingContext} gl + * @param {string} id + */ + static enable(gl, id = "default") { + const names = Object.getOwnPropertyNames(WebGLStateManager.prototype); + const WebGLRenderingContextProto = gl.__proto__; + names.forEach(name => { + if (name !== "constructor") { + const method = WebGLStateManager.prototype[name]; + if (name.charAt(0) === "$") { + name = name.substr(1); + } + if (gl[name]) { + if (!gl[name].name) { + // We do this for compatibility with the Chrome WebGL Inspector plugin. + gl[name].xname = name; + } + gl['_' + name] = gl[name]; + } + gl[name] = method; + } + }); + + WebGLStateManager.prototype._initStateManager.call(gl, id); + + return gl; + } + +} + +class TextureManager { + + constructor(stage) { + this.stage = stage; + + /** + * The currently used amount of texture memory. + * @type {number} + */ + this._usedMemory = 0; + + /** + * All uploaded texture sources. + * @type {TextureSource[]} + */ + this._uploadedTextureSources = []; + + /** + * The texture source lookup id to texture source hashmap. + * @type {Map} + */ + this.textureSourceHashmap = new Map(); + + } + + get usedMemory() { + return this._usedMemory; + } + + destroy() { + for (let i = 0, n = this._uploadedTextureSources.length; i < n; i++) { + this._nativeFreeTextureSource(this._uploadedTextureSources[i]); + } + + this.textureSourceHashmap.clear(); + this._usedMemory = 0; + } + + getReusableTextureSource(id) { + return this.textureSourceHashmap.get(id); + } + + getTextureSource(func, id) { + // Check if texture source is already known. + let textureSource = id ? this.textureSourceHashmap.get(id) : null; + if (!textureSource) { + // Create new texture source. + textureSource = new TextureSource(this, func); + + if (id) { + textureSource.lookupId = id; + this.textureSourceHashmap.set(id, textureSource); + } + } + + return textureSource; + } + + uploadTextureSource(textureSource, options) { + if (textureSource.isLoaded()) return; + + this._addMemoryUsage(textureSource.w * textureSource.h); + + // Load texture. + const nativeTexture = this._nativeUploadTextureSource(textureSource, options); + + textureSource._nativeTexture = nativeTexture; + + // We attach w and h to native texture (we need it in CoreRenderState._isRenderTextureReusable). + nativeTexture.w = textureSource.w; + nativeTexture.h = textureSource.h; + + nativeTexture.update = this.stage.frameCounter; + + this._uploadedTextureSources.push(textureSource); + + this.addToLookupMap(textureSource); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + this.stage.addMemoryUsage(delta); + } + + addToLookupMap(textureSource) { + const lookupId = textureSource.lookupId; + if (lookupId) { + if (!this.textureSourceHashmap.has(lookupId)) { + this.textureSourceHashmap.set(lookupId, textureSource); + } + } + } + + gc() { + this.freeUnusedTextureSources(); + this._cleanupLookupMap(); + } + + freeUnusedTextureSources() { + let remainingTextureSources = []; + for (let i = 0, n = this._uploadedTextureSources.length; i < n; i++) { + let ts = this._uploadedTextureSources[i]; + if (ts.allowCleanup()) { + this._freeManagedTextureSource(ts); + } else { + remainingTextureSources.push(ts); + } + } + + this._uploadedTextureSources = remainingTextureSources; + + this._cleanupLookupMap(); + } + + _freeManagedTextureSource(textureSource) { + if (textureSource.isLoaded()) { + this._nativeFreeTextureSource(textureSource); + this._addMemoryUsage(-textureSource.w * textureSource.h); + } + + // Should be reloaded. + textureSource.loadingSince = null; + } + + _cleanupLookupMap() { + // We keep those that still have value (are being loaded or already loaded, or are likely to be reused). + this.textureSourceHashmap.forEach((textureSource, lookupId) => { + if (!(textureSource.isLoaded() || textureSource.isLoading()) && !textureSource.isUsed()) { + this.textureSourceHashmap.delete(lookupId); + } + }); + } + + /** + * Externally free texture source. + * @param textureSource + */ + freeTextureSource(textureSource) { + const index = this._uploadedTextureSources.indexOf(textureSource); + const managed = (index !== -1); + + if (textureSource.isLoaded()) { + if (managed) { + this._addMemoryUsage(-textureSource.w * textureSource.h); + this._uploadedTextureSources.splice(index, 1); + } + this._nativeFreeTextureSource(textureSource); + } + + // Should be reloaded. + textureSource.loadingSince = null; + } + + _nativeUploadTextureSource(textureSource, options) { + return this.stage.renderer.uploadTextureSource(textureSource, options); + } + + _nativeFreeTextureSource(textureSource) { + this.stage.renderer.freeTextureSource(textureSource); + textureSource.clearNativeTexture(); + } + +} + +/** + * Allows throttling of loading texture sources, keeping the app responsive. + */ +class TextureThrottler { + + constructor(stage) { + this.stage = stage; + + this.genericCancelCb = (textureSource) => { + this._remove(textureSource); + }; + + this._sources = []; + this._data = []; + } + + destroy() { + this._sources = []; + this._data = []; + } + + processSome() { + if (this._sources.length) { + const start = Date.now(); + do { + this._processItem(); + } while(this._sources.length && (Date.now() - start < TextureThrottler.MAX_UPLOAD_TIME_PER_FRAME)); + } + } + + _processItem() { + const source = this._sources.pop(); + const data = this._data.pop(); + if (source.isLoading()) { + source.processLoadedSource(data); + } + } + + add(textureSource, data) { + this._sources.push(textureSource); + this._data.push(data); + } + + _remove(textureSource) { + const index = this._sources.indexOf(textureSource); + if (index >= 0) { + this._sources.splice(index, 1); + this._data.splice(index, 1); + } + } + +} + +TextureThrottler.MAX_UPLOAD_TIME_PER_FRAME = 10; + +class CoreContext { + + constructor(stage) { + this.stage = stage; + + this.root = null; + + this.updateTreeOrder = 0; + + this.renderState = this.stage.renderer.createCoreRenderState(this); + + this.renderExec = this.stage.renderer.createCoreRenderExecutor(this); + this.renderExec.init(); + + this._usedMemory = 0; + this._renderTexturePool = []; + + this._renderTextureId = 1; + + this._zSorts = []; + } + + get usedMemory() { + return this._usedMemory; + } + + destroy() { + this._renderTexturePool.forEach(texture => this._freeRenderTexture(texture)); + this._usedMemory = 0; + } + + hasRenderUpdates() { + return !!this.root._parent._hasRenderUpdates; + } + + render() { + // Clear flag to identify if anything changes before the next frame. + this.root._parent._hasRenderUpdates = 0; + + this._render(); + } + + update() { + this._update(); + + // Due to the boundsVisibility flag feature (and onAfterUpdate hook), it is possible that other elements were + // changed during the update loop (for example due to the txLoaded event). We process these changes immediately + // (but not recursively to prevent infinite loops). + if (this.root._hasUpdates) { + this._update(); + } + + this._performForcedZSorts(); + } + + /** + * Certain ElementCore items may be forced to zSort to strip out references to prevent memleaks.. + */ + _performForcedZSorts() { + const n = this._zSorts.length; + if (n) { + // Forced z-sorts (ElementCore may force a z-sort in order to free memory/prevent memory leaks). + for (let i = 0, n = this._zSorts.length; i < n; i++) { + if (this._zSorts[i].zSort) { + this._zSorts[i].sortZIndexedChildren(); + } + } + this._zSorts = []; + } + } + + _update() { + this.updateTreeOrder = 0; + + this.root.update(); + } + + _render() { + // Obtain a sequence of the quad operations. + this._fillRenderState(); + + if (this.stage.getOption('readPixelsBeforeDraw')) { + const pixels = new Uint8Array(4); + const gl = this.stage.gl; + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + } + + // Now run them with the render executor. + this._performRender(); + } + + _fillRenderState() { + this.renderState.reset(); + this.root.render(); + this.renderState.finish(); + } + + _performRender() { + this.renderExec.execute(); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + this.stage.addMemoryUsage(delta); + } + + allocateRenderTexture(w, h) { + let prec = this.stage.getRenderPrecision(); + let pw = Math.max(1, Math.round(w * prec)); + let ph = Math.max(1, Math.round(h * prec)); + + // Search last item first, so that last released render texture is preferred (may cause memory cache benefits). + const n = this._renderTexturePool.length; + for (let i = n - 1; i >= 0; i--) { + const texture = this._renderTexturePool[i]; + // We don't want to reuse the same render textures within the same frame because that will create gpu stalls. + if (texture.w === pw && texture.h === ph && (texture.update !== this.stage.frameCounter)) { + texture.f = this.stage.frameCounter; + this._renderTexturePool.splice(i, 1); + return texture; + } + } + + const texture = this._createRenderTexture(w, h, pw, ph); + texture.precision = prec; + return texture; + } + + releaseRenderTexture(texture) { + this._renderTexturePool.push(texture); + } + + freeUnusedRenderTextures(maxAge = 60) { + // Clean up all textures that are no longer used. + // This cache is short-lived because it is really just meant to supply running shaders that are + // updated during a number of frames. + let limit = this.stage.frameCounter - maxAge; + + this._renderTexturePool = this._renderTexturePool.filter(texture => { + if (texture.f <= limit) { + this._freeRenderTexture(texture); + return false; + } + return true; + }); + } + + _createRenderTexture(w, h, pw, ph) { + this._addMemoryUsage(pw * ph); + + const texture = this.stage.renderer.createRenderTexture(w, h, pw, ph); + texture.id = this._renderTextureId++; + texture.f = this.stage.frameCounter; + texture.ow = w; + texture.oh = h; + texture.w = pw; + texture.h = ph; + + return texture; + } + + _freeRenderTexture(nativeTexture) { + this.stage.renderer.freeRenderTexture(nativeTexture); + this._addMemoryUsage(-nativeTexture.w * nativeTexture.h); + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + this.stage.renderer.copyRenderTexture(renderTexture, nativeTexture, options); + } + + forceZSort(elementCore) { + this._zSorts.push(elementCore); + } + +} + +class TransitionSettings { + constructor(stage) { + this.stage = stage; + this._timingFunction = 'ease'; + this._timingFunctionImpl = StageUtils.getTimingFunction(this._timingFunction); + this.delay = 0; + this.duration = 0.2; + this.merger = null; + } + + get timingFunction() { + return this._timingFunction; + } + + set timingFunction(v) { + this._timingFunction = v; + this._timingFunctionImpl = StageUtils.getTimingFunction(v); + } + + get timingFunctionImpl() { + return this._timingFunctionImpl; + } + + patch(settings) { + Base.patchObject(this, settings); + } +} + +TransitionSettings.prototype.isTransitionSettings = true; + +class TransitionManager { + + constructor(stage) { + this.stage = stage; + + this.stage.on('frameStart', () => this.progress()); + + /** + * All transitions that are running and attached. + * (we don't support transitions on un-attached elements to prevent memory leaks) + * @type {Set} + */ + this.active = new Set(); + + this.defaultTransitionSettings = new TransitionSettings(this.stage); + } + + progress() { + if (this.active.size) { + let dt = this.stage.dt; + + let filter = false; + this.active.forEach(function(a) { + a.progress(dt); + if (!a.isRunning()) { + filter = true; + } + }); + + if (filter) { + this.active = new Set([...this.active].filter(t => (t.isRunning()))); + } + } + } + + createSettings(settings) { + const transitionSettings = new TransitionSettings(); + Base.patchObject(transitionSettings, settings); + return transitionSettings; + } + + addActive(transition) { + this.active.add(transition); + } + + removeActive(transition) { + this.active.delete(transition); + } +} + +class MultiSpline { + + constructor() { + this._clear(); + } + + _clear() { + this._p = []; + this._pe = []; + this._idp = []; + this._f = []; + this._v = []; + this._lv = []; + this._sm = []; + this._s = []; + this._ve = []; + this._sme = []; + this._se = []; + + this._length = 0; + } + + parse(rgba, def) { + let i, n; + if (!Utils.isObjectLiteral(def)) { + def = {0: def}; + } + + let defaultSmoothness = 0.5; + + let items = []; + for (let key in def) { + if (def.hasOwnProperty(key)) { + let obj = def[key]; + if (!Utils.isObjectLiteral(obj)) { + obj = {v: obj}; + } + + let p = parseFloat(key); + + if (key === "sm") { + defaultSmoothness = obj.v; + } else if (!isNaN(p) && p >= 0 && p <= 2) { + obj.p = p; + + obj.f = Utils.isFunction(obj.v); + obj.lv = obj.f ? obj.v(0, 0) : obj.v; + + items.push(obj); + } + } + } + + // Sort by progress value. + items = items.sort(function(a, b) {return a.p - b.p}); + + n = items.length; + + for (i = 0; i < n; i++) { + let last = (i === n - 1); + if (!items[i].hasOwnProperty('pe')) { + // Progress. + items[i].pe = last ? (items[i].p <= 1 ? 1 : 2 /* support onetotwo stop */) : items[i + 1].p; + } else { + // Prevent multiple items at the same time. + const max = i < n - 1 ? items[i + 1].p : 1; + if (items[i].pe > max) { + items[i].pe = max; + } + } + if (items[i].pe === items[i].p) { + items[i].idp = 0; + } else { + items[i].idp = 1 / (items[i].pe - items[i].p); + } + } + + // Color merger: we need to split/combine RGBA components. + + // Calculate bezier helper values.; + for (i = 0; i < n; i++) { + if (!items[i].hasOwnProperty('sm')) { + // Smoothness.; + items[i].sm = defaultSmoothness; + } + if (!items[i].hasOwnProperty('s')) { + // Slope.; + if (i === 0 || i === n - 1 || (items[i].p === 1 /* for onetotwo */)) { + // Horizontal slope at start and end.; + items[i].s = rgba ? [0, 0, 0, 0] : 0; + } else { + const pi = items[i - 1]; + const ni = items[i + 1]; + if (pi.p === ni.p) { + items[i].s = rgba ? [0, 0, 0, 0] : 0; + } else { + if (rgba) { + const nc = MultiSpline.getRgbaComponents(ni.lv); + const pc = MultiSpline.getRgbaComponents(pi.lv); + const d = 1 / (ni.p - pi.p); + items[i].s = [ + d * (nc[0] - pc[0]), + d * (nc[1] - pc[1]), + d * (nc[2] - pc[2]), + d * (nc[3] - pc[3]) + ]; + } else { + items[i].s = (ni.lv - pi.lv) / (ni.p - pi.p); + } + } + } + } + } + + for (i = 0; i < n - 1; i++) { + // Calculate value function.; + if (!items[i].f) { + + let last = (i === n - 1); + if (!items[i].hasOwnProperty('ve')) { + items[i].ve = last ? items[i].lv : items[i + 1].lv; + } + + // We can only interpolate on numeric values. Non-numeric values are set literally when reached time. + if (Utils.isNumber(items[i].v) && Utils.isNumber(items[i].lv)) { + if (!items[i].hasOwnProperty('sme')) { + items[i].sme = last ? defaultSmoothness : items[i + 1].sm; + } + if (!items[i].hasOwnProperty('se')) { + items[i].se = last ? (rgba ? [0, 0, 0, 0] : 0) : items[i + 1].s; + } + + // Generate spline.; + if (rgba) { + items[i].v = MultiSpline.getSplineRgbaValueFunction(items[i].v, items[i].ve, items[i].p, items[i].pe, items[i].sm, items[i].sme, items[i].s, items[i].se); + } else { + items[i].v = MultiSpline.getSplineValueFunction(items[i].v, items[i].ve, items[i].p, items[i].pe, items[i].sm, items[i].sme, items[i].s, items[i].se); + } + + items[i].f = true; + } + } + } + + if (this.length) { + this._clear(); + } + + for (i = 0, n = items.length; i < n; i++) { + this._add(items[i]); + } + } + + _add(item) { + this._p.push(item.p || 0); + this._pe.push(item.pe || 0); + this._idp.push(item.idp || 0); + this._f.push(item.f || false); + this._v.push(item.hasOwnProperty('v') ? item.v : 0 /* v might be false or null */ ); + this._lv.push(item.lv || 0); + this._sm.push(item.sm || 0); + this._s.push(item.s || 0); + this._ve.push(item.ve || 0); + this._sme.push(item.sme || 0); + this._se.push(item.se || 0); + this._length++; + } + + _getItem(p) { + const n = this._length; + if (!n) { + return -1; + } + + if (p < this._p[0]) { + return 0; + } + + for (let i = 0; i < n; i++) { + if (this._p[i] <= p && p < this._pe[i]) { + return i; + } + } + + return n - 1; + } + + getValue(p) { + const i = this._getItem(p); + if (i === -1) { + return undefined; + } else { + if (this._f[i]) { + const o = Math.min(1, Math.max(0, (p - this._p[i]) * this._idp[i])); + return this._v[i](o); + } else { + return this._v[i]; + } + } + } + + get length() { + return this._length; + } + + static getRgbaComponents(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r, g, b, a]; + }; + + static getSplineValueFunction(v1, v2, p1, p2, o1, i2, s1, s2) { + // Normalize slopes because we use a spline that goes from 0 to 1. + let dp = p2 - p1; + s1 *= dp; + s2 *= dp; + + let helpers = MultiSpline.getSplineHelpers(v1, v2, o1, i2, s1, s2); + if (!helpers) { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + + return v2 * p + v1 * (1 - p); + }; + } else { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + return MultiSpline.calculateSpline(helpers, p); + }; + } + }; + + static getSplineRgbaValueFunction(v1, v2, p1, p2, o1, i2, s1, s2) { + // Normalize slopes because we use a spline that goes from 0 to 1. + let dp = p2 - p1; + s1[0] *= dp; + s1[1] *= dp; + s1[2] *= dp; + s1[3] *= dp; + s2[0] *= dp; + s2[1] *= dp; + s2[2] *= dp; + s2[3] *= dp; + + let cv1 = MultiSpline.getRgbaComponents(v1); + let cv2 = MultiSpline.getRgbaComponents(v2); + + let helpers = [ + MultiSpline.getSplineHelpers(cv1[0], cv2[0], o1, i2, s1[0], s2[0]), + MultiSpline.getSplineHelpers(cv1[1], cv2[1], o1, i2, s1[1], s2[1]), + MultiSpline.getSplineHelpers(cv1[2], cv2[2], o1, i2, s1[2], s2[2]), + MultiSpline.getSplineHelpers(cv1[3], cv2[3], o1, i2, s1[3], s2[3]) + ]; + + if (!helpers[0]) { + return function (p) { + // Linear. + if (p === 0) return v1; + if (p === 1) return v2; + + return MultiSpline.mergeColors(v2, v1, p); + }; + } else { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + + return MultiSpline.getArgbNumber([ + Math.min(255, MultiSpline.calculateSpline(helpers[0], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[1], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[2], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[3], p)) + ]); + }; + } + + }; + + /** + * Creates helpers to be used in the spline function. + * @param {number} v1 + * From value. + * @param {number} v2 + * To value. + * @param {number} o1 + * From smoothness (0 = linear, 1 = smooth). + * @param {number} s1 + * From slope (0 = horizontal, infinite = vertical). + * @param {number} i2 + * To smoothness. + * @param {number} s2 + * To slope. + * @returns {Number[]} + * The helper values to be supplied to the spline function. + * If the configuration is actually linear, null is returned. + */ + static getSplineHelpers(v1, v2, o1, i2, s1, s2) { + if (!o1 && !i2) { + // Linear. + return null; + } + + // Cubic bezier points. + // http://cubic-bezier.com/ + let csx = o1; + let csy = v1 + s1 * o1; + let cex = 1 - i2; + let cey = v2 - s2 * i2; + + let xa = 3 * csx - 3 * cex + 1; + let xb = -6 * csx + 3 * cex; + let xc = 3 * csx; + + let ya = 3 * csy - 3 * cey + v2 - v1; + let yb = 3 * (cey + v1) - 6 * csy; + let yc = 3 * (csy - v1); + let yd = v1; + + return [xa, xb, xc, ya, yb, yc, yd]; + }; + + /** + * Calculates the intermediate spline value based on the specified helpers. + * @param {number[]} helpers + * Obtained from getSplineHelpers. + * @param {number} p + * @return {number} + */ + static calculateSpline(helpers, p) { + let xa = helpers[0]; + let xb = helpers[1]; + let xc = helpers[2]; + let ya = helpers[3]; + let yb = helpers[4]; + let yc = helpers[5]; + let yd = helpers[6]; + + if (xa === -2 && ya === -2 && xc === 0 && yc === 0) { + // Linear. + return p; + } + + // Find t for p. + let t = 0.5, cbx, dx; + + for (let it = 0; it < 20; it++) { + // Cubic bezier function: f(t)=t*(t*(t*a+b)+c). + cbx = t * (t * (t * xa + xb) + xc); + + dx = p - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc) + yd; + } + + // Cubic bezier derivative function: f'(t)=t*(t*(3*a)+2*b)+c + let cbxd = t * (t * (3 * xa) + 2 * xb) + xc; + + if (cbxd > 1e-10 && cbxd < 1e-10) { + // Problematic. Fall back to binary search method. + break; + } + + t += dx / cbxd; + } + + // Fallback: binary search method. This is more reliable when there are near-0 slopes. + let minT = 0; + let maxT = 1; + for (let it = 0; it < 20; it++) { + t = 0.5 * (minT + maxT); + + // Cubic bezier function: f(t)=t*(t*(t*a+b)+c)+d. + cbx = t * (t * (t * xa + xb) + xc); + + dx = p - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc) + yd; + } + + if (dx < 0) { + maxT = t; + } else { + minT = t; + } + } + + return t; + }; + + static mergeColors(c1, c2, p) { + let r1 = ((c1 / 65536) | 0) % 256; + let g1 = ((c1 / 256) | 0) % 256; + let b1 = c1 % 256; + let a1 = ((c1 / 16777216) | 0); + + let r2 = ((c2 / 65536) | 0) % 256; + let g2 = ((c2 / 256) | 0) % 256; + let b2 = c2 % 256; + let a2 = ((c2 / 16777216) | 0); + + let r = r1 * p + r2 * (1 - p); + let g = g1 * p + g2 * (1 - p); + let b = b1 * p + b2 * (1 - p); + let a = a1 * p + a2 * (1 - p); + + return Math.round(a) * 16777216 + Math.round(r) * 65536 + Math.round(g) * 256 + Math.round(b); + }; + + static getArgbNumber(rgba) { + rgba[0] = Math.max(0, Math.min(255, rgba[0])); + rgba[1] = Math.max(0, Math.min(255, rgba[1])); + rgba[2] = Math.max(0, Math.min(255, rgba[2])); + rgba[3] = Math.max(0, Math.min(255, rgba[3])); + let v = ((rgba[3] | 0) << 24) + ((rgba[0] | 0) << 16) + ((rgba[1] | 0) << 8) + (rgba[2] | 0); + if (v < 0) { + v = 0xFFFFFFFF + v + 1; + } + return v; + }; +} + +class AnimationActionSettings { + + constructor(animationSettings) { + + this.animationSettings = animationSettings; + + /** + * The selector that selects the elements. + * @type {string} + */ + this._selector = ""; + + /** + * The value items, ordered by progress offset. + * @type {MultiSpline} + * @private; + */ + this._items = new MultiSpline(); + + /** + * The affected properties (paths). + * @private; + */ + this._props = []; + + /** + * Property setters, indexed according to props. + * @private; + */ + this._propSetters = []; + + this._resetValue = undefined; + this._hasResetValue = false; + + this._hasColorProperty = undefined; + } + + getResetValue() { + if (this._hasResetValue) { + return this._resetValue; + } else { + return this._items.getValue(0); + } + } + + apply(element, p, factor) { + const elements = this.getAnimatedElements(element); + + let v = this._items.getValue(p); + + if (v === undefined || !elements.length) { + return; + } + + if (factor !== 1) { + // Stop factor.; + let sv = this.getResetValue(); + + if (Utils.isNumber(v) && Utils.isNumber(sv)) { + if (this.hasColorProperty()) { + v = StageUtils.mergeColors(v, sv, factor); + } else { + v = StageUtils.mergeNumbers(v, sv, factor); + } + } + } + + // Apply transformation to all components.; + const n = this._propSetters.length; + + const m = elements.length; + for (let j = 0; j < m; j++) { + for (let i = 0; i < n; i++) { + this._propSetters[i](elements[j], v); + } + } + } + + getAnimatedElements(element) { + return element.select(this._selector); + } + + reset(element) { + const elements = this.getAnimatedElements(element); + + let v = this.getResetValue(); + + if (v === undefined || !elements.length) { + return; + } + + // Apply transformation to all components. + const n = this._propSetters.length; + + const m = elements.length; + for (let j = 0; j < m; j++) { + for (let i = 0; i < n; i++) { + this._propSetters[i](elements[j], v); + } + } + } + + set selector(v) { + this._selector = v; + } + + set t(v) { + this.selector = v; + } + + get resetValue() { + return this._resetValue; + } + + set resetValue(v) { + this._resetValue = v; + this._hasResetValue = (v !== undefined); + } + + set rv(v) { + this.resetValue = v; + } + + set value(v) { + this._items.parse(this.hasColorProperty(), v); + } + + set v(v) { + this.value = v; + } + + set properties(v) { + if (!Array.isArray(v)) { + v = [v]; + } + + this._props = []; + + v.forEach((prop) => { + this._props.push(prop); + this._propSetters.push(Element.getSetter(prop)); + }); + } + + set property(v) { + this._hasColorProperty = undefined; + this.properties = v; + } + + set p(v) { + this.properties = v; + } + + patch(settings) { + Base.patchObject(this, settings); + } + + hasColorProperty() { + if (this._hasColorProperty === undefined) { + this._hasColorProperty = this._props.length ? Element.isColorProperty(this._props[0]) : false; + } + return this._hasColorProperty; + } +} + +AnimationActionSettings.prototype.isAnimationActionSettings = true; + +class AnimationSettings { + constructor() { + /** + * @type {AnimationActionSettings[]} + */ + this._actions = []; + + this.delay = 0; + this.duration = 1; + + this.repeat = 0; + this.repeatOffset = 0; + this.repeatDelay = 0; + + this.autostop = false; + + this.stopMethod = AnimationSettings.STOP_METHODS.FADE; + this._stopTimingFunction = 'ease'; + this._stopTimingFunctionImpl = StageUtils.getTimingFunction(this._stopTimingFunction); + this.stopDuration = 0; + this.stopDelay = 0; + } + + get actions() { + return this._actions; + } + + set actions(v) { + this._actions = []; + for (let i = 0, n = v.length; i < n; i++) { + const e = v[i]; + if (!e.isAnimationActionSettings) { + const aas = new AnimationActionSettings(this); + aas.patch(e); + this._actions.push(aas); + } else { + this._actions.push(e); + } + } + } + + /** + * Applies the animation to the specified element, for the specified progress between 0 and 1. + * @param {Element} element; + * @param {number} p; + * @param {number} factor; + */ + apply(element, p, factor = 1) { + this._actions.forEach(function(action) { + action.apply(element, p, factor); + }); + } + + /** + * Resets the animation to the reset values. + * @param {Element} element; + */ + reset(element) { + this._actions.forEach(function(action) { + action.reset(element); + }); + } + + get stopTimingFunction() { + return this._stopTimingFunction; + } + + set stopTimingFunction(v) { + this._stopTimingFunction = v; + this._stopTimingFunctionImpl = StageUtils.getTimingFunction(v); + } + + get stopTimingFunctionImpl() { + return this._stopTimingFunctionImpl; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +AnimationSettings.STOP_METHODS = { + FADE: 'fade', + REVERSE: 'reverse', + FORWARD: 'forward', + IMMEDIATE: 'immediate', + ONETOTWO: 'onetotwo' +}; + +class Animation extends EventEmitter { + + constructor(manager, settings, element) { + super(); + + this.manager = manager; + + this._settings = settings; + + this._element = element; + + this._state = Animation.STATES.IDLE; + + this._p = 0; + this._delayLeft = 0; + this._repeatsLeft = 0; + + this._stopDelayLeft = 0; + this._stopP = 0; + } + + start() { + if (this._element && this._element.attached) { + this._p = 0; + this._delayLeft = this.settings.delay; + this._repeatsLeft = this.settings.repeat; + this._state = Animation.STATES.PLAYING; + this.emit('start'); + this.checkActive(); + } else { + console.warn("Element must be attached before starting animation"); + } + } + + play() { + if (this._state === Animation.STATES.PAUSED) { + // Continue.; + this._state = Animation.STATES.PLAYING; + this.checkActive(); + this.emit('resume'); + } else if (this._state == Animation.STATES.STOPPING && this.settings.stopMethod == AnimationSettings.STOP_METHODS.REVERSE) { + // Continue.; + this._state = Animation.STATES.PLAYING; + this.emit('stopContinue'); + } else if (this._state != Animation.STATES.PLAYING && this._state != Animation.STATES.FINISHED) { + // Restart.; + this.start(); + } + } + + pause() { + if (this._state === Animation.STATES.PLAYING) { + this._state = Animation.STATES.PAUSED; + this.emit('pause'); + } + } + + replay() { + if (this._state == Animation.STATES.FINISHED) { + this.start(); + } else { + this.play(); + } + } + + skipDelay() { + this._delayLeft = 0; + this._stopDelayLeft = 0; + } + + finish() { + if (this._state === Animation.STATES.PLAYING) { + this._delayLeft = 0; + this._p = 1; + } else if (this._state === Animation.STATES.STOPPING) { + this._stopDelayLeft = 0; + this._p = 0; + } + } + + stop() { + if (this._state === Animation.STATES.STOPPED || this._state === Animation.STATES.IDLE) return; + + this._stopDelayLeft = this.settings.stopDelay || 0; + + if (((this.settings.stopMethod === AnimationSettings.STOP_METHODS.IMMEDIATE) && !this._stopDelayLeft) || this._delayLeft > 0) { + // Stop upon next progress.; + this._state = Animation.STATES.STOPPING; + this.emit('stop'); + } else { + if (this.settings.stopMethod === AnimationSettings.STOP_METHODS.FADE) { + this._stopP = 0; + } + + this._state = Animation.STATES.STOPPING; + this.emit('stop'); + } + + this.checkActive(); + } + + stopNow() { + if (this._state !== Animation.STATES.STOPPED || this._state !== Animation.STATES.IDLE) { + this._state = Animation.STATES.STOPPING; + this._p = 0; + this.emit('stop'); + this.reset(); + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } + + isPaused() { + return this._state === Animation.STATES.PAUSED; + } + + isPlaying() { + return this._state === Animation.STATES.PLAYING; + } + + isStopping() { + return this._state === Animation.STATES.STOPPING; + } + + isFinished() { + return this._state === Animation.STATES.FINISHED; + } + + checkActive() { + if (this.isActive()) { + this.manager.addActive(this); + } + } + + isActive() { + return (this._state == Animation.STATES.PLAYING || this._state == Animation.STATES.STOPPING) && this._element && this._element.attached; + } + + progress(dt) { + if (!this._element) return; + this._progress(dt); + this.apply(); + } + + _progress(dt) { + if (this._state == Animation.STATES.STOPPING) { + this._stopProgress(dt); + return; + } + + if (this._state != Animation.STATES.PLAYING) { + return; + } + + if (this._delayLeft > 0) { + this._delayLeft -= dt; + + if (this._delayLeft < 0) { + dt = -this._delayLeft; + this._delayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + if (this.settings.duration === 0) { + this._p = 1; + } else if (this.settings.duration > 0) { + this._p += dt / this.settings.duration; + } + if (this._p >= 1) { + // Finished!; + if (this.settings.repeat == -1 || this._repeatsLeft > 0) { + if (this._repeatsLeft > 0) { + this._repeatsLeft--; + } + this._p = this.settings.repeatOffset; + + if (this.settings.repeatDelay) { + this._delayLeft = this.settings.repeatDelay; + } + + this.emit('repeat', this._repeatsLeft); + } else { + this._p = 1; + this._state = Animation.STATES.FINISHED; + this.emit('finish'); + if (this.settings.autostop) { + this.stop(); + } + } + } else { + this.emit('progress', this._p); + } + } + + _stopProgress(dt) { + let duration = this._getStopDuration(); + + if (this._stopDelayLeft > 0) { + this._stopDelayLeft -= dt; + + if (this._stopDelayLeft < 0) { + dt = -this._stopDelayLeft; + this._stopDelayLeft = 0; + + this.emit('stopDelayEnd'); + } else { + return; + } + } + if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.IMMEDIATE) { + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.REVERSE) { + if (duration === 0) { + this._p = 0; + } else if (duration > 0) { + this._p -= dt / duration; + } + + if (this._p <= 0) { + this._p = 0; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FADE) { + this._progressStopTransition(dt); + if (this._stopP >= 1) { + this._p = 0; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.ONETOTWO) { + if (this._p < 2) { + if (duration === 0) { + this._p = 2; + } else if (duration > 0) { + if (this._p < 1) { + this._p += dt / this.settings.duration; + } else { + this._p += dt / duration; + } + } + if (this._p >= 2) { + this._p = 2; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else { + this.emit('progress', this._p); + } + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FORWARD) { + if (this._p < 1) { + if (this.settings.duration == 0) { + this._p = 1; + } else { + this._p += dt / this.settings.duration; + } + if (this._p >= 1) { + if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FORWARD) { + this._p = 1; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else { + if (this._repeatsLeft > 0) { + this._repeatsLeft--; + this._p = 0; + this.emit('repeat', this._repeatsLeft); + } else { + this._p = 1; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } + } else { + this.emit('progress', this._p); + } + } + } + + } + + _progressStopTransition(dt) { + if (this._stopP < 1) { + if (this._stopDelayLeft > 0) { + this._stopDelayLeft -= dt; + + if (this._stopDelayLeft < 0) { + dt = -this._stopDelayLeft; + this._stopDelayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + const duration = this._getStopDuration(); + + if (duration == 0) { + this._stopP = 1; + } else { + this._stopP += dt / duration; + } + if (this._stopP >= 1) { + // Finished!; + this._stopP = 1; + } + } + } + + _getStopDuration() { + return this.settings.stopDuration || this.settings.duration; + } + + apply() { + if (this._state === Animation.STATES.STOPPED) { + this.reset(); + } else { + let factor = 1; + if (this._state === Animation.STATES.STOPPING && this.settings.stopMethod === AnimationSettings.STOP_METHODS.FADE) { + factor = (1 - this.settings.stopTimingFunctionImpl(this._stopP)); + } + this._settings.apply(this._element, this._p, factor); + } + } + + reset() { + this._settings.reset(this._element); + } + + get state() { + return this._state; + } + + get p() { + return this._p; + } + + get delayLeft() { + return this._delayLeft; + } + + get element() { + return this._element; + } + + get frame() { + return Math.round(this._p * this._settings.duration * 60); + } + + get settings() { + return this._settings; + } + +} + +Animation.STATES = { + IDLE: 0, + PLAYING: 1, + STOPPING: 2, + STOPPED: 3, + FINISHED: 4, + PAUSED: 5 +}; + +class AnimationManager { + + constructor(stage) { + this.stage = stage; + + this.stage.on('frameStart', () => this.progress()); + + /** + * All running animations on attached subjects. + * @type {Set} + */ + this.active = new Set(); + } + + progress() { + if (this.active.size) { + let dt = this.stage.dt; + + let filter = false; + this.active.forEach(function(a) { + if (a.isActive()) { + a.progress(dt); + } else { + filter = true; + } + }); + + if (filter) { + this.active = new Set([...this.active].filter(t => t.isActive())); + } + } + } + + createAnimation(element, settings) { + if (Utils.isObjectLiteral(settings)) { + // Convert plain object to proper settings object. + settings = this.createSettings(settings); + } + + return new Animation( + this, + settings, + element + ); + } + + createSettings(settings) { + const animationSettings = new AnimationSettings(); + Base.patchObject(animationSettings, settings); + return animationSettings; + } + + addActive(transition) { + this.active.add(transition); + } +} + +class RectangleTexture extends Texture { + + _getLookupId() { + return '__whitepix'; + } + + _getSourceLoader() { + return function(cb) { + var whitePixel = new Uint8Array([255, 255, 255, 255]); + cb(null, {source: whitePixel, w: 1, h: 1, permanent: true}); + } + } + + isAutosizeTexture() { + return false; + } +} + +/** + * Application render tree. + * Copyright Metrological, 2017; + */ + +class Stage extends EventEmitter { + + constructor(options = {}) { + super(); + this._setOptions(options); + + this._usedMemory = 0; + this._lastGcFrame = 0; + + const platformType = Stage.platform ? Stage.platform : PlatformLoader.load(options); + this.platform = new platformType(); + + if (this.platform.init) { + this.platform.init(this); + } + + this.gl = null; + this.c2d = null; + + const context = this.getOption('context'); + if (context) { + if (context.useProgram) { + this.gl = context; + } else { + this.c2d = context; + } + } else { + if (Utils.isWeb && (!Stage.isWebglSupported() || this.getOption('canvas2d'))) { + console.log('Using canvas2d renderer'); + this.c2d = this.platform.createCanvasContext(this.getOption('w'), this.getOption('h')); + } else { + this.gl = this.platform.createWebGLContext(this.getOption('w'), this.getOption('h')); + } + } + + if (this.gl) { + // Wrap in WebGLStateManager. + // This prevents unnecessary double WebGL commands from being executed, and allows context switching. + // Context switching is necessary when reusing the same context for Three.js. + // Note that the user must make sure that the WebGL context is untouched before creating the application, + // when manually passing over a canvas or context in the options. + WebGLStateManager.enable(this.gl, "lightning"); + } + + this._mode = this.gl ? 0 : 1; + + // Override width and height. + if (this.getCanvas()) { + this._options.w = this.getCanvas().width; + this._options.h = this.getCanvas().height; + } + + if (this._mode === 0) { + this._renderer = new WebGLRenderer(this); + } else { + this._renderer = new C2dRenderer(this); + } + + this.setClearColor(this.getOption('clearColor')); + + this.frameCounter = 0; + + this.transitions = new TransitionManager(this); + this.animations = new AnimationManager(this); + + this.textureManager = new TextureManager(this); + this.textureThrottler = new TextureThrottler(this); + + this.startTime = 0; + this.currentTime = 0; + this.dt = 0; + + // Preload rectangle texture, so that we can skip some border checks for loading textures. + this.rectangleTexture = new RectangleTexture(this); + this.rectangleTexture.load(); + + // Never clean up because we use it all the time. + this.rectangleTexture.source.permanent = true; + + this.ctx = new CoreContext(this); + + this._updateSourceTextures = new Set(); + } + + get renderer() { + return this._renderer; + } + + static isWebglSupported() { + if (Utils.isNode) { + return true; + } + + try { + return !!window.WebGLRenderingContext; + } catch(e) { + return false; + } + } + + /** + * Returns the rendering mode. + * @returns {number} + * 0: WebGL + * 1: Canvas2d + */ + get mode() { + return this._mode; + } + + isWebgl() { + return this.mode === 0; + } + + isC2d() { + return this.mode === 1; + } + + getOption(name) { + return this._options[name]; + } + + _setOptions(o) { + this._options = {}; + + let opt = (name, def) => { + let value = o[name]; + + if (value === undefined) { + this._options[name] = def; + } else { + this._options[name] = value; + } + }; + + opt('canvas', null); + opt('context', null); + opt('w', 1920); + opt('h', 1080); + opt('srcBasePath', null); + opt('memoryPressure', 24e6); + opt('bufferMemory', 2e6); + opt('textRenderIssueMargin', 0); + opt('clearColor', [0, 0, 0, 0]); + opt('defaultFontFace', 'sans-serif'); + opt('fixedDt', 0); + opt('useImageWorker', true); + opt('autostart', true); + opt('precision', 1); + opt('canvas2d', false); + opt('platform', null); + opt('readPixelsBeforeDraw', false); + } + + setApplication(app) { + this.application = app; + } + + init() { + this.application.setAsRoot(); + if (this.getOption('autostart')) { + this.platform.startLoop(); + } + } + + destroy() { + this.platform.stopLoop(); + this.platform.destroy(); + this.ctx.destroy(); + this.textureManager.destroy(); + this._renderer.destroy(); + } + + stop() { + this.platform.stopLoop(); + } + + resume() { + this.platform.startLoop(); + } + + get root() { + return this.application; + } + + getCanvas() { + return this._mode ? this.c2d.canvas : this.gl.canvas; + } + + getRenderPrecision() { + return this._options.precision; + } + + /** + * Marks a texture for updating it's source upon the next drawFrame. + * @param texture + */ + addUpdateSourceTexture(texture) { + if (this._updatingFrame) { + // When called from the upload loop, we must immediately load the texture in order to avoid a 'flash'. + texture._performUpdateSource(); + } else { + this._updateSourceTextures.add(texture); + } + } + + removeUpdateSourceTexture(texture) { + if (this._updateSourceTextures) { + this._updateSourceTextures.delete(texture); + } + } + + hasUpdateSourceTexture(texture) { + return (this._updateSourceTextures && this._updateSourceTextures.has(texture)); + } + + drawFrame() { + this.startTime = this.currentTime; + this.currentTime = this.platform.getHrTime(); + + if (this._options.fixedDt) { + this.dt = this._options.fixedDt; + } else { + this.dt = (!this.startTime) ? .02 : .001 * (this.currentTime - this.startTime); + } + + this.emit('frameStart'); + + if (this._updateSourceTextures.size) { + this._updateSourceTextures.forEach(texture => { + texture._performUpdateSource(); + }); + this._updateSourceTextures = new Set(); + } + + this.emit('update'); + + const changes = this.ctx.hasRenderUpdates(); + + // Update may cause textures to be loaded in sync, so by processing them here we may be able to show them + // during the current frame already. + this.textureThrottler.processSome(); + + if (changes) { + this._updatingFrame = true; + this.ctx.update(); + this.ctx.render(); + this._updatingFrame = false; + } + + this.platform.nextFrame(changes); + + this.emit('frameEnd'); + + this.frameCounter++; + } + + isUpdatingFrame() { + return this._updatingFrame; + } + + renderFrame() { + this.ctx.frame(); + } + + forceRenderUpdate() { + // Enforce re-rendering. + if (this.root) { + this.root.core._parent.setHasRenderUpdates(1); + } + } + + setClearColor(clearColor) { + this.forceRenderUpdate(); + if (clearColor === null) { + // Do not clear. + this._clearColor = null; + } else if (Array.isArray(clearColor)) { + this._clearColor = clearColor; + } else { + this._clearColor = StageUtils.getRgbaComponentsNormalized(clearColor); + } + } + + getClearColor() { + return this._clearColor; + } + + createElement(settings) { + if (settings) { + return this.element(settings); + } else { + return new Element(this); + } + } + + createShader(settings) { + return Shader.create(this, settings); + } + + element(settings) { + if (settings.isElement) return settings; + + let element; + if (settings.type) { + element = new settings.type(this); + } else { + element = new Element(this); + } + + element.patch(settings); + + return element; + } + + c(settings) { + return this.element(settings); + } + + get w() { + return this._options.w; + } + + get h() { + return this._options.h; + } + + get coordsWidth() { + return this.w / this._options.precision; + } + + get coordsHeight() { + return this.h / this._options.precision; + } + + addMemoryUsage(delta) { + this._usedMemory += delta; + if (this._lastGcFrame !== this.frameCounter) { + if (this._usedMemory > this.getOption('memoryPressure')) { + this.gc(false); + if (this._usedMemory > this.getOption('memoryPressure') - 2e6) { + // Too few released. Aggressive cleanup. + this.gc(true); + } + } + } + } + + get usedMemory() { + return this._usedMemory; + } + + gc(aggressive) { + if (this._lastGcFrame !== this.frameCounter) { + this._lastGcFrame = this.frameCounter; + const memoryUsageBefore = this._usedMemory; + this.gcTextureMemory(aggressive); + this.gcRenderTextureMemory(aggressive); + this.renderer.gc(aggressive); + + console.log(`GC${aggressive ? "[aggressive]" : ""}! Frame ${this._lastGcFrame} Freed ${((memoryUsageBefore - this._usedMemory) / 1e6).toFixed(2)}MP from GPU memory. Remaining: ${(this._usedMemory / 1e6).toFixed(2)}MP`); + const other = this._usedMemory - this.textureManager.usedMemory - this.ctx.usedMemory; + console.log(` Textures: ${(this.textureManager.usedMemory / 1e6).toFixed(2)}MP, Render Textures: ${(this.ctx.usedMemory / 1e6).toFixed(2)}MP, Renderer caches: ${(other / 1e6).toFixed(2)}MP`); + } + } + + gcTextureMemory(aggressive = false) { + if (aggressive && this.ctx.root.visible) { + // Make sure that ALL textures are cleaned; + this.ctx.root.visible = false; + this.textureManager.gc(); + this.ctx.root.visible = true; + } else { + this.textureManager.gc(); + } + } + + gcRenderTextureMemory(aggressive = false) { + if (aggressive && this.root.visible) { + // Make sure that ALL render textures are cleaned; + this.root.visible = false; + this.ctx.freeUnusedRenderTextures(0); + this.root.visible = true; + } else { + this.ctx.freeUnusedRenderTextures(0); + } + } + + getDrawingCanvas() { + return this.platform.getDrawingCanvas(); + } + + update() { + this.ctx.update(); + } + +} + +class Application extends Component { + + constructor(options = {}, properties) { + // Save options temporarily to avoid having to pass it through the constructor. + Application._temp_options = options; + + // Booting flag is used to postpone updateFocusSettings; + Application.booting = true; + const stage = new Stage(options.stage); + super(stage, properties); + Application.booting = false; + + this.__updateFocusCounter = 0; + + // We must construct while the application is not yet attached. + // That's why we 'init' the stage later (which actually emits the attach event). + this.stage.init(); + + // Initially, the focus settings are updated after both the stage and application are constructed. + this.updateFocusSettings(); + + this.__keymap = this.getOption('keys'); + if (this.__keymap) { + this.stage.platform.registerKeyHandler((e) => { + this._receiveKeydown(e); + }); + } + } + + getOption(name) { + return this.__options[name]; + } + + _setOptions(o) { + this.__options = {}; + + let opt = (name, def) => { + let value = o[name]; + + if (value === undefined) { + this.__options[name] = def; + } else { + this.__options[name] = value; + } + }; + + opt('debug', false); + opt('keys', { + 38: "Up", + 40: "Down", + 37: "Left", + 39: "Right", + 13: "Enter", + 8: "Back", + 27: "Exit" + }); + } + + __construct() { + this.stage.setApplication(this); + + this._setOptions(Application._temp_options); + delete Application._temp_options; + + super.__construct(); + } + + __init() { + super.__init(); + this.__updateFocus(); + } + + updateFocusPath() { + this.__updateFocus(); + } + + __updateFocus() { + const notOverridden = this.__updateFocusRec(); + + if (!Application.booting && notOverridden) { + this.updateFocusSettings(); + } + } + + __updateFocusRec() { + const updateFocusId = ++this.__updateFocusCounter; + this.__updateFocusId = updateFocusId; + + const newFocusPath = this.__getFocusPath(); + const newFocusedComponent = newFocusPath[newFocusPath.length - 1]; + const prevFocusedComponent = this._focusPath ? this._focusPath[this._focusPath.length - 1] : undefined; + + if (!prevFocusedComponent) { + // Focus events. + this._focusPath = []; + for (let i = 0, n = newFocusPath.length; i < n; i++) { + this._focusPath.push(newFocusPath[i]); + this._focusPath[i]._focus(newFocusedComponent, undefined); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + return true; + } else { + let m = Math.min(this._focusPath.length, newFocusPath.length); + let index; + for (index = 0; index < m; index++) { + if (this._focusPath[index] !== newFocusPath[index]) { + break; + } + } + + if (this._focusPath.length !== newFocusPath.length || index !== newFocusPath.length) { + if (this.__options.debug) { + console.log('FOCUS ' + newFocusedComponent.getLocationString()); + } + // Unfocus events. + for (let i = this._focusPath.length - 1; i >= index; i--) { + const unfocusedElement = this._focusPath.pop(); + unfocusedElement._unfocus(newFocusedComponent, prevFocusedComponent); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + + // Focus events. + for (let i = index, n = newFocusPath.length; i < n; i++) { + this._focusPath.push(newFocusPath[i]); + this._focusPath[i]._focus(newFocusedComponent, prevFocusedComponent); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + + // Focus changed events. + for (let i = 0; i < index; i++) { + this._focusPath[i]._focusChange(newFocusedComponent, prevFocusedComponent); + } + } + } + + return true; + } + + updateFocusSettings() { + const focusedComponent = this._focusPath[this._focusPath.length - 1]; + + // Get focus settings. These can be used for dynamic application-wide settings that depend on the + // focus directly (such as the application background). + const focusSettings = {}; + const defaultSetFocusSettings = Component.prototype._setFocusSettings; + for (let i = 0, n = this._focusPath.length; i < n; i++) { + if (this._focusPath[i]._setFocusSettings !== defaultSetFocusSettings) { + this._focusPath[i]._setFocusSettings(focusSettings); + } + } + + const defaultHandleFocusSettings = Component.prototype._handleFocusSettings; + for (let i = 0, n = this._focusPath.length; i < n; i++) { + if (this._focusPath[i]._handleFocusSettings !== defaultHandleFocusSettings) { + this._focusPath[i]._handleFocusSettings(focusSettings, this.__prevFocusSettings, focusedComponent); + } + } + + this.__prevFocusSettings = focusSettings; + } + + _handleFocusSettings(settings, prevSettings, focused, prevFocused) { + // Override to handle focus-based settings. + } + + __getFocusPath() { + const path = [this]; + let current = this; + do { + const nextFocus = current._getFocused(); + if (!nextFocus || (nextFocus === current)) { + // Found!; + break; + } + + + let ptr = nextFocus.cparent; + if (ptr === current) { + path.push(nextFocus); + } else { + // Not an immediate child: include full path to descendant. + const newParts = [nextFocus]; + do { + if (!ptr) { + current._throwError("Return value for _getFocused must be an attached descendant component but its '" + nextFocus.getLocationString() + "'"); + } + newParts.push(ptr); + ptr = ptr.cparent; + } while (ptr !== current); + + // Add them reversed. + for (let i = 0, n = newParts.length; i < n; i++) { + path.push(newParts[n - i - 1]); + } + } + + current = nextFocus; + } while(true); + + return path; + } + + get focusPath() { + return this._focusPath; + } + + /** + * Injects an event in the state machines, top-down from application to focused component. + */ + focusTopDownEvent(events, ...args) { + const path = this.focusPath; + const n = path.length; + + // Multiple events. + for (let i = 0; i < n; i++) { + const event = path[i]._getMostSpecificHandledMember(events); + if (event !== undefined) { + const returnValue = path[i][event](...args); + if (returnValue !== false) { + return true; + } + } + } + + return false; + } + + /** + * Injects an event in the state machines, bottom-up from focused component to application. + */ + focusBottomUpEvent(events, ...args) { + const path = this.focusPath; + const n = path.length; + + // Multiple events. + for (let i = n - 1; i >= 0; i--) { + const event = path[i]._getMostSpecificHandledMember(events); + if (event !== undefined) { + const returnValue = path[i][event](...args); + if (returnValue !== false) { + return true; + } + } + } + + return false; + } + + _receiveKeydown(e) { + const obj = e; + if (this.__keymap[e.keyCode]) { + if (!this.stage.application.focusTopDownEvent(["_capture" + this.__keymap[e.keyCode], "_captureKey"], obj)) { + this.stage.application.focusBottomUpEvent(["_handle" + this.__keymap[e.keyCode], "_handleKey"], obj); + } + } else { + if (!this.stage.application.focusTopDownEvent(["_captureKey"], obj)) { + this.stage.application.focusBottomUpEvent(["_handleKey"], obj); + } + } + this.updateFocusPath(); + } + + destroy() { + if (!this._destroyed) { + this._destroy(); + this.stage.destroy(); + this._destroyed = true; + } + } + + _destroy() { + // This forces the _detach, _disabled and _active events to be called. + this.stage.setApplication(undefined); + this._updateAttachedFlag(); + this._updateEnabledFlag(); + } + + getCanvas() { + return this.stage.getCanvas(); + } + +} + +class StaticCanvasTexture extends Texture { + + constructor(stage) { + super(stage); + this._factory = undefined; + this._lookupId = undefined; + } + + set content({factory, lookupId = undefined}) { + this._factory = factory; + this._lookupId = lookupId; + this._changed(); + } + + _getIsValid() { + return !!this._factory; + } + + _getLookupId() { + return this._lookupId; + } + + _getSourceLoader() { + const f = this._factory; + return (cb) => { + return f((err, canvas) => { + if (err) { + return cb(err); + } + cb(null, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas)); + }, this.stage); + } + } + +} + +class Tools { + + static getCanvasTexture(canvasFactory, lookupId) { + return {type: StaticCanvasTexture, content: {factory: canvasFactory, lookupId: lookupId}} + } + + static getRoundRect(w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (!Array.isArray(radius)){ + // upper-left, upper-right, bottom-right, bottom-left. + radius = [radius, radius, radius, radius]; + } + + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createRoundRect(cb, stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor); + } else { + cb(null, this.createRoundRect(stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor)); + } + }; + let id = 'rect' + [w, h, strokeWidth, strokeColor, fill ? 1 : 0, fillColor].concat(radius).join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createRoundRect(stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (fill === undefined) fill = true; + if (strokeWidth === undefined) strokeWidth = 0; + + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + canvas.width = w + strokeWidth + 2; + canvas.height = h + strokeWidth + 2; + + ctx.beginPath(); + let x = 0.5 * strokeWidth + 1, y = 0.5 * strokeWidth + 1; + + ctx.moveTo(x + radius[0], y); + ctx.lineTo(x + w - radius[1], y); + ctx.arcTo(x + w, y, x + w, y + radius[1], radius[1]); + ctx.lineTo(x + w, y + h - radius[2]); + ctx.arcTo(x + w, y + h, x + w - radius[2], y + h, radius[2]); + ctx.lineTo(x + radius[3], y + h); + ctx.arcTo(x, y + h, x, y + h - radius[3], radius[3]); + ctx.lineTo(x, y + radius[0]); + ctx.arcTo(x, y, x + radius[0], y, radius[0]); + ctx.closePath(); + + if (fill) { + if (Utils.isNumber(fillColor)) { + ctx.fillStyle = StageUtils.getRgbaString(fillColor); + } else { + ctx.fillStyle = "white"; + } + ctx.fill(); + } + + if (strokeWidth) { + if (Utils.isNumber(strokeColor)) { + ctx.strokeStyle = StageUtils.getRgbaString(strokeColor); + } else { + ctx.strokeStyle = "white"; + } + ctx.lineWidth = strokeWidth; + ctx.stroke(); + } + + return canvas; + } + + static getShadowRect(w, h, radius = 0, blur = 5, margin = blur * 2) { + if (!Array.isArray(radius)){ + // upper-left, upper-right, bottom-right, bottom-left. + radius = [radius, radius, radius, radius]; + } + + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createShadowRect(cb, stage, w, h, radius, blur, margin); + } else { + cb(null, this.createShadowRect(stage, w, h, radius, blur, margin)); + } + }; + let id = 'shadow' + [w, h, blur, margin].concat(radius).join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createShadowRect(stage, w, h, radius, blur, margin) { + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + canvas.width = w + margin * 2; + canvas.height = h + margin * 2; + + // WpeWebKit bug: we experienced problems without this with shadows in noncompositedwebgl mode. + ctx.globalAlpha = 0.01; + ctx.fillRect(0, 0, 0.01, 0.01); + ctx.globalAlpha = 1.0; + + ctx.shadowColor = StageUtils.getRgbaString(0xFFFFFFFF); + ctx.fillStyle = StageUtils.getRgbaString(0xFFFFFFFF); + ctx.shadowBlur = blur; + ctx.shadowOffsetX = (w + 10) + margin; + ctx.shadowOffsetY = margin; + + ctx.beginPath(); + const x = -(w + 10); + const y = 0; + + ctx.moveTo(x + radius[0], y); + ctx.lineTo(x + w - radius[1], y); + ctx.arcTo(x + w, y, x + w, y + radius[1], radius[1]); + ctx.lineTo(x + w, y + h - radius[2]); + ctx.arcTo(x + w, y + h, x + w - radius[2], y + h, radius[2]); + ctx.lineTo(x + radius[3], y + h); + ctx.arcTo(x, y + h, x, y + h - radius[3], radius[3]); + ctx.lineTo(x, y + radius[0]); + ctx.arcTo(x, y, x + radius[0], y, radius[0]); + ctx.closePath(); + ctx.fill(); + + return canvas; + } + + static getSvgTexture(url, w, h) { + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createSvg(cb, stage, url, w, h); + } else { + this.createSvg(cb, stage, url, w, h); + } + }; + let id = 'svg' + [w, h, url].join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createSvg(cb, stage, url, w, h) { + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + let img = new Image(); + img.onload = () => { + canvas.width = w; + canvas.height = h; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + cb(null, canvas); + }; + img.onError = (err) => { + cb(err); + }; + img.src = url; + } + +} + +class ObjMerger { + + static isMf(f) { + return Utils.isFunction(f) && f.__mf; + } + + static mf(f) { + // Set as merge function. + f.__mf = true; + return f; + } + + static merge(a, b) { + const aks = Object.keys(a); + const bks = Object.keys(b); + + if (!bks.length) { + return a; + } + + // Create index array for all elements. + const ai = {}; + const bi = {}; + for (let i = 0, n = bks.length; i < n; i++) { + const key = bks[i]; + ai[key] = -1; + bi[key] = i; + } + for (let i = 0, n = aks.length; i < n; i++) { + const key = aks[i]; + ai[key] = i; + if (bi[key] === undefined) { + bi[key] = -1; + } + } + + const aksl = aks.length; + + const result = {}; + for (let i = 0, n = bks.length; i < n; i++) { + const key = bks[i]; + + // Prepend all items in a that are not in b - before the now added b attribute. + const aIndex = ai[key]; + let curIndex = aIndex; + while(--curIndex >= 0) { + const akey = aks[curIndex]; + if (bi[akey] !== -1) { + // Already found? Stop processing. + // Not yet found but exists in b? Also stop processing: wait until we find it in b. + break; + } + } + while(++curIndex < aIndex) { + const akey = aks[curIndex]; + result[akey] = a[akey]; + } + + const bv = b[key]; + const av = a[key]; + let r; + if (this.isMf(bv)) { + r = bv(av); + } else { + if (!Utils.isObjectLiteral(av) || !Utils.isObjectLiteral(bv)) { + r = bv; + } else { + r = ObjMerger.merge(av, bv); + } + } + + // When marked as undefined, property is deleted. + if (r !== undefined) { + result[key] = r; + } + } + + // Append remaining final items in a. + let curIndex = aksl; + while(--curIndex >= 0) { + const akey = aks[curIndex]; + if (bi[akey] !== -1) { + break; + } + } + while(++curIndex < aksl) { + const akey = aks[curIndex]; + result[akey] = a[akey]; + } + + return result; + } + +} + +/** + * Manages the list of children for an element. + */ + +class ObjectListProxy extends ObjectList { + + constructor(target) { + super(); + this._target = target; + } + + onAdd(item, index) { + this._target.addAt(item, index); + } + + onRemove(item, index) { + this._target.removeAt(index); + } + + onSync(removed, added, order) { + this._target._setByArray(order); + } + + onSet(item, index) { + this._target.setAt(item, index); + } + + onMove(item, fromIndex, toIndex) { + this._target.setAt(item, toIndex); + } + + createItem(object) { + return this._target.createItem(object); + } + + isItem(object) { + return this._target.isItem(object); + } + +} + +/** + * Manages the list of children for an element. + */ + +class ObjectListWrapper extends ObjectListProxy { + + constructor(target, wrap) { + super(target); + this._wrap = wrap; + } + + wrap(item) { + let wrapper = this._wrap(item); + item._wrapper = wrapper; + return wrapper; + } + + onAdd(item, index) { + item = this.wrap(item); + super.onAdd(item, index); + } + + onRemove(item, index) { + super.onRemove(item, index); + } + + onSync(removed, added, order) { + added.forEach(a => this.wrap(a)); + order = order.map(a => a._wrapper); + super.onSync(removed, added, order); + } + + onSet(item, index) { + item = this.wrap(item); + super.onSet(item, index); + } + + onMove(item, fromIndex, toIndex) { + super.onMove(item, fromIndex, toIndex); + } + +} + +class NoiseTexture extends Texture { + + _getLookupId() { + return '__noise'; + } + + _getSourceLoader() { + const gl = this.stage.gl; + return function(cb) { + const noise = new Uint8Array(128 * 128 * 4); + for (let i = 0; i < 128 * 128 * 4; i+=4) { + const v = Math.floor(Math.random() * 256); + noise[i] = v; + noise[i+1] = v; + noise[i+2] = v; + noise[i+3] = 255; + } + const texParams = {}; + + if (gl) { + texParams[gl.TEXTURE_WRAP_S] = gl.REPEAT; + texParams[gl.TEXTURE_WRAP_T] = gl.REPEAT; + texParams[gl.TEXTURE_MIN_FILTER] = gl.NEAREST; + texParams[gl.TEXTURE_MAG_FILTER] = gl.NEAREST; + } + + cb(null, {source: noise, w: 128, h: 128, texParams: texParams}); + } + } + +} + +class HtmlTexture extends Texture { + + constructor(stage) { + super(stage); + this._htmlElement = undefined; + this._scale = 1; + } + + set htmlElement(v) { + this._htmlElement = v; + this._changed(); + } + + get htmlElement() { + return this._htmlElement; + } + + set scale(v) { + this._scale = v; + this._changed(); + } + + get scale() { + return this._scale; + } + + set html(v) { + if (!v) { + this.htmlElement = undefined; + } else { + const d = document.createElement('div'); + d.innerHTML = "
" + v + "
"; + this.htmlElement = d.firstElementChild; + } + } + + get html() { + return this._htmlElement.innerHTML; + } + + _getIsValid() { + return this.htmlElement; + } + + _getLookupId() { + return this._scale + ":" + this._htmlElement.innerHTML; + } + + _getSourceLoader() { + const htmlElement = this._htmlElement; + const scale = this._scale; + return function(cb) { + if (!window.html2canvas) { + return cb(new Error("Please include html2canvas (https://html2canvas.hertzen.com/)")); + } + + const area = HtmlTexture.getPreloadArea(); + area.appendChild(htmlElement); + + html2canvas(htmlElement, {backgroundColor: null, scale: scale}).then(function(canvas) { + area.removeChild(htmlElement); + if (canvas.height === 0) { + return cb(new Error("Canvas height is 0")); + } + cb(null, {source: canvas, width: canvas.width, height: canvas.height}); + }).catch(e => { + console.error(e); + }); + } + } + + static getPreloadArea() { + if (!this._preloadArea) { + // Preload area must be included in document body and must be visible to trigger html element rendering. + this._preloadArea = document.createElement('div'); + if (this._preloadArea.attachShadow) { + // Use a shadow DOM if possible to prevent styling from interfering. + this._preloadArea.attachShadow({mode: 'closed'}); + } + this._preloadArea.style.opacity = 0; + this._preloadArea.style.pointerEvents = 'none'; + this._preloadArea.style.position = 'fixed'; + this._preloadArea.style.display = 'block'; + this._preloadArea.style.top = '100vh'; + this._preloadArea.style.overflow = 'hidden'; + document.body.appendChild(this._preloadArea); + } + return this._preloadArea; + } +} + +class StaticTexture extends Texture { + + constructor(stage, options) { + super(stage); + + this._options = options; + } + + set options(v) { + if (this._options !== v) { + this._options = v; + this._changed(); + } + } + + get options() { + return this._options; + } + + _getIsValid() { + return !!this._options; + } + + _getSourceLoader() { + return (cb) => { + cb(null, this._options); + } + } +} + +class ListComponent extends Component { + + constructor(stage) { + super(stage); + + this._wrapper = super._children.a({}); + + this._reloadVisibleElements = false; + + this._visibleItems = new Set(); + + this._index = 0; + + this._started = false; + + /** + * The transition definition that is being used when scrolling the items. + * @type TransitionSettings + */ + this._scrollTransitionSettings = this.stage.transitions.createSettings({}); + + /** + * The scroll area size in pixels per item. + */ + this._itemSize = 100; + + this._viewportScrollOffset = 0; + + this._itemScrollOffset = 0; + + /** + * Should the list jump when scrolling between end to start, or should it be continuous, like a carrousel? + */ + this._roll = false; + + /** + * Allows restricting the start scroll position. + */ + this._rollMin = 0; + + /** + * Allows restricting the end scroll position. + */ + this._rollMax = 0; + + /** + * Definition for a custom animation that is applied when an item is (partially) selected. + * @type AnimationSettings + */ + this._progressAnimation = null; + + /** + * Inverts the scrolling direction. + * @type {boolean} + * @private + */ + this._invertDirection = false; + + /** + * Layout the items horizontally or vertically? + * @type {boolean} + * @private + */ + this._horizontal = true; + + this.itemList = new ListItems(this); + } + + _allowChildrenAccess() { + return false; + } + + get items() { + return this.itemList.get(); + } + + set items(children) { + this.itemList.patch(children); + } + + start() { + this._wrapper.transition(this.property, this._scrollTransitionSettings); + this._scrollTransition = this._wrapper.transition(this.property); + this._scrollTransition.on('progress', p => this.update()); + + this.setIndex(0, true, true); + + this._started = true; + + this.update(); + } + + setIndex(index, immediate = false, closest = false) { + let nElements = this.length; + if (!nElements) return; + + this.emit('unfocus', this.getElement(this.realIndex), this._index, this.realIndex); + + if (closest) { + // Scroll to same offset closest to the index. + let offset = Utils.getModuloIndex(index, nElements); + let o = Utils.getModuloIndex(this.index, nElements); + let diff = offset - o; + if (diff > 0.5 * nElements) { + diff -= nElements; + } else if (diff < -0.5 * nElements) { + diff += nElements; + } + this._index += diff; + } else { + this._index = index; + } + + if (this._roll || (this.viewportSize > this._itemSize * nElements)) { + this._index = Utils.getModuloIndex(this._index, nElements); + } + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + let value = direction * this._index * this._itemSize; + + if (this._roll) { + let min, max, scrollDelta; + if (direction == 1) { + max = (nElements - 1) * this._itemSize; + scrollDelta = this._viewportScrollOffset * this.viewportSize - this._itemScrollOffset * this._itemSize; + + max -= scrollDelta; + + min = this.viewportSize - (this._itemSize + scrollDelta); + + if (this._rollMin) min -= this._rollMin; + if (this._rollMax) max += this._rollMax; + + value = Math.max(Math.min(value, max), min); + } else { + max = (nElements * this._itemSize - this.viewportSize); + scrollDelta = this._viewportScrollOffset * this.viewportSize - this._itemScrollOffset * this._itemSize; + + max += scrollDelta; + + let min = scrollDelta; + + if (this._rollMin) min -= this._rollMin; + if (this._rollMax) max += this._rollMax; + + value = Math.min(Math.max(-max, value), -min); + } + } + + this._scrollTransition.start(value); + + if (immediate) { + this._scrollTransition.finish(); + } + + this.emit('focus', this.getElement(this.realIndex), this._index, this.realIndex); + } + + getAxisPosition() { + let target = -this._scrollTransition._targetValue; + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + let value = -direction * this._index * this._itemSize; + + return this._viewportScrollOffset * this.viewportSize + (value - target); + } + + update() { + if (!this._started) return; + + let nElements = this.length; + if (!nElements) return; + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + + // Map position to index value. + let v = (this._horizontal ? this._wrapper.x : this._wrapper.y); + + let viewportSize = this.viewportSize; + let scrollDelta = this._viewportScrollOffset * viewportSize - this._itemScrollOffset * this._itemSize; + v += scrollDelta; + + let s, e, ps, pe; + if (direction == -1) { + s = Math.floor(-v / this._itemSize); + ps = 1 - ((-v / this._itemSize) - s); + e = Math.floor((viewportSize - v) / this._itemSize); + pe = (((viewportSize - v) / this._itemSize) - e); + } else { + s = Math.ceil(v / this._itemSize); + ps = 1 + (v / this._itemSize) - s; + e = Math.ceil((v - viewportSize) / this._itemSize); + pe = e - ((v - viewportSize) / this._itemSize); + } + if (this._roll || (viewportSize > this._itemSize * nElements)) { + // Don't show additional items. + if (e >= nElements) { + e = nElements - 1; + pe = 1; + } + if (s >= nElements) { + s = nElements - 1; + ps = 1; + } + if (e <= -1) { + e = 0; + pe = 1; + } + if (s <= -1) { + s = 0; + ps = 1; + } + } + + let offset = -direction * s * this._itemSize; + + let item; + for (let index = s; (direction == -1 ? index <= e : index >= e); (direction == -1 ? index++ : index--)) { + let realIndex = Utils.getModuloIndex(index, nElements); + + let element = this.getElement(realIndex); + item = element.parent; + this._visibleItems.delete(item); + if (this._horizontal) { + item.x = offset + scrollDelta; + } else { + item.y = offset + scrollDelta; + } + + let wasVisible = item.visible; + item.visible = true; + + if (!wasVisible || this._reloadVisibleElements) { + // Turned visible. + this.emit('visible', index, realIndex); + } + + + + if (this._progressAnimation) { + let p = 1; + if (index == s) { + p = ps; + } else if (index == e) { + p = pe; + } + + // Use animation to progress. + this._progressAnimation.apply(element, p); + } + + offset += this._itemSize; + } + + // Handle item visibility. + let self = this; + this._visibleItems.forEach(function(invisibleItem) { + invisibleItem.visible = false; + self._visibleItems.delete(invisibleItem); + }); + + for (let index = s; (direction == -1 ? index <= e : index >= e); (direction == -1 ? index++ : index--)) { + let realIndex = Utils.getModuloIndex(index, nElements); + this._visibleItems.add(this.getWrapper(realIndex)); + } + + this._reloadVisibleElements = false; + } + + setPrevious() { + this.setIndex(this._index - 1); + } + + setNext() { + this.setIndex(this._index + 1); + } + + getWrapper(index) { + return this._wrapper.children[index]; + } + + getElement(index) { + let e = this._wrapper.children[index]; + return e ? e.children[0] : null; + } + + reload() { + this._reloadVisibleElements = true; + this.update(); + } + + get element() { + let e = this._wrapper.children[this.realIndex]; + return e ? e.children[0] : null; + } + + get length() { + return this._wrapper.children.length; + } + + get property() { + return this._horizontal ? 'x' : 'y'; + } + + get viewportSize() { + return this._horizontal ? this.w : this.h; + } + + get index() { + return this._index; + } + + get realIndex() { + return Utils.getModuloIndex(this._index, this.length); + } + + get itemSize() { + return this._itemSize; + } + + set itemSize(v) { + this._itemSize = v; + this.update(); + } + + get viewportScrollOffset() { + return this._viewportScrollOffset; + } + + set viewportScrollOffset(v) { + this._viewportScrollOffset = v; + this.update(); + } + + get itemScrollOffset() { + return this._itemScrollOffset; + } + + set itemScrollOffset(v) { + this._itemScrollOffset = v; + this.update(); + } + + get scrollTransitionSettings() { + return this._scrollTransitionSettings; + } + + set scrollTransitionSettings(v) { + this._scrollTransitionSettings.patch(v); + } + + set scrollTransition(v) { + this._scrollTransitionSettings.patch(v); + } + + get scrollTransition() { + return this._scrollTransition; + } + + get progressAnimation() { + return this._progressAnimation; + } + + set progressAnimation(v) { + if (Utils.isObjectLiteral(v)) { + this._progressAnimation = this.stage.animations.createSettings(v); + } else { + this._progressAnimation = v; + } + this.update(); + } + + get roll() { + return this._roll; + } + + set roll(v) { + this._roll = v; + this.update(); + } + + get rollMin() { + return this._rollMin; + } + + set rollMin(v) { + this._rollMin = v; + this.update(); + } + + get rollMax() { + return this._rollMax; + } + + set rollMax(v) { + this._rollMax = v; + this.update(); + } + + get invertDirection() { + return this._invertDirection; + } + + set invertDirection(v) { + if (!this._started) { + this._invertDirection = v; + } + } + + get horizontal() { + return this._horizontal; + } + + set horizontal(v) { + if (v !== this._horizontal) { + if (!this._started) { + this._horizontal = v; + } + } + } + +} +class ListItems extends ObjectListWrapper { + constructor(list) { + let wrap = (item => { + let parent = item.stage.createElement(); + parent.add(item); + parent.visible = false; + return parent; + }); + + super(list._wrapper._children, wrap); + this.list = list; + } + + onAdd(item, index) { + super.onAdd(item, index); + this.checkStarted(index); + } + + checkStarted(index) { + this.list._reloadVisibleElements = true; + if (!this.list._started) { + this.list.start(); + } else { + if (this.list.length === 1) { + this.list.setIndex(0, true, true); + } else { + if (this.list._index >= this.list.length) { + this.list.setIndex(0); + } + } + this.list.update(); + } + } + + onRemove(item, index) { + super.onRemove(item, index); + let ri = this.list.realIndex; + if (ri === index) { + if (ri === this.list.length) { + ri--; + } + if (ri >= 0) { + this.list.setIndex(ri); + } + } else if (ri > index) { + this.list.setIndex(ri - 1); + } + + this.list._reloadVisibleElements = true; + } + + onSet(item, index) { + super.onSet(item, index); + this.checkStarted(index); + } + + onSync(removed, added, order) { + super.onSync(removed, added, order); + this.checkStarted(0); + } + + get _signalProxy() { + return true; + } + +} + +class LinearBlurShader extends DefaultShader { + + constructor(context) { + super(context); + + this._direction = new Float32Array([1, 0]); + this._kernelRadius = 1; + } + + get x() { + return this._direction[0]; + } + + set x(v) { + this._direction[0] = v; + this.redraw(); + } + + get y() { + return this._direction[1]; + } + + set y(v) { + this._direction[1] = v; + this.redraw(); + } + + get kernelRadius() { + return this._kernelRadius; + } + + set kernelRadius(v) { + this._kernelRadius = v; + this.redraw(); + } + + + useDefault() { + return (this._kernelRadius === 0); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("direction", this._direction, this.gl.uniform2fv); + this._setUniform("kernelRadius", this._kernelRadius, this.gl.uniform1i); + + const w = operation.getRenderWidth(); + const h = operation.getRenderHeight(); + this._setUniform("resolution", new Float32Array([w, h]), this.gl.uniform2fv); + } +} + +LinearBlurShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + uniform vec2 resolution; + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform vec2 direction; + uniform int kernelRadius; + + vec4 blur1(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.3333333333333333) * direction; + color += texture2D(image, uv) * 0.29411764705882354; + color += texture2D(image, uv + (off1 / resolution)) * 0.35294117647058826; + color += texture2D(image, uv - (off1 / resolution)) * 0.35294117647058826; + return color; + } + + vec4 blur2(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.3846153846) * direction; + vec2 off2 = vec2(3.2307692308) * direction; + color += texture2D(image, uv) * 0.2270270270; + color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162; + color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162; + color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703; + color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703; + return color; + } + + vec4 blur3(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.411764705882353) * direction; + vec2 off2 = vec2(3.2941176470588234) * direction; + vec2 off3 = vec2(5.176470588235294) * direction; + color += texture2D(image, uv) * 0.1964825501511404; + color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344; + color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344; + color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732; + color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732; + color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057; + color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057; + return color; + } + + void main(void){ + if (kernelRadius == 1) { + gl_FragColor = blur1(uSampler, vTextureCoord, resolution, direction) * vColor; + } else if (kernelRadius == 2) { + gl_FragColor = blur2(uSampler, vTextureCoord, resolution, direction) * vColor; + } else { + gl_FragColor = blur3(uSampler, vTextureCoord, resolution, direction) * vColor; + } + } +`; + +/** + * 4x4 box blur shader which works in conjunction with a 50% rescale. + */ +class BoxBlurShader extends DefaultShader { + + setupUniforms(operation) { + super.setupUniforms(operation); + const dx = 1.0 / operation.getTextureWidth(0); + const dy = 1.0 / operation.getTextureHeight(0); + this._setUniform("stepTextureCoord", new Float32Array([dx, dy]), this.gl.uniform2fv); + } + +} + +BoxBlurShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + uniform vec2 stepTextureCoord; + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec4 vColor; + varying vec2 vTextureCoordUl; + varying vec2 vTextureCoordUr; + varying vec2 vTextureCoordBl; + varying vec2 vTextureCoordBr; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoordUl = aTextureCoord - stepTextureCoord; + vTextureCoordBr = aTextureCoord + stepTextureCoord; + vTextureCoordUr = vec2(vTextureCoordBr.x, vTextureCoordUl.y); + vTextureCoordBl = vec2(vTextureCoordUl.x, vTextureCoordBr.y); + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +BoxBlurShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoordUl; + varying vec2 vTextureCoordUr; + varying vec2 vTextureCoordBl; + varying vec2 vTextureCoordBr; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + vec4 color = 0.25 * (texture2D(uSampler, vTextureCoordUl) + texture2D(uSampler, vTextureCoordUr) + texture2D(uSampler, vTextureCoordBl) + texture2D(uSampler, vTextureCoordBr)); + gl_FragColor = color * vColor; + } +`; + +class BlurShader extends DefaultShader$1 { + + constructor(context) { + super(context); + this._kernelRadius = 1; + } + + get kernelRadius() { + return this._kernelRadius; + } + + set kernelRadius(v) { + this._kernelRadius = v; + this.redraw(); + } + + useDefault() { + return this._amount === 0; + } + + _beforeDrawEl({target}) { + target.ctx.filter = "blur(" + this._kernelRadius + "px)"; + } + + _afterDrawEl({target}) { + target.ctx.filter = "none"; + } + +} + +class FastBlurComponent extends Component { + static _template() { + return {} + } + + get wrap() { + return this.tag("Wrap"); + } + + set content(v) { + return this.wrap.content = v; + } + + get content() { + return this.wrap.content; + } + + set padding(v) { + this.wrap._paddingX = v; + this.wrap._paddingY = v; + this.wrap._updateBlurSize(); + } + + set paddingX(v) { + this.wrap._paddingX = v; + this.wrap._updateBlurSize(); + } + + set paddingY(v) { + this.wrap._paddingY = v; + this.wrap._updateBlurSize(); + } + + set amount(v) { + return this.wrap.amount = v; + } + + get amount() { + return this.wrap.amount; + } + + _onResize() { + this.wrap.w = this.renderWidth; + this.wrap.h = this.renderHeight; + } + + get _signalProxy() { + return true; + } + + _build() { + this.patch({ + Wrap: {type: this.stage.gl ? WebGLFastBlurComponent : C2dFastBlurComponent} + }); + } + +} + + +class C2dFastBlurComponent extends Component { + + static _template() { + return { + forceZIndexContext: true, + rtt: true, + Textwrap: {shader: {type: BlurShader}, Content: {}} + } + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap>Content"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + + } + + static getSpline() { + if (!this._multiSpline) { + this._multiSpline = new MultiSpline(); + this._multiSpline.parse(false, {0: 0, 0.25: 1.5, 0.5: 5.5, 0.75: 18, 1: 39}); + } + return this._multiSpline; + } + + get content() { + return this.sel('Textwrap>Content'); + } + + set content(v) { + this.sel('Textwrap>Content').patch(v, true); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + this._wrapper.x = paddingX; + this._textwrap.x = -paddingX; + + this._wrapper.y = paddingY; + this._textwrap.y = -paddingY; + + this._textwrap.w = w + paddingX * 2; + this._textwrap.h = h + paddingY * 2; + } + + get amount() { + return this._amount; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._textwrap.shader.kernelRadius = C2dFastBlurComponent._amountToKernelRadius(v); + } + + static _amountToKernelRadius(v) { + return C2dFastBlurComponent.getSpline().getValue(Math.min(1, v * 0.25)); + } + + get _signalProxy() { + return true; + } + +} + +class WebGLFastBlurComponent extends Component { + + static _template() { + const onUpdate = function(element, elementCore) { + if ((elementCore._recalc & (2 + 128))) { + const w = elementCore.w; + const h = elementCore.h; + let cur = elementCore; + do { + cur = cur._children[0]; + cur._element.w = w; + cur._element.h = h; + } while(cur._children); + } + }; + + return { + Textwrap: {rtt: true, forceZIndexContext: true, renderOffscreen: true, Content: {}}, + Layers: { + L0: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L1: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L2: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L3: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}} + }, + Result: {shader: {type: FastBlurOutputShader}, visible: false} + } + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap>Content"); + this._layers = this.sel("Layers"); + this._output = this.sel("Result"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + } + + _buildLayers() { + const filterShaderSettings = [{x:1,y:0,kernelRadius:1},{x:0,y:1,kernelRadius:1},{x:1.5,y:0,kernelRadius:1},{x:0,y:1.5,kernelRadius:1}]; + const filterShaders = filterShaderSettings.map(s => { + const shader = Shader.create(this.stage, Object.assign({type: LinearBlurShader}, s)); + return shader; + }); + + this._setLayerTexture(this.getLayerContents(0), this._textwrap.getTexture(), []); + this._setLayerTexture(this.getLayerContents(1), this.getLayer(0).getTexture(), [filterShaders[0], filterShaders[1]]); + + // Notice that 1.5 filters should be applied before 1.0 filters. + this._setLayerTexture(this.getLayerContents(2), this.getLayer(1).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + this._setLayerTexture(this.getLayerContents(3), this.getLayer(2).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + } + + _setLayerTexture(element, texture, steps) { + if (!steps.length) { + element.texture = texture; + } else { + const step = steps.pop(); + const child = element.stage.c({rtt: true, shader: step}); + + // Recurse. + this._setLayerTexture(child, texture, steps); + + element.childList.add(child); + } + return element; + } + + get content() { + return this.sel('Textwrap>Content'); + } + + set content(v) { + this.sel('Textwrap>Content').patch(v, true); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + getLayer(i) { + return this._layers.sel("L" + i); + } + + getLayerContents(i) { + return this.getLayer(i).sel("Content"); + } + + _onResize() { + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + let fw = w + paddingX * 2; + let fh = h + paddingY * 2; + this._textwrap.w = fw; + this._wrapper.x = paddingX; + this.getLayer(0).w = this.getLayerContents(0).w = fw / 2; + this.getLayer(1).w = this.getLayerContents(1).w = fw / 4; + this.getLayer(2).w = this.getLayerContents(2).w = fw / 8; + this.getLayer(3).w = this.getLayerContents(3).w = fw / 16; + this._output.x = -paddingX; + this._textwrap.x = -paddingX; + this._output.w = fw; + + this._textwrap.h = fh; + this._wrapper.y = paddingY; + this.getLayer(0).h = this.getLayerContents(0).h = fh / 2; + this.getLayer(1).h = this.getLayerContents(1).h = fh / 4; + this.getLayer(2).h = this.getLayerContents(2).h = fh / 8; + this.getLayer(3).h = this.getLayerContents(3).h = fh / 16; + this._output.y = -paddingY; + this._textwrap.y = -paddingY; + this._output.h = fh; + + this.w = w; + this.h = h; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._update(); + } + + get amount() { + return this._amount; + } + + _update() { + let v = Math.min(4, Math.max(0, this._amount)); + if (v === 0) { + this._textwrap.renderToTexture = false; + this._output.shader.otherTextureSource = null; + this._output.visible = false; + } else { + this._textwrap.renderToTexture = true; + this._output.visible = true; + + this.getLayer(0).visible = (v > 0); + this.getLayer(1).visible = (v > 1); + this.getLayer(2).visible = (v > 2); + this.getLayer(3).visible = (v > 3); + + if (v <= 1) { + this._output.texture = this._textwrap.getTexture(); + this._output.shader.otherTextureSource = this.getLayer(0).getTexture(); + this._output.shader.a = v; + } else if (v <= 2) { + this._output.texture = this.getLayer(0).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(1).getTexture(); + this._output.shader.a = v - 1; + } else if (v <= 3) { + this._output.texture = this.getLayer(1).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(2).getTexture(); + this._output.shader.a = v - 2; + } else if (v <= 4) { + this._output.texture = this.getLayer(2).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(3).getTexture(); + this._output.shader.a = v - 3; + } + } + } + + set shader(s) { + super.shader = s; + if (!this.renderToTexture) { + console.warn("Please enable renderToTexture to use with a shader."); + } + } + + _firstActive() { + this._buildLayers(); + } + +} + +/** + * Shader that combines two textures into one output. + */ +class FastBlurOutputShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._a = 0; + this._otherTextureSource = null; + } + + get a() { + return this._a; + } + + set a(v) { + this._a = v; + this.redraw(); + } + + set otherTextureSource(v) { + this._otherTextureSource = v; + this.redraw(); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("a", this._a, this.gl.uniform1f); + this._setUniform("uSampler2", 1, this.gl.uniform1i); + } + + beforeDraw(operation) { + let glTexture = this._otherTextureSource ? this._otherTextureSource.nativeTexture : null; + + let gl = this.gl; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.activeTexture(gl.TEXTURE0); + } +} + +FastBlurOutputShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform sampler2D uSampler2; + uniform float a; + void main(void){ + if (a == 1.0) { + gl_FragColor = texture2D(uSampler2, vTextureCoord) * vColor; + } else { + gl_FragColor = ((1.0 - a) * texture2D(uSampler, vTextureCoord) + (a * texture2D(uSampler2, vTextureCoord))) * vColor; + } + } +`; + +class BloomComponent extends Component { + + static _template() { + const onUpdate = function(element, elementCore) { + if ((elementCore._recalc & (2 + 128))) { + const w = elementCore.w; + const h = elementCore.h; + let cur = elementCore; + do { + cur = cur._children[0]; + cur._element.w = w; + cur._element.h = h; + } while(cur._children); + } + }; + + return { + Textwrap: {rtt: true, forceZIndexContext: true, renderOffscreen: true, + BloomBase: {shader: {type: BloomBaseShader}, + Content: {} + } + }, + Layers: { + L0: {rtt: true, onUpdate: onUpdate, scale: 2, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L1: {rtt: true, onUpdate: onUpdate, scale: 4, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L2: {rtt: true, onUpdate: onUpdate, scale: 8, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L3: {rtt: true, onUpdate: onUpdate, scale: 16, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}} + } + } + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap.Content"); + this._layers = this.sel("Layers"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + } + + _build() { + const filterShaderSettings = [{x:1,y:0,kernelRadius:3},{x:0,y:1,kernelRadius:3},{x:1.5,y:0,kernelRadius:3},{x:0,y:1.5,kernelRadius:3}]; + const filterShaders = filterShaderSettings.map(s => { + const shader = this.stage.createShader(Object.assign({type: LinearBlurShader}, s)); + return shader; + }); + + this._setLayerTexture(this.getLayerContents(0), this._textwrap.getTexture(), []); + this._setLayerTexture(this.getLayerContents(1), this.getLayer(0).getTexture(), [filterShaders[0], filterShaders[1]]); + + // Notice that 1.5 filters should be applied before 1.0 filters. + this._setLayerTexture(this.getLayerContents(2), this.getLayer(1).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + this._setLayerTexture(this.getLayerContents(3), this.getLayer(2).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + } + + _setLayerTexture(element, texture, steps) { + if (!steps.length) { + element.texture = texture; + } else { + const step = steps.pop(); + const child = element.stage.c({rtt: true, shader: step}); + + // Recurse. + this._setLayerTexture(child, texture, steps); + + element.childList.add(child); + } + return element; + } + + get content() { + return this.sel('Textwrap.Content'); + } + + set content(v) { + this.sel('Textwrap.Content').patch(v); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + getLayer(i) { + return this._layers.sel("L" + i); + } + + getLayerContents(i) { + return this.getLayer(i).sel("Content"); + } + + _onResize() { + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + let fw = w + paddingX * 2; + let fh = h + paddingY * 2; + this._textwrap.w = fw; + this._wrapper.x = paddingX; + this.getLayer(0).w = this.getLayerContents(0).w = fw / 2; + this.getLayer(1).w = this.getLayerContents(1).w = fw / 4; + this.getLayer(2).w = this.getLayerContents(2).w = fw / 8; + this.getLayer(3).w = this.getLayerContents(3).w = fw / 16; + this._textwrap.x = -paddingX; + + this._textwrap.h = fh; + this._wrapper.y = paddingY; + this.getLayer(0).h = this.getLayerContents(0).h = fh / 2; + this.getLayer(1).h = this.getLayerContents(1).h = fh / 4; + this.getLayer(2).h = this.getLayerContents(2).h = fh / 8; + this.getLayer(3).h = this.getLayerContents(3).h = fh / 16; + this._textwrap.y = -paddingY; + + this.w = w; + this.h = h; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._update(); + } + + get amount() { + return this._amount; + } + + _update() { + let v = Math.min(4, Math.max(0, this._amount)); + if (v > 0) { + this.getLayer(0).visible = (v > 0); + this.getLayer(1).visible = (v > 1); + this.getLayer(2).visible = (v > 2); + this.getLayer(3).visible = (v > 3); + } + } + + set shader(s) { + super.shader = s; + if (!this.renderToTexture) { + console.warn("Please enable renderToTexture to use with a shader."); + } + } + + _firstActive() { + this._build(); + } + +} + +class BloomBaseShader extends DefaultShader { +} + +BloomBaseShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + float m = max(max(color.r, color.g), color.b); + float c = max(0.0, (m - 0.80)) * 5.0; + color = color * c; + gl_FragColor = color; + } +`; + +class SmoothScaleComponent extends Component { + + static _template() { + return { + ContentWrap: {renderOffscreen: true, forceZIndexContext: true, onAfterUpdate: SmoothScaleComponent._updateDimensions, + Content: {} + }, + Scale: {visible: false} + } + } + + constructor(stage) { + super(stage); + + this._smoothScale = 1; + this._iterations = 0; + } + + get content() { + return this.tag('Content'); + } + + set content(v) { + this.tag('Content').patch(v, true); + } + + get smoothScale() { + return this._smoothScale; + } + + set smoothScale(v) { + if (this._smoothScale !== v) { + let its = 0; + while(v < 0.5 && its < 12) { + its++; + v = v * 2; + } + + this.scale = v; + this._setIterations(its); + + this._smoothScale = v; + } + } + + _setIterations(its) { + if (this._iterations !== its) { + const scalers = this.sel("Scale").childList; + const content = this.sel("ContentWrap"); + while (scalers.length < its) { + const first = scalers.length === 0; + const texture = (first ? content.getTexture() : scalers.last.getTexture()); + scalers.a({rtt: true, renderOffscreen: true, texture: texture}); + } + + SmoothScaleComponent._updateDimensions(this.tag("ContentWrap"), true); + + const useScalers = (its > 0); + this.patch({ + ContentWrap: {renderToTexture: useScalers}, + Scale: {visible: useScalers} + }); + + for (let i = 0, n = scalers.length; i < n; i++) { + scalers.getAt(i).patch({ + visible: i < its, + renderOffscreen: i !== its - 1 + }); + } + this._iterations = its; + } + } + + static _updateDimensions(contentWrap, force) { + const content = contentWrap.children[0]; + let w = content.renderWidth; + let h = content.renderHeight; + if (w !== contentWrap.w || h !== contentWrap.h || force) { + contentWrap.w = w; + contentWrap.h = h; + + const scalers = contentWrap.parent.tag("Scale").children; + for (let i = 0, n = scalers.length; i < n; i++) { + w = w * 0.5; + h = h * 0.5; + scalers[i].w = w; + scalers[i].h = h; + } + } + } + + get _signalProxy() { + return true; + } + +} + +class BorderComponent extends Component { + + static _template() { + return { + Content: {}, + Borders: { + Top: {rect: true, visible: false, mountY: 1}, + Right: {rect: true, visible: false}, + Bottom: {rect: true, visible: false}, + Left: {rect: true, visible: false, mountX: 1} + } + }; + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + + this._borderTop = this.tag("Top"); + this._borderRight = this.tag("Right"); + this._borderBottom = this.tag("Bottom"); + this._borderLeft = this.tag("Left"); + + this.onAfterUpdate = function (element) { + const content = element.childList.first; + let w = element.core.w || content.renderWidth; + let h = element.core.h || content.renderHeight; + element._borderTop.w = w; + element._borderBottom.y = h; + element._borderBottom.w = w; + element._borderLeft.h = h + element._borderTop.h + element._borderBottom.h; + element._borderLeft.y = -element._borderTop.h; + element._borderRight.x = w; + element._borderRight.h = h + element._borderTop.h + element._borderBottom.h; + element._borderRight.y = -element._borderTop.h; + }; + + this.borderWidth = 1; + } + + get content() { + return this.sel('Content'); + } + + set content(v) { + this.sel('Content').patch(v, true); + } + + get borderWidth() { + return this.borderWidthTop; + } + + get borderWidthTop() { + return this._borderTop.h; + } + + get borderWidthRight() { + return this._borderRight.w; + } + + get borderWidthBottom() { + return this._borderBottom.h; + } + + get borderWidthLeft() { + return this._borderLeft.w; + } + + set borderWidth(v) { + this.borderWidthTop = v; + this.borderWidthRight = v; + this.borderWidthBottom = v; + this.borderWidthLeft = v; + } + + set borderWidthTop(v) { + this._borderTop.h = v; + this._borderTop.visible = (v > 0); + } + + set borderWidthRight(v) { + this._borderRight.w = v; + this._borderRight.visible = (v > 0); + } + + set borderWidthBottom(v) { + this._borderBottom.h = v; + this._borderBottom.visible = (v > 0); + } + + set borderWidthLeft(v) { + this._borderLeft.w = v; + this._borderLeft.visible = (v > 0); + } + + get colorBorder() { + return this.colorBorderTop; + } + + get colorBorderTop() { + return this._borderTop.color; + } + + get colorBorderRight() { + return this._borderRight.color; + } + + get colorBorderBottom() { + return this._borderBottom.color; + } + + get colorBorderLeft() { + return this._borderLeft.color; + } + + set colorBorder(v) { + this.colorBorderTop = v; + this.colorBorderRight = v; + this.colorBorderBottom = v; + this.colorBorderLeft = v; + } + + set colorBorderTop(v) { + this._borderTop.color = v; + } + + set colorBorderRight(v) { + this._borderRight.color = v; + } + + set colorBorderBottom(v) { + this._borderBottom.color = v; + } + + set colorBorderLeft(v) { + this._borderLeft.color = v; + } + + get borderTop() { + return this._borderTop; + } + + set borderTop(settings) { + this.borderTop.patch(settings); + } + + get borderRight() { + return this._borderRight; + } + + set borderRight(settings) { + this.borderRight.patch(settings); + } + + get borderBottom() { + return this._borderBottom; + } + + set borderBottom(settings) { + this.borderBottom.patch(settings); + } + + get borderLeft() { + return this._borderLeft; + } + + set borderLeft(settings) { + this.borderLeft.patch(settings); + } + + set borders(settings) { + this.borderTop = settings; + this.borderLeft = settings; + this.borderBottom = settings; + this.borderRight = settings; + } + +} + +class GrayscaleShader extends DefaultShader$1 { + + constructor(context) { + super(context); + this._amount = 1; + } + + static getWebGL() { + return GrayscaleShader$1; + } + + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + _beforeDrawEl({target}) { + target.ctx.filter = "grayscale(" + this._amount + ")"; + } + + _afterDrawEl({target}) { + target.ctx.filter = "none"; + } + +} + +class GrayscaleShader$1 extends DefaultShader { + + constructor(context) { + super(context); + this._amount = 1; + } + + static getC2d() { + return GrayscaleShader; + } + + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("amount", this._amount, this.gl.uniform1f); + } + +} + +GrayscaleShader$1.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float amount; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + float grayness = 0.2 * color.r + 0.6 * color.g + 0.2 * color.b; + gl_FragColor = vec4(amount * vec3(grayness, grayness, grayness) + (1.0 - amount) * color.rgb, color.a); + } +`; + +/** + * This shader can be used to fix a problem that is known as 'gradient banding'. + */ +class DitheringShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._noiseTexture = new NoiseTexture(ctx.stage); + + this._graining = 1/256; + + this._random = false; + } + + set graining(v) { + this._graining = v; + this.redraw(); + } + + set random(v) { + this._random = v; + this.redraw(); + } + + setExtraAttribsInBuffer(operation) { + // Make sure that the noise texture is uploaded to the GPU. + this._noiseTexture.load(); + + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + + for (let i = 0; i < length; i++) { + + // Calculate noise texture coordinates so that it spans the full element. + let brx = operation.getElementWidth(i) / this._noiseTexture.getRenderWidth(); + let bry = operation.getElementHeight(i) / this._noiseTexture.getRenderHeight(); + + let ulx = 0; + let uly = 0; + if (this._random) { + ulx = Math.random(); + uly = Math.random(); + + brx += ulx; + bry += uly; + + if (Math.random() < 0.5) { + // Flip for more randomness. + const t = ulx; + ulx = brx; + brx = t; + } + + if (Math.random() < 0.5) { + // Flip for more randomness. + const t = uly; + uly = bry; + bry = t; + } + } + + // Specify all corner points. + floats[offset] = ulx; + floats[offset + 1] = uly; + + floats[offset + 2] = brx; + floats[offset + 3] = uly; + + floats[offset + 4] = brx; + floats[offset + 5] = bry; + + floats[offset + 6] = ulx; + floats[offset + 7] = bry; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aNoiseTextureCoord"), 2, gl.FLOAT, false, 8, this.getVertexAttribPointerOffset(operation)); + + let glTexture = this._noiseTexture.source.nativeTexture; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.activeTexture(gl.TEXTURE0); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("uNoiseSampler", 1, this.gl.uniform1i); + this._setUniform("graining", 2 * this._graining, this.gl.uniform1f); + } + + enableAttribs() { + super.enableAttribs(); + let gl = this.gl; + gl.enableVertexAttribArray(this._attrib("aNoiseTextureCoord")); + } + + disableAttribs() { + super.disableAttribs(); + let gl = this.gl; + gl.disableVertexAttribArray(this._attrib("aNoiseTextureCoord")); + } + + useDefault() { + return this._graining === 0; + } + + afterDraw(operation) { + if (this._random) { + this.redraw(); + } + } + +} + +DitheringShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec2 aNoiseTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec2 vNoiseTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vNoiseTextureCoord = aNoiseTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +DitheringShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec2 vNoiseTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform sampler2D uNoiseSampler; + uniform float graining; + void main(void){ + vec4 noise = texture2D(uNoiseSampler, vNoiseTextureCoord); + vec4 color = texture2D(uSampler, vTextureCoord); + gl_FragColor = (color * vColor) + graining * (noise.r - 0.5); + } +`; + +class CircularPushShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._inputValue = 0; + + this._maxDerivative = 0.01; + + this._normalizedValue = 0; + + // The offset between buckets. A value between 0 and 1. + this._offset = 0; + + this._amount = 0.1; + + this._aspectRatio = 1; + + this._offsetX = 0; + + this._offsetY = 0; + + this.buckets = 100; + } + + get aspectRatio() { + return this._aspectRatio; + } + + set aspectRatio(v) { + this._aspectRatio = v; + this.redraw(); + } + + get offsetX() { + return this._offsetX; + } + + set offsetX(v) { + this._offsetX = v; + this.redraw(); + } + + get offsetY() { + return this._offsetY; + } + + set offsetY(v) { + this._offsetY = v; + this.redraw(); + } + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + set inputValue(v) { + this._inputValue = v; + } + + get inputValue() { + return this._inputValue; + } + + set maxDerivative(v) { + this._maxDerivative = v; + } + + get maxDerivative() { + return this._maxDerivative; + } + + set buckets(v) { + if (v > 100) { + console.warn("CircularPushShader: supports max 100 buckets"); + v = 100; + } + + // This should be set before starting. + this._buckets = v; + + // Init values array in the correct length. + this._values = new Uint8Array(this._getValues(v)); + + this.redraw(); + } + + get buckets() { + return this._buckets; + } + + _getValues(n) { + const v = []; + for (let i = 0; i < n; i++) { + v.push(this._inputValue); + } + return v; + } + + /** + * Progresses the shader with the specified (fractional) number of buckets. + * @param {number} o; + * A number from 0 to 1 (1 = all buckets). + */ + progress(o) { + this._offset += o * this._buckets; + const full = Math.floor(this._offset); + this._offset -= full; + this._shiftBuckets(full); + this.redraw(); + } + + _shiftBuckets(n) { + for (let i = this._buckets - 1; i >= 0; i--) { + const targetIndex = i - n; + if (targetIndex < 0) { + this._normalizedValue = Math.min(this._normalizedValue + this._maxDerivative, Math.max(this._normalizedValue - this._maxDerivative, this._inputValue)); + this._values[i] = 255 * this._normalizedValue; + } else { + this._values[i] = this._values[targetIndex]; + } + } + } + + set offset(v) { + this._offset = v; + this.redraw(); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("aspectRatio", this._aspectRatio, this.gl.uniform1f); + this._setUniform("offsetX", this._offsetX, this.gl.uniform1f); + this._setUniform("offsetY", this._offsetY, this.gl.uniform1f); + this._setUniform("amount", this._amount, this.gl.uniform1f); + this._setUniform("offset", this._offset, this.gl.uniform1f); + this._setUniform("buckets", this._buckets, this.gl.uniform1f); + this._setUniform("uValueSampler", 1, this.gl.uniform1i); + } + + useDefault() { + return this._amount === 0; + } + + beforeDraw(operation) { + const gl = this.gl; + gl.activeTexture(gl.TEXTURE1); + if (!this._valuesTexture) { + this._valuesTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._valuesTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (Utils.isNode) { + gl.pixelStorei(gl.UNPACK_FLIP_BLUE_RED, false); + } + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + } else { + gl.bindTexture(gl.TEXTURE_2D, this._valuesTexture); + } + + // Upload new values. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this._buckets, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this._values); + gl.activeTexture(gl.TEXTURE0); + } + + cleanup() { + if (this._valuesTexture) { + this.gl.deleteTexture(this._valuesTexture); + } + } + + +} + +CircularPushShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + uniform float offsetX; + uniform float offsetY; + uniform float aspectRatio; + varying vec2 vTextureCoord; + varying vec2 vPos; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vPos = vTextureCoord * 2.0 - 1.0; + vPos.y = vPos.y * aspectRatio; + vPos.y = vPos.y + offsetY; + vPos.x = vPos.x + offsetX; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +CircularPushShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vPos; + uniform float amount; + uniform float offset; + uniform float values[100]; + uniform float buckets; + uniform sampler2D uSampler; + uniform sampler2D uValueSampler; + void main(void){ + float l = length(vPos); + float m = (l * buckets * 0.678 - offset) / buckets; + float f = texture2D(uValueSampler, vec2(m, 0.0)).a * amount; + vec2 unit = vPos / l; + gl_FragColor = texture2D(uSampler, vTextureCoord - f * unit) * vColor; + } +`; + +class InversionShader extends DefaultShader { + + constructor(context) { + super(context); + this._amount = 1; + } + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("amount", this._amount, this.gl.uniform1f); + } + +} + +InversionShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float amount; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord); + color.rgb = color.rgb * (1.0 - amount) + amount * (1.0 * color.a - color.rgb); + gl_FragColor = color * vColor; + } +`; + +class OutlineShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + this._width = 5; + this._col = 0xFFFFFFFF; + this._color = [1,1,1,1]; + } + + set width(v) { + this._width = v; + this.redraw(); + } + + get color() { + return this._col; + } + + set color(v) { + if (this._col !== v) { + const col = StageUtils.getRgbaComponentsNormalized(v); + col[0] = col[0] * col[3]; + col[1] = col[1] * col[3]; + col[2] = col[2] * col[3]; + + this._color = col; + + this.redraw(); + + this._col = v; + } + } + + useDefault() { + return (this._width === 0 || this._col[3] === 0); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + let gl = this.gl; + this._setUniform("color", new Float32Array(this._color), gl.uniform4fv); + } + + enableAttribs() { + super.enableAttribs(); + this.gl.enableVertexAttribArray(this._attrib("aCorner")); + } + + disableAttribs() { + super.disableAttribs(); + this.gl.disableVertexAttribArray(this._attrib("aCorner")); + } + + setExtraAttribsInBuffer(operation) { + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + + for (let i = 0; i < length; i++) { + + const elementCore = operation.getElementCore(i); + + // We are setting attributes such that if the value is < 0 or > 1, a border should be drawn. + const ddw = this._width / elementCore.w; + const dw = ddw / (1 - 2 * ddw); + const ddh = this._width / elementCore.h; + const dh = ddh / (1 - 2 * ddh); + + // Specify all corner points. + floats[offset] = -dw; + floats[offset + 1] = -dh; + + floats[offset + 2] = 1 + dw; + floats[offset + 3] = -dh; + + floats[offset + 4] = 1 + dw; + floats[offset + 5] = 1 + dh; + + floats[offset + 6] = -dw; + floats[offset + 7] = 1 + dh; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aCorner"), 2, gl.FLOAT, false, 8, this.getVertexAttribPointerOffset(operation)); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + +} + +OutlineShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + attribute vec2 aCorner; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec2 vCorner; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vCorner = aCorner; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +OutlineShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vCorner; + uniform vec4 color; + uniform sampler2D uSampler; + void main(void){ + vec2 m = min(vCorner, 1.0 - vCorner); + float value = step(0.0, min(m.x, m.y)); + gl_FragColor = mix(color, texture2D(uSampler, vTextureCoord) * vColor, value); + } +`; + +/** + * @see https://github.com/pixijs/pixi-filters/tree/master/filters/pixelate/src + */ +class PixelateShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._size = new Float32Array([4, 4]); + } + + get x() { + return this._size[0]; + } + + set x(v) { + this._size[0] = v; + this.redraw(); + } + + get y() { + return this._size[1]; + } + + set y(v) { + this._size[1] = v; + this.redraw(); + } + + get size() { + return this._size[0]; + } + + set size(v) { + this._size[0] = v; + this._size[1] = v; + this.redraw(); + } + + useDefault() { + return ((this._size[0] === 0) && (this._size[1] === 0)); + } + + static getWebGLImpl() { + return WebGLPixelateShaderImpl; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + let gl = this.gl; + this._setUniform("size", new Float32Array(this._size), gl.uniform2fv); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + + enableAttribs() { + super.enableAttribs(); + this.gl.enableVertexAttribArray(this._attrib("aTextureRes")); + } + + disableAttribs() { + super.disableAttribs(); + this.gl.disableVertexAttribArray(this._attrib("aTextureRes")); + } + + setExtraAttribsInBuffer(operation) { + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + for (let i = 0; i < length; i++) { + let w = operation.quads.getTextureWidth(operation.index + i); + let h = operation.quads.getTextureHeight(operation.index + i); + + floats[offset] = w; + floats[offset + 1] = h; + floats[offset + 2] = w; + floats[offset + 3] = h; + floats[offset + 4] = w; + floats[offset + 5] = h; + floats[offset + 6] = w; + floats[offset + 7] = h; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aTextureRes"), 2, gl.FLOAT, false, this.getExtraAttribBytesPerVertex(), this.getVertexAttribPointerOffset(operation)); + } +} + +PixelateShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + attribute vec2 aTextureRes; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vTextureRes; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + vTextureRes = aTextureRes; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +PixelateShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vTextureRes; + + uniform vec2 size; + uniform sampler2D uSampler; + + vec2 mapCoord( vec2 coord ) + { + coord *= vTextureRes.xy; + return coord; + } + + vec2 unmapCoord( vec2 coord ) + { + coord /= vTextureRes.xy; + return coord; + } + + vec2 pixelate(vec2 coord, vec2 size) + { + return floor( coord / size ) * size; + } + + void main(void) + { + vec2 coord = mapCoord(vTextureCoord); + coord = pixelate(coord, size); + coord = unmapCoord(coord); + gl_FragColor = texture2D(uSampler, coord) * vColor; + } +`; + +class RadialFilterShader extends DefaultShader { + constructor(context) { + super(context); + this._radius = 0; + this._cutoff = 1; + } + + set radius(v) { + this._radius = v; + this.redraw(); + } + + get radius() { + return this._radius; + } + + set cutoff(v) { + this._cutoff = v; + this.redraw(); + } + + get cutoff() { + return this._cutoff; + } + + useDefault() { + return this._radius === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + // We substract half a pixel to get a better cutoff effect. + this._setUniform("radius", 2 * (this._radius - 0.5) / operation.getRenderWidth(), this.gl.uniform1f); + this._setUniform("cutoff", 0.5 * operation.getRenderWidth() / this._cutoff, this.gl.uniform1f); + } + +} + +RadialFilterShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 pos; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + pos = gl_Position.xy; + } +`; + +RadialFilterShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec2 pos; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float radius; + uniform float cutoff; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord); + float f = max(0.0, min(1.0, 1.0 - (length(pos) - radius) * cutoff)); + gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor * f; + } +`; + +class RadialGradientShader extends DefaultShader { + + constructor(context) { + super(context); + + this._x = 0; + this._y = 0; + + this.color = 0xFFFF0000; + + this._radiusX = 100; + this._radiusY = 100; + } + + set x(v) { + this._x = v; + this.redraw(); + } + + set y(v) { + this._y = v; + this.redraw(); + } + + set radiusX(v) { + this._radiusX = v; + this.redraw(); + } + + get radiusX() { + return this._radiusX; + } + + set radiusY(v) { + this._radiusY = v; + this.redraw(); + } + + get radiusY() { + return this._radiusY; + } + + set radius(v) { + this.radiusX = v; + this.radiusY = v; + } + + get color() { + return this._color; + } + + set color(v) { + if (this._color !== v) { + const col = StageUtils.getRgbaComponentsNormalized(v); + col[0] = col[0] * col[3]; + col[1] = col[1] * col[3]; + col[2] = col[2] * col[3]; + + this._rawColor = new Float32Array(col); + + this.redraw(); + + this._color = v; + } + } + + setupUniforms(operation) { + super.setupUniforms(operation); + // We substract half a pixel to get a better cutoff effect. + const rtc = operation.getNormalRenderTextureCoords(this._x, this._y); + this._setUniform("center", new Float32Array(rtc), this.gl.uniform2fv); + + this._setUniform("radius", 2 * this._radiusX / operation.getRenderWidth(), this.gl.uniform1f); + + + // Radial gradient shader is expected to be used on a single element. That element's alpha is used. + this._setUniform("alpha", operation.getElementCore(0).renderContext.alpha, this.gl.uniform1f); + + this._setUniform("color", this._rawColor, this.gl.uniform4fv); + this._setUniform("aspectRatio", (this._radiusX/this._radiusY) * operation.getRenderHeight()/operation.getRenderWidth(), this.gl.uniform1f); + } + +} + +RadialGradientShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + uniform vec2 center; + uniform float aspectRatio; + varying vec2 pos; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + pos = gl_Position.xy - center; + pos.y = pos.y * aspectRatio; + } +`; + +RadialGradientShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 pos; + uniform sampler2D uSampler; + uniform float radius; + uniform vec4 color; + uniform float alpha; + void main(void){ + float dist = length(pos); + gl_FragColor = mix(color * alpha, texture2D(uSampler, vTextureCoord) * vColor, min(1.0, dist / radius)); + } +`; + +class Light3dShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._strength = 0.5; + this._ambient = 0.5; + this._fudge = 0.4; + + this._rx = 0; + this._ry = 0; + + this._z = 0; + this._pivotX = NaN; + this._pivotY = NaN; + this._pivotZ = 0; + + this._lightY = 0; + this._lightZ = 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + + let vr = operation.shaderOwner; + let element = vr.element; + + let pivotX = isNaN(this._pivotX) ? element.pivotX * vr.w : this._pivotX; + let pivotY = isNaN(this._pivotY) ? element.pivotY * vr.h : this._pivotY; + let coords = vr.getRenderTextureCoords(pivotX, pivotY); + + // Counter normal rotation. + + let rz = -Math.atan2(vr._renderContext.tc, vr._renderContext.ta); + + let gl = this.gl; + this._setUniform("pivot", new Float32Array([coords[0], coords[1], this._pivotZ]), gl.uniform3fv); + this._setUniform("rot", new Float32Array([this._rx, this._ry, rz]), gl.uniform3fv); + + this._setUniform("z", this._z, gl.uniform1f); + this._setUniform("lightY", this.lightY, gl.uniform1f); + this._setUniform("lightZ", this.lightZ, gl.uniform1f); + this._setUniform("strength", this._strength, gl.uniform1f); + this._setUniform("ambient", this._ambient, gl.uniform1f); + this._setUniform("fudge", this._fudge, gl.uniform1f); + } + + set strength(v) { + this._strength = v; + this.redraw(); + } + + get strength() { + return this._strength; + } + + set ambient(v) { + this._ambient = v; + this.redraw(); + } + + get ambient() { + return this._ambient; + } + + set fudge(v) { + this._fudge = v; + this.redraw(); + } + + get fudge() { + return this._fudge; + } + + get rx() { + return this._rx; + } + + set rx(v) { + this._rx = v; + this.redraw(); + } + + get ry() { + return this._ry; + } + + set ry(v) { + this._ry = v; + this.redraw(); + } + + get z() { + return this._z; + } + + set z(v) { + this._z = v; + this.redraw(); + } + + get pivotX() { + return this._pivotX; + } + + set pivotX(v) { + this._pivotX = v + 1; + this.redraw(); + } + + get pivotY() { + return this._pivotY; + } + + set pivotY(v) { + this._pivotY = v + 1; + this.redraw(); + } + + get lightY() { + return this._lightY; + } + + set lightY(v) { + this._lightY = v; + this.redraw(); + } + + get pivotZ() { + return this._pivotZ; + } + + set pivotZ(v) { + this._pivotZ = v; + this.redraw(); + } + + get lightZ() { + return this._lightZ; + } + + set lightZ(v) { + this._lightZ = v; + this.redraw(); + } + + useDefault() { + return (this._rx === 0 && this._ry === 0 && this._z === 0 && this._strength === 0 && this._ambient === 1); + } + +} + +Light3dShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + + uniform float fudge; + uniform float strength; + uniform float ambient; + uniform float z; + uniform float lightY; + uniform float lightZ; + uniform vec3 pivot; + uniform vec3 rot; + varying vec3 pos; + + void main(void) { + pos = vec3(aVertexPosition.xy, z); + + pos -= pivot; + + // Undo XY rotation + mat2 iRotXy = mat2( cos(rot.z), sin(rot.z), + -sin(rot.z), cos(rot.z)); + pos.xy = iRotXy * pos.xy; + + // Perform 3d rotations + gl_Position.x = cos(rot.x) * pos.x - sin(rot.x) * pos.z; + gl_Position.y = pos.y; + gl_Position.z = sin(rot.x) * pos.x + cos(rot.x) * pos.z; + + pos.x = gl_Position.x; + pos.y = cos(rot.y) * gl_Position.y - sin(rot.y) * gl_Position.z; + pos.z = sin(rot.y) * gl_Position.y + cos(rot.y) * gl_Position.z; + + // Redo XY rotation + iRotXy[0][1] = -iRotXy[0][1]; + iRotXy[1][0] = -iRotXy[1][0]; + pos.xy = iRotXy * pos.xy; + + // Undo translate to pivot position + pos.xyz += pivot; + + pos = vec3(pos.x * projection.x - 1.0, pos.y * -abs(projection.y) + 1.0, pos.z * projection.x); + + // Set depth perspective + float perspective = 1.0 + fudge * pos.z; + + pos.z += lightZ * projection.x; + + // Map coords to gl coordinate space. + // Set z to 0 because we don't want to perform z-clipping + gl_Position = vec4(pos.xy, 0.0, perspective); + + // Correct light source position. + pos.y += lightY * abs(projection.y); + + vTextureCoord = aTextureCoord; + vColor = aColor; + + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +Light3dShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec3 pos; + uniform sampler2D uSampler; + uniform float ambient; + uniform float strength; + void main(void){ + vec4 rgba = texture2D(uSampler, vTextureCoord); + float d = length(pos); + float n = 1.0 / max(0.1, d); + rgba.rgb = rgba.rgb * (strength * n + ambient); + gl_FragColor = rgba * vColor; + } +`; + +const lightning = { + Application, + Component, + Base, + Utils, + StageUtils, + Element, + Tools, + Stage, + ElementCore, + ElementTexturizer, + Texture, + EventEmitter, + shaders: { + Grayscale: GrayscaleShader$1, + BoxBlur: BoxBlurShader, + Dithering: DitheringShader, + CircularPush: CircularPushShader, + Inversion: InversionShader, + LinearBlur: LinearBlurShader, + Outline: OutlineShader, + Pixelate: PixelateShader, + RadialFilter: RadialFilterShader, + RadialGradient: RadialGradientShader, + Light3d: Light3dShader, + WebGLShader, + WebGLDefaultShader: DefaultShader, + C2dShader, + C2dDefaultShader: DefaultShader$1, + c2d: { + Grayscale: GrayscaleShader, + Blur: BlurShader + } + }, + textures: { + RectangleTexture, + NoiseTexture, + TextTexture, + ImageTexture, + HtmlTexture, + StaticTexture, + StaticCanvasTexture, + SourceTexture + }, + components: { + FastBlurComponent, + BloomComponent, + SmoothScaleComponent, + BorderComponent, + ListComponent + }, + tools: { + ObjMerger, + ObjectListProxy, + ObjectListWrapper + } +}; + +if (Utils.isWeb) { + window.lng = lightning; +} + +class SparkMediaplayer extends lightning.Component { + + _construct(){ + this._skipRenderToTexture = false; + } + + static _supportedEvents() + { + return ['onProgressUpdate', 'onEndOfStream']; + } + + static _template() { + return { + Video: { + VideoWrap: { + VideoTexture: { + visible: false, + pivot: 0.5, + texture: {type: lightning.textures.StaticTexture, options: {}} + } + } + } + }; + } + + set skipRenderToTexture (v) { + this._skipRenderToTexture = v; + } + + set textureMode(v) { + return this._textureMode = v; + } + + get textureMode() { + return this._textureMode; + } + + get videoView() { + return this.tag("Video"); + } + + _init() { + + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + + this.videoEl = sparkscene.create({ + t: "video", + id: "video-player", + autoPlay: "false", + proxy:proxyServer + }); + + var _this = this; + sparkscene.on('onClose' , function(e) { + _this.close(); + }); + + this.eventHandlers = []; + } + + _registerListeners() { + SparkMediaplayer._supportedEvents().forEach(event => { + const handler = (e) => { + this.fire(event, {videoElement: this.videoEl, event: e}); + }; + this.eventHandlers.push(handler); + this.videoEl.on(event, handler); + }); + } + + _deregisterListeners() { + SparkMediaplayer._supportedEvents().forEach((event, index) => { + this.videoEl.delListener(event, this.eventHandlers[index]); + }); + this.eventHandlers = []; + } + + _attach() { + this._registerListeners(); + } + + _detach() { + this._deregisterListeners(); + } + + updateSettings(settings = {}) { + // The Component that 'consumes' the media player. + this._consumer = settings.consumer; + + if (this._consumer && this._consumer.getMediaplayerSettings) { + // Allow consumer to add settings. + settings = Object.assign(settings, this._consumer.getMediaplayerSettings()); + } + + if (!lightning.Utils.equalValues(this._stream, settings.stream)) { + if (settings.stream && settings.stream.keySystem) { + navigator.requestMediaKeySystemAccess(settings.stream.keySystem.id, settings.stream.keySystem.config).then((keySystemAccess) => { + return keySystemAccess.createMediaKeys(); + }).then((createdMediaKeys) => { + return this.videoEl.setMediaKeys(createdMediaKeys); + }).then(() => { + if (settings.stream && settings.stream.src) + this.open(settings.stream.src); + }).catch(() => { + console.error('Failed to set up MediaKeys'); + }); + } else if (settings.stream && settings.stream.src) { + this.open(settings.stream.src); + this._setHide(settings.hide); + this._setVideoArea(settings.videoPos); + this.doPlay(); + } else { + this.close(); + } + this._stream = settings.stream; + } + } + + _setHide(hide) { + this.videoEl.a = hide ? 0 : 1; + } + + open(url) { + console.log('Playing stream', url); + if (this.application.noVideo) { + console.log('noVideo option set, so ignoring: ' + url); + return; + } + if (this.videoEl.url === url) return this.reload(); + this.videoEl.url = url; + } + + close() { + this.videoEl.stop(); + this._clearSrc(); + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + this.videoEl.play(); + } + + doPause() { + this.videoEl.pause(); + } + + reload() { + var url = this.videoEl.url; + this.close(); + this.videoEl.url = url; + } + + getPosition() { + return Promise.resolve(this.videoEl.position); + } + + setPosition(pos) { + this.videoEl.position = pos; + } + + getDuration() { + return Promise.resolve(this.videoEl.duration); + } + + seek(time, absolute = false) { + if(absolute) { + this.videoEl.position = time; + } + else { + this.videoEl.setPositionRelative(time); + } + } + + _setVideoArea(videoPos) { + if (lightning.Utils.equalValues(this._videoPos, videoPos)) { + return; + } + + this._videoPos = videoPos; + + if (this.textureMode) { + this.videoTextureView.patch({ + smooth: { + x: videoPos[0], + y: videoPos[1], + w: videoPos[2] - videoPos[0], + h: videoPos[3] - videoPos[1] + } + }); + } else { + const precision = this.stage.getRenderPrecision(); + this.videoEl.x = Math.round(videoPos[0] * precision) + 'px'; + this.videoEl.y = Math.round(videoPos[1] * precision) + 'px'; + this.videoEl.w = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px'; + this.videoEl.h = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px'; + } + } + + _fireConsumer(event, args) { + if (this._consumer) { + this._consumer.fire(event, args); + } + } + + _equalInitData(buf1, buf2) { + if (!buf1 || !buf2) return false; + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Int8Array(buf1); + const dv2 = new Int8Array(buf2); + for (let i = 0 ; i != buf1.byteLength ; i++) + if (dv1[i] != dv2[i]) return false; + return true; + } + + error(args) { + this._fireConsumer('$mediaplayerError', args); + this._setState(""); + return ""; + } + + loadeddata(args) { + this._fireConsumer('$mediaplayerLoadedData', args); + } + + play(args) { + this._fireConsumer('$mediaplayerPlay', args); + } + + playing(args) { + this._fireConsumer('$mediaplayerPlaying', args); + this._setState("Playing"); + } + + canplay(args) { + this.videoEl.play(); + this._fireConsumer('$mediaplayerStart', args); + } + + loadstart(args) { + this._fireConsumer('$mediaplayerLoad', args); + } + + seeked(args) { + this._fireConsumer('$mediaplayerSeeked', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + seeking(args) { + this._fireConsumer('$mediaplayerSeeking', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + onEndOfStream(args) { + this._fireConsumer('$mediaplayerEnded', args); + this._setState(""); + } + + onProgressUpdate(args) { + this._fireConsumer('$mediaplayerProgress', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + durationchange(args) { + this._fireConsumer('$mediaplayerDurationChange', args); + } + + encrypted(args) { + const video = args.videoElement; + const event = args.event; + // FIXME: Double encrypted events need to be properly filtered by Gstreamer + if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { + this._previousInitData = event.initData; + this._fireConsumer('$mediaplayerEncrypted', args); + } + } + + static _states() { + return [ + class Playing extends this { + $enter() { + this._startUpdatingVideoTexture(); + } + $exit() { + this._stopUpdatingVideoTexture(); + } + pause(args) { + this._fireConsumer('$mediaplayerPause', args); + this._setState("Playing.Paused"); + } + _clearSrc() { + this._fireConsumer('$mediaplayerStop', {}); + this._setState(""); + } + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } +} + +class SparkPlatform { + + init(stage) { + this.stage = stage; + this._looping = false; + this._awaitingLoop = false; + this._sparkCanvas = null; + } + + destroy() { + } + + startLoop() { + this._looping = true; + if (!this._awaitingLoop) { + this.loop(); + } + } + + stopLoop() { + this._looping = false; + } + + loop() { + let self = this; + let lp = function() { + self._awaitingLoop = false; + if (self._looping) { + self.stage.drawFrame(); + if (self.changes) { + // We depend on blit to limit to 60fps. + setImmediate(lp); + } else { + setTimeout(lp, 32); + } + self._awaitingLoop = true; + } + }; + setTimeout(lp, 32); + } + + uploadGlTexture(gl, textureSource, source, options) { + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, textureSource.w, textureSource.h, 0, options.format, options.type, source); + } + + loadSrcTexture({src}, cb) { + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + let imageResource = sparkscene.create({t:"imageResource", url:src, proxy:proxyServer}); + imageResource.ready.then(function(res) { + let sparkImage = sparkscene.create({t:"image", resource:res}); + const sparkGl = this.stage.gl; + sparkImage.ready.then( function(obj) { + let texture = sparkImage.texture(); + cb(null, {source: sparkGl.createWebGLTexture(texture), w: sparkImage.resource.w, h: sparkImage.resource.h, premultiplyAlpha: false, flipBlueRed: false, imageRef: sparkImage, flipTextureY:true}); + }); + }.bind(this)); + } + + createRoundRect(cb, stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (fill === undefined) fill = true; + if (strokeWidth === undefined) strokeWidth = 0; + if (fillColor === undefined) fillColor = 0; + + fillColor = fill ? fillColor : 0; + fillColor = fillColor.toString(16); + let opacity = 1; + if (fillColor.length >= 8) + { + let alpha = fillColor.substring(0,2); + let red = fillColor.substring(2,4); + let green = fillColor.substring(4,6); + let blue = fillColor.substring(6); + fillColor = "#" + red + green + blue; + opacity = "0x"+alpha; + opacity = parseInt(opacity, 16) / 255; + } + let boundW = w+strokeWidth; + let boundH = h+strokeWidth; + let data = "data:image/svg,"+ + `` + + `` + + ''; + + let imageObj = sparkscene.create({ t: "image", url:data}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }); + } + + createShadowRect(cb, stage, w, h, radius, blur, margin) { + let boundW = w + margin * 2; + let boundH = h + margin * 2; + let data = "data:image/svg,"+ + ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + let imageObj = sparkscene.create({ t: "image", url:data}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }); + } + + createSvg(cb, stage, url, w, h) { + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + let imageResource = sparkscene.create({t:"imageResource", url:src, proxy:proxyServer}); + let imageObj = sparkscene.create({ t: "image", resource:imageResource}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }, function(obj) { + let canvas = {}; + canvas.internal = imageObj; + cb(null, canvas); }); + } + + createWebGLContext(w, h) { + let options = {width: w, height: h, title: "WebGL"}; + const windowOptions = this.stage.getOption('window'); + if (windowOptions) { + options = Object.assign(options, windowOptions); + } + let gl = sparkgles2.init(options); + return gl; + } + + getWebGLCanvas() { + return; + } + + getTextureOptionsForDrawingCanvas(canvas) { + let options = {}; + + if (canvas && canvas.internal) + { + options.source = this.stage.gl.createWebGLTexture(canvas.internal.texture()); + options.w = canvas.width; + options.h = canvas.height; + options.imageRef = canvas.internal; + if (canvas.flipTextureY) { + options.flipTextureY = true; + } + } + options.premultiplyAlpha = false; + options.flipBlueRed = false; + return options; + } + + getHrTime() { + let hrTime = process.hrtime(); + return 1e3 * hrTime[0] + (hrTime[1] / 1e6); + } + + getDrawingCanvas() { + let sparkCanvas; + { + this._sparkCanvas = null; + } + if (this._sparkCanvas === null) { + sparkCanvas = {}; + sparkCanvas.internal = sparkscene.create({t: "textCanvas"}); + sparkCanvas.internal.colorMode = "ARGB"; + this._sparkCanvas = sparkCanvas; + this._sparkCanvas.getContext = function() { + return sparkCanvas.internal; + }; + } + return this._sparkCanvas; + } + + nextFrame(changes) { + this.changes = changes; + if (this.stage && this.stage.gl) { + this.stage.gl.scissor(0,0,0,0); + } + //gles2.nextFrame(changes); + } + + registerKeyHandler(keyhandler) { + console.warn("No support for key handling"); + } + + drawText(textTextureRenderer) { + let canvasInternal = textTextureRenderer._canvas.internal; // _canvas.internal is a pxTextCanvas object created in getDrawingCanvas() + let drawPromise = new Promise((resolve, reject) => { + canvasInternal.ready.then( () => { // waiting for the empty scene + canvasInternal.parent = sparkscene.root; + textTextureRenderer.setFontProperties(); + canvasInternal.font.ready.then(() => { // the font might have been coerced + canvasInternal.pixelSize = textTextureRenderer._settings.fontSize * textTextureRenderer.getPrecision(); + // Original Lightining code with some changes begins here + // Changes to the original code are: + // Replaced: `this.` => `textTextureRenderer.` + // Replaced `StageUtils.getRgbaString(color)` => `color` + // Replaced `this._canvas.width` => `canvasInternal.width` and `this._canvas.height` => `canvasInternal.height` after the line: // Add extra margin to prevent issue with clipped text when scaling. + // setFontProperties() calls are commented out as redundant + // Setting canvas label to faciliatate debugging (this is optional and can be removed): + // canvasInternal.label = textTextureRenderer._settings.text.slice(0, 10) + '..'; + let renderInfo = {}; + const precision = textTextureRenderer.getPrecision(); + let paddingLeft = textTextureRenderer._settings.paddingLeft * precision; + let paddingRight = textTextureRenderer._settings.paddingRight * precision; + const fontSize = textTextureRenderer._settings.fontSize * precision; + let offsetY = textTextureRenderer._settings.offsetY === null ? null : (textTextureRenderer._settings.offsetY * precision); + let lineHeight = textTextureRenderer._settings.lineHeight * precision; + const w = textTextureRenderer._settings.w * precision; + const h = textTextureRenderer._settings.h * precision; + let wordWrapWidth = textTextureRenderer._settings.wordWrapWidth * precision; + const cutSx = textTextureRenderer._settings.cutSx * precision; + const cutEx = textTextureRenderer._settings.cutEx * precision; + const cutSy = textTextureRenderer._settings.cutSy * precision; + const cutEy = textTextureRenderer._settings.cutEy * precision; + + canvasInternal.label = textTextureRenderer._settings.text.slice(0, 10) + '..'; // allows to distinguish different canvases by label, useful for debugging + // Set font properties. + // textTextureRenderer.setFontProperties(); + // Total width. + let width = w || (2048 / textTextureRenderer.getPrecision()); + // Inner width. + let innerWidth = width - (paddingLeft); + if (innerWidth < 10) { + width += (10 - innerWidth); + innerWidth += (10 - innerWidth); + } + if (!wordWrapWidth) { + wordWrapWidth = innerWidth; + } + // word wrap + // preserve original text + let linesInfo; + if (textTextureRenderer._settings.wordWrap) { + linesInfo = textTextureRenderer.wrapText(textTextureRenderer._settings.text, wordWrapWidth); + } else { + linesInfo = {l: textTextureRenderer._settings.text.split(/(?:\r\n|\r|\n)/), n: []}; + let n = linesInfo.l.length; + for (let i = 0; i < n - 1; i++) { + linesInfo.n.push(i); + } + } + let lines = linesInfo.l; + if (textTextureRenderer._settings.maxLines && lines.length > textTextureRenderer._settings.maxLines) { + let usedLines = lines.slice(0, textTextureRenderer._settings.maxLines); + let otherLines = null; + if (textTextureRenderer._settings.maxLinesSuffix) { + // Wrap again with max lines suffix enabled. + let w = textTextureRenderer._settings.maxLinesSuffix ? textTextureRenderer._context.measureText(textTextureRenderer._settings.maxLinesSuffix).width : 0; + let al = textTextureRenderer.wrapText(usedLines[usedLines.length - 1], wordWrapWidth - w); + usedLines[usedLines.length - 1] = al.l[0] + textTextureRenderer._settings.maxLinesSuffix; + otherLines = [al.l.length > 1 ? al.l[1] : '']; + } else { + otherLines = ['']; + } + // Re-assemble the remaining text. + let i, n = lines.length; + let j = 0; + let m = linesInfo.n.length; + for (i = textTextureRenderer._settings.maxLines; i < n; i++) { + otherLines[j] += (otherLines[j] ? " " : "") + lines[i]; + if (i + 1 < m && linesInfo.n[i + 1]) { + j++; + } + } + renderInfo.remainingText = otherLines.join("\n"); + renderInfo.moreTextLines = true; + lines = usedLines; + } else { + renderInfo.moreTextLines = false; + renderInfo.remainingText = ""; + } + // calculate text width + let maxLineWidth = 0; + let lineWidths = []; + for (let i = 0; i < lines.length; i++) { + let lineWidth = textTextureRenderer._context.measureText(lines[i]).width; + lineWidths.push(lineWidth); + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + renderInfo.lineWidths = lineWidths; + if (!w) { + // Auto-set width to max text length. + width = maxLineWidth + paddingLeft + paddingRight; + innerWidth = maxLineWidth; + } + // calculate text height + lineHeight = lineHeight || fontSize; + let height; + if (h) { + height = h; + } else { + height = lineHeight * (lines.length - 1) + 0.5 * fontSize + Math.max(lineHeight, fontSize) + offsetY; + } + if (offsetY === null) { + offsetY = fontSize; + } + renderInfo.w = width; + renderInfo.h = height; + renderInfo.lines = lines; + renderInfo.precision = precision; + if (!width) { + // To prevent canvas errors. + width = 1; + } + if (!height) { + // To prevent canvas errors. + height = 1; + } + if (cutSx || cutEx) { + width = Math.min(width, cutEx - cutSx); + } + if (cutSy || cutEy) { + height = Math.min(height, cutEy - cutSy); + } + // Add extra margin to prevent issue with clipped text when scaling. + canvasInternal.width = Math.ceil(width + textTextureRenderer._stage.getOption('textRenderIssueMargin')); + canvasInternal.height = Math.ceil(height); + // Canvas context has been reset. + // textTextureRenderer.setFontProperties(); + if (fontSize >= 128) { + // WpeWebKit bug: must force compositing because cairo-traps-compositor will not work with text first. + textTextureRenderer._context.globalAlpha = 0.01; + textTextureRenderer._context.fillRect(0, 0, 0.01, 0.01); + textTextureRenderer._context.globalAlpha = 1.0; + } + if (cutSx || cutSy) { + textTextureRenderer._context.translate(-cutSx, -cutSy); + } + let linePositionX; + let linePositionY; + let drawLines = []; + // Draw lines line by line. + for (let i = 0, n = lines.length; i < n; i++) { + linePositionX = 0; + linePositionY = (i * lineHeight) + offsetY; + if (textTextureRenderer._settings.textAlign === 'right') { + linePositionX += (innerWidth - lineWidths[i]); + } else if (textTextureRenderer._settings.textAlign === 'center') { + linePositionX += ((innerWidth - lineWidths[i]) / 2); + } + linePositionX += paddingLeft; + drawLines.push({text: lines[i], x: linePositionX, y: linePositionY, w: lineWidths[i]}); + } + // Highlight. + if (textTextureRenderer._settings.highlight) { + let color = textTextureRenderer._settings.highlightColor || 0x00000000; + let hlHeight = (textTextureRenderer._settings.highlightHeight * precision || fontSize * 1.5); + let offset = (textTextureRenderer._settings.highlightOffset !== null ? textTextureRenderer._settings.highlightOffset * precision : -0.5 * fontSize); + const hlPaddingLeft = (textTextureRenderer._settings.highlightPaddingLeft !== null ? textTextureRenderer._settings.highlightPaddingLeft * precision : paddingLeft); + const hlPaddingRight = (textTextureRenderer._settings.highlightPaddingRight !== null ? textTextureRenderer._settings.highlightPaddingRight * precision : paddingRight); + + textTextureRenderer._context.fillStyle = color; + for (let i = 0; i < drawLines.length; i++) { + let drawLine = drawLines[i]; + textTextureRenderer._context.fillRect((drawLine.x - hlPaddingLeft), (drawLine.y + offset), (drawLine.w + hlPaddingRight + hlPaddingLeft), hlHeight); + } + } + // Text shadow. + let prevShadowSettings = null; + if (textTextureRenderer._settings.shadow) { + prevShadowSettings = [textTextureRenderer._context.shadowColor, textTextureRenderer._context.shadowOffsetX, textTextureRenderer._context.shadowOffsetY, textTextureRenderer._context.shadowBlur]; + textTextureRenderer._context.shadowColor = textTextureRenderer._settings.shadowColor; + textTextureRenderer._context.shadowOffsetX = textTextureRenderer._settings.shadowOffsetX * precision; + textTextureRenderer._context.shadowOffsetY = textTextureRenderer._settings.shadowOffsetY * precision; + textTextureRenderer._context.shadowBlur = textTextureRenderer._settings.shadowBlur * precision; + } + textTextureRenderer._context.fillStyle = textTextureRenderer._settings.textColor; + for (let i = 0, n = drawLines.length; i < n; i++) { + let drawLine = drawLines[i]; + textTextureRenderer._context.fillText(drawLine.text, drawLine.x, drawLine.y); + } + + if (prevShadowSettings) { + textTextureRenderer._context.shadowColor = prevShadowSettings[0]; + textTextureRenderer._context.shadowOffsetX = prevShadowSettings[1]; + textTextureRenderer._context.shadowOffsetY = prevShadowSettings[2]; + textTextureRenderer._context.shadowBlur = prevShadowSettings[3]; + } + + if (cutSx || cutSy) { + textTextureRenderer._context.translate(cutSx, cutSy); + } + // Original Lightining code ends here + canvasInternal.ready.then(() => { // everything is drawn + renderInfo.w = canvasInternal.w; + renderInfo.h = canvasInternal.h; + textTextureRenderer._canvas.width = canvasInternal.w; + textTextureRenderer._canvas.height = canvasInternal.h; + textTextureRenderer.renderInfo = renderInfo; + resolve(); + }); + }); + }); + }); + return drawPromise; + } + + loadFonts(fonts) { + let promises = []; + let fontResources = new Map(); + for (let font of fonts) { + let fontResource = sparkscene.create({t: "fontResource", url: font.url}); + promises.push(fontResource.ready); + fontResources.set(font.family, fontResource); + } + + // load fonts and then store a + // reference to them so they can be used + // in getFontSetting calls + Promise.all(promises) + .then(() => this._fontResources = fontResources); + + // continue to return promise/font object + // to maintain compatibility with SDK client + return { + promises: promises, + fontResources: fontResources + }; + } + + getFontSetting(textTextureRenderer) { + let fontResource = textTextureRenderer._context.font; + let fontFace = textTextureRenderer._settings.fontFace; + let fontStyle = textTextureRenderer._settings.fontStyle.toLowerCase(); + + if (this._fontResources !== undefined && this._fontResources.has(fontFace)) { + fontResource = this._fontResources.get(fontFace); + if (fontResource.needsStyleCoercion(fontStyle)) { + let url = fontResource.url; + fontResource = sparkscene.create({t: "fontResource", url: url, fontStyle: fontStyle}); + } + } + return fontResource; + } + + + static createMediaPlayer() + { + return SparkMediaplayer; + } +} + +const lightning$1 = lightning; + +lightning$1.Stage.platform = SparkPlatform; + +const Headers = fetch.Headers; + + +const events = ['timeupdate', 'error', 'ended', 'loadeddata', 'canplay', 'play', 'playing', 'pause', 'loadstart', 'seeking', 'seeked', 'encrypted']; + +class Mediaplayer extends lightning$1.Component { + + _construct(){ + this._skipRenderToTexture = false; + } + + static _template() { + return { + Video: { + VideoWrap: { + VideoTexture: { + visible: false, + pivot: 0.5, + texture: {type: lightning$1.textures.StaticTexture, options: {}} + } + } + } + }; + } + + set skipRenderToTexture (v) { + this._skipRenderToTexture = v; + } + + set textureMode(v) { + return this._textureMode = v; + } + + get textureMode() { + return this._textureMode; + } + + get videoView() { + return this.tag("Video"); + } + + _init() { + //re-use videotag if already there + const videoEls = document.getElementsByTagName('video'); + if (videoEls && videoEls.length > 0) + this.videoEl = videoEls[0]; + else { + this.videoEl = document.createElement('video'); + this.videoEl.setAttribute('id', 'video-player'); + this.videoEl.style.position = 'absolute'; + this.videoEl.style.zIndex = '1'; + this.videoEl.style.display = 'none'; + this.videoEl.setAttribute('width', '100%'); + this.videoEl.setAttribute('height', '100%'); + + this.videoEl.style.visibility = (this.textureMode) ? 'hidden' : 'visible'; + document.body.appendChild(this.videoEl); + } + if (this.textureMode && !this._skipRenderToTexture) { + this._createVideoTexture(); + } + + this.eventHandlers = []; + } + + _registerListeners() { + events.forEach(event => { + const handler = (e) => { + this.fire(event, {videoElement: this.videoEl, event: e}); + }; + this.eventHandlers.push(handler); + this.videoEl.addEventListener(event, handler); + }); + } + + _deregisterListeners() { + events.forEach((event, index) => { + this.videoEl.removeEventListener(event, this.eventHandlers[index]); + }); + this.eventHandlers = []; + } + + _attach() { + this._registerListeners(); + } + + _detach() { + this._deregisterListeners(); + } + + _createVideoTexture() { + const stage = this.stage; + + const gl = stage.gl; + const glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + this.videoTexture.options = {source: glTexture, w: this.videoEl.width, h: this.videoEl.height}; + } + + _startUpdatingVideoTexture() { + if (this.textureMode && !this._skipRenderToTexture) { + const stage = this.stage; + if (!this._updateVideoTexture) { + this._updateVideoTexture = () => { + if (this.videoTexture.options.source && this.videoEl.videoWidth && this.active) { + const gl = stage.gl; + + const currentTime = (new Date()).getTime(); + + // When BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DEBUGUTILS is not set in WPE, webkitDecodedFrameCount will not be available. + // We'll fallback to fixed 30fps in this case. + const frameCount = this.videoEl.webkitDecodedFrameCount; + + const mustUpdate = (frameCount ? (this._lastFrame !== frameCount) : (this._lastTime < currentTime - 30)); + + if (mustUpdate) { + this._lastTime = currentTime; + this._lastFrame = frameCount; + try { + gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.videoEl); + this._lastFrame = this.videoEl.webkitDecodedFrameCount; + this.videoTextureView.visible = true; + + this.videoTexture.options.w = this.videoEl.videoWidth; + this.videoTexture.options.h = this.videoEl.videoHeight; + const expectedAspectRatio = this.videoTextureView.w / this.videoTextureView.h; + const realAspectRatio = this.videoEl.videoWidth / this.videoEl.videoHeight; + if (expectedAspectRatio > realAspectRatio) { + this.videoTextureView.scaleX = (realAspectRatio / expectedAspectRatio); + this.videoTextureView.scaleY = 1; + } else { + this.videoTextureView.scaleY = expectedAspectRatio / realAspectRatio; + this.videoTextureView.scaleX = 1; + } + } catch (e) { + console.error('texImage2d video', e); + this._stopUpdatingVideoTexture(); + this.videoTextureView.visible = false; + } + this.videoTexture.source.forceRenderUpdate(); + } + } + }; + } + if (!this._updatingVideoTexture) { + stage.on('frameStart', this._updateVideoTexture); + this._updatingVideoTexture = true; + } + } + } + + _stopUpdatingVideoTexture() { + if (this.textureMode) { + const stage = this.stage; + stage.removeListener('frameStart', this._updateVideoTexture); + this._updatingVideoTexture = false; + this.videoTextureView.visible = false; + + if (this.videoTexture.options.source) { + const gl = stage.gl; + gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } + } + + updateSettings(settings = {}) { + // The Component that 'consumes' the media player. + this._consumer = settings.consumer; + + if (this._consumer && this._consumer.getMediaplayerSettings) { + // Allow consumer to add settings. + settings = Object.assign(settings, this._consumer.getMediaplayerSettings()); + } + + if (!lightning$1.Utils.equalValues(this._stream, settings.stream)) { + if (settings.stream && settings.stream.keySystem) { + navigator.requestMediaKeySystemAccess(settings.stream.keySystem.id, settings.stream.keySystem.config).then((keySystemAccess) => { + return keySystemAccess.createMediaKeys(); + }).then((createdMediaKeys) => { + return this.videoEl.setMediaKeys(createdMediaKeys); + }).then(() => { + if (settings.stream && settings.stream.src) + this.open(settings.stream.src); + }).catch(() => { + console.error('Failed to set up MediaKeys'); + }); + } else if (settings.stream && settings.stream.src) { + if(!window.Hls){ + window.Hls = class Hls{ + static isSupported(){ + console.warn("hls-light not included"); + return false; + } + }; + } + if (ux.Ui.hasOption("hls") && Hls.isSupported()) { + if (!this._hls) this._hls = new Hls({liveDurationInfinity: true}); + this._hls.loadSource(settings.stream.src); + this._hls.attachMedia(this.videoEl); + this.videoEl.style.display = "block"; + } else { + this.open(settings.stream.src); + } + } else { + this.close(); + } + this._stream = settings.stream; + } + + this._setHide(settings.hide); + this._setVideoArea(settings.videoPos); + } + + _setHide(hide) { + if (this.textureMode) { + this.tag("Video").setSmooth('alpha', hide ? 0 : 1); + } else { + this.videoEl.style.visibility = hide ? 'hidden' : 'visible'; + } + } + + open(url) { + console.log('Playing stream', url); + if (this.application.noVideo) { + console.log('noVideo option set, so ignoring: ' + url); + return; + } + if (this.videoEl.getAttribute('src') === url) return this.reload(); + this.videoEl.setAttribute('src', url); + + this.videoEl.style.display = 'block'; + } + + close() { + // We need to pause first in order to stop sound. + this.videoEl.pause(); + this.videoEl.removeAttribute('src'); + + // force load to reset everything without errors + this.videoEl.load(); + + this._clearSrc(); + + this.videoEl.style.display = 'none'; + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + this.videoEl.play(); + } + + doPause() { + this.videoEl.pause(); + } + + reload() { + var url = this.videoEl.getAttribute('src'); + this.close(); + this.videoEl.src = url; + } + + getPosition() { + return Promise.resolve(this.videoEl.currentTime); + } + + setPosition(pos) { + this.videoEl.currentTime = pos; + } + + getDuration() { + return Promise.resolve(this.videoEl.duration); + } + + seek(time, absolute = false) { + if(absolute) { + this.videoEl.currentTime = time; + } + else { + this.videoEl.currentTime += time; + } + } + + get videoTextureView() { + return this.tag("Video").tag("VideoTexture"); + } + + get videoTexture() { + return this.videoTextureView.texture; + } + + _setVideoArea(videoPos) { + if (lightning$1.Utils.equalValues(this._videoPos, videoPos)) { + return; + } + + this._videoPos = videoPos; + + if (this.textureMode) { + this.videoTextureView.patch({ + smooth: { + x: videoPos[0], + y: videoPos[1], + w: videoPos[2] - videoPos[0], + h: videoPos[3] - videoPos[1] + } + }); + } else { + const precision = this.stage.getRenderPrecision(); + this.videoEl.style.left = Math.round(videoPos[0] * precision) + 'px'; + this.videoEl.style.top = Math.round(videoPos[1] * precision) + 'px'; + this.videoEl.style.width = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px'; + this.videoEl.style.height = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px'; + } + } + + _fireConsumer(event, args) { + if (this._consumer) { + this._consumer.fire(event, args); + } + } + + _equalInitData(buf1, buf2) { + if (!buf1 || !buf2) return false; + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Int8Array(buf1); + const dv2 = new Int8Array(buf2); + for (let i = 0 ; i != buf1.byteLength ; i++) + if (dv1[i] != dv2[i]) return false; + return true; + } + + error(args) { + this._fireConsumer('$mediaplayerError', args); + this._setState(""); + return ""; + } + + loadeddata(args) { + this._fireConsumer('$mediaplayerLoadedData', args); + } + + play(args) { + this._fireConsumer('$mediaplayerPlay', args); + } + + playing(args) { + this._fireConsumer('$mediaplayerPlaying', args); + this._setState("Playing"); + } + + canplay(args) { + this.videoEl.play(); + this._fireConsumer('$mediaplayerStart', args); + } + + loadstart(args) { + this._fireConsumer('$mediaplayerLoad', args); + } + + seeked(args) { + this._fireConsumer('$mediaplayerSeeked', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + + seeking(args) { + this._fireConsumer('$mediaplayerSeeking', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + + durationchange(args) { + this._fireConsumer('$mediaplayerDurationChange', args); + } + + encrypted(args) { + const video = args.videoElement; + const event = args.event; + // FIXME: Double encrypted events need to be properly filtered by Gstreamer + if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { + this._previousInitData = event.initData; + this._fireConsumer('$mediaplayerEncrypted', args); + } + } + + static _states() { + return [ + class Playing extends this { + $enter() { + this._startUpdatingVideoTexture(); + } + $exit() { + this._stopUpdatingVideoTexture(); + } + timeupdate() { + this._fireConsumer('$mediaplayerProgress', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + ended(args) { + this._fireConsumer('$mediaplayerEnded', args); + this._setState(""); + } + pause(args) { + this._fireConsumer('$mediaplayerPause', args); + this._setState("Playing.Paused"); + } + _clearSrc() { + this._fireConsumer('$mediaplayerStop', {}); + this._setState(""); + } + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } + +} + +class NoopMediaplayer extends lightning$1.Component { + + static _template() { + return { + Video: { + w: 1920, h: 1080 + } + }; + } + + open(url) { + console.log('Playing stream', url); + } + + close() { + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + } + + doPause() { + } + + reload() { + } + + getPosition() { + return Promise.resolve(0); + } + + setPosition(pos) { + } + + getDuration() { + return Promise.resolve(0); + } + + seek(time, absolute = false) { + } + + updateSettings(settings = {}) { + } + + static _states() { + return [ + class Playing extends this { + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } + +} + +class ScaledImageTexture extends lightning$1.textures.ImageTexture { + + constructor(stage) { + super(stage); + + this._scalingOptions = undefined; + this.precision = 1; + } + + set scalingOptions(options) { + if (!lightning$1.Utils.equalValues(this._scalingOptions, options)) { + this._scalingOptions = options; + this._changed(); + } + } + + _getLookupId() { + const opts = this._scalingOptions; + return `${this._src}-${opts.type}-${opts.width}-${opts.height}`; + } + + _getSourceLoader() { + let src = this._src; + if (this.stage.getOption('srcBasePath')) { + var fc = src.charCodeAt(0); + if ((src.indexOf("//") === -1) && ((fc >= 65 && fc <= 90) || (fc >= 97 && fc <= 122) || fc == 46)) { + // Alphabetical or dot: prepend base path. + src = this.stage.getOption('srcBasePath') + src; + } + } + + if (this.stage.application.useImageServer) { + src = this._getImageServerSrc(src); + } else { + this.resizeMode = ScaledImageTexture._convertScalingOptions(this._scalingOptions); + } + + const platform = this.stage.platform; + return function(cb) { + return platform.loadSrcTexture({src: src, hasAlpha: this._hasAlpha}, cb); + } + } + + static _convertScalingOptions(options) { + const opts = lightning$1.Utils.clone(options); + switch(options.type) { + case "crop": + opts.type = "cover"; + break; + case "fit": + case "parent": + case "exact": + case "height": + case "portrait": + case "width": + case "landscape": + case "auto": + default: + opts.type = "contain"; + break; + } + opts.w = opts.w || opts.width || 0; + opts.h = opts.h || opts.height || 0; + return opts; + } + + get precision() { + return this._customPrecision; + } + + set precision(v) { + this._customPrecision = v; + super.precision = this.stage.getRenderPrecision() * this._customPrecision; + } + + _getImageServerSrc(src) { + if (this._scalingOptions && (this._precision !== 1)) { + const opts = lightning$1.Utils.clone(this._scalingOptions); + if (opts.width) { + opts.width = Math.round(opts.width * this._precision); + } + + if (opts.height) { + opts.height = Math.round(opts.height * this._precision); + } + src = ScaledImageTexture.getImageUrl(src, opts); + } else { + src = ScaledImageTexture.getImageUrl(src, this._scalingOptions); + } + return src; + } + + static getImageUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/image" + this.getQueryString(url, opts); + } + + static _getCdnProtocol() { + return lightning$1.Utils.isWeb && location.protocol === "https:" ? "https" : "http"; + } + + static getQueryString(url, opts, key = "url") { + let str = `?operator=${encodeURIComponent('metrological')}`; + const keys = Object.keys(opts); + keys.forEach(key => { + str += "&" + encodeURIComponent(key) + "=" + encodeURIComponent("" + opts[key]); + }); + str += `&${key}=${encodeURIComponent(url)}`; + return str; + } + + getNonDefaults() { + const obj = super.getNonDefaults(); + if (this._src) { + obj.src = this._src; + } + return obj; + } + +} + +class Ui extends lightning$1.Application { + + constructor(options) { + options.defaultFontFace = options.defaultFontFace || "RobotoRegular"; + super(options); + this._options = options; + } + + static _template() { + let mediaPlayerType = NoopMediaplayer; + if (lightning$1.Utils.isWeb) { + mediaPlayerType = Mediaplayer; + } +/* + else if (lightning$1.Utils.isSpark) { + mediaPlayerType = lightning$1.Stage.platform.createMediaPlayer(); + } +*/ + return { + Mediaplayer: {type: mediaPlayerType, textureMode: Ui.hasOption('texture')}, + AppWrapper: {} + }; + } + + static set staticFilesPath(path) { + this._staticFilesPath = path; + } + + get useImageServer() { + return !Ui.hasOption("noImageServer"); + } + + get mediaplayer() { + return this.tag("Mediaplayer"); + } + + _active() { + this.tag('Mediaplayer').skipRenderToTexture = this._options.skipRenderToTexture; + } + + startApp(appClass) { + this._setState("App.Loading", [appClass]); + } + + stopApp() { + } + + _handleBack() { + if (lightning$1.Utils.isWeb) { + window.close(); + } + } + + loadPlatformFonts(fonts) { + if (lightning$1.Utils.isNode && !lightning$1.Utils.isSpark) { + // Font loading not supported. Fonts should be installed in Linux system and then they can be picked up by cairo. + return Promise.resolve(); + } + + if (lightning$1.Utils.isSpark) { + let ret = this.stage.platform.loadFonts(fonts); + return Promise.all(ret.promises).then(() => {return ret.fontResources}); + } + } + + static loadFonts(fonts) { + const fontFaces = fonts.map(({family, url, descriptors}) => new FontFace(family, `url(${url})`, descriptors)); + fontFaces.forEach(fontFace => { + document.fonts.add(fontFace); + }); + return Promise.all(fontFaces.map(ff => ff.load())).then(() => {return fontFaces}); + } + + static getPath(relPath) { + return this._staticFilesPath + "static-ux/" + relPath; + } + + static getFonts() { + return [ + {family: 'RobotoRegular', url: Ui.getPath('fonts/roboto-regular.ttf'), descriptors: {}}, + {family: 'Material-Icons', url: Ui.getPath('fonts/Material-Icons.ttf'), descriptors: {}} + ] + } + + static _states() { + return [ + class App extends this { + stopApp() { + this._setState(""); + } + static _states() { + return [ + class Loading extends this { + $enter(context, appClass) { + this._startApp(appClass); + } + _startApp(appClass) { + this._currentApp = { + type: appClass, + fontFaces: [] + }; + + // Preload fonts. + const fonts = this._currentApp.type.getFonts().concat(Ui.getFonts()); + let fn = lightning$1.Utils.isWeb ? Ui.loadFonts(fonts): this.loadPlatformFonts(fonts); + fn.then((fontFaces) => { + this._currentApp.fontFaces = fontFaces; + }).catch((e) => { + console.warn('Font loading issues: ' + e); + }).finally(()=>{ + this._done(); + }); + } + _done() { + this._setState("App.Started"); + } + }, + class Started extends this { + $enter() { + this.tag("AppWrapper").children = [{ref: "App", type: this._currentApp.type}]; + } + $exit() { + this.tag("AppWrapper").children = []; + } + } + ] + } + } + ] + } + + _getFocused() { + return this.tag("App"); + } + + _setFocusSettings(settings) { + settings.clearColor = this.stage.getOption('clearColor'); + settings.mediaplayer = { + consumer: null, + stream: null, + hide: false, + videoPos: [0, 0, 1920, 1080] + }; + } + + _handleFocusSettings(settings) { + if (this._clearColor !== settings.clearColor) { + this._clearColor = settings.clearColor; + this.stage.setClearColor(settings.clearColor); + } + + if (this.tag("Mediaplayer").attached) { + this.tag("Mediaplayer").updateSettings(settings.mediaplayer); + } + } + + static getProxyUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/proxy" + this.getQueryString(url, opts); + } + + static getImage(url, opts = {}) { + return {type: ScaledImageTexture, src: url, scalingOptions: opts}; + } + + static getImageUrl(url, opts = {}) { + throw new Error("{src: Ui.getImageUrl(...)} is deprecated. Please use {texture: Ui.getImage(...)} instead."); + } + + static getQrUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/qr" + this.getQueryString(url, opts, "q"); + } + + static _getCdnProtocol() { + return lightning$1.Utils.isWeb && location.protocol === "https:" ? "https" : "http"; + } + + static hasOption(name) { + if (lightning$1.Utils.isNode) { + return false; + } + + return new URL(document.location.href).searchParams.has(name); + } + + static getOption(name) { + if (lightning$1.Utils.isNode) { + return undefined; + } + + return new URL(document.location.href).searchParams.get(name); + } + + static getQueryString(url, opts, key = "url") { + let str = `?operator=${encodeURIComponent(this.getOption('operator') || 'metrological')}`; + const keys = Object.keys(opts); + keys.forEach(key => { + str += "&" + encodeURIComponent(key) + "=" + encodeURIComponent("" + opts[key]); + }); + str += `&${key}=${encodeURIComponent(url)}`; + return str; + } + + +} + +Ui._staticFilesPath = "./"; + +class App extends lightning$1.Component { + + static g(c) { + return c.seekAncestorByType(this); + } + + /** + * Returns all fonts to be preloaded before entering this app. + * @returns {{family: string, url: string, descriptors: {}}[]} + */ + static getFonts() { + return []; + } + + getPath(relPath) { + return App.getPath(this.constructor, relPath); + } + + static getPath(relPath) { + return Ui._staticFilesPath + "static/" + relPath; + } + + static get identifier() { + throw new Error("Please supply an identifier in the App definition file."); + } + +} + +class PlayerButton extends lightning$1.Component { + + static _template() { + const o = this.options; + return { + w: o.w, h: o.h, + Background: {x: -1, y: -1, texture: lightning$1.Tools.getRoundRect(o.w, o.h, 4, 0, 0, true), color: o.colors.deselected}, + Icon: {x: o.w/2, y: o.h/2, mount: 0.5, color: o.colors.selected} + }; + } + + set icon(source) { + this.tag("Icon").src = Ui.getPath(`tools/player/img/${source}`); + } + + set active(v) { + this.alpha = v ? 1 : 0.3; + } + + get active() { + return this.alpha === 1; + } + + static _states() { + return [ + class Selected extends this { + $enter() { + this.tag("Background").color = this.constructor.options.colors.selected; + this.tag("Icon").color = this.constructor.options.colors.deselected; + } + $exit() { + this.tag("Background").color = this.constructor.options.colors.deselected; + this.tag("Icon").color = this.constructor.options.colors.selected; + } + } + ] + } + + _focus() { + this._setState("Selected"); + } + + _unfocus() { + this._setState(""); + } + + static get options() { + if (!this._options) { + this._options = this._buildOptions(); + } + return this._options; + } + + static _buildOptions() { + return { + colors: { + selected: 0xFFFFFFFF, + deselected: 0xFF606060 + }, + w: 60, + h: 60 + }; + } + +} + +class PlayerControls extends lightning$1.Component { + + static _template() { + return { + Buttons: { + Previous: {type: this.PlayerButton, icon: "prev.png"}, + Play: {type: this.PlayerButton, icon: "play.png"}, + Next: {type: this.PlayerButton, icon: "next.png"} + }, + Title: {text: {fontSize: 46, lineHeight: 56, maxLines: 1, shadow: true}, y: 2} + }; + } + + static get PlayerButton() { + return PlayerButton; + } + + showButtons(previous, next) { + const o = this.constructor.options; + let buttons = []; + if (previous) buttons.push("Previous"); + buttons = buttons.concat(o.buttons); + if (next) buttons.push("Next"); + this._setActiveButtons(buttons); + } + + set title(title) { + this.tag("Title").text = title || ""; + } + + get _activeButtonIndex() { + let button = this.tag("Buttons").getByRef(this._getState()); + if (!button.active) { + button = this.tag("Play"); + } + return this._activeButtons.indexOf(button); + } + + get _activeButton() { + return this._activeButtons[this._activeButtonIndex]; + } + + _setActiveButtons(buttons) { + const o = this.constructor.options; + + let x = 0; + this._activeButtons = []; + this.tag("Buttons").children.map(button => { + button.active = (buttons.indexOf(button.ref) !== -1); + button.x = x; + if (button.active) { + this._activeButtons.push(button); + } + x += button.renderWidth + o.margin; + }); + this.tag("Title").x = x + 20; + + + this._checkActiveButton(); + } + + _setup() { + this._setState("Play"); + } + + _init() { + this.showButtons(false, false); + this._setState("Play"); + } + + _checkActiveButton() { + // After changing the active buttons, make sure that an active button is selected. + let index = this._activeButtonIndex; + if (index === -1) { + if (this._index >= this._activeButtons.length) { + this._index = this._activeButtons.length - 1; + } + } + this._setState(this._activeButtons[index].ref); + } + + _handleLeft() { + let index = this._activeButtonIndex; + if (index > 0) { + index--; + } + this._setState(this._activeButtons[index].ref); + } + + _handleRight() { + let index = this._activeButtonIndex; + if (index < this._activeButtons.length - 1) { + index++; + } + this._setState(this._activeButtons[index].ref); + } + + _handleEnter() { + this.signal('press' + this._activeButton.ref); + } + + + set paused(v) { + this.tag("Play").icon = v ? "play.png" : "pause.png"; + } + + static _states() { + return [ + class Previous extends this { + }, + class Play extends this { + }, + class Next extends this { + } + ] + } + + _getFocused() { + return this.tag(this._getState()); + } + + static get options() { + if (!this._options) { + this._options = this._buildOptions(); + } + return this._options; + } + + static _buildOptions() { + return { + buttons: ["Play"], + margin: 10 + }; + } + +} + +class PlayerProgress extends lightning$1.Component { + + static _template() { + return { + Progress: { + forceZIndexContext: true, + Total: { + x: -1, y: -1, texture: lightning$1.Tools.getRoundRect(1720, 10, 4), color: 0xFF606060, + Scroller: { + x: 0, y: 6, mount: 0.5, w: 16, h: 16, zIndex: 2, + Shadow: { + texture: lightning$1.Tools.getShadowRect(16, 16, 8), + mount: 0.5, + x: 8, + y: 8, + color: 0xFF000000 + }, + Main: {texture: lightning$1.Tools.getRoundRect(16, 16, 8), mount: 0.5, x: 8, y: 8, color: 0xFFF1F1F1} + } + }, + Active: {x: -1, y: -1, color: 0xFFF1F1F1}, + CurrentTime: { + x: 0, + y: 21, + text: {fontSize: 28, lineHeight: 34, maxLines: 1, shadow: true, text: "00:00"} + }, + Duration: { + x: 1720, + mountX: 1, + y: 21, + text: {fontSize: 28, lineHeight: 34, maxLines: 1, shadow: true, text: "00:00"} + } + } + }; + } + + set _progress(v) { + const now = Date.now(); + let estimation = 0; + if (!this._last || (this._last < now - 1000)) { + estimation = 500; + } else { + estimation = now - this._last; + } + this._last = now; + const x = v * 1720; + + estimation *= 0.001; + this.tag("Total").setSmooth('x', x, {timingFunction: 'linear', duration: estimation}); + this.tag("Total").setSmooth('texture.x', x, {timingFunction: 'linear', duration: estimation}); + this.tag("Active").setSmooth('texture.w', Math.max(x, 0.0001) /* force clipping */, { + timingFunction: 'linear', + duration: estimation + }); + } + + setProgress(currentTime, duration) { + this._progress = currentTime / Math.max(duration, 1); + this.tag("CurrentTime").text = Player.formatTime(currentTime); + this.tag("Duration").text = Player.formatTime(duration); + } + + static formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + seconds = Math.floor(seconds); + const parts = []; + if (hours) parts.push(hours); + parts.push(minutes); + parts.push(seconds); + return parts.map(number => (number < 10 ? "0" + number : "" + number)).join(":"); + } + + _alter() { + } + + _setup() { + this._alter(); + } + + _init() { + this.tag("Active").texture = { + type: lightning$1.textures.SourceTexture, + textureSource: this.tag("Total").texture.source + }; + } + +} + +class Player extends lightning$1.Component { + + static _template() { + return { + Gradient: { + x: 0, + y: 1080, + h: 300, + w: 1920, + mountY: 1, + colorTop: 0x00101010, + colorBottom: 0xE6101010, + rect: true + }, + Controls: { + x: 99, + y: 890, + type: this.PlayerControls, + signals: {pressPlay: true, pressPrevious: true, pressNext: "_pressNext"} + }, + Progress: {x: 99, y: 970, type: this.PlayerProgress} + }; + } + + static get PlayerControls() { + return PlayerControls; + } + + static get PlayerProgress() { + return PlayerProgress; + } + + _setItem(item) { + this.tag("Progress").setProgress(0, 0); + this._item = item; + this._stream = item.stream; + this.tag("Controls").title = item.title; + + this._index = this._items.indexOf(item); + this.tag("Controls").showButtons(this._index > 0, this._index < this._items.length - 1); + + this.application.updateFocusSettings(); + } + + static formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + seconds = Math.floor(seconds); + const parts = []; + if (hours) parts.push(hours); + parts.push(minutes); + parts.push(seconds); + return parts.map(number => (number < 10 ? "0" + number : "" + number)).join(":"); + } + + _setInterfaceTimeout() { + if (this._timeout) { + clearTimeout(this._timeout); + } + this._timeout = setTimeout(() => { + this._hide(); + }, 8000); + } + + _init() { + this._setState("Controls"); + } + + _focus() { + this._setInterfaceTimeout(); + } + + _unfocus() { + clearTimeout(this._timeout); + } + + $mediaplayerEnded() { + this._pressNext(); + } + + play({item, items = [item]}) { + this._items = items; + this._setItem(item); + return !!this._stream; + } + + pressPrevious() { + const index = this._index - 1; + if (index < 0) { + this._index = this._items.length - 1; + } + this._setItem(this._items[index]); + } + + _pressNext() { + if (!this._items.length) { + return this.signal('playerStop'); + } + const index = (this._index + 1) % this._items.length; + this._setItem(this._items[index]); + } + + pressPlay() { + this.application.mediaplayer.playPause(); + } + + $mediaplayerPause() { + this.tag("Controls").paused = true; + } + + $mediaplayerPlay() { + this.tag("Controls").paused = false; + } + + $mediaplayerStop() { + this.signal('playerStop'); + } + + $mediaplayerProgress({currentTime, duration}) { + this.tag("Progress").setProgress(currentTime, duration); + } + + _captureKey() { + this._setInterfaceTimeout(); + return false; + } + + _hide() { + this._setState("Hidden"); + } + + static _states() { + return [ + class Hidden extends this { + $enter({prevState}) { + this._prevState = prevState; + this.setSmooth('alpha', 0); + } + $exit() { + this._setInterfaceTimeout(); + this.setSmooth('alpha', 1); + } + _captureKey() { + this._setState(this._prevState); + } + }, + class Controls extends this { + } + ]; + } + + _getFocused() { + return this.tag("Controls"); + } + + _setFocusSettings(settings) { + settings.mediaplayer.consumer = this; + } + + getMediaplayerSettings() { + if (this._stream.link) { + // Backwards compatibility. + this._stream.src = this._stream.link; + } + + return { + stream: this._stream + }; + } + + +} + +const obj = { + Player, + PlayerButton, + PlayerControls, + PlayerProgress +}; + +class Light3dComponent extends lightning$1.Component { + + constructor(stage) { + super(stage); + + this.patch({ + __create: true, + Main: { + x: -1, + y: -1, + shader: {type: lightning$1.shaders.Light3d, fudge: 0.3}, + renderToTexture: true, + Wrapper: { + x: 1, + y: 1, + clipping: true, + Content: {} + } + } + }); + + this._shaderZ = 0; + this._shaderZ0 = 0; + this._shaderZ1 = 0; + + this._shaderRx = 0; + this._shaderRx0 = 0; + this._shaderRx1 = 0; + + this._shaderRy = 0; + this._shaderRy0 = 0; + this._shaderRy1 = 0; + + this._focusedZ = -150; + this._createAnimations(); + + this.transition('lightShader.strength', {duration: 0.2}); + this.transition('lightShader.ambient', {duration: 0.2}); + } + + get focusedZ() { + return this._focusedZ; + } + + set focusedZ(v) { + this._focusedZ = v; + this._createAnimations(); + } + + _createAnimations() { + this._anims = { + neutral: this.animation({ + duration: 0.4, actions: [ + {p: 'shaderZ0', merger: lightning$1.StageUtils.mergeNumbers, v: {0: 0, 0.5: -140, 1: -150}} + ] + }), + left: this._createAnimation('x', -1, 0), + right: this._createAnimation('x', 1, 1), + up: this._createAnimation('y', -1, 0), + down: this._createAnimation('y', 1, 0) + }; + } + + _createAnimation(axis, sign, idx) { + return this.animation({ + duration: 0.4, stopDuration: 0.2, actions: [ + {p: 'shaderR' + axis + idx, merger: lightning$1.StageUtils.mergeNumbers, v: {0: 0, 0.3: -0.20 * sign, 1: 0}}, + { + p: 'shaderZ' + idx, + merger: lightning$1.StageUtils.mergeNumbers, + v: {0: 0, 0.5: this._focusedZ + 10, 1: this._focusedZ} + } + ] + }); + } + + set w(v) { + this.tag('Main').w = v + 2; + this.tag('Wrapper').w = v; + } + + set h(v) { + this.tag('Main').h = v + 2; + this.tag('Wrapper').h = v; + } + + get lightShader() { + return this.tag('Main').shader; + } + + set lightShader(v) { + this.tag('Main').shader = v; + } + + get content() { + return this.tag('Content'); + } + + set content(v) { + this.tag('Content').patch(v, true); + } + + _recalc() { + this.tag('Main').shader.rx = this._shaderRx0 + this._shaderRx1 + this._shaderRx; + this.tag('Main').shader.ry = this._shaderRy0 + this._shaderRy1 + this._shaderRy; + this.tag('Main').shader.z = this._shaderZ0 + this._shaderZ1 + this._shaderZ; + this.tag('Main').shader.pivotZ = this._shaderZ0 + this._shaderZ1 + this._shaderZ; + } + + get shaderZ() { + return this._shaderZ; + } + + set shaderZ(v) { + this._shaderZ = v; + this._recalc(); + } + + get shaderZ0() { + return this._shaderZ0; + } + + set shaderZ0(v) { + this._shaderZ0 = v; + this._recalc(); + } + + get shaderZ1() { + return this._shaderZ1; + } + + set shaderZ1(v) { + this._shaderZ1 = v; + this._recalc(); + } + + get shaderRx() { + return this._shaderRx; + } + + set shaderRx(v) { + this._shaderRx = v; + this._recalc(); + } + + get shaderRx0() { + return this._shaderRx0; + } + + set shaderRx0(v) { + this._shaderRx0 = v; + this._recalc(); + } + + get shaderRx1() { + return this._shaderRx1; + } + + set shaderRx1(v) { + this._shaderRx1 = v; + this._recalc(); + } + + get shaderRy() { + return this._shaderRy; + } + + set shaderRy(v) { + this._shaderRy = v; + this._recalc(); + } + + get shaderRy0() { + return this._shaderRy0; + } + + set shaderRy0(v) { + this._shaderRy0 = v; + this._recalc(); + } + + get shaderRy1() { + return this._shaderRy1; + } + + set shaderRy1(v) { + this._shaderRy1 = v; + this._recalc(); + } + + leftEnter() { + this._anims['left'].start(); + this._enable3dShader(); + } + + leftExit() { + this.neutralExit(); + } + + rightEnter() { + this._anims['right'].start(); + this._enable3dShader(); + } + + rightExit() { + this.neutralExit(); + } + + upEnter() { + this._anims['up'].start(); + this._enable3dShader(); + } + + upExit() { + this.neutralExit(); + } + + downEnter() { + this._anims['down'].start(); + this._enable3dShader(); + } + + downExit() { + this.neutralExit(); + } + + neutralEnter() { + this._anims['neutral'].start(); + this._enable3dShader(); + } + + neutralExit() { + this._anims['up'].stop(); + this._anims['down'].stop(); + this._anims['left'].stop(); + this._anims['right'].stop(); + this._anims['neutral'].stop(); + this._disable3dShader(); + } + + _enable3dShader() { + this.patch({smooth: {'lightShader.strength': 0.4, 'lightShader.ambient': 0.6}}); + } + + _disable3dShader() { + this.patch({smooth: {'lightShader.strength': 0, 'lightShader.ambient': 1}}); + } + + +} + +const obj$1 = { + Light3dComponent +}; + +const template = { + keyWidth: 74, + keyHeight: 74, + horizontalSpacing: 8, + verticalSpacing: 12, + layouts: { + 'ABC': { + rows: [ + { + keys: [ + {c: 'A'}, + {c: 'B'}, + {c: 'C'}, + {c: 'D'}, + {c: 'E'}, + {c: 'F'}, + {c: 'G'}, + {action: 'backspace', w: 148, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {c: 'H'}, + {c: 'I'}, + {c: 'J'}, + {c: 'K'}, + {c: 'L'}, + {c: 'M'}, + {c: 'N'}, + {action: 'toggleToLayout', toLayout: '#123', w: 148, c: '#123'} + ] + }, + { + keys: [ + {c: 'O'}, + {c: 'P'}, + {c: 'Q'}, + {c: 'R'}, + {c: 'S'}, + {c: 'T'}, + {c: 'U'} + ] + }, + { + keys: [ + {c: 'V'}, + {c: 'W'}, + {c: 'X'}, + {c: 'Y'}, + {c: 'Z'}, + {c: '-'}, + {c: '\''} + ] + }, + { + keys: [ + {action: 'space', c: 'space', w: 183}, + {action: 'delete', c: 'delete', w: 183}, + {action: 'ok', c: 'ok', w: 183} + ] + } + ] + }, + '#123': { + rows: [ + { + keys: [ + {c: '1'}, + {c: '2'}, + {c: '3'}, + {c: '&'}, + {c: '#'}, + {c: '('}, + {c: ')'}, + {action: 'backspace', w: 148, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {c: '4'}, + {c: '5'}, + {c: '6'}, + {c: '@'}, + {c: '!'}, + {c: '?'}, + {c: ':'}, + {action: 'toggleToLayout', toLayout: 'ABC', w: 148, c: 'ABC'} + ] + }, + { + keys: [ + {c: '7'}, + {c: '8'}, + {c: '9'}, + {c: '0'}, + {c: '.'}, + {c: '_'}, + {c: '\"'} + ] + }, + { + keys: [ + {action: 'space', c: 'space', w: 183}, + {action: 'delete', c: 'delete', w: 183}, + {action: 'ok', c: 'ok', w: 183} + ] + } + ] + } + } +}; + +class KeyboardButton extends lightning$1.Component { + static _template() { + return { + Background: {colorTop: 0x80e8e8e8, colorBottom: 0x80d1d1d1}, + Content: {} + }; + } + + set action(v) { + this._action = v; + } + + get action() { + return this._action; + } + + get c() { + return this.key.c; + } + + set key(v) { + this._key = v; + if(this.active) { + this._update(); + } + } + + _update() { + this.patch(this._getPatch(this._key)); + } + + _getPatch(key) { + let content = key.patch || {text: {text: key.c, fontFace: 'RobotoRegular', textAlign: 'center', fontSize: 36}}; + return { + Background: {texture: lightning$1.Tools.getRoundRect(this.w, this.h, 7, 0, 0xffffffff, true, 0xffffffff)}, + Content: {mountX: 0.5, mountY: 0.4, x: this.w/2, y: this.h/2, ...content} + }; + } + + get key() { + return this._key; + } + + _focus() { + this.patch({ + Background: {smooth: {colorTop: 0xff3777ee, colorBottom: 0xff2654a8}} + }); + } + + _unfocus() { + this.patch({ + Background: {smooth: {colorTop: 0x80e8e8e8, colorBottom: 0x80d1d1d1}} + }); + } + + _firstActive() { + this._update(); + } +} + +class Keyboard extends lightning$1.Component { + static _template() { + return { + + }; + } + + _construct() { + this._template = template; + } + + set template(v) { + this._template = v; + } + + get keyboardTemplate() { + return this._template; + } + + get keyboardButton() { + return KeyboardButton; + } + + get maxCharacters() { + return 40; + } + + set value(v) { + if(v.length < this.maxCharacters) { + this._value = v; + this.signal('valueChanged', {value: v}); + } + } + + get value() { + return this._value; + } + + get rows() { + return this.children; + } + + get rowLength() { + return this.rows[this.rowIndex].children.length; + } + + get currentKey() { + return this.children[this.rowIndex].children[this.colIndex] || null; + } + + set layout(layout) { + this._layout = layout; + this._update(); + } + + get layout() { + return this._layout; + } + + _getFocused() { + return this.currentKey; + } + + _navigate(dir, value) { + dir = (dir === 'up' || dir === 'down') ? 'vertical' : 'horizontal'; + if(dir === 'horizontal' && this.colIndex + value < this.rowLength && this.colIndex + value > -1) { + this.previous = null; + return this.colIndex += value; + } + else if(dir === 'vertical' && this.rowIndex + value < this.rows.length && this.rowIndex + value > -1) { + const currentColIndex = this.colIndex; + const targetRow = this.rowIndex + value; + if(this.previous && this.previous.row === targetRow) { + const tmp = this.previous.col; + this.previous.col = this.colIndex; + this.colIndex = tmp; + } + else { + const targetRow = this.children[(this.rowIndex + value)]; + const targetItems = targetRow.children; + const ck = this.currentKey; + let target = 0; + for(let i = 0; i < targetItems.length; i++) { + const ckx = this.children[this.rowIndex].x + ck.x; + const tix = targetRow.x + targetItems[i].x; + target = i; + if((ckx >= tix && ckx <= tix + targetItems[i].w) || (tix >= ckx && tix <= ckx + ck.w)) { + break; + } + } + this.colIndex = target; + } + this.previous = {col: currentColIndex, row: this.rowIndex}; + return this.rowIndex += value; + } + return false; + } + + _update() { + if(this._layout && this.keyboardTemplate.layouts[this._layout] === undefined) { + console.error(`Configured layout "${this.layout}" does not exist. Reverting to "${Object.keys(this.keyboardTemplate.layouts)[0]}"`); + this._layout = null; + } + if(!this._layout) { + this._layout = Object.keys(this.keyboardTemplate.layouts)[0]; + } + const {keyWidth, keyHeight, horizontalSpacing = 0, verticalSpacing = 0, layouts} = this.keyboardTemplate; + + this.children = layouts[this._layout].rows.map((row, rowIndex) => { + let keyOffset = 0; + const {x = 0, rowVerticalSpacing = verticalSpacing, rowHorizontalSpacing = horizontalSpacing, keys = []} = row; + return {y: keyHeight * rowIndex + (rowIndex * rowVerticalSpacing), x, + children: keys.map((key) => { + key = Object.assign({action: 'input'}, key); + const prevOffset = keyOffset; + const {w = keyWidth, h = keyHeight, action, toLayout} = key; + keyOffset += w + rowHorizontalSpacing; + return {key, action, toLayout, x: prevOffset, w, h, type: this.keyboardButton} + }) + }; + }); + } + + reset() { + this.colIndex = 0; + this.rowIndex = 0; + this._value = ''; + this.previous = null; + } + + _init() { + this.reset(); + this._update(); + } + + _handleRight() { + return this._navigate('right', 1); + } + + _handleLeft() { + return this._navigate('left', -1); + } + + _handleUp() { + return this._navigate('up', -1); + } + + _handleDown() { + return this._navigate('down', 1); + } + + _handleEnter() { + const key = this.currentKey; + switch(key.action) { + case 'input': + this.value += key.c; + break; + case 'backspace': + this.value = this.value.slice(0, -1); + break + case 'space': + if(this.value.length > 0){ + this.value += ' '; + } + break; + case 'delete': + this.value = ''; + break; + case 'toggleToLayout': + this.layout = key.toLayout; + break; + default: + this.signal(key.action, key); + break; + } + } +} + +const template$1 = { + keyWidth: 64, + keyHeight: 84, + horizontalSpacing: 8, + verticalSpacing: 12, + layouts: { + 'ABC': { + rows: [ + { + keys: [ + {c: 'Q'}, + {c: 'W'}, + {c: 'E'}, + {c: 'R'}, + {c: 'T'}, + {c: 'Y'}, + {c: 'U'}, + {c: 'I'}, + {c: 'O'}, + {c: 'P'} + ] + }, + { + x: 34, + keys: [ + {c: 'A'}, + {c: 'S'}, + {c: 'D'}, + {c: 'F'}, + {c: 'G'}, + {c: 'H'}, + {c: 'J'}, + {c: 'K'}, + {c: 'L'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'abc', c: 'Aa', w: 98}, + {c: 'Z'}, + {c: 'X'}, + {c: 'C'}, + {c: 'V'}, + {c: 'B'}, + {c: 'N'}, + {c: 'M'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', w: 136, c: '#123'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + 'abc': { + rows: [ + { + keys: [ + {c: 'q'}, + {c: 'w'}, + {c: 'e'}, + {c: 'r'}, + {c: 't'}, + {c: 'y'}, + {c: 'u'}, + {c: 'i'}, + {c: 'o'}, + {c: 'p'} + ] + }, + { + x: 34, + keys: [ + {c: 'a'}, + {c: 's'}, + {c: 'd'}, + {c: 'f'}, + {c: 'g'}, + {c: 'h'}, + {c: 'j'}, + {c: 'k'}, + {c: 'l'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', c: 'aA', w: 98}, + {c: 'z'}, + {c: 'x'}, + {c: 'c'}, + {c: 'v'}, + {c: 'b'}, + {c: 'n'}, + {c: 'm'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', w: 136, c: '#123'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + '#123': { + rows: [ + { + keys: [ + {c: '1'}, + {c: '2'}, + {c: '3'}, + {c: '4'}, + {c: '5'}, + {c: '6'}, + {c: '7'}, + {c: '8'}, + {c: '9'}, + {c: '0'} + ] + }, + { + x: 34, + keys: [ + {c: '@'}, + {c: '#'}, + {c: '€'}, + {c: '_'}, + {c: '&'}, + {c: '-'}, + {c: '+'}, + {c: '('}, + {c: ')'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '{&=', c: '{&=', w: 98}, + {c: '*'}, + {c: '\"'}, + {c: '\''}, + {c: ':'}, + {c: ';'}, + {c: '!'}, + {c: '?'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', w: 136, c: 'ABC'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + '{&=': { + rows: [ + { + keys: [ + {c: '~'}, + {c: '\`'}, + {c: '|'}, + {c: '\u2022'}, + {c: '√'}, + {c: 'π'}, + {c: '\u00f7'}, + {c: '\u00d7'}, + {c: '¶'}, + {c: '∆'} + ] + }, + { + keys: [ + {c: '£'}, + {c: '¥'}, + {c: '€'}, + {c: '¢'}, + {c: '^'}, + {c: '°'}, + {c: '='}, + {c: '{'}, + {c: '}'}, + {c: 'a'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', c: '#123', w: 98}, + {c: '%'}, + {c: '©'}, + {c: '®'}, + {c: '™'}, + {c: '\u2713'}, + {c: '['}, + {c: ']'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', w: 136, c: 'ABC'}, + {c: '<'}, + {action: 'space', c: '', w: 276}, + {c: '>'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + } + } +}; + +const obj$2 = { + Keyboard, + KeyboardButton, + SimpleKeyboardTemplate: template, + AdvancedKeyboardTemplate: template$1 +}; + +class ItemList extends lightning$1.Component { + static _template() { + return { + Wrapper: { + flex: {direction: 'row'} + } + }; + } + + set items(items) { + this.tag('Wrapper').children = items; + this._index = 0; + if(items.length > 0) { + this._setState('Filled'); + } + else { + this._setState('Empty'); + } + } + + get items() { + return this.tag('Wrapper').children + } + + get currentItem() { + return this.items[this._index]; + } + + get length() { + return this.items.length; + } + + set orientation(v) { + this._orientation = v; + if(v === 'horizontal') { + this.tag('Wrapper').patch({flex: {direction: 'row'}}); + } + else { + this.tag('Wrapper').patch({flex: {direction: 'column'}}); + } + } + + get orientation() { + return this._orientation || 'horizontal'; + } + + set jump(bool) { + this._jump = bool; + } + + get jump() { + return this._jump || false; + } + + set jumpToStart(bool) { + this._jumpToStart = bool; + } + + get jumpToStart() { + return this._jumpToStart !== undefined ? this._jumpToStart : this.jump; + } + + set jumpToEnd(bool) { + this._jumpToEnd = bool; + } + + get jumpToEnd() { + return this._jumpToEnd !== undefined ? this._jumpToEnd : this.jump; + } + + _navigate(dir) { + const ori = this.orientation; + if(((dir === 'right' || dir === 'left') && ori === 'horizontal') || ((dir === 'up' || dir === 'down') && ori === 'vertical')) { + const length = this.items.length; + const currentIndex = this._index; + let targetIndex = currentIndex + 1; + if(dir === 'left' || dir === 'up') { + targetIndex = currentIndex - 1; + } + + if(targetIndex > -1 && targetIndex < length) { + this._index = targetIndex; + } + else if(this.jump || (this.jumpToStart || this.jumpToEnd)) { + if(targetIndex < 0 && this.jumpToEnd) { + this._index = targetIndex + length; + } + else if(targetIndex === length && this.jumpToStart){ + this._index = 0; + } + } + else { + return false; + } + + if(currentIndex !== this._index) { + this.indexChanged({index: this._index, previousIndex: currentIndex}); + } + } + return false; + } + + setIndex(targetIndex) { + if(targetIndex > -1 && targetIndex < this.items.length) { + const currentIndex = this._index; + this._index = targetIndex; + this.indexChanged({index: this._index, previousIndex: currentIndex}); + } + } + + indexChanged(event) { + this.signal('indexChanged', event); + } + + _getFocused() { + return this; + } + + _construct() { + this._index = 0; + } + + _init() { + this._setState('Empty'); + } + + static _states() { + return [ + class Empty extends this { + }, + class Filled extends this { + _getFocused() { + return this.currentItem; + } + _handleRight() { + return this._navigate('right'); + } + + _handleLeft() { + return this._navigate('left'); + } + + _handleUp() { + return this._navigate('up'); + } + + _handleDown() { + return this._navigate('down'); + } + } + ] + } +} + +const obj$3 = { + ItemList +}; + +class Slider extends lightning$1.Component { + static _template() { + return { + Wrapper: { + flex: {direction: 'row'} + } + } + } + + set items(items) { + this._reset(); + this.tag('Wrapper').children = items; + this.scrollToFocus(true); + if(items.length > 0) { + this._setState('Filled'); + } + else { + this._setState('Empty'); + } + } + + get items() { + return this.tag('Wrapper').children; + } + + get currentItem() { + return this.items[this._index]; + } + + get index() { + return this._index; + } + + set orientation(v) { + this._orientation = v; + if(v === 'horizontal') { + this.tag('Wrapper').patch({flex: {direction: 'row'}}); + } + else { + this.tag('Wrapper').patch({flex: {direction: 'column'}}); + } + } + + get orientation() { + return this._orientation || 'horizontal'; + } + + set margin(v) { + this._margin = v; + } + + get margin() { + return this._margin || 0; + } + + set marginStart(v) { + this._marginStart = v; + } + + get marginStart() { + return this._marginStart || this.margin; + } + + set marginEnd(v) { + this._marginEnd = v; + } + + get marginEnd() { + return this._marginEnd || this.margin; + } + + set jump(bool) { + this._jump = bool; + } + + get jump() { + return this._jump || false; + } + + set jumpToStart(bool) { + this._jumpToStart = bool; + } + + get jumpToStart() { + return this._jumpToStart !== undefined ? this._jumpToStart : this.jump; + } + + set jumpToEnd(bool) { + this._jumpToEnd = bool; + } + + get jumpToEnd() { + return this._jumpToEnd !== undefined ? this._jumpToEnd : this.jump; + } + + get scrollTransitionSettings() { + return this._scrollTransitionSettings; + } + + set scrollTransition(v) { + this._scrollTransitionSettings.patch(v); + } + + get scrollTransition() { + return this._scrollTransition; + } + + get viewportSize() { + return this.orientation === 'horizontal' ? this.w : this.h; + } + + _getItemCenterPosition(item) { + if(this.orientation === 'horizontal') { + return item.finalX + (item.finalW * 0.5); + } + return item.finalY + (item.finalH * 0.5); + } + + _getScrollPosition(position) { + const s = this._fullSize; + + const viewportSize = this.viewportSize; + const marginStart = this.marginStart; + const marginEnd = this.marginEnd; + + const maxDistanceStart = 0.5 * viewportSize - marginStart; + const maxDistanceEnd = 0.5 * viewportSize - marginEnd; + if((position < maxDistanceStart) || (s < viewportSize - (marginStart + marginEnd))) { + position = maxDistanceStart; + } + else if(position > s - maxDistanceEnd) { + position = s - maxDistanceEnd; + } + return position - 0.5 * viewportSize; + } + + _navigate(dir) { + const ori = this.orientation; + if(((dir === 'right' || dir === 'left') && ori === 'horizontal') || ((dir === 'up' || dir === 'down') && ori === 'vertical')) { + const length = this.items.length; + const currentIndex = this._index; + let targetIndex = currentIndex + 1; + if(dir === 'left' || dir === 'up') { + targetIndex = currentIndex - 1; + } + + if(targetIndex > -1 && targetIndex < length) { + this._index = targetIndex; + } + else if(this.jump || (this.jumpToStart || this.jumpToEnd)) { + if(targetIndex < 0 && this.jumpToEnd) { + this._index = targetIndex + length; + } + else if(targetIndex === length && this.jumpToStart){ + this._index = 0; + } + } + + if(currentIndex !== this._index) { + this.indexChanged({index: this._index, previousIndex: currentIndex, length: this.items.length}); + } + this.scrollToFocus(); + } + return false; + } + + scrollToFocus(immediate) { + if(this.currentItem) { + const focusPosition = this._getItemCenterPosition(this.currentItem); + const scrollPosition = this._getScrollPosition(focusPosition); + if(this._scrollTransition.isRunning()) { + this._scrollTransition.reset(-scrollPosition, 0.1); + } + else { + this._scrollTransition.start(-scrollPosition); + } + if(immediate) { + this._scrollTransition.finish(); + } + } + } + + setIndex(targetIndex, immediate = false) { + if(targetIndex > -1 && targetIndex < this.items.length) { + const currentIndex = this._index; + this._index = targetIndex; + this.indexChanged({index: this._index, previousIndex: currentIndex, immediate}); + this.scrollToFocus(immediate); + } + } + + indexChanged(event) { + this.signal('indexChanged', event); + } + + _getFocused() { + return this; + } + + _reset() { + this._index = 0; + } + + _construct() { + this._index = 0; + this._scrollTransitionSettings = this.stage.transitions.createSettings({}); + } + + _init() { + const wrapper = this.tag('Wrapper'); + const or = this.orientation === 'horizontal' ? 'x' : 'y'; + wrapper.transition(or, this._scrollTransitionSettings); + this._scrollTransition = wrapper.transition(or); + wrapper.onAfterUpdate = () => { + if(this.orientation === 'horizontal') { + this._fullSize = wrapper.finalW; + } + else { + this._fullSize = wrapper.finalH; + } + }; + this._setState('Empty'); + } + + static _states() { + return [ + class Empty extends this { + }, + class Filled extends this { + _getFocused() { + return this.currentItem; + } + _handleRight() { + return this._navigate('right'); + } + + _handleLeft() { + return this._navigate('left'); + } + + _handleUp() { + return this._navigate('up'); + } + + _handleDown() { + return this._navigate('down'); + } + } + ] + } +} + +const obj$4 = { + Slider +}; + +const tools = { + player: obj, + effects: obj$1, + keyboard: obj$2, + itemlist: obj$3, + slider: obj$4 +}; + +// Exposes the ux namespace for apps. + +const ux$1 = { + Ui, + App, + tools +}; + +if (typeof window !== "undefined") { + window.ux = ux$1; +} + +class DevLauncher { + + constructor() { + } + + launch(appType, lightningOptions, options = {}) { + this._appType = appType; + this._options = options; + return this._start(lightningOptions); + } + + _handleKey(event) { + this._ui._receiveKeydown(event); + } + + _start(lightningOptions = {}) { + this._openFirewall(); + this._lightningOptions = this._getLightningOptions(lightningOptions); + return this._startApp(); + } + + _startApp() { + ux$1.Ui.staticFilesPath = __dirname + "/"; + + this._ui = new ux$1.Ui(this._lightningOptions); + this._ui.startApp(this._appType); + } + + _loadInspector() { + if (this._options.useInspector) { + /* Attach the inspector to create a fake DOM that shows where lightning elements can be found. */ + return this.loadScript(DevLauncher._uxPath + "../wpe-lightning/devtools/lightning-inspect.js"); + } else { + return Promise.resolve(); + } + } + + _openFirewall() { + // Fetch app store to ensure that proxy/image servers firewall is opened. + //fetch(`http://widgets.metrological.com/${encodeURIComponent(ux.Ui.getOption('operator') || 'metrological')}/nl/test`).then(() => {}); + } + + _getLightningOptions(customOptions = {}) { + let options = {stage: {w: 1920, h: 1080}, debug: false, keys: this._getNavigationKeys()}; + + const config = options.stage; + if (customOptions.h === 720) { + config['w'] = 1280; + config['h'] = 720; + config['precision'] = 0.6666666667; + } else { + config['w'] = 1920; + config['h'] = 1080; + + config.useImageWorker = true; + } + + options = lightning$1.tools.ObjMerger.merge(options, customOptions); + + return options; + } + + _getNavigationKeys() { + return { + 8: "Back", + 13: "Enter", + 27: "Menu", + 37: "Left", + 38: "Up", + 39: "Right", + 40: "Down", + 174: "ChannelDown", + 175: "ChannelUp", + 178: "Stop", + 250: "PlayPause", + 191: "Search", // Use "/" for keyboard + 409: "Search" + }; + } +} + +const Headers$1 = fetch.Headers; + +class MyApp extends ux$1.App { + static _template() { + return { + x: 50, + y: 50, + RoundRectangle: { + zIndex: 2, + texture: lightning$1.Tools.getRoundRect(1280, 720, 360, 3, 0xffff00ff, true, 0xff00ffff), + RoundRectangleChild: { + zIndex: 2, + x:200, + y:200, + texture: lightning$1.Tools.getRoundRect(720, 480, 260, 3, 0x00ffffff, true, 0x00ffffff), + }, + } + } + } + _handleUp(){ + this.tag("RoundRectangleChild").alpha = 0; + } + _handleDown(){ + this.tag("RoundRectangleChild").alpha = 1; + } +} + +MyApp.COLORS = { + BACKGROUND: 0xff282e32 +}; + +const launcher = new DevLauncher(); + +sparkview.on('onKeyDown', function(e) { + console.log('webgl onKeyDown keyCode:', e.keyCode); + launcher._handleKey(e); +}); + +var keymaps = { + 49: "Up", + 50: "Down" +}; +launcher.launch(MyApp, {debug:false, h:sparkscene.h, keys:keymaps}, {useInspector: false}); diff --git a/shaderApp.js b/shaderApp.js new file mode 100644 index 0000000..97c4a70 --- /dev/null +++ b/shaderApp.js @@ -0,0 +1,23770 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +require('fs'); +require('http'); +require('https'); +var fetch = _interopDefault(require('node-fetch')); + +class StageUtils { + + static mergeNumbers(v1, v2, p) { + return v1 * p + v2 * (1 - p); + }; + + static rgb(r, g, b) { + return (r << 16) + (g << 8) + b + (255 * 16777216); + }; + + static rgba(r, g, b, a) { + return (r << 16) + (g << 8) + b + (((a * 255) | 0) * 16777216); + }; + + static getRgbString(color) { + let r = ((color / 65536) | 0) % 256; + let g = ((color / 256) | 0) % 256; + let b = color % 256; + return 'rgb(' + r + ',' + g + ',' + b + ')'; + }; + + static getRgbaString(color) { + let r = ((color / 65536) | 0) % 256; + let g = ((color / 256) | 0) % 256; + let b = color % 256; + let a = ((color / 16777216) | 0) / 255; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(4) + ')'; + }; + + static getRgbaStringFromArray(color) { + let r = Math.floor(color[0] * 255); + let g = Math.floor(color[1] * 255); + let b = Math.floor(color[2] * 255); + let a = Math.floor(color[3] * 255) / 255; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(4) + ')'; + }; + + static getRgbaComponentsNormalized(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r / 255, g / 255, b / 255, a / 255]; + }; + + static getRgbComponentsNormalized(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + return [r / 255, g / 255, b / 255]; + }; + + static getRgbaComponents(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r, g, b, a]; + }; + + static getArgbNumber(rgba) { + rgba[0] = Math.max(0, Math.min(255, rgba[0])); + rgba[1] = Math.max(0, Math.min(255, rgba[1])); + rgba[2] = Math.max(0, Math.min(255, rgba[2])); + rgba[3] = Math.max(0, Math.min(255, rgba[3])); + let v = ((rgba[3] | 0) << 24) + ((rgba[0] | 0) << 16) + ((rgba[1] | 0) << 8) + (rgba[2] | 0); + if (v < 0) { + v = 0xFFFFFFFF + v + 1; + } + return v; + }; + + static mergeColors(c1, c2, p) { + let r1 = ((c1 / 65536) | 0) % 256; + let g1 = ((c1 / 256) | 0) % 256; + let b1 = c1 % 256; + let a1 = ((c1 / 16777216) | 0); + + let r2 = ((c2 / 65536) | 0) % 256; + let g2 = ((c2 / 256) | 0) % 256; + let b2 = c2 % 256; + let a2 = ((c2 / 16777216) | 0); + + let r = r1 * p + r2 * (1 - p); + let g = g1 * p + g2 * (1 - p); + let b = b1 * p + b2 * (1 - p); + let a = a1 * p + a2 * (1 - p); + + return Math.round(a) * 16777216 + Math.round(r) * 65536 + Math.round(g) * 256 + Math.round(b); + }; + + static mergeMultiColors(c, p) { + let r = 0, g = 0, b = 0, a = 0, t = 0; + let n = c.length; + for (let i = 0; i < n; i++) { + let r1 = ((c[i] / 65536) | 0) % 256; + let g1 = ((c[i] / 256) | 0) % 256; + let b1 = c[i] % 256; + let a1 = ((c[i] / 16777216) | 0); + r += r1 * p[i]; + g += g1 * p[i]; + b += b1 * p[i]; + a += a1 * p[i]; + t += p[i]; + } + + t = 1 / t; + return Math.round(a * t) * 16777216 + Math.round(r * t) * 65536 + Math.round(g * t) * 256 + Math.round(b * t); + }; + + static mergeMultiColorsEqual(c) { + let r = 0, g = 0, b = 0, a = 0, t = 0; + let n = c.length; + for (let i = 0; i < n; i++) { + let r1 = ((c[i] / 65536) | 0) % 256; + let g1 = ((c[i] / 256) | 0) % 256; + let b1 = c[i] % 256; + let a1 = ((c[i] / 16777216) | 0); + r += r1; + g += g1; + b += b1; + a += a1; + t += 1.0; + } + + t = 1 / t; + return Math.round(a * t) * 16777216 + Math.round(r * t) * 65536 + Math.round(g * t) * 256 + Math.round(b * t); + }; + + static mergeColorAlpha(c, alpha) { + let a = ((c / 16777216 | 0) * alpha) | 0; + return (((((c >> 16) & 0xff) * a) / 255) & 0xff) + + ((((c & 0xff00) * a) / 255) & 0xff00) + + (((((c & 0xff) << 16) * a) / 255) & 0xff0000) + + (a << 24); + }; + + static rad(deg) { + return deg * (Math.PI / 180); + }; + + static getTimingBezier(a, b, c, d) { + let xc = 3.0 * a; + let xb = 3.0 * (c - a) - xc; + let xa = 1.0 - xc - xb; + let yc = 3.0 * b; + let yb = 3.0 * (d - b) - yc; + let ya = 1.0 - yc - yb; + + return function (time) { + if (time >= 1.0) { + return 1; + } + if (time <= 0) { + return 0; + } + + let t = 0.5, cbx, cbxd, dx; + + for (let it = 0; it < 20; it++) { + cbx = t * (t * (t * xa + xb) + xc); + dx = time - cbx; + if (dx > -1e-8 && dx < 1e-8) { + return t * (t * (t * ya + yb) + yc); + } + + // Cubic bezier derivative. + cbxd = t * (t * (3 * xa) + 2 * xb) + xc; + + if (cbxd > 1e-10 && cbxd < 1e-10) { + // Problematic. Fall back to binary search method. + break; + } + + t += dx / cbxd; + } + + // Fallback: binary search method. This is more reliable when there are near-0 slopes. + let minT = 0; + let maxT = 1; + for (let it = 0; it < 20; it++) { + t = 0.5 * (minT + maxT); + + cbx = t * (t * (t * xa + xb) + xc); + + dx = time - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc); + } + + if (dx < 0) { + maxT = t; + } else { + minT = t; + } + } + + }; + }; + + static getTimingFunction(str) { + switch (str) { + case "linear": + return function (time) { + return time + }; + case "ease": + return StageUtils.getTimingBezier(0.25, 0.1, 0.25, 1.0); + case "ease-in": + return StageUtils.getTimingBezier(0.42, 0, 1.0, 1.0); + case "ease-out": + return StageUtils.getTimingBezier(0, 0, 0.58, 1.0); + case "ease-in-out": + return StageUtils.getTimingBezier(0.42, 0, 0.58, 1.0); + case "step-start": + return function () { + return 1 + }; + case "step-end": + return function (time) { + return time === 1 ? 1 : 0; + }; + default: + let s = "cubic-bezier("; + if (str && str.indexOf(s) === 0) { + let parts = str.substr(s.length, str.length - s.length - 1).split(","); + if (parts.length !== 4) { + console.warn("Unknown timing function: " + str); + + // Fallback: use linear. + return function (time) { + return time + }; + } + let a = parseFloat(parts[0]); + let b = parseFloat(parts[1]); + let c = parseFloat(parts[2]); + let d = parseFloat(parts[3]); + if (isNaN(a) || isNaN(b) || isNaN(c) || isNaN(d)) { + console.warn("Unknown timing function: " + str); + // Fallback: use linear. + return function (time) { + return time + }; + } + + return StageUtils.getTimingBezier(a, b, c, d); + } else { + console.warn("Unknown timing function: " + str); + // Fallback: use linear. + return function (time) { + return time + }; + } + } + }; + +} + +class Utils { + + static isFunction(value) { + return typeof value === 'function'; + } + + static isNumber(value) { + return typeof value === 'number'; + } + + static isInteger(value) { + return (typeof value === 'number' && (value % 1) === 0); + } + + static isBoolean(value) { + return value === true || value === false; + } + + static isString(value) { + return typeof value === 'string'; + } + + static clone(v) { + if (Utils.isObjectLiteral(v) || Array.isArray(v)) { + return Utils.getDeepClone(v); + } else { + // Copy by value. + return v; + } + } + + static cloneObjShallow(obj) { + let keys = Object.keys(obj); + let clone = {}; + for (let i = 0; i < keys.length; i++) { + clone[keys[i]] = obj[keys[i]]; + } + return clone; + } + + static merge(obj1, obj2) { + let keys = Object.keys(obj2); + for (let i = 0; i < keys.length; i++) { + obj1[keys[i]] = obj2[keys[i]]; + } + return obj1; + } + + static isObject(value) { + let type = typeof value; + return !!value && (type === 'object' || type === 'function'); + } + + static isPlainObject(value) { + let type = typeof value; + return !!value && (type === 'object'); + } + + static isObjectLiteral(value){ + return typeof value === 'object' && value && value.constructor === Object; + } + + static getArrayIndex(index, arr) { + return Utils.getModuloIndex(index, arr.length); + } + + static getModuloIndex(index, len) { + if (len === 0) return index; + while (index < 0) { + index += Math.ceil(-index / len) * len; + } + index = index % len; + return index; + } + + static getDeepClone(obj) { + let i, c; + if (Utils.isFunction(obj)) { + // Copy functions by reference. + return obj; + } + if (Array.isArray(obj)) { + c = []; + let keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + c[keys[i]] = Utils.getDeepClone(obj[keys[i]]); + } + return c; + } else if (Utils.isObject(obj)) { + c = {}; + let keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + c[keys[i]] = Utils.getDeepClone(obj[keys[i]]); + } + return c; + } else { + return obj; + } + } + + static equalValues(v1, v2) { + if ((typeof v1) !== (typeof v2)) return false; + if (Utils.isObjectLiteral(v1)) { + return Utils.isObjectLiteral(v2) && Utils.equalObjectLiterals(v1, v2); + } else if (Array.isArray(v1)) { + return Array.isArray(v2) && Utils.equalArrays(v1, v2); + } else { + return v1 === v2; + } + } + + static equalObjectLiterals(obj1, obj2) { + let keys1 = Object.keys(obj1); + let keys2 = Object.keys(obj2); + if (keys1.length !== keys2.length) { + return false; + } + + for (let i = 0, n = keys1.length; i < n; i++) { + const k1 = keys1[i]; + const k2 = keys2[i]; + if (k1 !== k2) { + return false; + } + + const v1 = obj1[k1]; + const v2 = obj2[k2]; + + if (!Utils.equalValues(v1, v2)) { + return false; + } + } + + return true; + } + + static equalArrays(v1, v2) { + if (v1.length !== v2.length) { + return false; + } + for (let i = 0, n = v1.length; i < n; i++) { + if (!this.equalValues(v1[i], v2[i])) { + return false; + } + } + + return true; + } + + static setToArray(s) { + let result = []; + s.forEach(function (value) { + result.push(value); + }); + return result; + } + + static iteratorToArray(iterator) { + let result = []; + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + result.push(iteratorResult.value); + iteratorResult = iterator.next(); + } + return result; + } + + static isUcChar(charcode) { + return charcode >= 65 && charcode <= 90; + } + +} + +Utils.isNode = (typeof window === "undefined"); +Utils.isWeb = (typeof window !== "undefined"); +Utils.isWPE = Utils.isWeb && (navigator.userAgent.indexOf("WPE") !== -1); +Utils.isSpark = (typeof window === "undefined") && (typeof sparkscene !== "undefined"); + +class Base { + + static defaultSetter(obj, name, value) { + obj[name] = value; + } + + static patchObject(obj, settings) { + if (!Utils.isObjectLiteral(settings)) { + console.error("Settings must be object literal"); + } else { + let names = Object.keys(settings); + for (let i = 0, n = names.length; i < n; i++) { + let name = names[i]; + + this.patchObjectProperty(obj, name, settings[name]); + } + } + } + + static patchObjectProperty(obj, name, value) { + let setter = obj.setSetting || Base.defaultSetter; + + if (name.charAt(0) === "_") { + // Disallow patching private variables. + if (name !== "__create") { + console.error("Patch of private property '" + name + "' is not allowed"); + } + } else if (name !== "type") { + // Type is a reserved keyword to specify the class type on creation. + if (Utils.isFunction(value) && value.__local) { + // Local function (Base.local(s => s.something)) + value = value.__local(obj); + } + + setter(obj, name, value); + } + } + + static local(func) { + // This function can be used as an object setting, which is called with the target object. + func.__local = true; + } + + +} + +class SpacingCalculator { + + static getSpacing(mode, numberOfItems, remainingSpace) { + const itemGaps = (numberOfItems - 1); + let spacePerGap; + + let spacingBefore, spacingBetween; + + switch(mode) { + case "flex-start": + spacingBefore = 0; + spacingBetween = 0; + break; + case "flex-end": + spacingBefore = remainingSpace; + spacingBetween = 0; + break; + case "center": + spacingBefore = remainingSpace / 2; + spacingBetween = 0; + break; + case "space-between": + spacingBefore = 0; + spacingBetween = Math.max(0, remainingSpace) / itemGaps; + break; + case "space-around": + if (remainingSpace < 0) { + return this.getSpacing("center", numberOfItems, remainingSpace); + } else { + spacePerGap = remainingSpace / (itemGaps + 1); + spacingBefore = 0.5 * spacePerGap; + spacingBetween = spacePerGap; + } + break; + case "space-evenly": + if (remainingSpace < 0) { + return this.getSpacing("center", numberOfItems, remainingSpace); + } else { + spacePerGap = remainingSpace / (itemGaps + 2); + spacingBefore = spacePerGap; + spacingBetween = spacePerGap; + } + break; + case "stretch": + spacingBefore = 0; + spacingBetween = 0; + break; + default: + throw new Error("Unknown mode: " + mode); + } + + return {spacingBefore, spacingBetween} + } + +} + +class ContentAligner { + + constructor(layout) { + this._layout = layout; + this._totalCrossAxisSize = 0; + } + + get _lines() { + return this._layout._lines; + } + + init() { + this._totalCrossAxisSize = this._getTotalCrossAxisSize(); + } + + align() { + const crossAxisSize = this._layout.crossAxisSize; + const remainingSpace = crossAxisSize - this._totalCrossAxisSize; + + const {spacingBefore, spacingBetween} = this._getSpacing(remainingSpace); + + const lines = this._lines; + + const mode = this._layout._flexContainer.alignContent; + let growSize = 0; + if (mode === "stretch" && lines.length && (remainingSpace > 0)) { + growSize = remainingSpace / lines.length; + } + + let currentPos = spacingBefore; + for (let i = 0, n = lines.length; i < n; i++) { + const crossAxisLayoutOffset = currentPos; + const aligner = lines[i].createItemAligner(); + + let finalCrossAxisLayoutSize = lines[i].crossAxisLayoutSize + growSize; + + aligner.setCrossAxisLayoutSize(finalCrossAxisLayoutSize); + aligner.setCrossAxisLayoutOffset(crossAxisLayoutOffset); + + aligner.align(); + + if (aligner.recursiveResizeOccured) { + lines[i].setItemPositions(); + } + + currentPos += finalCrossAxisLayoutSize; + currentPos += spacingBetween; + } + } + + get totalCrossAxisSize() { + return this._totalCrossAxisSize; + } + + _getTotalCrossAxisSize() { + const lines = this._lines; + let total = 0; + for (let i = 0, n = lines.length; i < n; i++) { + const line = lines[i]; + total += line.crossAxisLayoutSize; + } + return total; + } + + _getSpacing(remainingSpace) { + const mode = this._layout._flexContainer.alignContent; + const numberOfItems = this._lines.length; + return SpacingCalculator.getSpacing(mode, numberOfItems, remainingSpace); + } + +} + +class FlexUtils { + + static getParentAxisSizeWithPadding(item, horizontal) { + const target = item.target; + const parent = target.getParent(); + if (!parent) { + return 0; + } else { + const flexParent = item.flexParent; + if (flexParent) { + // Use pending layout size. + return this.getAxisLayoutSize(flexParent, horizontal) + this.getTotalPadding(flexParent, horizontal); + } else { + // Use 'absolute' size. + return horizontal ? parent.w : parent.h; + } + } + } + + static getRelAxisSize(item, horizontal) { + if (horizontal) { + if (item.funcW) { + if (this._allowRelAxisSizeFunction(item, true)) { + return item.funcW(this.getParentAxisSizeWithPadding(item, true)); + } else { + return 0; + } + } else { + return item.originalWidth; + } + } else { + if (item.funcH) { + if (this._allowRelAxisSizeFunction(item, false)) { + return item.funcH(this.getParentAxisSizeWithPadding(item, false)); + } else { + return 0; + } + } else { + return item.originalHeight; + } + } + } + + static _allowRelAxisSizeFunction(item, horizontal) { + const flexParent = item.flexParent; + if (flexParent && flexParent._flex._layout.isAxisFitToContents(horizontal)) { + // We don't allow relative width on fit-to-contents because it leads to conflicts. + return false; + } + return true; + } + + static isZeroAxisSize(item, horizontal) { + if (horizontal) { + return !item.originalWidth && !item.funcW; + } else { + return !item.originalHeight && !item.funcH; + } + } + + static getAxisLayoutPos(item, horizontal) { + return horizontal ? item.x : item.y; + } + + static getAxisLayoutSize(item, horizontal) { + return horizontal ? item.w : item.h; + } + + static setAxisLayoutPos(item, horizontal, pos) { + if (horizontal) { + item.x = pos; + } else { + item.y = pos; + } + } + + static setAxisLayoutSize(item, horizontal, size) { + if (horizontal) { + item.w = size; + } else { + item.h = size; + } + } + + static getAxisMinSize(item, horizontal) { + let minSize = this.getPlainAxisMinSize(item, horizontal); + + let flexItemMinSize = 0; + if (item.isFlexItemEnabled()) { + flexItemMinSize = item._flexItem._getMinSizeSetting(horizontal); + } + + const hasLimitedMinSize = (flexItemMinSize > 0); + if (hasLimitedMinSize) { + minSize = Math.max(minSize, flexItemMinSize); + } + return minSize; + } + + static getPlainAxisMinSize(item, horizontal) { + if (item.isFlexEnabled()) { + return item._flex._layout.getAxisMinSize(horizontal); + } else { + const isShrinkable = (item.flexItem.shrink !== 0); + if (isShrinkable) { + return 0; + } else { + return this.getRelAxisSize(item, horizontal); + } + } + } + + static resizeAxis(item, horizontal, size) { + if (item.isFlexEnabled()) { + const isMainAxis = (item._flex._horizontal === horizontal); + if (isMainAxis) { + item._flex._layout.resizeMainAxis(size); + } else { + item._flex._layout.resizeCrossAxis(size); + } + } else { + this.setAxisLayoutSize(item, horizontal, size); + } + } + + + static getPaddingOffset(item, horizontal) { + if (item.isFlexEnabled()) { + const flex = item._flex; + if (horizontal) { + return flex.paddingLeft; + } else { + return flex.paddingTop; + } + } else { + return 0; + } + } + + static getTotalPadding(item, horizontal) { + if (item.isFlexEnabled()) { + const flex = item._flex; + if (horizontal) { + return flex.paddingRight + flex.paddingLeft; + } else { + return flex.paddingTop + flex.paddingBottom; + } + } else { + return 0; + } + } + + static getMarginOffset(item, horizontal) { + const flexItem = item.flexItem; + if (flexItem) { + if (horizontal) { + return flexItem.marginLeft; + } else { + return flexItem.marginTop; + } + } else { + return 0; + } + } + + static getTotalMargin(item, horizontal) { + const flexItem = item.flexItem; + if (flexItem) { + if (horizontal) { + return flexItem.marginRight + flexItem.marginLeft; + } else { + return flexItem.marginTop + flexItem.marginBottom; + } + } else { + return 0; + } + } + +} + +class SizeShrinker { + + constructor(line) { + this._line = line; + this._amountRemaining = 0; + this._shrunkSize = 0; + } + + shrink(amount) { + this._shrunkSize = 0; + + this._amountRemaining = amount; + let totalShrinkAmount = this._getTotalShrinkAmount(); + if (totalShrinkAmount) { + const items = this._line.items; + do { + let amountPerShrink = this._amountRemaining / totalShrinkAmount; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + const shrinkAmount = flexItem.shrink; + const isShrinkableItem = (shrinkAmount > 0); + if (isShrinkableItem) { + let shrink = shrinkAmount * amountPerShrink; + const minSize = flexItem._getMainAxisMinSize(); + const size = flexItem._getMainAxisLayoutSize(); + if (size > minSize) { + const maxShrink = size - minSize; + const isFullyShrunk = (shrink >= maxShrink); + if (isFullyShrunk) { + shrink = maxShrink; + + // Destribute remaining amount over the other flex items. + totalShrinkAmount -= shrinkAmount; + } + + const finalSize = size - shrink; + flexItem._resizeMainAxis(finalSize); + + this._shrunkSize += shrink; + this._amountRemaining -= shrink; + + if (Math.abs(this._amountRemaining) < 10e-6) { + return; + } + } + } + } + } while(totalShrinkAmount && (Math.abs(this._amountRemaining) > 10e-6)); + } + } + + _getTotalShrinkAmount() { + let total = 0; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + + if (flexItem.shrink) { + const minSize = flexItem._getMainAxisMinSize(); + const size = flexItem._getMainAxisLayoutSize(); + + // Exclude those already fully shrunk. + if (size > minSize) { + total += flexItem.shrink; + } + } + } + return total; + } + + getShrunkSize() { + return this._shrunkSize; + } + +} + +class SizeGrower { + + constructor(line) { + this._line = line; + this._amountRemaining = 0; + this._grownSize = 0; + } + + grow(amount) { + this._grownSize = 0; + + this._amountRemaining = amount; + let totalGrowAmount = this._getTotalGrowAmount(); + if (totalGrowAmount) { + const items = this._line.items; + do { + let amountPerGrow = this._amountRemaining / totalGrowAmount; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + const growAmount = flexItem.grow; + const isGrowableItem = (growAmount > 0); + if (isGrowableItem) { + let grow = growAmount * amountPerGrow; + const maxSize = flexItem._getMainAxisMaxSizeSetting(); + const size = flexItem._getMainAxisLayoutSize(); + if (maxSize > 0) { + if (size >= maxSize) { + // Already fully grown. + grow = 0; + } else { + const maxGrow = maxSize - size; + const isFullyGrown = (grow >= maxGrow); + if (isFullyGrown) { + grow = maxGrow; + + // Destribute remaining amount over the other flex items. + totalGrowAmount -= growAmount; + } + } + } + + if (grow > 0) { + const finalSize = size + grow; + flexItem._resizeMainAxis(finalSize); + + this._grownSize += grow; + this._amountRemaining -= grow; + + if (Math.abs(this._amountRemaining) < 10e-6) { + return; + } + } + } + } + } while(totalGrowAmount && (Math.abs(this._amountRemaining) > 10e-6)); + } + } + + _getTotalGrowAmount() { + let total = 0; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + + if (flexItem.grow) { + const maxSize = flexItem._getMainAxisMaxSizeSetting(); + const size = flexItem._getMainAxisLayoutSize(); + + // Exclude those already fully grown. + if (maxSize === 0 || size < maxSize) { + total += flexItem.grow; + } + } + } + return total; + } + + getGrownSize() { + return this._grownSize; + } + +} + +class ItemPositioner { + + constructor(lineLayout) { + this._line = lineLayout; + } + + get _layout() { + return this._line._layout; + } + + position() { + const {spacingBefore, spacingBetween} = this._getSpacing(); + + let currentPos = spacingBefore; + + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + + item.flexItem._setMainAxisLayoutPos(currentPos); + currentPos += item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin(); + currentPos += spacingBetween; + } + } + + _getSpacing() { + const remainingSpace = this._line._availableSpace; + let mode = this._layout._flexContainer.justifyContent; + const numberOfItems = this._line.numberOfItems; + + return SpacingCalculator.getSpacing(mode, numberOfItems, remainingSpace); + } + +} + +class ItemAligner { + + constructor(line) { + this._line = line; + this._crossAxisLayoutSize = 0; + this._crossAxisLayoutOffset = 0; + this._alignItemsSetting = null; + this._recursiveResizeOccured = false; + this._isCrossAxisFitToContents = false; + } + + get _layout() { + return this._line._layout; + } + + get _flexContainer() { + return this._layout._flexContainer; + } + + setCrossAxisLayoutSize(size) { + this._crossAxisLayoutSize = size; + } + + setCrossAxisLayoutOffset(offset) { + this._crossAxisLayoutOffset = offset; + } + + align() { + this._alignItemsSetting = this._flexContainer.alignItems; + + this._isCrossAxisFitToContents = this._layout.isAxisFitToContents(!this._flexContainer._horizontal); + + this._recursiveResizeOccured = false; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + this._alignItem(item); + } + } + + get recursiveResizeOccured() { + return this._recursiveResizeOccured; + } + + _alignItem(item) { + const flexItem = item.flexItem; + let align = flexItem.alignSelf || this._alignItemsSetting; + + if (align === "stretch" && this._preventStretch(flexItem)) { + align = "flex-start"; + } + + if (align !== "stretch" && !this._isCrossAxisFitToContents) { + if (flexItem._hasRelCrossAxisSize()) { + // As cross axis size might have changed, we need to recalc the relative flex item's size. + flexItem._resetCrossAxisLayoutSize(); + } + } + + switch(align) { + case "flex-start": + this._alignItemFlexStart(flexItem); + break; + case "flex-end": + this._alignItemFlexEnd(flexItem); + break; + case "center": + this._alignItemFlexCenter(flexItem); + break; + case "stretch": + this._alignItemStretch(flexItem); + break; + } + } + + _alignItemFlexStart(flexItem) { + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset); + } + + _alignItemFlexEnd(flexItem) { + const itemCrossAxisSize = flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin(); + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset + (this._crossAxisLayoutSize - itemCrossAxisSize)); + } + + _alignItemFlexCenter(flexItem) { + const itemCrossAxisSize = flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin(); + const center = (this._crossAxisLayoutSize - itemCrossAxisSize) / 2; + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset + center); + } + + _alignItemStretch(flexItem) { + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset); + + const mainAxisLayoutSizeBeforeResize = flexItem._getMainAxisLayoutSize(); + let size = this._crossAxisLayoutSize - flexItem._getCrossAxisMargin() - flexItem._getCrossAxisPadding(); + + const crossAxisMinSizeSetting = flexItem._getCrossAxisMinSizeSetting(); + if (crossAxisMinSizeSetting > 0) { + size = Math.max(size, crossAxisMinSizeSetting); + } + + const crossAxisMaxSizeSetting = flexItem._getCrossAxisMaxSizeSetting(); + const crossAxisMaxSizeSettingEnabled = (crossAxisMaxSizeSetting > 0); + if (crossAxisMaxSizeSettingEnabled) { + size = Math.min(size, crossAxisMaxSizeSetting); + } + + flexItem._resizeCrossAxis(size); + const mainAxisLayoutSizeAfterResize = flexItem._getMainAxisLayoutSize(); + + const recursiveResize = (mainAxisLayoutSizeAfterResize !== mainAxisLayoutSizeBeforeResize); + if (recursiveResize) { + // Recursive resize can happen when this flex item has the opposite direction than the container + // and is wrapping and auto-sizing. Due to item/content stretching the main axis size of the flex + // item may decrease. If it does so, we must re-justify-content the complete line. + // Notice that we don't account for changes to the (if autosized) main axis size caused by recursive + // resize, which may cause the container's main axis to not shrink to the contents properly. + // This is by design, because if we had re-run the main axis layout, we could run into issues such + // as slow layout or endless loops. + this._recursiveResizeOccured = true; + } + } + + _preventStretch(flexItem) { + const hasFixedCrossAxisSize = flexItem._hasFixedCrossAxisSize(); + const forceStretch = (flexItem.alignSelf === "stretch"); + return hasFixedCrossAxisSize && !forceStretch; + } + +} + +class LineLayout { + + constructor(layout, startIndex, endIndex, availableSpace) { + this._layout = layout; + this.items = layout.items; + this.startIndex = startIndex; + this.endIndex = endIndex; + this._availableSpace = availableSpace; + } + + performLayout() { + this._setItemSizes(); + this.setItemPositions(); + this._calcLayoutInfo(); + } + + _setItemSizes() { + if (this._availableSpace > 0) { + this._growItemSizes(this._availableSpace); + } else if (this._availableSpace < 0) { + this._shrinkItemSizes(-this._availableSpace); + } + } + + _growItemSizes(amount) { + const grower = new SizeGrower(this); + grower.grow(amount); + this._availableSpace -= grower.getGrownSize(); + } + + _shrinkItemSizes(amount) { + const shrinker = new SizeShrinker(this); + shrinker.shrink(amount); + this._availableSpace += shrinker.getShrunkSize(); + } + + setItemPositions() { + const positioner = new ItemPositioner(this); + positioner.position(); + } + + createItemAligner() { + return new ItemAligner(this); + } + + _calcLayoutInfo() { + this._calcCrossAxisMaxLayoutSize(); + } + + getMainAxisMinSize() { + let mainAxisMinSize = 0; + for (let i = this.startIndex; i <= this.endIndex; i++) { + const item = this.items[i]; + mainAxisMinSize += item.flexItem._getMainAxisMinSizeWithPaddingAndMargin(); + } + return mainAxisMinSize; + } + + get numberOfItems() { + return this.endIndex - this.startIndex + 1; + } + + get crossAxisLayoutSize() { + const noSpecifiedCrossAxisSize = (this._layout.isCrossAxisFitToContents() && !this._layout.resizingCrossAxis); + const shouldFitToContents = (this._layout.isWrapping() || noSpecifiedCrossAxisSize); + if (shouldFitToContents) { + return this._crossAxisMaxLayoutSize; + } else { + return this._layout.crossAxisSize; + } + } + + _calcCrossAxisMaxLayoutSize() { + this._crossAxisMaxLayoutSize = this._getCrossAxisMaxLayoutSize(); + } + + _getCrossAxisMaxLayoutSize() { + let crossAxisMaxSize = 0; + for (let i = this.startIndex; i <= this.endIndex; i++) { + const item = this.items[i]; + crossAxisMaxSize = Math.max(crossAxisMaxSize, item.flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin()); + } + return crossAxisMaxSize; + } + + +} + +/** + * Distributes items over layout lines. + */ +class LineLayouter { + + constructor(layout) { + this._layout = layout; + this._mainAxisMinSize = -1; + this._crossAxisMinSize = -1; + this._mainAxisContentSize = 0; + } + + get lines() { + return this._lines; + } + + get mainAxisMinSize() { + if (this._mainAxisMinSize === -1) { + this._mainAxisMinSize = this._getMainAxisMinSize(); + } + return this._mainAxisMinSize; + } + + get crossAxisMinSize() { + if (this._crossAxisMinSize === -1) { + this._crossAxisMinSize = this._getCrossAxisMinSize(); + } + return this._crossAxisMinSize; + } + + get mainAxisContentSize() { + return this._mainAxisContentSize; + } + + layoutLines() { + this._setup(); + const items = this._layout.items; + const wrap = this._layout.isWrapping(); + + let startIndex = 0; + let i; + const n = items.length; + for (i = 0; i < n; i++) { + const item = items[i]; + + this._layoutFlexItem(item); + + // Get predicted main axis size. + const itemMainAxisSize = item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin(); + + if (wrap && (i > startIndex)) { + const isOverflowing = (this._curMainAxisPos + itemMainAxisSize > this._mainAxisSize); + if (isOverflowing) { + this._layoutLine(startIndex, i - 1); + this._curMainAxisPos = 0; + startIndex = i; + } + } + + this._addToMainAxisPos(itemMainAxisSize); + } + + if (startIndex < i) { + this._layoutLine(startIndex, i - 1); + } + } + + _layoutFlexItem(item) { + if (item.isFlexEnabled()) { + item.flexLayout.updateTreeLayout(); + } else { + item.flexItem._resetLayoutSize(); + } + } + + _setup() { + this._mainAxisSize = this._layout.mainAxisSize; + this._curMainAxisPos = 0; + this._maxMainAxisPos = 0; + this._lines = []; + + this._mainAxisMinSize = -1; + this._crossAxisMinSize = -1; + this._mainAxisContentSize = 0; + } + + _addToMainAxisPos(itemMainAxisSize) { + this._curMainAxisPos += itemMainAxisSize; + if (this._curMainAxisPos > this._maxMainAxisPos) { + this._maxMainAxisPos = this._curMainAxisPos; + } + } + + _layoutLine(startIndex, endIndex) { + const availableSpace = this._getAvailableMainAxisLayoutSpace(); + const line = new LineLayout(this._layout, startIndex, endIndex, availableSpace); + line.performLayout(); + this._lines.push(line); + + if (this._mainAxisContentSize === 0 || (this._curMainAxisPos > this._mainAxisContentSize)) { + this._mainAxisContentSize = this._curMainAxisPos; + } + } + + _getAvailableMainAxisLayoutSpace() { + if (!this._layout.resizingMainAxis && this._layout.isMainAxisFitToContents()) { + return 0; + } else { + return this._mainAxisSize - this._curMainAxisPos; + } + } + + _getCrossAxisMinSize() { + let crossAxisMinSize = 0; + const items = this._layout.items; + for (let i = 0, n = items.length; i < n; i++) { + const item = items[i]; + const itemCrossAxisMinSize = item.flexItem._getCrossAxisMinSizeWithPaddingAndMargin(); + crossAxisMinSize = Math.max(crossAxisMinSize, itemCrossAxisMinSize); + } + return crossAxisMinSize; + } + + _getMainAxisMinSize() { + if (this._lines.length === 1) { + return this._lines[0].getMainAxisMinSize(); + } else { + // Wrapping lines: specified width is used as min width (in accordance to W3C flexbox). + return this._layout.mainAxisSize; + } + } + +} + +class ItemCoordinatesUpdater { + + constructor(layout) { + this._layout = layout; + this._isReverse = this._flexContainer._reverse; + this._horizontalPaddingOffset = this._layout._getHorizontalPaddingOffset(); + this._verticalPaddingOffset = this._layout._getVerticalPaddingOffset(); + } + + get _flexContainer() { + return this._layout._flexContainer; + } + + finalize() { + const parentFlex = this._layout.getParentFlexContainer(); + if (parentFlex) { + // We must update it from the parent to set padding offsets and reverse position. + const updater = new ItemCoordinatesUpdater(parentFlex._layout); + updater._finalizeItemAndChildren(this._flexContainer.item); + } else { + this._finalizeRoot(); + this._finalizeItems(); + } + } + + _finalizeRoot() { + const item = this._flexContainer.item; + let x = FlexUtils.getAxisLayoutPos(item, true); + let y = FlexUtils.getAxisLayoutPos(item, false); + let w = FlexUtils.getAxisLayoutSize(item, true); + let h = FlexUtils.getAxisLayoutSize(item, false); + + w += this._layout._getHorizontalPadding(); + h += this._layout._getVerticalPadding(); + + item.clearRecalcFlag(); + + item.setLayout(x, y, w, h); + } + + _finalizeItems() { + const items = this._layout.items; + for (let i = 0, n = items.length; i < n; i++) { + const item = items[i]; + const validCache = this._validateItemCache(item); + + // Notice that we must also finalize a cached items, as it's coordinates may have changed. + this._finalizeItem(item); + + if (!validCache) { + this._finalizeItemChildren(item); + } + } + } + + _validateItemCache(item) { + if (item.recalc === 0) { + if (item.isFlexEnabled()) { + const layout = item._flex._layout; + + const dimensionsMatchPreviousResult = (item.w === item.target.w && item.h === item.target.h); + if (dimensionsMatchPreviousResult) { + // Cache is valid. + return true; + } else { + const crossAxisSize = layout.crossAxisSize; + layout.performResizeMainAxis(layout.mainAxisSize); + layout.performResizeCrossAxis(crossAxisSize); + } + } + } + return false; + } + + _finalizeItemAndChildren(item) { + this._finalizeItem(item); + this._finalizeItemChildren(item); + } + + _finalizeItem(item) { + if (this._isReverse) { + this._reverseMainAxisLayoutPos(item); + } + + let x = FlexUtils.getAxisLayoutPos(item, true); + let y = FlexUtils.getAxisLayoutPos(item, false); + let w = FlexUtils.getAxisLayoutSize(item, true); + let h = FlexUtils.getAxisLayoutSize(item, false); + + x += this._horizontalPaddingOffset; + y += this._verticalPaddingOffset; + + const flex = item.flex; + if (flex) { + w += item._flex._layout._getHorizontalPadding(); + h += item._flex._layout._getVerticalPadding(); + } + + const flexItem = item.flexItem; + if (flexItem) { + x += flexItem._getHorizontalMarginOffset(); + y += flexItem._getVerticalMarginOffset(); + } + + item.clearRecalcFlag(); + item.setLayout(x, y, w, h); + } + + _finalizeItemChildren(item) { + const flex = item._flex; + if (flex) { + const updater = new ItemCoordinatesUpdater(flex._layout); + updater._finalizeItems(); + } + } + + _reverseMainAxisLayoutPos(item) { + const endPos = (item.flexItem._getMainAxisLayoutPos() + item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin()); + const reversedPos = this._layout.mainAxisSize - endPos; + item.flexItem._setMainAxisLayoutPos(reversedPos); + } + +} + +/** + * Layouts a flex container (and descendants). + */ +class FlexLayout { + + constructor(flexContainer) { + this._flexContainer = flexContainer; + + this._lineLayouter = new LineLayouter(this); + + this._resizingMainAxis = false; + this._resizingCrossAxis = false; + + this._cachedMainAxisSizeAfterLayout = 0; + this._cachedCrossAxisSizeAfterLayout = 0; + + this._shrunk = false; + } + + get shrunk() { + return this._shrunk; + } + + get recalc() { + return this.item.recalc; + } + + layoutTree() { + const isSubTree = (this.item.flexParent !== null); + if (isSubTree) { + // Use the dimensions set by the parent flex tree. + this._updateSubTreeLayout(); + } else { + this.updateTreeLayout(); + } + this.updateItemCoords(); + } + + updateTreeLayout() { + if (this.recalc) { + this._performUpdateLayoutTree(); + } else { + this._performUpdateLayoutTreeFromCache(); + } + } + + _performUpdateLayoutTree() { + this._setInitialAxisSizes(); + this._layoutAxes(); + this._refreshLayoutCache(); + } + + _refreshLayoutCache() { + this._cachedMainAxisSizeAfterLayout = this.mainAxisSize; + this._cachedCrossAxisSizeAfterLayout = this.crossAxisSize; + } + + _performUpdateLayoutTreeFromCache() { + const sizeMightHaveChanged = (this.item.funcW || this.item.funcH); + if (sizeMightHaveChanged) { + // Update after all. + this.item.enableLocalRecalcFlag(); + this._performUpdateLayoutTree(); + } else { + this.mainAxisSize = this._cachedMainAxisSizeAfterLayout; + this.crossAxisSize = this._cachedCrossAxisSizeAfterLayout; + } + } + + updateItemCoords() { + const updater = new ItemCoordinatesUpdater(this); + updater.finalize(); + } + + _updateSubTreeLayout() { + // The dimensions of this container are guaranteed not to have changed. + // That's why we can safely 'reuse' those and re-layout the contents. + const crossAxisSize = this.crossAxisSize; + this._layoutMainAxis(); + this.performResizeCrossAxis(crossAxisSize); + } + + _setInitialAxisSizes() { + if (this.item.isFlexItemEnabled()) { + this.item.flexItem._resetLayoutSize(); + } else { + this.mainAxisSize = this._getMainAxisBasis(); + this.crossAxisSize = this._getCrossAxisBasis(); + } + this._resizingMainAxis = false; + this._resizingCrossAxis = false; + this._shrunk = false; + } + + _layoutAxes() { + this._layoutMainAxis(); + this._layoutCrossAxis(); + } + + /** + * @pre mainAxisSize should exclude padding. + */ + _layoutMainAxis() { + this._layoutLines(); + this._fitMainAxisSizeToContents(); + } + + _layoutLines() { + this._lineLayouter.layoutLines(); + } + + get _lines() { + return this._lineLayouter.lines; + } + + _fitMainAxisSizeToContents() { + if (!this._resizingMainAxis) { + if (this.isMainAxisFitToContents()) { + this.mainAxisSize = this._lineLayouter.mainAxisContentSize; + } + } + } + + /** + * @pre crossAxisSize should exclude padding. + */ + _layoutCrossAxis() { + const aligner = new ContentAligner(this); + aligner.init(); + this._totalCrossAxisSize = aligner.totalCrossAxisSize; + this._fitCrossAxisSizeToContents(); + aligner.align(); + } + + _fitCrossAxisSizeToContents() { + if (!this._resizingCrossAxis) { + if (this.isCrossAxisFitToContents()) { + this.crossAxisSize = this._totalCrossAxisSize; + } + } + } + + isWrapping() { + return this._flexContainer.wrap; + } + + isAxisFitToContents(horizontal) { + if (this._horizontal === horizontal) { + return this.isMainAxisFitToContents(); + } else { + return this.isCrossAxisFitToContents(); + } + } + + isMainAxisFitToContents() { + return !this.isWrapping() && !this._hasFixedMainAxisBasis(); + } + + isCrossAxisFitToContents() { + return !this._hasFixedCrossAxisBasis(); + } + + _hasFixedMainAxisBasis() { + return !FlexUtils.isZeroAxisSize(this.item, this._horizontal); + } + + _hasFixedCrossAxisBasis() { + return !FlexUtils.isZeroAxisSize(this.item, !this._horizontal); + } + + getAxisMinSize(horizontal) { + if (this._horizontal === horizontal) { + return this._getMainAxisMinSize(); + } else { + return this._getCrossAxisMinSize(); + } + } + + _getMainAxisMinSize() { + return this._lineLayouter.mainAxisMinSize; + } + + _getCrossAxisMinSize() { + return this._lineLayouter.crossAxisMinSize; + } + + resizeMainAxis(size) { + if (this.mainAxisSize !== size) { + if (this.recalc > 0) { + this.performResizeMainAxis(size); + } else { + if (this._checkValidCacheMainAxisResize()) { + this.mainAxisSize = size; + this._fitCrossAxisSizeToContents(); + } else { + // Cache miss. + this.item.enableLocalRecalcFlag(); + this.performResizeMainAxis(size); + } + } + } + } + + _checkValidCacheMainAxisResize(size) { + const isFinalMainAxisSize = (size === this.targetMainAxisSize); + if (isFinalMainAxisSize) { + return true; + } + const canIgnoreCacheMiss = !this.isCrossAxisFitToContents(); + if (canIgnoreCacheMiss) { + // Allow other main axis resizes and check if final resize matches the target main axis size + // (ItemCoordinatesUpdater). + return true; + } + return false; + } + + performResizeMainAxis(size) { + const isShrinking = (size < this.mainAxisSize); + this._shrunk = isShrinking; + + this.mainAxisSize = size; + + this._resizingMainAxis = true; + this._layoutAxes(); + this._resizingMainAxis = false; + } + + resizeCrossAxis(size) { + if (this.crossAxisSize !== size) { + if (this.recalc > 0) { + this.performResizeCrossAxis(size); + } else { + this.crossAxisSize = size; + } + } + } + + performResizeCrossAxis(size) { + this.crossAxisSize = size; + + this._resizingCrossAxis = true; + this._layoutCrossAxis(); + this._resizingCrossAxis = false; + } + + get targetMainAxisSize() { + return this._horizontal ? this.item.target.w : this.item.target.h; + } + + get targetCrossAxisSize() { + return this._horizontal ? this.item.target.h : this.item.target.w; + } + + getParentFlexContainer() { + return this.item.isFlexItemEnabled() ? this.item.flexItem.ctr : null; + } + + _getHorizontalPadding() { + return FlexUtils.getTotalPadding(this.item, true); + } + + _getVerticalPadding() { + return FlexUtils.getTotalPadding(this.item, false); + } + + _getHorizontalPaddingOffset() { + return FlexUtils.getPaddingOffset(this.item, true); + } + + _getVerticalPaddingOffset() { + return FlexUtils.getPaddingOffset(this.item, false); + } + + _getMainAxisBasis() { + return FlexUtils.getRelAxisSize(this.item, this._horizontal); + } + + _getCrossAxisBasis() { + return FlexUtils.getRelAxisSize(this.item, !this._horizontal); + } + + get _horizontal() { + return this._flexContainer._horizontal; + } + + get _reverse() { + return this._flexContainer._reverse; + } + + get item() { + return this._flexContainer.item; + } + + get items() { + return this.item.items; + } + + get resizingMainAxis() { + return this._resizingMainAxis; + } + + get resizingCrossAxis() { + return this._resizingCrossAxis; + } + + get numberOfItems() { + return this.items.length; + } + + get mainAxisSize() { + return FlexUtils.getAxisLayoutSize(this.item, this._horizontal); + } + + get crossAxisSize() { + return FlexUtils.getAxisLayoutSize(this.item, !this._horizontal); + } + + set mainAxisSize(v) { + FlexUtils.setAxisLayoutSize(this.item, this._horizontal, v); + } + + set crossAxisSize(v) { + FlexUtils.setAxisLayoutSize(this.item, !this._horizontal, v); + } + +} + +class FlexContainer { + + + constructor(item) { + this._item = item; + + this._layout = new FlexLayout(this); + this._horizontal = true; + this._reverse = false; + this._wrap = false; + this._alignItems = 'stretch'; + this._justifyContent = 'flex-start'; + this._alignContent = 'flex-start'; + + this._paddingLeft = 0; + this._paddingTop = 0; + this._paddingRight = 0; + this._paddingBottom = 0; + } + + get item() { + return this._item; + } + + _changedDimensions() { + this._item.changedDimensions(); + } + + _changedContents() { + this._item.changedContents(); + } + + get direction() { + return (this._horizontal ? "row" : "column") + (this._reverse ? "-reverse" : ""); + } + + set direction(f) { + if (this.direction === f) return; + + this._horizontal = (f === 'row' || f === 'row-reverse'); + this._reverse = (f === 'row-reverse' || f === 'column-reverse'); + + this._changedContents(); + } + + set wrap(v) { + this._wrap = v; + this._changedContents(); + } + + get wrap() { + return this._wrap; + } + + get alignItems() { + return this._alignItems; + } + + set alignItems(v) { + if (this._alignItems === v) return; + if (FlexContainer.ALIGN_ITEMS.indexOf(v) === -1) { + throw new Error("Unknown alignItems, options: " + FlexContainer.ALIGN_ITEMS.join(",")); + } + this._alignItems = v; + + this._changedContents(); + } + + get alignContent() { + return this._alignContent; + } + + set alignContent(v) { + if (this._alignContent === v) return; + if (FlexContainer.ALIGN_CONTENT.indexOf(v) === -1) { + throw new Error("Unknown alignContent, options: " + FlexContainer.ALIGN_CONTENT.join(",")); + } + this._alignContent = v; + + this._changedContents(); + } + + get justifyContent() { + return this._justifyContent; + } + + set justifyContent(v) { + if (this._justifyContent === v) return; + + if (FlexContainer.JUSTIFY_CONTENT.indexOf(v) === -1) { + throw new Error("Unknown justifyContent, options: " + FlexContainer.JUSTIFY_CONTENT.join(",")); + } + this._justifyContent = v; + + this._changedContents(); + } + + set padding(v) { + this.paddingLeft = v; + this.paddingTop = v; + this.paddingRight = v; + this.paddingBottom = v; + } + + get padding() { + return this.paddingLeft; + } + + set paddingLeft(v) { + this._paddingLeft = v; + this._changedDimensions(); + } + + get paddingLeft() { + return this._paddingLeft; + } + + set paddingTop(v) { + this._paddingTop = v; + this._changedDimensions(); + } + + get paddingTop() { + return this._paddingTop; + } + + set paddingRight(v) { + this._paddingRight = v; + this._changedDimensions(); + } + + get paddingRight() { + return this._paddingRight; + } + + set paddingBottom(v) { + this._paddingBottom = v; + this._changedDimensions(); + } + + get paddingBottom() { + return this._paddingBottom; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +FlexContainer.ALIGN_ITEMS = ["flex-start", "flex-end", "center", "stretch"]; +FlexContainer.ALIGN_CONTENT = ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly", "stretch"]; +FlexContainer.JUSTIFY_CONTENT = ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]; + +class FlexItem { + + constructor(item) { + this._ctr = null; + this._item = item; + this._grow = 0; + this._shrink = FlexItem.SHRINK_AUTO; + this._alignSelf = undefined; + this._minWidth = 0; + this._minHeight = 0; + this._maxWidth = 0; + this._maxHeight = 0; + + this._marginLeft = 0; + this._marginTop = 0; + this._marginRight = 0; + this._marginBottom = 0; + } + + get item() { + return this._item; + } + + get grow() { + return this._grow; + } + + set grow(v) { + if (this._grow === v) return; + + this._grow = parseInt(v) || 0; + + this._changed(); + } + + get shrink() { + if (this._shrink === FlexItem.SHRINK_AUTO) { + return this._getDefaultShrink(); + } + return this._shrink; + } + + _getDefaultShrink() { + if (this.item.isFlexEnabled()) { + return 1; + } else { + // All non-flex containers are absolutely positioned items with fixed dimensions, and by default not shrinkable. + return 0; + } + } + + set shrink(v) { + if (this._shrink === v) return; + + this._shrink = parseInt(v) || 0; + + this._changed(); + } + + get alignSelf() { + return this._alignSelf; + } + + set alignSelf(v) { + if (this._alignSelf === v) return; + + if (v === undefined) { + this._alignSelf = undefined; + } else { + if (FlexContainer.ALIGN_ITEMS.indexOf(v) === -1) { + throw new Error("Unknown alignSelf, options: " + FlexContainer.ALIGN_ITEMS.join(",")); + } + this._alignSelf = v; + } + + this._changed(); + } + + get minWidth() { + return this._minWidth; + } + + set minWidth(v) { + this._minWidth = Math.max(0, v); + this._item.changedDimensions(true, false); + } + + get minHeight() { + return this._minHeight; + } + + set minHeight(v) { + this._minHeight = Math.max(0, v); + this._item.changedDimensions(false, true); + } + + get maxWidth() { + return this._maxWidth; + } + + set maxWidth(v) { + this._maxWidth = Math.max(0, v); + this._item.changedDimensions(true, false); + } + + get maxHeight() { + return this._maxHeight; + } + + set maxHeight(v) { + this._maxHeight = Math.max(0, v); + this._item.changedDimensions(false, true); + } + + /** + * @note margins behave slightly different than in HTML with regard to shrinking. + * In HTML, (outer) margins can be removed when shrinking. In this engine, they will not shrink at all. + */ + set margin(v) { + this.marginLeft = v; + this.marginTop = v; + this.marginRight = v; + this.marginBottom = v; + } + + get margin() { + return this.marginLeft; + } + + set marginLeft(v) { + this._marginLeft = v; + this._changed(); + } + + get marginLeft() { + return this._marginLeft; + } + + set marginTop(v) { + this._marginTop = v; + this._changed(); + } + + get marginTop() { + return this._marginTop; + } + + set marginRight(v) { + this._marginRight = v; + this._changed(); + } + + get marginRight() { + return this._marginRight; + } + + set marginBottom(v) { + this._marginBottom = v; + this._changed(); + } + + get marginBottom() { + return this._marginBottom; + } + + _changed() { + if (this.ctr) this.ctr._changedContents(); + } + + set ctr(v) { + this._ctr = v; + } + + get ctr() { + return this._ctr; + } + + patch(settings) { + Base.patchObject(this, settings); + } + + _resetLayoutSize() { + this._resetHorizontalAxisLayoutSize(); + this._resetVerticalAxisLayoutSize(); + } + + _resetCrossAxisLayoutSize() { + if (this.ctr._horizontal) { + this._resetVerticalAxisLayoutSize(); + } else { + this._resetHorizontalAxisLayoutSize(); + } + } + + _resetHorizontalAxisLayoutSize() { + let w = FlexUtils.getRelAxisSize(this.item, true); + if (this._minWidth) { + w = Math.max(this._minWidth, w); + } + if (this._maxWidth) { + w = Math.min(this._maxWidth, w); + } + FlexUtils.setAxisLayoutSize(this.item, true, w); + } + + _resetVerticalAxisLayoutSize() { + let h = FlexUtils.getRelAxisSize(this.item, false); + if (this._minHeight) { + h = Math.max(this._minHeight, h); + } + if (this._maxHeight) { + h = Math.min(this._maxHeight, h); + } + FlexUtils.setAxisLayoutSize(this.item, false, h); + } + + _getCrossAxisMinSizeSetting() { + return this._getMinSizeSetting(!this.ctr._horizontal); + } + + _getCrossAxisMaxSizeSetting() { + return this._getMaxSizeSetting(!this.ctr._horizontal); + } + + _getMainAxisMaxSizeSetting() { + return this._getMaxSizeSetting(this.ctr._horizontal); + } + + _getMinSizeSetting(horizontal) { + if (horizontal) { + return this._minWidth; + } else { + return this._minHeight; + } + } + + _getMaxSizeSetting(horizontal) { + if (horizontal) { + return this._maxWidth; + } else { + return this._maxHeight; + } + } + + _getMainAxisMinSize() { + return FlexUtils.getAxisMinSize(this.item, this.ctr._horizontal); + } + + _getCrossAxisMinSize() { + return FlexUtils.getAxisMinSize(this.item, !this.ctr._horizontal); + } + + _getMainAxisLayoutSize() { + return FlexUtils.getAxisLayoutSize(this.item, this.ctr._horizontal); + } + + _getMainAxisLayoutPos() { + return FlexUtils.getAxisLayoutPos(this.item, this.ctr._horizontal); + } + + _setMainAxisLayoutPos(pos) { + return FlexUtils.setAxisLayoutPos(this.item, this.ctr._horizontal, pos); + } + + _setCrossAxisLayoutPos(pos) { + return FlexUtils.setAxisLayoutPos(this.item, !this.ctr._horizontal, pos); + } + + _getCrossAxisLayoutSize() { + return FlexUtils.getAxisLayoutSize(this.item, !this.ctr._horizontal); + } + + _resizeCrossAxis(size) { + return FlexUtils.resizeAxis(this.item, !this.ctr._horizontal, size); + } + + _resizeMainAxis(size) { + return FlexUtils.resizeAxis(this.item, this.ctr._horizontal, size); + } + + _getMainAxisPadding() { + return FlexUtils.getTotalPadding(this.item, this.ctr._horizontal); + } + + _getCrossAxisPadding() { + return FlexUtils.getTotalPadding(this.item, !this.ctr._horizontal); + } + + _getMainAxisMargin() { + return FlexUtils.getTotalMargin(this.item, this.ctr._horizontal); + } + + _getCrossAxisMargin() { + return FlexUtils.getTotalMargin(this.item, !this.ctr._horizontal); + } + + _getHorizontalMarginOffset() { + return FlexUtils.getMarginOffset(this.item, true); + } + + _getVerticalMarginOffset() { + return FlexUtils.getMarginOffset(this.item, false); + } + + _getMainAxisMinSizeWithPaddingAndMargin() { + return this._getMainAxisMinSize() + this._getMainAxisPadding() + this._getMainAxisMargin(); + } + + _getCrossAxisMinSizeWithPaddingAndMargin() { + return this._getCrossAxisMinSize() + this._getCrossAxisPadding() + this._getCrossAxisMargin(); + } + + _getMainAxisLayoutSizeWithPaddingAndMargin() { + return this._getMainAxisLayoutSize() + this._getMainAxisPadding() + this._getMainAxisMargin(); + } + + _getCrossAxisLayoutSizeWithPaddingAndMargin() { + return this._getCrossAxisLayoutSize() + this._getCrossAxisPadding() + this._getCrossAxisMargin(); + } + + _hasFixedCrossAxisSize() { + return !FlexUtils.isZeroAxisSize(this.item, !this.ctr._horizontal); + } + + _hasRelCrossAxisSize() { + return !!(this.ctr._horizontal ? this.item.funcH : this.item.funcW); + } + +} + + +FlexItem.SHRINK_AUTO = -1; + +/** + * This is the connection between the render tree with the layout tree of this flex container/item. + */ +class FlexTarget { + + constructor(target) { + this._target = target; + + /** + * Possible values (only in case of container): + * bit 0: has changed or contains items with changes + * bit 1: width changed + * bit 2: height changed + */ + this._recalc = 0; + + this._enabled = false; + + this.x = 0; + this.y = 0; + this.w = 0; + this.h = 0; + + this._originalX = 0; + this._originalY = 0; + this._originalWidth = 0; + this._originalHeight = 0; + + this._flex = null; + this._flexItem = null; + this._flexItemDisabled = false; + + this._items = null; + } + + get flexLayout() { + return this.flex ? this.flex._layout : null; + } + + layoutFlexTree() { + if (this.isFlexEnabled() && this.isChanged()) { + this.flexLayout.layoutTree(); + } + } + + get target() { + return this._target; + } + + get flex() { + return this._flex; + } + + set flex(v) { + if (!v) { + if (this.isFlexEnabled()) { + this._disableFlex(); + } + } else { + if (!this.isFlexEnabled()) { + this._enableFlex(); + } + this._flex.patch(v); + } + } + + get flexItem() { + if (this._flexItemDisabled) { + return false; + } + this._ensureFlexItem(); + return this._flexItem; + } + + set flexItem(v) { + if (v === false) { + if (!this._flexItemDisabled) { + const parent = this.flexParent; + this._flexItemDisabled = true; + this._checkEnabled(); + if (parent) { + parent._clearFlexItemsCache(); + parent.changedContents(); + } + } + } else { + this._ensureFlexItem(); + + this._flexItem.patch(v); + + if (this._flexItemDisabled) { + this._flexItemDisabled = false; + this._checkEnabled(); + const parent = this.flexParent; + if (parent) { + parent._clearFlexItemsCache(); + parent.changedContents(); + } + } + } + } + + _enableFlex() { + this._flex = new FlexContainer(this); + this._checkEnabled(); + this.changedDimensions(); + this._enableChildrenAsFlexItems(); + } + + _disableFlex() { + this.changedDimensions(); + this._flex = null; + this._checkEnabled(); + this._disableChildrenAsFlexItems(); + } + + _enableChildrenAsFlexItems() { + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + child.layout._enableFlexItem(); + } + } + } + + _disableChildrenAsFlexItems() { + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + child.layout._disableFlexItem(); + } + } + } + + _enableFlexItem() { + this._ensureFlexItem(); + const flexParent = this._target._parent._layout; + this._flexItem.ctr = flexParent._flex; + flexParent.changedContents(); + this._checkEnabled(); + } + + _disableFlexItem() { + if (this._flexItem) { + this._flexItem.ctr = null; + } + + // We keep the flexItem object because it may contain custom settings. + this._checkEnabled(); + + // Offsets have been changed. We can't recover them, so we'll just clear them instead. + this._resetOffsets(); + } + + _resetOffsets() { + this.x = 0; + this.y = 0; + } + + _ensureFlexItem() { + if (!this._flexItem) { + this._flexItem = new FlexItem(this); + } + } + + _checkEnabled() { + const enabled = this.isEnabled(); + if (this._enabled !== enabled) { + if (enabled) { + this._enable(); + } else { + this._disable(); + } + this._enabled = enabled; + } + } + + _enable() { + this._setupTargetForFlex(); + this._target.enableFlexLayout(); + } + + _disable() { + this._restoreTargetToNonFlex(); + this._target.disableFlexLayout(); + } + + isEnabled() { + return this.isFlexEnabled() || this.isFlexItemEnabled(); + } + + isFlexEnabled() { + return this._flex !== null; + } + + isFlexItemEnabled() { + return this.flexParent !== null; + } + + _restoreTargetToNonFlex() { + const target = this._target; + target.x = this._originalX; + target.y = this._originalY; + target.setDimensions(this._originalWidth, this._originalHeight); + } + + _setupTargetForFlex() { + const target = this._target; + this._originalX = target._x; + this._originalY = target._y; + this._originalWidth = target._w; + this._originalHeight = target._h; + } + + setParent(from, to) { + if (from && from.isFlexContainer()) { + from._layout._changedChildren(); + } + + if (to && to.isFlexContainer()) { + this._enableFlexItem(); + to._layout._changedChildren(); + } + this._checkEnabled(); + } + + get flexParent() { + if (this._flexItemDisabled) { + return null; + } + + const parent = this._target._parent; + if (parent && parent.isFlexContainer()) { + return parent._layout; + } + return null; + } + + setVisible(v) { + const parent = this.flexParent; + if (parent) { + parent._changedChildren(); + } + } + + get items() { + if (!this._items) { + this._items = this._getFlexItems(); + } + return this._items; + } + + _getFlexItems() { + const items = []; + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const item = children[i]; + if (item.visible) { + if (item.isFlexItem()) { + items.push(item.layout); + } + } + } + } + return items; + } + + _changedChildren() { + this._clearFlexItemsCache(); + this.changedContents(); + } + + _clearFlexItemsCache() { + this._items = null; + } + + setLayout(x, y, w, h) { + let originalX = this._originalX; + let originalY = this._originalY; + if (this.funcX) { + originalX = this.funcX(FlexUtils.getParentAxisSizeWithPadding(this, true)); + } + if (this.funcY) { + originalY = this.funcY(FlexUtils.getParentAxisSizeWithPadding(this, false)); + } + + if (this.isFlexItemEnabled()) { + this.target.setLayout(x + originalX, y + originalY, w, h); + } else { + // Reuse the x,y 'settings'. + this.target.setLayout(originalX, originalY, w, h); + } + } + + changedDimensions(changeWidth = true, changeHeight = true) { + this._updateRecalc(changeWidth, changeHeight); + } + + changedContents() { + this._updateRecalc(); + } + + forceLayout() { + this._updateRecalc(); + } + + isChanged() { + return this._recalc > 0; + } + + _updateRecalc(changeExternalWidth = false, changeExternalHeight = false) { + if (this.isFlexEnabled()) { + const layout = this._flex._layout; + + // When something internal changes, it can have effect on the external dimensions. + changeExternalWidth = changeExternalWidth || layout.isAxisFitToContents(true); + changeExternalHeight = changeExternalHeight || layout.isAxisFitToContents(false); + } + + const recalc = 1 + (changeExternalWidth ? 2 : 0) + (changeExternalHeight ? 4 : 0); + const newRecalcFlags = this.getNewRecalcFlags(recalc); + this._recalc |= recalc; + if (newRecalcFlags > 1) { + if (this.flexParent) { + this.flexParent._updateRecalcBottomUp(recalc); + } else { + this._target.triggerLayout(); + } + } else { + this._target.triggerLayout(); + } + } + + getNewRecalcFlags(flags) { + return (7 - this._recalc) & flags; + } + + _updateRecalcBottomUp(childRecalc) { + const newRecalc = this._getRecalcFromChangedChildRecalc(childRecalc); + const newRecalcFlags = this.getNewRecalcFlags(newRecalc); + this._recalc |= newRecalc; + if (newRecalcFlags > 1) { + const flexParent = this.flexParent; + if (flexParent) { + flexParent._updateRecalcBottomUp(newRecalc); + } else { + this._target.triggerLayout(); + } + } else { + this._target.triggerLayout(); + } + } + + _getRecalcFromChangedChildRecalc(childRecalc) { + const layout = this._flex._layout; + + const mainAxisRecalcFlag = layout._horizontal ? 1 : 2; + const crossAxisRecalcFlag = layout._horizontal ? 2 : 1; + + const crossAxisDimensionsChangedInChild = (childRecalc & crossAxisRecalcFlag); + if (!crossAxisDimensionsChangedInChild) { + const mainAxisDimensionsChangedInChild = (childRecalc & mainAxisRecalcFlag); + if (mainAxisDimensionsChangedInChild) { + const mainAxisIsWrapping = layout.isWrapping(); + if (mainAxisIsWrapping) { + const crossAxisIsFitToContents = layout.isCrossAxisFitToContents(); + if (crossAxisIsFitToContents) { + // Special case: due to wrapping, the cross axis size may be changed. + childRecalc += crossAxisRecalcFlag; + } + } + } + } + + let isWidthDynamic = layout.isAxisFitToContents(true); + let isHeightDynamic = layout.isAxisFitToContents(false); + + if (layout.shrunk) { + // If during previous layout this container was 'shrunk', any changes may change the 'min axis size' of the + // contents, leading to a different axis size on this container even when it was not 'fit to contents'. + if (layout._horizontal) { + isWidthDynamic = true; + } else { + isHeightDynamic = true; + } + } + + const localRecalc = 1 + (isWidthDynamic ? 2 : 0) + (isHeightDynamic ? 4 : 0); + + const combinedRecalc = childRecalc & localRecalc; + return combinedRecalc; + } + + get recalc() { + return this._recalc; + } + + clearRecalcFlag() { + this._recalc = 0; + } + + enableLocalRecalcFlag() { + this._recalc = 1; + } + + get originalX() { + return this._originalX; + } + + setOriginalXWithoutUpdatingLayout(v) { + this._originalX = v; + } + + get originalY() { + return this._originalY; + } + + setOriginalYWithoutUpdatingLayout(v) { + this._originalY = v; + } + + get originalWidth() { + return this._originalWidth; + } + + set originalWidth(v) { + if (this._originalWidth !== v) { + this._originalWidth = v; + this.changedDimensions(true, false); + } + } + + get originalHeight() { + return this._originalHeight; + } + + set originalHeight(v) { + if (this._originalHeight !== v) { + this._originalHeight = v; + this.changedDimensions(false, true); + } + } + + get funcX() { + return this._target.funcX; + } + + get funcY() { + return this._target.funcY; + } + + get funcW() { + return this._target.funcW; + } + + get funcH() { + return this._target.funcH; + } +} + +class TextureSource { + + constructor(manager, loader = null) { + this.id = TextureSource.id++; + + this.manager = manager; + + this.stage = manager.stage; + + /** + * All enabled textures (textures that are used by visible elements). + * @type {Set} + */ + this.textures = new Set(); + + /** + * The number of active textures (textures that have at least one active element). + * @type {number} + * @private + */ + this._activeTextureCount = 0; + + /** + * The factory for the source of this texture. + * @type {Function} + */ + this.loader = loader; + + /** + * Identifier for reuse. + * @type {String} + */ + this.lookupId = null; + + /** + * If set, this.is called when the texture source is no longer displayed (this.components.size becomes 0). + * @type {Function} + */ + this._cancelCb = null; + + /** + * Loading since timestamp in millis. + * @type {number} + */ + this.loadingSince = 0; + + this.w = 0; + this.h = 0; + + this._nativeTexture = null; + + /** + * If true, then this.texture source is never freed from memory during garbage collection. + * @type {boolean} + */ + this.permanent = false; + + /** + * Sub-object with texture-specific rendering information. + * For images, contains the src property, for texts, contains handy rendering information. + * @type {Object} + */ + this.renderInfo = null; + + /** + * Generated for 'renderToTexture'. + * @type {boolean} + * @private + */ + this._isResultTexture = !this.loader; + + /** + * Contains the load error, if the texture source could previously not be loaded. + * @type {object} + * @private + */ + this._loadError = null; + + /** + * Hold a reference to the javascript variable which contains the texture, this is not required for WebGL in WebBrowsers but is required for Spark runtime. + * @type {object} + * @private + */ + this._imageRef = null; + + } + + get loadError() { + return this._loadError; + } + + addTexture(v) { + if (!this.textures.has(v)) { + this.textures.add(v); + } + } + + removeTexture(v) { + this.textures.delete(v); + } + + incActiveTextureCount() { + this._activeTextureCount++; + if (this._activeTextureCount === 1) { + this.becomesUsed(); + } + } + + decActiveTextureCount() { + this._activeTextureCount--; + if (this._activeTextureCount === 0) { + this.becomesUnused(); + } + } + + get isResultTexture() { + return this._isResultTexture; + } + + set isResultTexture(v) { + this._isResultTexture = v; + } + + forEachEnabledElement(cb) { + this.textures.forEach(texture => { + texture.elements.forEach(cb); + }); + } + + hasEnabledElements() { + return this.textures.size > 0; + } + + forEachActiveElement(cb) { + this.textures.forEach(texture => { + texture.elements.forEach(element => { + if (element.active) { + cb(element); + } + }); + }); + } + + getRenderWidth() { + return this.w; + } + + getRenderHeight() { + return this.h; + } + + allowCleanup() { + return !this.permanent && !this.isUsed(); + } + + becomesUsed() { + // Even while the texture is being loaded, make sure it is on the lookup map so that others can reuse it. + this.load(); + } + + becomesUnused() { + this.cancel(); + } + + cancel() { + if (this.isLoading()) { + if (this._cancelCb) { + this._cancelCb(this); + + // Clear callback to avoid memory leaks. + this._cancelCb = null; + } + this.loadingSince = 0; + } + } + + isLoaded() { + return !!this._nativeTexture; + } + + isLoading() { + return (this.loadingSince > 0); + } + + isError() { + return !!this._loadError; + } + + reload() { + this.free(); + if (this.isUsed()) { + this.load(); + } + } + + load(forceSync = false) { + // From the moment of loading (when a texture source becomes used by active elements) + if (this.isResultTexture) { + // Element result texture source, for which the loading is managed by the core. + return; + } + + if (!this._nativeTexture && !this.isLoading()) { + this.loadingSince = (new Date()).getTime(); + this._cancelCb = this.loader((err, options) => { + // Ignore loads that come in after a cancel. + if (this.isLoading()) { + // Clear callback to avoid memory leaks. + this._cancelCb = null; + + if (this.manager.stage.destroyed) { + // Ignore async load when stage is destroyed. + return; + } + if (err) { + // Emit txError. + this.onError(err); + } else if (options && options.source) { + if (!this.stage.isUpdatingFrame() && !forceSync && (options.throttle !== false)) { + const textureThrottler = this.stage.textureThrottler; + this._cancelCb = textureThrottler.genericCancelCb; + textureThrottler.add(this, options); + } else { + this.processLoadedSource(options); + } + } + } + }, this); + } + } + + processLoadedSource(options) { + this.loadingSince = 0; + this.setSource(options); + } + + setSource(options) { + const source = options.source; + + this.w = source.width || (options && options.w) || 0; + this.h = source.height || (options && options.h) || 0; + + if (options && options.renderInfo) { + // Assign to id in cache so that it can be reused. + this.renderInfo = options.renderInfo; + } + + this.permanent = !!options.permanent; + + if (options && options.imageRef) + this._imageRef = options.imageRef; + if (options && options.flipTextureY) { + this._flipTextureY = options.flipTextureY; + } else { + this._flipTextureY = false; + } + + if (this._isNativeTexture(source)) { + // Texture managed by caller. + this._nativeTexture = source; + + this.w = this.w || source.w; + this.h = this.h || source.h; + + // WebGLTexture objects are by default; + this.permanent = options.hasOwnProperty('permanent') ? options.permanent : true; + } else { + this.manager.uploadTextureSource(this, options); + } + + // Must be cleared when reload is succesful. + this._loadError = null; + + this.onLoad(); + } + + isUsed() { + return this._activeTextureCount > 0; + } + + onLoad() { + if (this.isUsed()) { + this.textures.forEach(texture => { + texture.onLoad(); + }); + } + } + + forceRenderUpdate() { + // Userland should call this method after changing the nativeTexture manually outside of the framework + // (using tex[Sub]Image2d for example). + + if (this._nativeTexture) { + // Change 'update' flag. This is currently not used by the framework but is handy in userland. + this._nativeTexture.update = this.stage.frameCounter; + } + + this.forEachActiveElement(function (element) { + element.forceRenderUpdate(); + }); + + } + + forceUpdateRenderCoords() { + this.forEachActiveElement(function (element) { + element._updateTextureCoords(); + }); + } + + get nativeTexture() { + return this._nativeTexture; + } + + clearNativeTexture() { + this._nativeTexture = null; + //also clear the reference to the texture variable. + this._imageRef = null; + } + + /** + * Used for result textures. + */ + replaceNativeTexture(newNativeTexture, w, h) { + let prevNativeTexture = this._nativeTexture; + // Loaded by core. + this._nativeTexture = newNativeTexture; + this.w = w; + this.h = h; + + if (!prevNativeTexture && this._nativeTexture) { + this.forEachActiveElement(element => element.onTextureSourceLoaded()); + } + + if (!this._nativeTexture) { + this.forEachActiveElement(element => element._setDisplayedTexture(null)); + } + + // Dimensions must be updated also on enabled elements, as it may force it to go within bounds. + this.forEachEnabledElement(element => element._updateDimensions()); + + // Notice that the sprite map must never contain render textures. + } + + onError(e) { + this._loadError = e; + this.loadingSince = 0; + console.error('texture load error', e, this.lookupId); + this.forEachActiveElement(element => element.onTextureSourceLoadError(e)); + } + + free() { + if (this.isLoaded()) { + this.manager.freeTextureSource(this); + } + } + + _isNativeTexture(source) { + return ((Utils.isNode ? source.constructor.name === "WebGLTexture" : source instanceof WebGLTexture)); + } + +} + +TextureSource.prototype.isTextureSource = true; + +TextureSource.id = 1; + +class ElementTexturizer { + + constructor(elementCore) { + + this._element = elementCore.element; + this._core = elementCore; + + this.ctx = this._core.ctx; + + this._enabled = false; + this.lazy = false; + this._colorize = false; + + this._renderTexture = null; + + this._renderTextureReused = false; + + this._resultTextureSource = null; + + this._renderOffscreen = false; + + this.empty = false; + } + + get enabled() { + return this._enabled; + } + + set enabled(v) { + this._enabled = v; + this._core.updateRenderToTextureEnabled(); + } + + get renderOffscreen() { + return this._renderOffscreen; + } + + set renderOffscreen(v) { + this._renderOffscreen = v; + this._core.setHasRenderUpdates(1); + + // This enforces rechecking the 'within bounds'. + this._core._setRecalc(6); + } + + get colorize() { + return this._colorize; + } + + set colorize(v) { + if (this._colorize !== v) { + this._colorize = v; + + // Only affects the finally drawn quad. + this._core.setHasRenderUpdates(1); + } + } + + _getTextureSource() { + if (!this._resultTextureSource) { + this._resultTextureSource = new TextureSource(this._element.stage.textureManager); + this.updateResultTexture(); + } + return this._resultTextureSource; + } + + hasResultTexture() { + return !!this._resultTextureSource; + } + + resultTextureInUse() { + return this._resultTextureSource && this._resultTextureSource.hasEnabledElements(); + } + + updateResultTexture() { + let resultTexture = this.getResultTexture(); + if (this._resultTextureSource) { + if (this._resultTextureSource.nativeTexture !== resultTexture) { + let w = resultTexture ? resultTexture.w : 0; + let h = resultTexture ? resultTexture.h : 0; + this._resultTextureSource.replaceNativeTexture(resultTexture, w, h); + } + + // Texture will be updated: all elements using the source need to be updated as well. + this._resultTextureSource.forEachEnabledElement(element => { + element._updateDimensions(); + element.core.setHasRenderUpdates(3); + }); + } + } + + mustRenderToTexture() { + // Check if we must really render as texture. + if (this._enabled && !this.lazy) { + return true; + } else if (this._enabled && this.lazy && this._core._hasRenderUpdates < 3) { + // Static-only: if renderToTexture did not need to update during last drawn frame, generate it as a cache. + return true; + } + return false; + } + + deactivate() { + this.release(); + } + + get renderTextureReused() { + return this._renderTextureReused; + } + + release() { + this.releaseRenderTexture(); + } + + releaseRenderTexture() { + if (this._renderTexture) { + if (!this._renderTextureReused) { + this.ctx.releaseRenderTexture(this._renderTexture); + } + this._renderTexture = null; + this._renderTextureReused = false; + this.updateResultTexture(); + } + } + + // Reuses the specified texture as the render texture (in ancestor). + reuseTextureAsRenderTexture(nativeTexture) { + if (this._renderTexture !== nativeTexture) { + this.releaseRenderTexture(); + this._renderTexture = nativeTexture; + this._renderTextureReused = true; + } + } + + hasRenderTexture() { + return !!this._renderTexture; + } + + getRenderTexture() { + if (!this._renderTexture) { + this._renderTexture = this.ctx.allocateRenderTexture(this._core._w, this._core._h); + this._renderTextureReused = false; + } + return this._renderTexture; + } + + getResultTexture() { + return this._renderTexture; + } + +} + +class ElementCore { + + constructor(element) { + this._element = element; + + this.ctx = element.stage.ctx; + + // The memory layout of the internal variables is affected by their position in the constructor. + // It boosts performance to order them by usage of cpu-heavy functions (renderSimple and update). + + this._recalc = 0; + + this._parent = null; + + this._onUpdate = null; + + this._pRecalc = 0; + + this._worldContext = new ElementCoreContext(); + + this._hasUpdates = false; + + this._localAlpha = 1; + + this._onAfterCalcs = null; + + this._onAfterUpdate = null; + + // All local translation/transform updates: directly propagated from x/y/w/h/scale/whatever. + this._localPx = 0; + this._localPy = 0; + + this._localTa = 1; + this._localTb = 0; + this._localTc = 0; + this._localTd = 1; + + this._isComplex = false; + + this._dimsUnknown = false; + + this._clipping = false; + + // Used by both update and render. + this._zSort = false; + + this._outOfBounds = 0; + + /** + * The texture source to be displayed. + * @type {TextureSource} + */ + this._displayedTextureSource = null; + + this._zContextUsage = 0; + + this._children = null; + + this._hasRenderUpdates = 0; + + this._zIndexedChildren = null; + + this._renderContext = this._worldContext; + + this.renderState = this.ctx.renderState; + + this._scissor = null; + + // The ancestor ElementCore that owns the inherited shader. Null if none is active (default shader). + this._shaderOwner = null; + + + this._updateTreeOrder = 0; + + this._colorUl = this._colorUr = this._colorBl = this._colorBr = 0xFFFFFFFF; + + this._x = 0; + this._y = 0; + this._w = 0; + this._h = 0; + + this._optFlags = 0; + this._funcX = null; + this._funcY = null; + this._funcW = null; + this._funcH = null; + + this._scaleX = 1; + this._scaleY = 1; + this._pivotX = 0.5; + this._pivotY = 0.5; + this._mountX = 0; + this._mountY = 0; + this._rotation = 0; + + this._alpha = 1; + this._visible = true; + + this._ulx = 0; + this._uly = 0; + this._brx = 1; + this._bry = 1; + + this._zIndex = 0; + this._forceZIndexContext = false; + this._zParent = null; + + this._isRoot = false; + + /** + * Iff true, during zSort, this element should be 're-sorted' because either: + * - zIndex did chang + * - zParent did change + * - element was moved in the render tree + * @type {boolean} + */ + this._zIndexResort = false; + + this._shader = null; + + // Element is rendered on another texture. + this._renderToTextureEnabled = false; + + this._texturizer = null; + + this._useRenderToTexture = false; + + this._boundsMargin = null; + + this._recBoundsMargin = null; + + this._withinBoundsMargin = false; + + this._viewport = null; + + this._clipbox = true; + + this.render = this._renderSimple; + + this._layout = null; + } + + get offsetX() { + if (this._funcX) { + return this._funcX; + } else { + if (this.hasFlexLayout()) { + return this._layout.originalX; + } else { + return this._x; + } + } + } + + set offsetX(v) { + if (Utils.isFunction(v)) { + this.funcX = v; + } else { + this._disableFuncX(); + if (this.hasFlexLayout()) { + this.x += (v - this._layout.originalX); + this._layout.setOriginalXWithoutUpdatingLayout(v); + } else { + this.x = v; + } + } + } + + get x() { + return this._x; + } + + set x(v) { + if (v !== this._x) { + this._updateLocalTranslateDelta(v - this._x, 0); + this._x = v; + } + } + + get funcX() { + return (this._optFlags & 1 ? this._funcX : null); + } + + set funcX(v) { + if (this._funcX !== v) { + this._optFlags |= 1; + this._funcX = v; + if (this.hasFlexLayout()) { + this._layout.setOriginalXWithoutUpdatingLayout(0); + this.layout.forceLayout(); + } else { + this._x = 0; + this._triggerRecalcTranslate(); + } + } + } + + _disableFuncX() { + this._optFlags = this._optFlags & (0xFFFF - 1); + this._funcX = null; + } + + get offsetY() { + if (this._funcY) { + return this._funcY; + } else { + if (this.hasFlexLayout()) { + return this._layout.originalY; + } else { + return this._y; + } + } + } + + set offsetY(v) { + if (Utils.isFunction(v)) { + this.funcY = v; + } else { + this._disableFuncY(); + if (this.hasFlexLayout()) { + this.y += (v - this._layout.originalY); + this._layout.setOriginalYWithoutUpdatingLayout(v); + } else { + this.y = v; + } + } + } + + get y() { + return this._y; + } + + set y(v) { + if (v !== this._y) { + this._updateLocalTranslateDelta(0, v - this._y); + this._y = v; + } + } + + get funcY() { + return (this._optFlags & 2 ? this._funcY : null); + } + + set funcY(v) { + if (this._funcY !== v) { + this._optFlags |= 2; + this._funcY = v; + if (this.hasFlexLayout()) { + this._layout.setOriginalYWithoutUpdatingLayout(0); + this.layout.forceLayout(); + } else { + this._y = 0; + this._triggerRecalcTranslate(); + } + } + } + + _disableFuncY() { + this._optFlags = this._optFlags & (0xFFFF - 2); + this._funcY = null; + } + + get funcW() { + return (this._optFlags & 4 ? this._funcW : null); + } + + set funcW(v) { + if (this._funcW !== v) { + this._optFlags |= 4; + this._funcW = v; + if (this.hasFlexLayout()) { + this._layout._originalWidth = 0; + this.layout.changedDimensions(true, false); + } else { + this._w = 0; + this._triggerRecalcTranslate(); + } + } + } + + disableFuncW() { + this._optFlags = this._optFlags & (0xFFFF - 4); + this._funcW = null; + } + + get funcH() { + return (this._optFlags & 8 ? this._funcH : null); + } + + set funcH(v) { + if (this._funcH !== v) { + this._optFlags |= 8; + this._funcH = v; + if (this.hasFlexLayout()) { + this._layout._originalHeight = 0; + this.layout.changedDimensions(false, true); + } else { + this._h = 0; + this._triggerRecalcTranslate(); + } + } + } + + disableFuncH() { + this._optFlags = this._optFlags & (0xFFFF - 8); + this._funcH = null; + } + + get w() { + return this._w; + } + + getRenderWidth() { + if (this.hasFlexLayout()) { + return this._layout.originalWidth; + } else { + return this._w; + } + } + + get h() { + return this._h; + } + + getRenderHeight() { + if (this.hasFlexLayout()) { + return this._layout.originalHeight; + } else { + return this._h; + } + } + + get scaleX() { + return this._scaleX; + } + + set scaleX(v) { + if (this._scaleX !== v) { + this._scaleX = v; + this._updateLocalTransform(); + } + } + + get scaleY() { + return this._scaleY; + } + + set scaleY(v) { + if (this._scaleY !== v) { + this._scaleY = v; + this._updateLocalTransform(); + } + } + + get scale() { + return this.scaleX; + } + + set scale(v) { + if (this._scaleX !== v || this._scaleY !== v) { + this._scaleX = v; + this._scaleY = v; + this._updateLocalTransform(); + } + } + + get pivotX() { + return this._pivotX; + } + + set pivotX(v) { + if (this._pivotX !== v) { + this._pivotX = v; + this._updateLocalTranslate(); + } + } + + get pivotY() { + return this._pivotY; + } + + set pivotY(v) { + if (this._pivotY !== v) { + this._pivotY = v; + this._updateLocalTranslate(); + } + } + + get pivot() { + return this._pivotX; + } + + set pivot(v) { + if (this._pivotX !== v || this._pivotY !== v) { + this._pivotX = v; + this._pivotY = v; + this._updateLocalTranslate(); + } + } + + get mountX() { + return this._mountX; + } + + set mountX(v) { + if (this._mountX !== v) { + this._mountX = v; + this._updateLocalTranslate(); + } + } + + get mountY() { + return this._mountY; + } + + set mountY(v) { + if (this._mountY !== v) { + this._mountY = v; + this._updateLocalTranslate(); + } + } + + get mount() { + return this._mountX; + } + + set mount(v) { + if (this._mountX !== v || this._mountY !== v) { + this._mountX = v; + this._mountY = v; + this._updateLocalTranslate(); + } + } + + get rotation() { + return this._rotation; + } + + set rotation(v) { + if (this._rotation !== v) { + this._rotation = v; + this._updateLocalTransform(); + } + } + + get alpha() { + return this._alpha; + } + + set alpha(v) { + // Account for rounding errors. + v = (v > 1 ? 1 : (v < 1e-14 ? 0 : v)); + if (this._alpha !== v) { + let prev = this._alpha; + this._alpha = v; + this._updateLocalAlpha(); + if ((prev === 0) !== (v === 0)) { + this._element._updateEnabledFlag(); + } + } + } + + get visible() { + return this._visible; + } + + set visible(v) { + if (this._visible !== v) { + this._visible = v; + this._updateLocalAlpha(); + this._element._updateEnabledFlag(); + + if (this.hasFlexLayout()) { + this.layout.setVisible(v); + } + } + } + + _updateLocalTransform() { + if (this._rotation !== 0 && this._rotation % (2 * Math.PI)) { + // check to see if the rotation is the same as the previous render. This means we only need to use sin and cos when rotation actually changes + let _sr = Math.sin(this._rotation); + let _cr = Math.cos(this._rotation); + + this._setLocalTransform( + _cr * this._scaleX, + -_sr * this._scaleY, + _sr * this._scaleX, + _cr * this._scaleY + ); + } else { + this._setLocalTransform( + this._scaleX, + 0, + 0, + this._scaleY + ); + } + this._updateLocalTranslate(); + }; + + _updateLocalTranslate() { + this._recalcLocalTranslate(); + this._triggerRecalcTranslate(); + }; + + _recalcLocalTranslate() { + let pivotXMul = this._pivotX * this._w; + let pivotYMul = this._pivotY * this._h; + let px = this._x - (pivotXMul * this._localTa + pivotYMul * this._localTb) + pivotXMul; + let py = this._y - (pivotXMul * this._localTc + pivotYMul * this._localTd) + pivotYMul; + px -= this._mountX * this._w; + py -= this._mountY * this._h; + this._localPx = px; + this._localPy = py; + } + + _updateLocalTranslateDelta(dx, dy) { + this._addLocalTranslate(dx, dy); + }; + + _updateLocalAlpha() { + this._setLocalAlpha(this._visible ? this._alpha : 0); + }; + + /** + * @param {number} type + * 0: no updates + * 1: re-invoke shader + * 3: re-create render texture and re-invoke shader + */ + setHasRenderUpdates(type) { + if (this._worldContext.alpha) { + // Ignore if 'world invisible'. Render updates will be reset to 3 for every element that becomes visible. + let p = this; + p._hasRenderUpdates = Math.max(type, p._hasRenderUpdates); + while ((p = p._parent) && (p._hasRenderUpdates !== 3)) { + p._hasRenderUpdates = 3; + } + } + } + + /** + * @param {Number} type + * 1: alpha + * 2: translate + * 4: transform + * 128: becomes visible + * 256: flex layout updated + */ + _setRecalc(type) { + this._recalc |= type; + + this._setHasUpdates(); + + // Any changes in descendants should trigger texture updates. + if (this._parent) { + this._parent.setHasRenderUpdates(3); + } + } + + _setHasUpdates() { + let p = this; + while(p && !p._hasUpdates) { + p._hasUpdates = true; + p = p._parent; + } + } + + getParent() { + return this._parent; + } + + setParent(parent) { + if (parent !== this._parent) { + let prevIsZContext = this.isZContext(); + let prevParent = this._parent; + this._parent = parent; + + // Notify flex layout engine. + if (this._layout || (parent && parent.isFlexContainer())) { + this.layout.setParent(prevParent, parent); + } + + if (prevParent) { + // When elements are deleted, the render texture must be re-rendered. + prevParent.setHasRenderUpdates(3); + } + + this._setRecalc(1 + 2 + 4); + + if (this._parent) { + // Force parent to propagate hasUpdates flag. + this._parent._setHasUpdates(); + } + + if (this._zIndex === 0) { + this.setZParent(parent); + } else { + this.setZParent(parent ? parent.findZContext() : null); + } + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(prevParent.findZContext()); + } + } + + // Tree order did change: even if zParent stays the same, we must resort. + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + + if (!this._shader) { + let newShaderOwner = parent && !parent._renderToTextureEnabled ? parent._shaderOwner : null; + if (newShaderOwner !== this._shaderOwner) { + this.setHasRenderUpdates(1); + this._setShaderOwnerRecursive(newShaderOwner); + } + } + } + }; + + enableZSort(force = false) { + if (!this._zSort && this._zContextUsage > 0) { + this._zSort = true; + if (force) { + // ZSort must be done, even if this element is invisible. + // This is done to prevent memory leaks when removing element from inactive render branches. + this.ctx.forceZSort(this); + } + } + } + + addChildAt(index, child) { + if (!this._children) this._children = []; + this._children.splice(index, 0, child); + child.setParent(this); + }; + + setChildAt(index, child) { + if (!this._children) this._children = []; + this._children[index].setParent(null); + this._children[index] = child; + child.setParent(this); + } + + removeChildAt(index) { + let child = this._children[index]; + this._children.splice(index, 1); + child.setParent(null); + }; + + removeChildren() { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].setParent(null); + } + + this._children.splice(0); + + if (this._zIndexedChildren) { + this._zIndexedChildren.splice(0); + } + } + }; + + syncChildren(removed, added, order) { + this._children = order; + for (let i = 0, n = removed.length; i < n; i++) { + removed[i].setParent(null); + } + for (let i = 0, n = added.length; i < n; i++) { + added[i].setParent(this); + } + } + + moveChild(fromIndex, toIndex) { + let c = this._children[fromIndex]; + this._children.splice(fromIndex, 1); + this._children.splice(toIndex, 0, c); + + // Tree order changed: must resort!; + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + } + + _setLocalTransform(a, b, c, d) { + this._setRecalc(4); + this._localTa = a; + this._localTb = b; + this._localTc = c; + this._localTd = d; + + // We also regard negative scaling as a complex case, so that we can optimize the non-complex case better. + this._isComplex = (b !== 0) || (c !== 0) || (a < 0) || (d < 0); + }; + + _addLocalTranslate(dx, dy) { + this._localPx += dx; + this._localPy += dy; + this._triggerRecalcTranslate(); + } + + _setLocalAlpha(a) { + if (!this._worldContext.alpha && ((this._parent && this._parent._worldContext.alpha) && a)) { + // Element is becoming visible. We need to force update. + this._setRecalc(1 + 128); + } else { + this._setRecalc(1); + } + + if (a < 1e-14) { + // Tiny rounding errors may cause failing visibility tests. + a = 0; + } + + this._localAlpha = a; + }; + + setDimensions(w, h, isEstimate = this._dimsUnknown) { + // In case of an estimation, the update loop should perform different bound checks. + this._dimsUnknown = isEstimate; + + if (this.hasFlexLayout()) { + this._layout.originalWidth = w; + this._layout.originalHeight = h; + } else { + if (this._w !== w || this._h !== h) { + this._updateDimensions(w, h); + return true; + } + } + return false; + }; + + _updateDimensions(w, h) { + if (this._w !== w || this._h !== h) { + this._w = w; + this._h = h; + + this._triggerRecalcTranslate(); + + if (this._texturizer) { + this._texturizer.releaseRenderTexture(); + this._texturizer.updateResultTexture(); + } + // Due to width/height change: update the translation vector. + this._updateLocalTranslate(); + } + } + + setTextureCoords(ulx, uly, brx, bry) { + this.setHasRenderUpdates(3); + + this._ulx = ulx; + this._uly = uly; + this._brx = brx; + this._bry = bry; + }; + + get displayedTextureSource() { + return this._displayedTextureSource; + } + + setDisplayedTextureSource(textureSource) { + this.setHasRenderUpdates(3); + this._displayedTextureSource = textureSource; + }; + + get isRoot() { + return this._isRoot; + } + + setAsRoot() { + // Use parent dummy. + this._parent = new ElementCore(this._element); + + // After setting root, make sure it's updated. + this._parent._hasRenderUpdates = 3; + this._parent._hasUpdates = true; + + // Root is, and will always be, the primary zContext. + this._isRoot = true; + + this.ctx.root = this; + + // Set scissor area of 'fake parent' to stage's viewport. + this._parent._viewport = [0, 0, this.ctx.stage.coordsWidth, this.ctx.stage.coordsHeight]; + this._parent._scissor = this._parent._viewport; + + // When recBoundsMargin is null, the defaults are used (100 for all sides). + this._parent._recBoundsMargin = null; + + this._setRecalc(1 + 2 + 4); + }; + + isAncestorOf(c) { + let p = c; + while (p = p._parent) { + if (this === p) { + return true; + } + } + return false; + }; + + isZContext() { + return (this._forceZIndexContext || this._renderToTextureEnabled || this._zIndex !== 0 || this._isRoot || !this._parent); + }; + + findZContext() { + if (this.isZContext()) { + return this; + } else { + return this._parent.findZContext(); + } + }; + + setZParent(newZParent) { + if (this._zParent !== newZParent) { + if (this._zParent !== null) { + if (this._zIndex !== 0) { + this._zParent.decZContextUsage(); + } + + // We must filter out this item upon the next resort. + this._zParent.enableZSort(); + } + + if (newZParent !== null) { + let hadZContextUsage = (newZParent._zContextUsage > 0); + + // @pre: new parent's children array has already been modified. + if (this._zIndex !== 0) { + newZParent.incZContextUsage(); + } + + if (newZParent._zContextUsage > 0) { + if (!hadZContextUsage && (this._parent === newZParent)) ; else { + // Add new child to array. + newZParent._zIndexedChildren.push(this); + } + + // Order should be checked. + newZParent.enableZSort(); + } + } + + this._zParent = newZParent; + + // Newly added element must be marked for resort. + this._zIndexResort = true; + } + }; + + incZContextUsage() { + this._zContextUsage++; + if (this._zContextUsage === 1) { + if (!this._zIndexedChildren) { + this._zIndexedChildren = []; + } + if (this._children) { + // Copy. + for (let i = 0, n = this._children.length; i < n; i++) { + this._zIndexedChildren.push(this._children[i]); + } + // Initially, children are already sorted properly (tree order). + this._zSort = false; + } + } + }; + + decZContextUsage() { + this._zContextUsage--; + if (this._zContextUsage === 0) { + this._zSort = false; + this._zIndexedChildren.splice(0); + } + }; + + get zIndex() { + return this._zIndex; + } + + set zIndex(zIndex) { + if (this._zIndex !== zIndex) { + this.setHasRenderUpdates(1); + + let newZParent = this._zParent; + + let prevIsZContext = this.isZContext(); + if (zIndex === 0 && this._zIndex !== 0) { + if (this._parent === this._zParent) { + if (this._zParent) { + this._zParent.decZContextUsage(); + } + } else { + newZParent = this._parent; + } + } else if (zIndex !== 0 && this._zIndex === 0) { + newZParent = this._parent ? this._parent.findZContext() : null; + if (newZParent === this._zParent) { + if (this._zParent) { + this._zParent.incZContextUsage(); + this._zParent.enableZSort(); + } + } + } else if (zIndex !== this._zIndex) { + if (this._zParent && this._zParent._zContextUsage) { + this._zParent.enableZSort(); + } + } + + if (newZParent !== this._zParent) { + this.setZParent(null); + } + + this._zIndex = zIndex; + + if (newZParent !== this._zParent) { + this.setZParent(newZParent); + } + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(this._parent.findZContext()); + } + } + + // Make sure that resort is done. + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + } + }; + + get forceZIndexContext() { + return this._forceZIndexContext; + } + + set forceZIndexContext(v) { + this.setHasRenderUpdates(1); + + let prevIsZContext = this.isZContext(); + this._forceZIndexContext = v; + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(this._parent.findZContext()); + } + } + }; + + enableZContext(prevZContext) { + if (prevZContext && prevZContext._zContextUsage > 0) { + // Transfer from upper z context to this z context. + const results = this._getZIndexedDescs(); + results.forEach((c) => { + if (this.isAncestorOf(c) && c._zIndex !== 0) { + c.setZParent(this); + } + }); + } + } + + _getZIndexedDescs() { + const results = []; + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i]._getZIndexedDescsRec(results); + } + } + return results; + } + + _getZIndexedDescsRec(results) { + if (this._zIndex) { + results.push(this); + } else if (this._children && !this.isZContext()) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i]._getZIndexedDescsRec(results); + } + } + } + + disableZContext() { + // Transfer from this z context to upper z context. + if (this._zContextUsage > 0) { + let newZParent = this._parent.findZContext(); + + // Make sure that z-indexed children are up to date (old ones removed). + if (this._zSort) { + this.sortZIndexedChildren(); + } + + this._zIndexedChildren.slice().forEach(function (c) { + if (c._zIndex !== 0) { + c.setZParent(newZParent); + } + }); + } + }; + + get colorUl() { + return this._colorUl; + } + + set colorUl(color) { + if (this._colorUl !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorUl = color; + } + } + + get colorUr() { + return this._colorUr; + } + + set colorUr(color) { + if (this._colorUr !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorUr = color; + } + }; + + get colorBl() { + return this._colorBl; + } + + set colorBl(color) { + if (this._colorBl !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorBl = color; + } + }; + + get colorBr() { + return this._colorBr; + } + + set colorBr(color) { + if (this._colorBr !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorBr = color; + } + }; + + + set onUpdate(f) { + this._onUpdate = f; + this._setRecalc(7); + } + + set onAfterUpdate(f) { + this._onAfterUpdate = f; + this._setRecalc(7); + } + + set onAfterCalcs(f) { + this._onAfterCalcs = f; + this._setRecalc(7); + } + + get shader() { + return this._shader; + } + + set shader(v) { + this.setHasRenderUpdates(1); + + let prevShader = this._shader; + this._shader = v; + if (!v && prevShader) { + // Disabled shader. + let newShaderOwner = (this._parent && !this._parent._renderToTextureEnabled ? this._parent._shaderOwner : null); + this._setShaderOwnerRecursive(newShaderOwner); + } else if (v) { + // Enabled shader. + this._setShaderOwnerRecursive(this); + } + } + + get activeShader() { + return this._shaderOwner ? this._shaderOwner.shader : this.renderState.defaultShader; + } + + get activeShaderOwner() { + return this._shaderOwner; + } + + get clipping() { + return this._clipping; + } + + set clipping(v) { + if (this._clipping !== v) { + this._clipping = v; + + // Force update of scissor by updating translate. + // Alpha must also be updated because the scissor area may have been empty. + this._setRecalc(1 + 2); + } + } + + get clipbox() { + return this._clipbox; + } + + set clipbox(v) { + // In case of out-of-bounds element, all children will also be ignored. + // It will save us from executing the update/render loops for those. + // The optimization will be used immediately during the next frame. + this._clipbox = v; + } + + _setShaderOwnerRecursive(elementCore) { + this._shaderOwner = elementCore; + + if (this._children && !this._renderToTextureEnabled) { + for (let i = 0, n = this._children.length; i < n; i++) { + let c = this._children[i]; + if (!c._shader) { + c._setShaderOwnerRecursive(elementCore); + c._hasRenderUpdates = 3; + } + } + } + }; + + _setShaderOwnerChildrenRecursive(elementCore) { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + let c = this._children[i]; + if (!c._shader) { + c._setShaderOwnerRecursive(elementCore); + c._hasRenderUpdates = 3; + } + } + } + }; + + _hasRenderContext() { + return this._renderContext !== this._worldContext; + } + + get renderContext() { + return this._renderContext; + } + + updateRenderToTextureEnabled() { + // Enforce texturizer initialisation. + let v = this.texturizer._enabled; + + if (v) { + this._enableRenderToTexture(); + } else { + this._disableRenderToTexture(); + this._texturizer.releaseRenderTexture(); + } + } + + _enableRenderToTexture() { + if (!this._renderToTextureEnabled) { + let prevIsZContext = this.isZContext(); + + this._renderToTextureEnabled = true; + + this._renderContext = new ElementCoreContext(); + + // If render to texture is active, a new shader context is started. + this._setShaderOwnerChildrenRecursive(null); + + if (!prevIsZContext) { + // Render context forces z context. + this.enableZContext(this._parent ? this._parent.findZContext() : null); + } + + this.setHasRenderUpdates(3); + + // Make sure that the render coordinates get updated. + this._setRecalc(7); + + this.render = this._renderAdvanced; + } + } + + _disableRenderToTexture() { + if (this._renderToTextureEnabled) { + this._renderToTextureEnabled = false; + + this._setShaderOwnerChildrenRecursive(this._shaderOwner); + + this._renderContext = this._worldContext; + + if (!this.isZContext()) { + this.disableZContext(); + } + + // Make sure that the render coordinates get updated. + this._setRecalc(7); + + this.setHasRenderUpdates(3); + + this.render = this._renderSimple; + } + } + + isWhite() { + return (this._colorUl === 0xFFFFFFFF) && (this._colorUr === 0xFFFFFFFF) && (this._colorBl === 0xFFFFFFFF) && (this._colorBr === 0xFFFFFFFF); + } + + hasSimpleTexCoords() { + return (this._ulx === 0) && (this._uly === 0) && (this._brx === 1) && (this._bry === 1); + } + + _stashTexCoords() { + this._stashedTexCoords = [this._ulx, this._uly, this._brx, this._bry]; + this._ulx = 0; + this._uly = 0; + this._brx = 1; + this._bry = 1; + } + + _unstashTexCoords() { + this._ulx = this._stashedTexCoords[0]; + this._uly = this._stashedTexCoords[1]; + this._brx = this._stashedTexCoords[2]; + this._bry = this._stashedTexCoords[3]; + this._stashedTexCoords = null; + } + + _stashColors() { + this._stashedColors = [this._colorUl, this._colorUr, this._colorBr, this._colorBl]; + this._colorUl = 0xFFFFFFFF; + this._colorUr = 0xFFFFFFFF; + this._colorBr = 0xFFFFFFFF; + this._colorBl = 0xFFFFFFFF; + } + + _unstashColors() { + this._colorUl = this._stashedColors[0]; + this._colorUr = this._stashedColors[1]; + this._colorBr = this._stashedColors[2]; + this._colorBl = this._stashedColors[3]; + this._stashedColors = null; + } + + isVisible() { + return (this._localAlpha > 1e-14); + }; + + get outOfBounds() { + return this._outOfBounds; + } + + set boundsMargin(v) { + + /** + * null: inherit from parent. + * number[4]: specific margins: left, top, right, bottom. + */ + this._boundsMargin = v ? v.slice() : null; + + // We force recalc in order to set all boundsMargin recursively during the next update. + this._triggerRecalcTranslate(); + } + + get boundsMargin() { + return this._boundsMargin; + } + + update() { + this._recalc |= this._parent._pRecalc; + + if (this._layout && this._layout.isEnabled()) { + if (this._recalc & 256) { + this._layout.layoutFlexTree(); + } + } else if ((this._recalc & 2) && this._optFlags) { + this._applyRelativeDimFuncs(); + } + + if (this._onUpdate) { + // Block all 'upwards' updates when changing things in this branch. + this._hasUpdates = true; + this._onUpdate(this.element, this); + } + + const pw = this._parent._worldContext; + let w = this._worldContext; + const visible = (pw.alpha && this._localAlpha); + + /** + * We must update if: + * - branch contains updates (even when invisible because it may contain z-indexed descendants) + * - there are (inherited) updates and this branch is visible + * - this branch becomes invisible (descs may be z-indexed so we must update all alpha values) + */ + if (this._hasUpdates || (this._recalc && visible) || (w.alpha && !visible)) { + let recalc = this._recalc; + + // Update world coords/alpha. + if (recalc & 1) { + if (!w.alpha && visible) { + // Becomes visible. + this._hasRenderUpdates = 3; + } + w.alpha = pw.alpha * this._localAlpha; + + if (w.alpha < 1e-14) { + // Tiny rounding errors may cause failing visibility tests. + w.alpha = 0; + } + } + + if (recalc & 6) { + w.px = pw.px + this._localPx * pw.ta; + w.py = pw.py + this._localPy * pw.td; + if (pw.tb !== 0) w.px += this._localPy * pw.tb; + if (pw.tc !== 0) w.py += this._localPx * pw.tc; + } + + if (recalc & 4) { + w.ta = this._localTa * pw.ta; + w.tb = this._localTd * pw.tb; + w.tc = this._localTa * pw.tc; + w.td = this._localTd * pw.td; + + if (this._isComplex) { + w.ta += this._localTc * pw.tb; + w.tb += this._localTb * pw.ta; + w.tc += this._localTc * pw.td; + w.td += this._localTb * pw.tc; + } + } + + // Update render coords/alpha. + const pr = this._parent._renderContext; + if (this._parent._hasRenderContext()) { + const init = this._renderContext === this._worldContext; + if (init) { + // First render context build: make sure that it is fully initialized correctly. + // Otherwise, if we get into bounds later, the render context would not be initialized correctly. + this._renderContext = new ElementCoreContext(); + } + + const r = this._renderContext; + + // Update world coords/alpha. + if (init || (recalc & 1)) { + r.alpha = pr.alpha * this._localAlpha; + + if (r.alpha < 1e-14) { + r.alpha = 0; + } + } + + if (init || (recalc & 6)) { + r.px = pr.px + this._localPx * pr.ta; + r.py = pr.py + this._localPy * pr.td; + if (pr.tb !== 0) r.px += this._localPy * pr.tb; + if (pr.tc !== 0) r.py += this._localPx * pr.tc; + } + + if (init) { + // We set the recalc toggle, because we must make sure that the scissor is updated. + recalc |= 2; + } + + if (init || (recalc & 4)) { + r.ta = this._localTa * pr.ta; + r.tb = this._localTd * pr.tb; + r.tc = this._localTa * pr.tc; + r.td = this._localTd * pr.td; + + if (this._isComplex) { + r.ta += this._localTc * pr.tb; + r.tb += this._localTb * pr.ta; + r.tc += this._localTc * pr.td; + r.td += this._localTb * pr.tc; + } + } + } else { + this._renderContext = this._worldContext; + } + + if (this.ctx.updateTreeOrder === -1) { + this.ctx.updateTreeOrder = this._updateTreeOrder + 1; + } else { + this._updateTreeOrder = this.ctx.updateTreeOrder++; + } + + // Determine whether we must use a 'renderTexture'. + const useRenderToTexture = this._renderToTextureEnabled && this._texturizer.mustRenderToTexture(); + if (this._useRenderToTexture !== useRenderToTexture) { + // Coords must be changed. + this._recalc |= 2 + 4; + + // Scissor may change: force update. + recalc |= 2; + + if (!this._useRenderToTexture) { + // We must release the texture. + this._texturizer.release(); + } + } + this._useRenderToTexture = useRenderToTexture; + + const r = this._renderContext; + + const bboxW = this._dimsUnknown ? 2048 : this._w; + const bboxH = this._dimsUnknown ? 2048 : this._h; + + // Calculate a bbox for this element. + let sx, sy, ex, ey; + const rComplex = (r.tb !== 0) || (r.tc !== 0) || (r.ta < 0) || (r.td < 0); + if (rComplex) { + sx = Math.min(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + ex = Math.max(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + sy = Math.min(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + ey = Math.max(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + } else { + sx = r.px; + ex = r.px + r.ta * bboxW; + sy = r.py; + ey = r.py + r.td * bboxH; + } + + if (this._dimsUnknown && (rComplex || this._localTa < 1 || this._localTb < 1)) { + // If we are dealing with a non-identity matrix, we must extend the bbox so that withinBounds and + // scissors will include the complete range of (positive) dimensions up to ,lh. + const nx = this._x * pr.ta + this._y * pr.tb + pr.px; + const ny = this._x * pr.tc + this._y * pr.td + pr.py; + if (nx < sx) sx = nx; + if (ny < sy) sy = ny; + if (nx > ex) ex = nx; + if (ny > ey) ey = ny; + } + + if (recalc & 6 || !this._scissor /* initial */) { + // Determine whether we must 'clip'. + if (this._clipping && r.isSquare()) { + // If the parent renders to a texture, it's scissor should be ignored; + const area = this._parent._useRenderToTexture ? this._parent._viewport : this._parent._scissor; + if (area) { + // Merge scissor areas. + const lx = Math.max(area[0], sx); + const ly = Math.max(area[1], sy); + this._scissor = [ + lx, + ly, + Math.min(area[2] + area[0], ex) - lx, + Math.min(area[3] + area[1], ey) - ly + ]; + } else { + this._scissor = [sx, sy, ex - sx, ey - sy]; + } + } else { + // No clipping: reuse parent scissor. + this._scissor = this._parent._useRenderToTexture ? this._parent._viewport : this._parent._scissor; + } + } + + // Calculate the outOfBounds margin. + if (this._boundsMargin) { + this._recBoundsMargin = this._boundsMargin; + } else { + this._recBoundsMargin = this._parent._recBoundsMargin; + } + + if (this._onAfterCalcs) { + // After calcs may change render coords, scissor and/or recBoundsMargin. + if (this._onAfterCalcs(this.element)) { + // Recalculate bbox. + if (rComplex) { + sx = Math.min(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + ex = Math.max(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + sy = Math.min(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + ey = Math.max(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + } else { + sx = r.px; + ex = r.px + r.ta * bboxW; + sy = r.py; + ey = r.py + r.td * bboxH; + } + + if (this._dimsUnknown && (rComplex || this._localTa < 1 || this._localTb < 1)) { + const nx = this._x * pr.ta + this._y * pr.tb + pr.px; + const ny = this._x * pr.tc + this._y * pr.td + pr.py; + if (nx < sx) sx = nx; + if (ny < sy) sy = ny; + if (nx > ex) ex = nx; + if (ny > ey) ey = ny; + } + } + } + + if (this._parent._outOfBounds === 2) { + // Inherit parent out of boundsness. + this._outOfBounds = 2; + + if (this._withinBoundsMargin) { + this._withinBoundsMargin = false; + this.element._disableWithinBoundsMargin(); + } + } else { + if (recalc & 6) { + // Recheck if element is out-of-bounds (all settings that affect this should enable recalc bit 2 or 4). + this._outOfBounds = 0; + let withinMargin = true; + + // Offscreens are always rendered as long as the parent is within bounds. + if (!this._renderToTextureEnabled || !this._texturizer || !this._texturizer.renderOffscreen) { + if (this._scissor && (this._scissor[2] <= 0 || this._scissor[3] <= 0)) { + // Empty scissor area. + this._outOfBounds = 2; + } else { + // Use bbox to check out-of-boundness. + if ((this._scissor[0] > ex) || + (this._scissor[1] > ey) || + (sx > (this._scissor[0] + this._scissor[2])) || + (sy > (this._scissor[1] + this._scissor[3])) + ) { + this._outOfBounds = 1; + } + + if (this._outOfBounds) { + if (this._clipping || this._useRenderToTexture || (this._clipbox && (bboxW && bboxH))) { + this._outOfBounds = 2; + } + } + } + + withinMargin = (this._outOfBounds === 0); + if (!withinMargin) { + // Re-test, now with margins. + if (this._recBoundsMargin) { + withinMargin = !((ex < this._scissor[0] - this._recBoundsMargin[2]) || + (ey < this._scissor[1] - this._recBoundsMargin[3]) || + (sx > this._scissor[0] + this._scissor[2] + this._recBoundsMargin[0]) || + (sy > this._scissor[1] + this._scissor[3] + this._recBoundsMargin[1])); + } else { + withinMargin = !((ex < this._scissor[0] - 100) || + (ey < this._scissor[1] - 100) || + (sx > this._scissor[0] + this._scissor[2] + 100) || + (sy > this._scissor[1] + this._scissor[3] + 100)); + } + if (withinMargin && this._outOfBounds === 2) { + // Children must be visited because they may contain elements that are within margin, so must be visible. + this._outOfBounds = 1; + } + } + } + + if (this._withinBoundsMargin !== withinMargin) { + this._withinBoundsMargin = withinMargin; + + if (this._withinBoundsMargin) { + // This may update things (txLoaded events) in the element itself, but also in descendants and ancestors. + + // Changes in ancestors should be executed during the next call of the stage update. But we must + // take care that the _recalc and _hasUpdates flags are properly registered. That's why we clear + // both before entering the children, and use _pRecalc to transfer inherited updates instead of + // _recalc directly. + + // Changes in descendants are automatically executed within the current update loop, though we must + // take care to not update the hasUpdates flag unnecessarily in ancestors. We achieve this by making + // sure that the hasUpdates flag of this element is turned on, which blocks it for ancestors. + this._hasUpdates = true; + + const recalc = this._recalc; + this._recalc = 0; + this.element._enableWithinBoundsMargin(); + + if (this._recalc) { + // This element needs to be re-updated now, because we want the dimensions (and other changes) to be updated. + return this.update(); + } + + this._recalc = recalc; + } else { + this.element._disableWithinBoundsMargin(); + } + } + } + } + + if (this._useRenderToTexture) { + // Set viewport necessary for children scissor calculation. + if (this._viewport) { + this._viewport[2] = bboxW; + this._viewport[3] = bboxH; + } else { + this._viewport = [0, 0, bboxW, bboxH]; + } + } + + // Filter out bits that should not be copied to the children (currently all are). + this._pRecalc = (this._recalc & 135); + + // Clear flags so that future updates are properly detected. + this._recalc = 0; + this._hasUpdates = false; + + if (this._outOfBounds < 2) { + if (this._useRenderToTexture) { + if (this._worldContext.isIdentity()) { + // Optimization. + // The world context is already identity: use the world context as render context to prevents the + // ancestors from having to update the render context. + this._renderContext = this._worldContext; + } else { + // Temporarily replace the render coord attribs by the identity matrix. + // This allows the children to calculate the render context. + this._renderContext = ElementCoreContext.IDENTITY; + } + } + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].update(); + } + } + + if (this._useRenderToTexture) { + this._renderContext = r; + } + } else { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._hasUpdates) { + this._children[i].update(); + } else { + // Make sure we don't lose the 'inherited' updates. + this._children[i]._recalc |= this._pRecalc; + this._children[i].updateOutOfBounds(); + } + } + } + } + + if (this._onAfterUpdate) { + this._onAfterUpdate(this.element); + } + } else { + if (this.ctx.updateTreeOrder === -1 || this._updateTreeOrder >= this.ctx.updateTreeOrder) { + // If new tree order does not interfere with the current (gaps allowed) there's no need to traverse the branch. + this.ctx.updateTreeOrder = -1; + } else { + this.updateTreeOrder(); + } + } + } + + _applyRelativeDimFuncs() { + if (this._optFlags & 1) { + const x = this._funcX(this._parent.w); + if (x !== this._x) { + this._localPx += (x - this._x); + this._x = x; + } + } + if (this._optFlags & 2) { + const y = this._funcY(this._parent.h); + if (y !== this._y) { + this._localPy += (y - this._y); + this._y = y; + } + } + + let changedDims = false; + if (this._optFlags & 4) { + const w = this._funcW(this._parent.w); + if (w !== this._w) { + this._w = w; + changedDims = true; + } + } + if (this._optFlags & 8) { + const h = this._funcH(this._parent.h); + if (h !== this._h) { + this._h = h; + changedDims = true; + } + } + + if (changedDims) { + // Recalc mount, scale position. + this._recalcLocalTranslate(); + + this.element.onDimensionsChanged(this._w, this._h); + } + } + + updateOutOfBounds() { + // Propagate outOfBounds flag to descendants (necessary because of z-indexing). + // Invisible elements are not drawn anyway. When alpha is updated, so will _outOfBounds. + if (this._outOfBounds !== 2 && this._renderContext.alpha > 0) { + + // Inherit parent out of boundsness. + this._outOfBounds = 2; + + if (this._withinBoundsMargin) { + this._withinBoundsMargin = false; + this.element._disableWithinBoundsMargin(); + } + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].updateOutOfBounds(); + } + } + } + } + + updateTreeOrder() { + if (this._localAlpha && (this._outOfBounds !== 2)) { + this._updateTreeOrder = this.ctx.updateTreeOrder++; + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].updateTreeOrder(); + } + } + } + } + + _renderSimple() { + this._hasRenderUpdates = 0; + + if (this._zSort) { + this.sortZIndexedChildren(); + } + + if (this._outOfBounds < 2 && this._renderContext.alpha) { + let renderState = this.renderState; + + if ((this._outOfBounds === 0) && this._displayedTextureSource) { + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + this.renderState.addQuad(this); + } + + // Also add children to the VBO. + if (this._children) { + if (this._zContextUsage) { + for (let i = 0, n = this._zIndexedChildren.length; i < n; i++) { + this._zIndexedChildren[i].render(); + } + } else { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._zIndex === 0) { + // If zIndex is set, this item already belongs to a zIndexedChildren array in one of the ancestors. + this._children[i].render(); + } + + } + } + } + } + } + + _renderAdvanced() { + const hasRenderUpdates = this._hasRenderUpdates; + + // We must clear the hasRenderUpdates flag before rendering, because updating result textures in combination + // with z-indexing may trigger render updates on a render branch that is 'half done'. + // We need to ensure that the full render branch is marked for render updates, not only half (leading to freeze). + this._hasRenderUpdates = 0; + + if (this._zSort) { + this.sortZIndexedChildren(); + } + + if (this._outOfBounds < 2 && this._renderContext.alpha) { + let renderState = this.renderState; + + let mustRenderChildren = true; + let renderTextureInfo; + let prevRenderTextureInfo; + if (this._useRenderToTexture) { + if (this._w === 0 || this._h === 0) { + // Ignore this branch and don't draw anything. + return; + } else if (!this._texturizer.hasRenderTexture() || (hasRenderUpdates >= 3)) { + // Switch to default shader for building up the render texture. + renderState.setShader(renderState.defaultShader, this); + + prevRenderTextureInfo = renderState.renderTextureInfo; + + renderTextureInfo = { + nativeTexture: null, + offset: 0, // Set by CoreRenderState. + w: this._w, + h: this._h, + empty: true, + cleared: false, + ignore: false, + cache: false + }; + + if (this._texturizer.hasResultTexture() || (!renderState.isCachingTexturizer && (hasRenderUpdates < 3))) { + /** + * We don't always cache render textures. + * + * The rule is, that caching for a specific render texture is only enabled if: + * - There is a result texture to be updated. + * - There were no render updates -within the contents- since last frame (ElementCore.hasRenderUpdates < 3) + * - AND there are no ancestors that are being cached during this frame (CoreRenderState.isCachingTexturizer) + * If an ancestor is cached anyway, it's probably not necessary to keep deeper caches. If the top level is to + * change while a lower one is not, that lower level will be cached instead. + * + * In case of the fast blur element, this prevents having to cache all blur levels and stages, saving a huge amount + * of GPU memory! + * + * Especially when using multiple stacked layers of the same dimensions that are RTT this will have a very + * noticable effect on performance as less render textures need to be allocated. + */ + renderTextureInfo.cache = true; + renderState.isCachingTexturizer = true; + } + + if (!this._texturizer.hasResultTexture()) { + // We can already release the current texture to the pool, as it will be rebuild anyway. + // In case of multiple layers of 'filtering', this may save us from having to create one + // render-to-texture layer. + // Notice that we don't do this when there is a result texture, as any other element may rely on + // that result texture being filled. + this._texturizer.releaseRenderTexture(); + } + + renderState.setRenderTextureInfo(renderTextureInfo); + renderState.setScissor(null); + + if (this._displayedTextureSource) { + let r = this._renderContext; + + // Use an identity context for drawing the displayed texture to the render texture. + this._renderContext = ElementCoreContext.IDENTITY; + + // Add displayed texture source in local coordinates. + this.renderState.addQuad(this); + + this._renderContext = r; + } + } else { + mustRenderChildren = false; + } + } else { + if ((this._outOfBounds === 0) && this._displayedTextureSource) { + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + this.renderState.addQuad(this); + } + } + + // Also add children to the VBO. + if (mustRenderChildren && this._children) { + if (this._zContextUsage) { + for (let i = 0, n = this._zIndexedChildren.length; i < n; i++) { + this._zIndexedChildren[i].render(); + } + } else { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._zIndex === 0) { + // If zIndex is set, this item already belongs to a zIndexedChildren array in one of the ancestors. + this._children[i].render(); + } + } + } + } + + if (this._useRenderToTexture) { + let updateResultTexture = false; + if (mustRenderChildren) { + // Finished refreshing renderTexture. + renderState.finishedRenderTexture(); + + // If nothing was rendered, we store a flag in the texturizer and prevent unnecessary + // render-to-texture and filtering. + this._texturizer.empty = renderTextureInfo.empty; + + if (renderTextureInfo.empty) { + // We ignore empty render textures and do not draw the final quad. + + // The following cleans up memory and enforces that the result texture is also cleared. + this._texturizer.releaseRenderTexture(); + } else if (renderTextureInfo.nativeTexture) { + // If nativeTexture is set, we can reuse that directly instead of creating a new render texture. + this._texturizer.reuseTextureAsRenderTexture(renderTextureInfo.nativeTexture); + + renderTextureInfo.ignore = true; + } else { + if (this._texturizer.renderTextureReused) { + // Quad operations must be written to a render texture actually owned. + this._texturizer.releaseRenderTexture(); + } + // Just create the render texture. + renderTextureInfo.nativeTexture = this._texturizer.getRenderTexture(); + } + + // Restore the parent's render texture. + renderState.setRenderTextureInfo(prevRenderTextureInfo); + + updateResultTexture = true; + } + + if (!this._texturizer.empty) { + let resultTexture = this._texturizer.getResultTexture(); + if (updateResultTexture) { + if (resultTexture) { + // Logging the update frame can be handy for userland. + resultTexture.update = renderState.stage.frameCounter; + } + this._texturizer.updateResultTexture(); + } + + if (!this._texturizer.renderOffscreen) { + // Render result texture to the actual render target. + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + + // If no render texture info is set, the cache can be reused. + const cache = !renderTextureInfo || renderTextureInfo.cache; + + renderState.setTexturizer(this._texturizer, cache); + this._stashTexCoords(); + if (!this._texturizer.colorize) this._stashColors(); + this.renderState.addQuad(this, true); + if (!this._texturizer.colorize) this._unstashColors(); + this._unstashTexCoords(); + renderState.setTexturizer(null); + } + } + } + + if (renderTextureInfo && renderTextureInfo.cache) { + // Allow siblings to cache. + renderState.isCachingTexturizer = false; + } + } + } + + get zSort() { + return this._zSort; + } + + sortZIndexedChildren() { + /** + * We want to avoid resorting everything. Instead, we do a single pass of the full array: + * - filtering out elements with a different zParent than this (were removed) + * - filtering out, but also gathering (in a temporary array) the elements that have zIndexResort flag + * - then, finally, we merge-sort both the new array and the 'old' one + * - element may have been added 'double', so when merge-sorting also check for doubles. + * - if the old one is larger (in size) than it should be, splice off the end of the array. + */ + + const n = this._zIndexedChildren.length; + let ptr = 0; + const a = this._zIndexedChildren; + + // Notice that items may occur multiple times due to z-index changing. + const b = []; + for (let i = 0; i < n; i++) { + if (a[i]._zParent === this) { + if (a[i]._zIndexResort) { + b.push(a[i]); + } else { + if (ptr !== i) { + a[ptr] = a[i]; + } + ptr++; + } + } + } + + const m = b.length; + if (m) { + for (let j = 0; j < m; j++) { + b[j]._zIndexResort = false; + } + + b.sort(ElementCore.sortZIndexedChildren); + const n = ptr; + if (!n) { + ptr = 0; + let j = 0; + do { + a[ptr++] = b[j++]; + } while(j < m); + + if (a.length > ptr) { + // Slice old (unnecessary) part off array. + a.splice(ptr); + } + } else { + // Merge-sort arrays; + ptr = 0; + let i = 0; + let j = 0; + const mergeResult = []; + do { + const v = (a[i]._zIndex === b[j]._zIndex ? a[i]._updateTreeOrder - b[j]._updateTreeOrder : a[i]._zIndex - b[j]._zIndex); + + const add = v > 0 ? b[j++] : a[i++]; + + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + + if (i >= n) { + do { + const add = b[j++]; + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + } while(j < m); + break; + } else if (j >= m) { + do { + const add = a[i++]; + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + } while(i < n); + break; + } + } while(true); + + this._zIndexedChildren = mergeResult; + } + } else { + if (a.length > ptr) { + // Slice old (unnecessary) part off array. + a.splice(ptr); + } + } + + this._zSort = false; + }; + + get localTa() { + return this._localTa; + }; + + get localTb() { + return this._localTb; + }; + + get localTc() { + return this._localTc; + }; + + get localTd() { + return this._localTd; + }; + + get element() { + return this._element; + } + + get renderUpdates() { + return this._hasRenderUpdates; + } + + get texturizer() { + if (!this._texturizer) { + this._texturizer = new ElementTexturizer(this); + } + return this._texturizer; + } + + getCornerPoints() { + let w = this._worldContext; + + return [ + w.px, + w.py, + w.px + this._w * w.ta, + w.py + this._w * w.tc, + w.px + this._w * w.ta + this._h * w.tb, + w.py + this._w * w.tc + this._h * w.td, + w.px + this._h * w.tb, + w.py + this._h * w.td + ] + }; + + getRenderTextureCoords(relX, relY) { + let r = this._renderContext; + return [ + r.px + r.ta * relX + r.tb * relY, + r.py + r.tc * relX + r.td * relY + ] + } + + getAbsoluteCoords(relX, relY) { + let w = this._renderContext; + return [ + w.px + w.ta * relX + w.tb * relY, + w.py + w.tc * relX + w.td * relY + ] + } + + + get layout() { + this._ensureLayout(); + return this._layout; + } + + get flex() { + return this._layout ? this._layout.flex : null; + } + + set flex(v) { + this.layout.flex = v; + } + + get flexItem() { + return this._layout ? this._layout.flexItem : null; + } + + set flexItem(v) { + this.layout.flexItem = v; + } + + isFlexItem() { + return !!this._layout && this._layout.isFlexItemEnabled(); + } + + isFlexContainer() { + return !!this._layout && this._layout.isFlexEnabled(); + } + + enableFlexLayout() { + this._ensureLayout(); + } + + _ensureLayout() { + if (!this._layout) { + this._layout = new FlexTarget(this); + } + } + + disableFlexLayout() { + this._triggerRecalcTranslate(); + } + + hasFlexLayout() { + return (this._layout && this._layout.isEnabled()); + } + + setLayout(x, y, w, h) { + this.x = x; + this.y = y; + this._updateDimensions(w, h); + } + + triggerLayout() { + this._setRecalc(256); + } + + _triggerRecalcTranslate() { + this._setRecalc(2); + } + +} + +class ElementCoreContext { + + constructor() { + this.alpha = 1; + + this.px = 0; + this.py = 0; + + this.ta = 1; + this.tb = 0; + this.tc = 0; + this.td = 1; + } + + isIdentity() { + return this.alpha === 1 && + this.px === 0 && + this.py === 0 && + this.ta === 1 && + this.tb === 0 && + this.tc === 0 && + this.td === 1; + } + + isSquare() { + return this.tb === 0 && this.tc === 0; + } + +} + +ElementCoreContext.IDENTITY = new ElementCoreContext(); +ElementCore.sortZIndexedChildren = function(a, b) { + return (a._zIndex === b._zIndex ? a._updateTreeOrder - b._updateTreeOrder : a._zIndex - b._zIndex); +}; + +/** + * This is a partial (and more efficient) implementation of the event emitter. + * It attempts to maintain a one-to-one mapping between events and listeners, skipping an array lookup. + * Only if there are multiple listeners, they are combined in an array. + */ +class EventEmitter { + + constructor() { + // This is set (and kept) to true when events are used at all. + this._hasEventListeners = false; + } + + on(name, listener) { + if (!this._hasEventListeners) { + this._eventFunction = {}; + this._eventListeners = {}; + this._hasEventListeners = true; + } + + const current = this._eventFunction[name]; + if (!current) { + this._eventFunction[name] = listener; + } else { + if (this._eventFunction[name] !== EventEmitter.combiner) { + this._eventListeners[name] = [this._eventFunction[name], listener]; + this._eventFunction[name] = EventEmitter.combiner; + } else { + this._eventListeners[name].push(listener); + } + } + } + + has(name, listener) { + if (this._hasEventListeners) { + const current = this._eventFunction[name]; + if (current) { + if (current === EventEmitter.combiner) { + const listeners = this._eventListeners[name]; + let index = listeners.indexOf(listener); + return (index >= 0); + } else if (this._eventFunction[name] === listener) { + return true; + } + } + } + return false; + } + + off(name, listener) { + if (this._hasEventListeners) { + const current = this._eventFunction[name]; + if (current) { + if (current === EventEmitter.combiner) { + const listeners = this._eventListeners[name]; + let index = listeners.indexOf(listener); + if (index >= 0) { + listeners.splice(index, 1); + } + if (listeners.length === 1) { + this._eventFunction[name] = listeners[0]; + this._eventListeners[name] = undefined; + } + } else if (this._eventFunction[name] === listener) { + this._eventFunction[name] = undefined; + } + } + } + } + + removeListener(name, listener) { + this.off(name, listener); + } + + emit(name, arg1, arg2, arg3) { + if (this._hasEventListeners) { + const func = this._eventFunction[name]; + if (func) { + if (func === EventEmitter.combiner) { + func(this, name, arg1, arg2, arg3); + } else { + func(arg1, arg2, arg3); + } + } + } + } + + listenerCount(name) { + if (this._hasEventListeners) { + const func = this._eventFunction[name]; + if (func) { + if (func === EventEmitter.combiner) { + return this._eventListeners[name].length; + } else { + return 1; + } + } + } else { + return 0; + } + } + + removeAllListeners(name) { + if (this._hasEventListeners) { + delete this._eventFunction[name]; + delete this._eventListeners[name]; + } + } + +} + +EventEmitter.combiner = function(object, name, arg1, arg2, arg3) { + const listeners = object._eventListeners[name]; + if (listeners) { + // Because listener may detach itself while being invoked, we use a forEach instead of for loop. + listeners.forEach((listener) => { + listener(arg1, arg2, arg3); + }); + } +}; + +EventEmitter.addAsMixin = function(cls) { + cls.prototype.on = EventEmitter.prototype.on; + cls.prototype.has = EventEmitter.prototype.has; + cls.prototype.off = EventEmitter.prototype.off; + cls.prototype.removeListener = EventEmitter.prototype.removeListener; + cls.prototype.emit = EventEmitter.prototype.emit; + cls.prototype.listenerCount = EventEmitter.prototype.listenerCount; + cls.prototype.removeAllListeners = EventEmitter.prototype.removeAllListeners; +}; + +class Shader { + + constructor(coreContext) { + this._initialized = false; + + this.ctx = coreContext; + + /** + * The (enabled) elements that use this shader. + * @type {Set} + */ + this._elements = new Set(); + } + + static create(stage, v) { + let shader; + if (Utils.isObjectLiteral(v)) { + if (v.type) { + shader = stage.renderer.createShader(stage.ctx, v); + } else { + shader = this.shader; + } + + if (shader) { + Base.patchObject(shader, v); + } + } else if (v === null) { + shader = stage.ctx.renderState.defaultShader; + } else if (v === undefined) { + shader = null; + } else { + if (v.isShader) { + if (!stage.renderer.isValidShaderType(v.constructor)) { + console.error("Invalid shader type"); + v = null; + } + shader = v; + } else { + console.error("Please specify a shader type."); + return; + } + } + + return shader; + } + + static getWebGL() { + return undefined; + } + + static getC2d() { + return undefined; + } + + addElement(elementCore) { + this._elements.add(elementCore); + } + + removeElement(elementCore) { + this._elements.delete(elementCore); + if (!this._elements) { + this.cleanup(); + } + } + + redraw() { + this._elements.forEach(elementCore => { + elementCore.setHasRenderUpdates(2); + }); + } + + patch(settings) { + Base.patchObject(this, settings); + } + + useDefault() { + // Should return true if this shader is configured (using it's properties) to not have any effect. + // This may allow the render engine to avoid unnecessary shader program switches or even texture copies. + return false; + } + + addEmpty() { + // Draws this shader even if there are no quads to be added. + // This is handy for custom shaders. + return false; + } + + cleanup() { + // Called when no more enabled elements have this shader. + } + + get isShader() { + return true; + } +} + +class Texture { + + /** + * @param {Stage} stage + */ + constructor(stage) { + this.stage = stage; + + this.manager = this.stage.textureManager; + + this.id = Texture.id++; + + /** + * All enabled elements that use this texture object (either as texture or displayedTexture). + * @type {Set} + */ + this.elements = new Set(); + + /** + * The number of enabled elements that are active. + * @type {number} + */ + this._activeCount = 0; + + /** + * The associated texture source. + * Should not be changed. + * @type {TextureSource} + */ + this._source = null; + + /** + * A resize mode can be set to cover or contain a certain area. + * It will reset the texture clipping settings. + * When manual texture clipping is performed, the resizeMode is reset. + * @type {{type: string, width: number, height: number}} + * @private + */ + this._resizeMode = null; + + /** + * The texture clipping x-offset. + * @type {number} + */ + this._x = 0; + + /** + * The texture clipping y-offset. + * @type {number} + */ + this._y = 0; + + /** + * The texture clipping width. If 0 then full width. + * @type {number} + */ + this._w = 0; + + /** + * The texture clipping height. If 0 then full height. + * @type {number} + */ + this._h = 0; + + /** + * Render precision (0.5 = fuzzy, 1 = normal, 2 = sharp even when scaled twice, etc.). + * @type {number} + * @private + */ + this._precision = 1; + + /** + * The (maximum) expected texture source width. Used for within bounds determination while texture is not yet loaded. + * If not set, 2048 is used by ElementCore.update. + * @type {number} + */ + this.mw = 0; + + /** + * The (maximum) expected texture source height. Used for within bounds determination while texture is not yet loaded. + * If not set, 2048 is used by ElementCore.update. + * @type {number} + */ + this.mh = 0; + + /** + * Indicates if Texture.prototype.texture uses clipping. + * @type {boolean} + */ + this.clipping = false; + + /** + * Indicates whether this texture must update (when it becomes used again). + * @type {boolean} + * @private + */ + this._mustUpdate = true; + + } + + get source() { + if (this._mustUpdate || this.stage.hasUpdateSourceTexture(this)) { + this._performUpdateSource(true); + this.stage.removeUpdateSourceTexture(this); + } + return this._source; + } + + addElement(v) { + if (!this.elements.has(v)) { + this.elements.add(v); + + if (this.elements.size === 1) { + if (this._source) { + this._source.addTexture(this); + } + } + + if (v.active) { + this.incActiveCount(); + } + } + } + + removeElement(v) { + if (this.elements.delete(v)) { + if (this.elements.size === 0) { + if (this._source) { + this._source.removeTexture(this); + } + } + + if (v.active) { + this.decActiveCount(); + } + } + } + + incActiveCount() { + // Ensure that texture source's activeCount has transferred ownership. + const source = this.source; + + if (source) { + this._checkForNewerReusableTextureSource(); + } + + this._activeCount++; + if (this._activeCount === 1) { + this.becomesUsed(); + } + } + + decActiveCount() { + const source = this.source; // Force updating the source. + this._activeCount--; + if (!this._activeCount) { + this.becomesUnused(); + } + } + + becomesUsed() { + if (this.source) { + this.source.incActiveTextureCount(); + } + } + + onLoad() { + if (this._resizeMode) { + this._applyResizeMode(); + } + + this.elements.forEach(element => { + if (element.active) { + element.onTextureSourceLoaded(); + } + }); + } + + _checkForNewerReusableTextureSource() { + // When this source became unused and cleaned up, it may have disappeared from the reusable texture map. + // In the meantime another texture may have been generated loaded with the same lookup id. + // If this is the case, use that one instead to make sure only one active texture source per lookup id exists. + const source = this.source; + if (!source.isLoaded()) { + const reusable = this._getReusableTextureSource(); + if (reusable && reusable.isLoaded() && (reusable !== source)) { + this._replaceTextureSource(reusable); + } + } else { + if (this._resizeMode) { + this._applyResizeMode(); + } + } + } + + becomesUnused() { + if (this.source) { + this.source.decActiveTextureCount(); + } + } + + isUsed() { + return this._activeCount > 0; + } + + /** + * Returns the lookup id for the current texture settings, to be able to reuse it. + * @returns {string|null} + */ + _getLookupId() { + // Default: do not reuse texture. + return null; + } + + /** + * Generates a loader function that is able to generate the texture for the current settings of this texture. + * It should return a function that receives a single callback argument. + * That callback should be called with the following arguments: + * - err + * - options: object + * - source: ArrayBuffer|WebGlTexture|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap + * - w: Number + * - h: Number + * - permanent: Boolean + * - hasAlpha: boolean + * - permultiplyAlpha: boolean + * - flipBlueRed: boolean + * - renderInfo: object + * The loader itself may return a Function that is called when loading of the texture is cancelled. This can be used + * to stop fetching an image when it is no longer in element, for example. + */ + _getSourceLoader() { + throw new Error("Texture.generate must be implemented."); + } + + get isValid() { + return this._getIsValid(); + } + + /** + * If texture is not 'valid', no source can be created for it. + * @returns {boolean} + */ + _getIsValid() { + return true; + } + + /** + * This must be called when the texture source must be re-generated. + */ + _changed() { + // If no element is actively using this texture, ignore it altogether. + if (this.isUsed()) { + this._updateSource(); + } else { + this._mustUpdate = true; + } + } + + _updateSource() { + // We delay all updateSource calls to the next drawFrame, so that we can bundle them. + // Otherwise we may reload a texture more often than necessary, when, for example, patching multiple text + // properties. + this.stage.addUpdateSourceTexture(this); + } + + _performUpdateSource(force = false) { + // If, in the meantime, the texture was no longer used, just remember that it must update until it becomes used + // again. + if (force || this.isUsed()) { + this._mustUpdate = false; + let source = this._getTextureSource(); + this._replaceTextureSource(source); + } + } + + _getTextureSource() { + let source = null; + if (this._getIsValid()) { + const lookupId = this._getLookupId(); + source = this._getReusableTextureSource(lookupId); + if (!source) { + source = this.manager.getTextureSource(this._getSourceLoader(), lookupId); + } + } + return source; + } + + _getReusableTextureSource(lookupId = this._getLookupId()) { + if (this._getIsValid()) { + if (lookupId) { + return this.manager.getReusableTextureSource(lookupId); + } + } + return null; + } + + _replaceTextureSource(newSource = null) { + let oldSource = this._source; + + this._source = newSource; + + if (this.elements.size) { + if (oldSource) { + if (this._activeCount) { + oldSource.decActiveTextureCount(); + } + + oldSource.removeTexture(this); + } + + if (newSource) { + // Must happen before setDisplayedTexture to ensure sprite map texcoords are used. + newSource.addTexture(this); + if (this._activeCount) { + newSource.incActiveTextureCount(); + } + } + } + + if (this.isUsed()) { + if (newSource) { + if (newSource.isLoaded()) { + this.elements.forEach(element => { + if (element.active) { + element._setDisplayedTexture(this); + } + }); + } else { + const loadError = newSource.loadError; + if (loadError) { + this.elements.forEach(element => { + if (element.active) { + element.onTextureSourceLoadError(loadError); + } + }); + } + } + } else { + this.elements.forEach(element => { + if (element.active) { + element._setDisplayedTexture(null); + } + }); + } + } + } + + load() { + // Make sure that source is up to date. + if (this.source) { + if (!this.isLoaded()) { + this.source.load(true); + } + } + } + + isLoaded() { + return this._source && this._source.isLoaded(); + } + + get loadError() { + return this._source && this._source.loadError; + } + + free() { + if (this._source) { + this._source.free(); + } + } + + set resizeMode({type = "cover", w = 0, h = 0, clipX = 0.5, clipY = 0.5}) { + this._resizeMode = {type, w, h, clipX, clipY}; + if (this.isLoaded()) { + this._applyResizeMode(); + } + } + + get resizeMode() { + return this._resizeMode; + } + + _clearResizeMode() { + this._resizeMode = null; + } + + _applyResizeMode() { + if (this._resizeMode.type === "cover") { + this._applyResizeCover(); + } else if (this._resizeMode.type === "contain") { + this._applyResizeContain(); + } + this._updatePrecision(); + this._updateClipping(); + } + + _applyResizeCover() { + const scaleX = this._resizeMode.w / this._source.w; + const scaleY = this._resizeMode.h / this._source.h; + let scale = Math.max(scaleX, scaleY); + if (!scale) return; + this._precision = 1/scale; + if (scaleX && scaleX < scale) { + const desiredSize = this._precision * this._resizeMode.w; + const choppedOffPixels = this._source.w - desiredSize; + this._x = choppedOffPixels * this._resizeMode.clipX; + this._w = this._source.w - choppedOffPixels; + } + if (scaleY && scaleY < scale) { + const desiredSize = this._precision * this._resizeMode.h; + const choppedOffPixels = this._source.h - desiredSize; + this._y = choppedOffPixels * this._resizeMode.clipY; + this._h = this._source.h - choppedOffPixels; + } + } + + _applyResizeContain() { + const scaleX = this._resizeMode.w / this._source.w; + const scaleY = this._resizeMode.h / this._source.h; + let scale = scaleX; + if (!scale || scaleY < scale) { + scale = scaleY; + } + if (!scale) return; + this._precision = 1/scale; + } + + enableClipping(x, y, w, h) { + this._clearResizeMode(); + + x *= this._precision; + y *= this._precision; + w *= this._precision; + h *= this._precision; + if (this._x !== x || this._y !== y || this._w !== w || this._h !== h) { + this._x = x; + this._y = y; + this._w = w; + this._h = h; + + this._updateClipping(true); + } + } + + disableClipping() { + this._clearResizeMode(); + + if (this._x || this._y || this._w || this._h) { + this._x = 0; + this._y = 0; + this._w = 0; + this._h = 0; + + this._updateClipping(); + } + } + + _updateClipping() { + this.clipping = !!(this._x || this._y || this._w || this._h); + + let self = this; + this.elements.forEach(function(element) { + // Ignore if not the currently displayed texture. + if (element.displayedTexture === self) { + element.onDisplayedTextureClippingChanged(); + } + }); + } + + _updatePrecision() { + let self = this; + this.elements.forEach(function(element) { + // Ignore if not the currently displayed texture. + if (element.displayedTexture === self) { + element.onPrecisionChanged(); + } + }); + } + + getNonDefaults() { + let nonDefaults = {}; + nonDefaults['type'] = this.constructor.name; + if (this.x !== 0) nonDefaults['x'] = this.x; + if (this.y !== 0) nonDefaults['y'] = this.y; + if (this.w !== 0) nonDefaults['w'] = this.w; + if (this.h !== 0) nonDefaults['h'] = this.h; + if (this.precision !== 1) nonDefaults['precision'] = this.precision; + return nonDefaults; + } + + get px() { + return this._x; + } + + get py() { + return this._y; + } + + get pw() { + return this._w; + } + + get ph() { + return this._h; + } + + get x() { + return this._x / this._precision; + } + set x(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._x !== v) { + this._x = v; + this._updateClipping(); + } + } + + get y() { + return this._y / this._precision; + } + set y(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._y !== v) { + this._y = v; + this._updateClipping(); + } + } + + get w() { + return this._w / this._precision; + } + + set w(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._w !== v) { + this._w = v; + this._updateClipping(); + } + } + + get h() { + return this._h / this._precision; + } + + set h(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._h !== v) { + this._h = v; + this._updateClipping(); + } + } + + get precision() { + return this._precision; + } + + set precision(v) { + this._clearResizeMode(); + if (this._precision !== v) { + this._precision = v; + this._updatePrecision(); + } + } + + isAutosizeTexture() { + return true; + } + + getRenderWidth() { + if (!this.isAutosizeTexture()) { + // In case of the rectangle texture, we'd prefer to not cause a 1x1 w,h as it would interfere with flex layout fit-to-contents. + return 0; + } + + // If dimensions are unknown (texture not yet loaded), use maximum width as a fallback as render width to allow proper bounds checking. + return (this._w || (this._source ? this._source.getRenderWidth() - this._x : 0)) / this._precision; + } + + getRenderHeight() { + if (!this.isAutosizeTexture()) { + // In case of the rectangle texture, we'd prefer to not cause a 1x1 w,h as it would interfere with flex layout fit-to-contents. + return 0; + } + + return (this._h || (this._source ? this._source.getRenderHeight() - this._y : 0)) / this._precision; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +Texture.prototype.isTexture = true; + +Texture.id = 0; + +class ImageTexture extends Texture { + + constructor(stage) { + super(stage); + + this._src = undefined; + this._hasAlpha = false; + } + + get src() { + return this._src; + } + + set src(v) { + if (this._src !== v) { + this._src = v; + this._changed(); + } + } + + get hasAlpha() { + return this._hasAlpha; + } + + set hasAlpha(v) { + if (this._hasAlpha !== v) { + this._hasAlpha = v; + this._changed(); + } + } + + _getIsValid() { + return !!this._src; + } + + _getLookupId() { + return this._src; + } + + _getSourceLoader() { + let src = this._src; + let hasAlpha = this._hasAlpha; + if (this.stage.getOption('srcBasePath')) { + var fc = src.charCodeAt(0); + if ((src.indexOf("//") === -1) && ((fc >= 65 && fc <= 90) || (fc >= 97 && fc <= 122) || fc == 46)) { + // Alphabetical or dot: prepend base path. + src = this.stage.getOption('srcBasePath') + src; + } + } + + return (cb) => { + return this.stage.platform.loadSrcTexture({src: src, hasAlpha: hasAlpha}, cb); + } + } + + getNonDefaults() { + const obj = super.getNonDefaults(); + if (this._src) { + obj.src = this._src; + } + return obj; + } + +} + +class TextTextureRenderer { + + constructor(stage, canvas, settings) { + this._stage = stage; + this._canvas = canvas; + this._context = this._canvas.getContext('2d'); + this._settings = settings; + } + + getPrecision() { + return this._settings.precision; + }; + + setFontProperties() { + this._context.font = Utils.isSpark ? this._stage.platform.getFontSetting(this) : this._getFontSetting(); + this._context.textBaseline = this._settings.textBaseline; + }; + + _getFontSetting() { + let ff = this._settings.fontFace; + + if (!Array.isArray(ff)) { + ff = [ff]; + } + + let ffs = []; + for (let i = 0, n = ff.length; i < n; i++) { + if (ff[i] === "serif" || ff[i] === "sans-serif") { + ffs.push(ff[i]); + } else { + ffs.push(`"${ff[i]}"`); + } + } + + return `${this._settings.fontStyle} ${this._settings.fontSize * this.getPrecision()}px ${ffs.join(",")}` + } + + _load() { + if (Utils.isWeb && document.fonts) { + const fontSetting = this._getFontSetting(); + try { + if (!document.fonts.check(fontSetting, this._settings.text)) { + // Use a promise that waits for loading. + return document.fonts.load(fontSetting, this._settings.text).catch(err => { + // Just load the fallback font. + console.warn('Font load error', err, fontSetting); + }).then(() => { + if (!document.fonts.check(fontSetting, this._settings.text)) { + console.warn('Font not found', fontSetting); + } + }); + } + } catch(e) { + console.warn("Can't check font loading for " + fontSetting); + } + } + } + + draw() { + // We do not use a promise so that loading is performed syncronous when possible. + const loadPromise = this._load(); + if (!loadPromise) { + return Utils.isSpark ? this._stage.platform.drawText(this) : this._draw(); + } else { + return loadPromise.then(() => { + return Utils.isSpark ? this._stage.platform.drawText(this) : this._draw(); + }); + } + } + + _draw() { + let renderInfo = {}; + + const precision = this.getPrecision(); + + let paddingLeft = this._settings.paddingLeft * precision; + let paddingRight = this._settings.paddingRight * precision; + const fontSize = this._settings.fontSize * precision; + let offsetY = this._settings.offsetY === null ? null : (this._settings.offsetY * precision); + let lineHeight = this._settings.lineHeight * precision; + const w = this._settings.w * precision; + const h = this._settings.h * precision; + let wordWrapWidth = this._settings.wordWrapWidth * precision; + const cutSx = this._settings.cutSx * precision; + const cutEx = this._settings.cutEx * precision; + const cutSy = this._settings.cutSy * precision; + const cutEy = this._settings.cutEy * precision; + + // Set font properties. + this.setFontProperties(); + + // Total width. + let width = w || (2048 / this.getPrecision()); + + // Inner width. + let innerWidth = width - (paddingLeft); + if (innerWidth < 10) { + width += (10 - innerWidth); + innerWidth += (10 - innerWidth); + } + + if (!wordWrapWidth) { + wordWrapWidth = innerWidth; + } + + // word wrap + // preserve original text + let linesInfo; + if (this._settings.wordWrap) { + linesInfo = this.wrapText(this._settings.text, wordWrapWidth); + } else { + linesInfo = {l: this._settings.text.split(/(?:\r\n|\r|\n)/), n: []}; + let n = linesInfo.l.length; + for (let i = 0; i < n - 1; i++) { + linesInfo.n.push(i); + } + } + let lines = linesInfo.l; + + if (this._settings.maxLines && lines.length > this._settings.maxLines) { + let usedLines = lines.slice(0, this._settings.maxLines); + + let otherLines = null; + if (this._settings.maxLinesSuffix) { + // Wrap again with max lines suffix enabled. + let w = this._settings.maxLinesSuffix ? this._context.measureText(this._settings.maxLinesSuffix).width : 0; + let al = this.wrapText(usedLines[usedLines.length - 1], wordWrapWidth - w); + usedLines[usedLines.length - 1] = al.l[0] + this._settings.maxLinesSuffix; + otherLines = [al.l.length > 1 ? al.l[1] : '']; + } else { + otherLines = ['']; + } + + // Re-assemble the remaining text. + let i, n = lines.length; + let j = 0; + let m = linesInfo.n.length; + for (i = this._settings.maxLines; i < n; i++) { + otherLines[j] += (otherLines[j] ? " " : "") + lines[i]; + if (i + 1 < m && linesInfo.n[i + 1]) { + j++; + } + } + + renderInfo.remainingText = otherLines.join("\n"); + + renderInfo.moreTextLines = true; + + lines = usedLines; + } else { + renderInfo.moreTextLines = false; + renderInfo.remainingText = ""; + } + + // calculate text width + let maxLineWidth = 0; + let lineWidths = []; + for (let i = 0; i < lines.length; i++) { + let lineWidth = this._context.measureText(lines[i]).width; + lineWidths.push(lineWidth); + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + + renderInfo.lineWidths = lineWidths; + + if (!w) { + // Auto-set width to max text length. + width = maxLineWidth + paddingLeft + paddingRight; + innerWidth = maxLineWidth; + } + + // calculate text height + lineHeight = lineHeight || fontSize; + + let height; + if (h) { + height = h; + } else { + height = lineHeight * (lines.length - 1) + 0.5 * fontSize + Math.max(lineHeight, fontSize) + offsetY; + } + + if (offsetY === null) { + offsetY = fontSize; + } + + renderInfo.w = width; + renderInfo.h = height; + renderInfo.lines = lines; + renderInfo.precision = precision; + + if (!width) { + // To prevent canvas errors. + width = 1; + } + + if (!height) { + // To prevent canvas errors. + height = 1; + } + + if (cutSx || cutEx) { + width = Math.min(width, cutEx - cutSx); + } + + if (cutSy || cutEy) { + height = Math.min(height, cutEy - cutSy); + } + + // Add extra margin to prevent issue with clipped text when scaling. + this._canvas.width = Math.ceil(width + this._stage.getOption('textRenderIssueMargin')); + this._canvas.height = Math.ceil(height); + + // Canvas context has been reset. + this.setFontProperties(); + + if (fontSize >= 128) { + // WpeWebKit bug: must force compositing because cairo-traps-compositor will not work with text first. + this._context.globalAlpha = 0.01; + this._context.fillRect(0, 0, 0.01, 0.01); + this._context.globalAlpha = 1.0; + } + + if (cutSx || cutSy) { + this._context.translate(-cutSx, -cutSy); + } + + let linePositionX; + let linePositionY; + + let drawLines = []; + + // Draw lines line by line. + for (let i = 0, n = lines.length; i < n; i++) { + linePositionX = 0; + linePositionY = (i * lineHeight) + offsetY; + + if (this._settings.textAlign === 'right') { + linePositionX += (innerWidth - lineWidths[i]); + } else if (this._settings.textAlign === 'center') { + linePositionX += ((innerWidth - lineWidths[i]) / 2); + } + linePositionX += paddingLeft; + + drawLines.push({text: lines[i], x: linePositionX, y: linePositionY, w: lineWidths[i]}); + } + + // Highlight. + if (this._settings.highlight) { + let color = this._settings.highlightColor || 0x00000000; + + let hlHeight = (this._settings.highlightHeight * precision || fontSize * 1.5); + let offset = (this._settings.highlightOffset !== null ? this._settings.highlightOffset * precision : -0.5 * fontSize); + const hlPaddingLeft = (this._settings.highlightPaddingLeft !== null ? this._settings.highlightPaddingLeft * precision : paddingLeft); + const hlPaddingRight = (this._settings.highlightPaddingRight !== null ? this._settings.highlightPaddingRight * precision : paddingRight); + + this._context.fillStyle = StageUtils.getRgbaString(color); + for (let i = 0; i < drawLines.length; i++) { + let drawLine = drawLines[i]; + this._context.fillRect((drawLine.x - hlPaddingLeft), (drawLine.y + offset), (drawLine.w + hlPaddingRight + hlPaddingLeft), hlHeight); + } + } + + // Text shadow. + let prevShadowSettings = null; + if (this._settings.shadow) { + prevShadowSettings = [this._context.shadowColor, this._context.shadowOffsetX, this._context.shadowOffsetY, this._context.shadowBlur]; + + this._context.shadowColor = StageUtils.getRgbaString(this._settings.shadowColor); + this._context.shadowOffsetX = this._settings.shadowOffsetX * precision; + this._context.shadowOffsetY = this._settings.shadowOffsetY * precision; + this._context.shadowBlur = this._settings.shadowBlur * precision; + } + + this._context.fillStyle = StageUtils.getRgbaString(this._settings.textColor); + for (let i = 0, n = drawLines.length; i < n; i++) { + let drawLine = drawLines[i]; + this._context.fillText(drawLine.text, drawLine.x, drawLine.y); + } + + if (prevShadowSettings) { + this._context.shadowColor = prevShadowSettings[0]; + this._context.shadowOffsetX = prevShadowSettings[1]; + this._context.shadowOffsetY = prevShadowSettings[2]; + this._context.shadowBlur = prevShadowSettings[3]; + } + + if (cutSx || cutSy) { + this._context.translate(cutSx, cutSy); + } + + this.renderInfo = renderInfo; + }; + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + */ + wrapText(text, wordWrapWidth) { + // Greedy wrapping algorithm that will wrap words as the line grows longer. + // than its horizontal bounds. + let lines = text.split(/\r?\n/g); + let allLines = []; + let realNewlines = []; + for (let i = 0; i < lines.length; i++) { + let resultLines = []; + let result = ''; + let spaceLeft = wordWrapWidth; + let words = lines[i].split(' '); + for (let j = 0; j < words.length; j++) { + let wordWidth = this._context.measureText(words[j]).width; + let wordWidthWithSpace = wordWidth + this._context.measureText(' ').width; + if (j === 0 || wordWidthWithSpace > spaceLeft) { + // Skip printing the newline if it's the first word of the line that is. + // greater than the word wrap width. + if (j > 0) { + resultLines.push(result); + result = ''; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else { + spaceLeft -= wordWidthWithSpace; + result += ' ' + words[j]; + } + } + + if (result) { + resultLines.push(result); + result = ''; + } + + allLines = allLines.concat(resultLines); + + if (i < lines.length - 1) { + realNewlines.push(allLines.length); + } + } + + return {l: allLines, n: realNewlines}; + }; + +} + +class TextTexture extends Texture { + + constructor(stage) { + super(stage); + + // We use the stage precision as the default precision in case of a text texture. + this._precision = this.stage.getOption('precision'); + } + + get text() { + return this._text; + } + + set text(v) { + if (this._text !== v) { + this._text = "" + v; + this._changed(); + } + } + + get w() { + return this._w; + } + + set w(v) { + if (this._w !== v) { + this._w = v; + this._changed(); + } + } + + get h() { + return this._h; + } + + set h(v) { + if (this._h !== v) { + this._h = v; + this._changed(); + } + } + + get fontStyle() { + return this._fontStyle; + } + + set fontStyle(v) { + if (this._fontStyle !== v) { + this._fontStyle = v; + this._changed(); + } + } + + get fontSize() { + return this._fontSize; + } + + set fontSize(v) { + if (this._fontSize !== v) { + this._fontSize = v; + this._changed(); + } + } + + get fontFace() { + return this._fontFace; + } + + set fontFace(v) { + if (this._fontFace !== v) { + this._fontFace = v; + this._changed(); + } + } + + get wordWrap() { + return this._wordWrap; + } + + set wordWrap(v) { + if (this._wordWrap !== v) { + this._wordWrap = v; + this._changed(); + } + } + + get wordWrapWidth() { + return this._wordWrapWidth; + } + + set wordWrapWidth(v) { + if (this._wordWrapWidth !== v) { + this._wordWrapWidth = v; + this._changed(); + } + } + + get lineHeight() { + return this._lineHeight; + } + + set lineHeight(v) { + if (this._lineHeight !== v) { + this._lineHeight = v; + this._changed(); + } + } + + get textBaseline() { + return this._textBaseline; + } + + set textBaseline(v) { + if (this._textBaseline !== v) { + this._textBaseline = v; + this._changed(); + } + } + + get textAlign() { + return this._textAlign; + } + + set textAlign(v) { + if (this._textAlign !== v) { + this._textAlign = v; + this._changed(); + } + } + + get offsetY() { + return this._offsetY; + } + + set offsetY(v) { + if (this._offsetY !== v) { + this._offsetY = v; + this._changed(); + } + } + + get maxLines() { + return this._maxLines; + } + + set maxLines(v) { + if (this._maxLines !== v) { + this._maxLines = v; + this._changed(); + } + } + + get maxLinesSuffix() { + return this._maxLinesSuffix; + } + + set maxLinesSuffix(v) { + if (this._maxLinesSuffix !== v) { + this._maxLinesSuffix = v; + this._changed(); + } + } + + get textColor() { + return this._textColor; + } + + set textColor(v) { + if (this._textColor !== v) { + this._textColor = v; + this._changed(); + } + } + + get paddingLeft() { + return this._paddingLeft; + } + + set paddingLeft(v) { + if (this._paddingLeft !== v) { + this._paddingLeft = v; + this._changed(); + } + } + + get paddingRight() { + return this._paddingRight; + } + + set paddingRight(v) { + if (this._paddingRight !== v) { + this._paddingRight = v; + this._changed(); + } + } + + get shadow() { + return this._shadow; + } + + set shadow(v) { + if (this._shadow !== v) { + this._shadow = v; + this._changed(); + } + } + + get shadowColor() { + return this._shadowColor; + } + + set shadowColor(v) { + if (this._shadowColor !== v) { + this._shadowColor = v; + this._changed(); + } + } + + get shadowOffsetX() { + return this._shadowOffsetX; + } + + set shadowOffsetX(v) { + if (this._shadowOffsetX !== v) { + this._shadowOffsetX = v; + this._changed(); + } + } + + get shadowOffsetY() { + return this._shadowOffsetY; + } + + set shadowOffsetY(v) { + if (this._shadowOffsetY !== v) { + this._shadowOffsetY = v; + this._changed(); + } + } + + get shadowBlur() { + return this._shadowBlur; + } + + set shadowBlur(v) { + if (this._shadowBlur !== v) { + this._shadowBlur = v; + this._changed(); + } + } + + get highlight() { + return this._highlight; + } + + set highlight(v) { + if (this._highlight !== v) { + this._highlight = v; + this._changed(); + } + } + + get highlightHeight() { + return this._highlightHeight; + } + + set highlightHeight(v) { + if (this._highlightHeight !== v) { + this._highlightHeight = v; + this._changed(); + } + } + + get highlightColor() { + return this._highlightColor; + } + + set highlightColor(v) { + if (this._highlightColor !== v) { + this._highlightColor = v; + this._changed(); + } + } + + get highlightOffset() { + return this._highlightOffset; + } + + set highlightOffset(v) { + if (this._highlightOffset !== v) { + this._highlightOffset = v; + this._changed(); + } + } + + get highlightPaddingLeft() { + return this._highlightPaddingLeft; + } + + set highlightPaddingLeft(v) { + if (this._highlightPaddingLeft !== v) { + this._highlightPaddingLeft = v; + this._changed(); + } + } + + get highlightPaddingRight() { + return this._highlightPaddingRight; + } + + set highlightPaddingRight(v) { + if (this._highlightPaddingRight !== v) { + this._highlightPaddingRight = v; + this._changed(); + } + } + + get cutSx() { + return this._cutSx; + } + + set cutSx(v) { + if (this._cutSx !== v) { + this._cutSx = v; + this._changed(); + } + } + + get cutEx() { + return this._cutEx; + } + + set cutEx(v) { + if (this._cutEx !== v) { + this._cutEx = v; + this._changed(); + } + } + + get cutSy() { + return this._cutSy; + } + + set cutSy(v) { + if (this._cutSy !== v) { + this._cutSy = v; + this._changed(); + } + } + + get cutEy() { + return this._cutEy; + } + + set cutEy(v) { + if (this._cutEy !== v) { + this._cutEy = v; + this._changed(); + } + } + + get precision() { + return super.precision; + } + + set precision(v) { + // We actually draw differently when the precision changes. + if (this.precision !== v) { + super.precision = v; + this._changed(); + } + } + + _getIsValid() { + return !!this.text; + } + + _getLookupId() { + let parts = []; + + if (this.w !== 0) parts.push("w " + this.w); + if (this.h !== 0) parts.push("h " + this.h); + if (this.fontStyle !== "normal") parts.push("fS" + this.fontStyle); + if (this.fontSize !== 40) parts.push("fs" + this.fontSize); + if (this.fontFace !== null) parts.push("ff" + (Array.isArray(this.fontFace) ? this.fontFace.join(",") : this.fontFace)); + if (this.wordWrap !== true) parts.push("wr" + (this.wordWrap ? 1 : 0)); + if (this.wordWrapWidth !== 0) parts.push("ww" + this.wordWrapWidth); + if (this.lineHeight !== null) parts.push("lh" + this.lineHeight); + if (this.textBaseline !== "alphabetic") parts.push("tb" + this.textBaseline); + if (this.textAlign !== "left") parts.push("ta" + this.textAlign); + if (this.offsetY !== null) parts.push("oy" + this.offsetY); + if (this.maxLines !== 0) parts.push("ml" + this.maxLines); + if (this.maxLinesSuffix !== "..") parts.push("ms" + this.maxLinesSuffix); + parts.push("pc" + this.precision); + if (this.textColor !== 0xffffffff) parts.push("co" + this.textColor.toString(16)); + if (this.paddingLeft !== 0) parts.push("pl" + this.paddingLeft); + if (this.paddingRight !== 0) parts.push("pr" + this.paddingRight); + if (this.shadow !== false) parts.push("sh" + (this.shadow ? 1 : 0)); + if (this.shadowColor !== 0xff000000) parts.push("sc" + this.shadowColor.toString(16)); + if (this.shadowOffsetX !== 0) parts.push("sx" + this.shadowOffsetX); + if (this.shadowOffsetY !== 0) parts.push("sy" + this.shadowOffsetY); + if (this.shadowBlur !== 5) parts.push("sb" + this.shadowBlur); + if (this.highlight !== false) parts.push("hL" + (this.highlight ? 1 : 0)); + if (this.highlightHeight !== 0) parts.push("hh" + this.highlightHeight); + if (this.highlightColor !== 0xff000000) parts.push("hc" + this.highlightColor.toString(16)); + if (this.highlightOffset !== null) parts.push("ho" + this.highlightOffset); + if (this.highlightPaddingLeft !== null) parts.push("hl" + this.highlightPaddingLeft); + if (this.highlightPaddingRight !== null) parts.push("hr" + this.highlightPaddingRight); + + if (this.cutSx) parts.push("csx" + this.cutSx); + if (this.cutEx) parts.push("cex" + this.cutEx); + if (this.cutSy) parts.push("csy" + this.cutSy); + if (this.cutEy) parts.push("cey" + this.cutEy); + + let id = "TX$" + parts.join("|") + ":" + this.text; + return id; + } + + _getSourceLoader() { + const args = this.cloneArgs(); + + // Inherit font face from stage. + if (args.fontFace === null) { + args.fontFace = this.stage.getOption('defaultFontFace'); + } + + return function(cb) { + const canvas = this.stage.platform.getDrawingCanvas(); + const renderer = new TextTextureRenderer(this.stage, canvas, args); + const p = renderer.draw(); + + if (p) { + p.then(() => { + cb(null, Object.assign({renderInfo: renderer.renderInfo}, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas))); + }).catch((err) => { + cb(err); + }); + } else { + cb(null, Object.assign({renderInfo: renderer.renderInfo}, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas))); + } + } + } + + getNonDefaults() { + const nonDefaults = super.getNonDefaults(); + if (this.text !== "") nonDefaults['text'] = this.text; + if (this.w !== 0) nonDefaults['w'] = this.w; + if (this.h !== 0) nonDefaults['h'] = this.h; + if (this.fontStyle !== "normal") nonDefaults['fontStyle'] = this.fontStyle; + if (this.fontSize !== 40) nonDefaults["fontSize"] = this.fontSize; + if (this.fontFace !== null) nonDefaults["fontFace"] = this.fontFace; + if (this.wordWrap !== true) nonDefaults["wordWrap"] = this.wordWrap; + if (this.wordWrapWidth !== 0) nonDefaults["wordWrapWidth"] = this.wordWrapWidth; + if (this.lineHeight !== null) nonDefaults["lineHeight"] = this.lineHeight; + if (this.textBaseline !== "alphabetic") nonDefaults["textBaseline"] = this.textBaseline; + if (this.textAlign !== "left") nonDefaults["textAlign"] = this.textAlign; + if (this.offsetY !== null) nonDefaults["offsetY"] = this.offsetY; + if (this.maxLines !== 0) nonDefaults["maxLines"] = this.maxLines; + if (this.maxLinesSuffix !== "..") nonDefaults["maxLinesSuffix"] = this.maxLinesSuffix; + if (this.precision !== this.stage.getOption('precision')) nonDefaults["precision"] = this.precision; + if (this.textColor !== 0xffffffff) nonDefaults["textColor"] = this.textColor; + if (this.paddingLeft !== 0) nonDefaults["paddingLeft"] = this.paddingLeft; + if (this.paddingRight !== 0) nonDefaults["paddingRight"] = this.paddingRight; + if (this.shadow !== false) nonDefaults["shadow"] = this.shadow; + if (this.shadowColor !== 0xff000000) nonDefaults["shadowColor"] = this.shadowColor; + if (this.shadowOffsetX !== 0) nonDefaults["shadowOffsetX"] = this.shadowOffsetX; + if (this.shadowOffsetY !== 0) nonDefaults["shadowOffsetY"] = this.shadowOffsetY; + if (this.shadowBlur !== 5) nonDefaults["shadowBlur"] = this.shadowBlur; + if (this.highlight !== false) nonDefaults["highlight"] = this.highlight; + if (this.highlightHeight !== 0) nonDefaults["highlightHeight"] = this.highlightHeight; + if (this.highlightColor !== 0xff000000) nonDefaults["highlightColor"] = this.highlightColor; + if (this.highlightOffset !== 0) nonDefaults["highlightOffset"] = this.highlightOffset; + if (this.highlightPaddingLeft !== 0) nonDefaults["highlightPaddingLeft"] = this.highlightPaddingLeft; + if (this.highlightPaddingRight !== 0) nonDefaults["highlightPaddingRight"] = this.highlightPaddingRight; + + if (this.cutSx) nonDefaults["cutSx"] = this.cutSx; + if (this.cutEx) nonDefaults["cutEx"] = this.cutEx; + if (this.cutSy) nonDefaults["cutSy"] = this.cutSy; + if (this.cutEy) nonDefaults["cutEy"] = this.cutEy; + return nonDefaults; + } + + cloneArgs() { + let obj = {}; + obj.text = this._text; + obj.w = this._w; + obj.h = this._h; + obj.fontStyle = this._fontStyle; + obj.fontSize = this._fontSize; + obj.fontFace = this._fontFace; + obj.wordWrap = this._wordWrap; + obj.wordWrapWidth = this._wordWrapWidth; + obj.lineHeight = this._lineHeight; + obj.textBaseline = this._textBaseline; + obj.textAlign = this._textAlign; + obj.offsetY = this._offsetY; + obj.maxLines = this._maxLines; + obj.maxLinesSuffix = this._maxLinesSuffix; + obj.precision = this._precision; + obj.textColor = this._textColor; + obj.paddingLeft = this._paddingLeft; + obj.paddingRight = this._paddingRight; + obj.shadow = this._shadow; + obj.shadowColor = this._shadowColor; + obj.shadowOffsetX = this._shadowOffsetX; + obj.shadowOffsetY = this._shadowOffsetY; + obj.shadowBlur = this._shadowBlur; + obj.highlight = this._highlight; + obj.highlightHeight = this._highlightHeight; + obj.highlightColor = this._highlightColor; + obj.highlightOffset = this._highlightOffset; + obj.highlightPaddingLeft = this._highlightPaddingLeft; + obj.highlightPaddingRight = this._highlightPaddingRight; + obj.cutSx = this._cutSx; + obj.cutEx = this._cutEx; + obj.cutSy = this._cutSy; + obj.cutEy = this._cutEy; + return obj; + } + + +} + +// Because there are so many properties, we prefer to use the prototype for default values. +// This causes a decrease in performance, but also a decrease in memory usage. +let proto = TextTexture.prototype; +proto._text = ""; +proto._w = 0; +proto._h = 0; +proto._fontStyle = "normal"; +proto._fontSize = 40; +proto._fontFace = null; +proto._wordWrap = true; +proto._wordWrapWidth = 0; +proto._lineHeight = null; +proto._textBaseline = "alphabetic"; +proto._textAlign = "left"; +proto._offsetY = null; +proto._maxLines = 0; +proto._maxLinesSuffix = ".."; +proto._textColor = 0xFFFFFFFF; +proto._paddingLeft = 0; +proto._paddingRight = 0; +proto._shadow = false; +proto._shadowColor = 0xFF000000; +proto._shadowOffsetX = 0; +proto._shadowOffsetY = 0; +proto._shadowBlur = 5; +proto._highlight = false; +proto._highlightHeight = 0; +proto._highlightColor = 0xFF000000; +proto._highlightOffset = 0; +proto._highlightPaddingLeft = 0; +proto._highlightPaddingRight = 0; +proto._cutSx = 0; +proto._cutEx = 0; +proto._cutSy = 0; +proto._cutEy = 0; + +class SourceTexture extends Texture { + + constructor(stage) { + super(stage); + + this._textureSource = undefined; + } + + get textureSource() { + return this._textureSource; + } + + set textureSource(v) { + if (v !== this._textureSource) { + if (v.isResultTexture) { + // In case of a result texture, automatically inherit the precision. + this._precision = this.stage.getRenderPrecision(); + } + this._textureSource = v; + this._changed(); + } + } + + _getTextureSource() { + return this._textureSource; + } + +} + +class Transition extends EventEmitter { + + constructor(manager, settings, element, property) { + super(); + + this.manager = manager; + + this._settings = settings; + + this._element = element; + this._getter = Element.getGetter(property); + this._setter = Element.getSetter(property); + + this._merger = settings.merger; + + if (!this._merger) { + this._merger = Element.getMerger(property); + } + + this._startValue = this._getter(this._element); + this._targetValue = this._startValue; + + this._p = 1; + this._delayLeft = 0; + } + + start(targetValue) { + this._startValue = this._getter(this._element); + + if (!this.isAttached()) { + // We don't support transitions on non-attached elements. Just set value without invoking listeners. + this._targetValue = targetValue; + this._p = 1; + this._updateDrawValue(); + } else { + if (targetValue === this._startValue) { + this.reset(targetValue, 1); + } else { + this._targetValue = targetValue; + this._p = 0; + this._delayLeft = this._settings.delay; + this.emit('start'); + this.add(); + } + } + } + + finish() { + if (this._p < 1) { + // Value setting and will must be invoked (async) upon next transition cycle. + this._p = 1; + } + } + + stop() { + // Just stop where the transition is at. + this.emit('stop'); + this.manager.removeActive(this); + } + + pause() { + this.stop(); + } + + play() { + this.manager.addActive(this); + } + + reset(targetValue, p) { + if (!this.isAttached()) { + // We don't support transitions on non-attached elements. Just set value without invoking listeners. + this._startValue = this._getter(this._element); + this._targetValue = targetValue; + this._p = 1; + this._updateDrawValue(); + } else { + this._startValue = this._getter(this._element); + this._targetValue = targetValue; + this._p = p; + this.add(); + } + } + + _updateDrawValue() { + this._setter(this._element, this.getDrawValue()); + } + + add() { + this.manager.addActive(this); + } + + isAttached() { + return this._element.attached; + } + + isRunning() { + return (this._p < 1.0); + } + + progress(dt) { + if (!this.isAttached()) { + // Skip to end of transition so that it is removed. + this._p = 1; + } + + if (this.p < 1) { + if (this.delayLeft > 0) { + this._delayLeft -= dt; + + if (this.delayLeft < 0) { + dt = -this.delayLeft; + this._delayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + if (this._settings.duration == 0) { + this._p = 1; + } else { + this._p += dt / this._settings.duration; + } + if (this._p >= 1) { + // Finished!; + this._p = 1; + } + } + + this._updateDrawValue(); + + this.invokeListeners(); + } + + invokeListeners() { + this.emit('progress', this.p); + if (this.p === 1) { + this.emit('finish'); + } + } + + updateTargetValue(targetValue) { + let t = this._settings.timingFunctionImpl(this.p); + if (t === 1) { + this._targetValue = targetValue; + } else if (t === 0) { + this._startValue = this._targetValue; + this._targetValue = targetValue; + } else { + this._startValue = targetValue - ((targetValue - this._targetValue) / (1 - t)); + this._targetValue = targetValue; + } + } + + getDrawValue() { + if (this.p >= 1) { + return this.targetValue; + } else { + let v = this._settings._timingFunctionImpl(this.p); + return this._merger(this.targetValue, this.startValue, v); + } + } + + skipDelay() { + this._delayLeft = 0; + } + + get startValue() { + return this._startValue; + } + + get targetValue() { + return this._targetValue; + } + + get p() { + return this._p; + } + + get delayLeft() { + return this._delayLeft; + } + + get element() { + return this._element; + } + + get settings() { + return this._settings; + } + + set settings(v) { + this._settings = v; + } + +} + +Transition.prototype.isTransition = true; + +/** + * Manages a list of objects. + * Objects may be patched. Then, they can be referenced using the 'ref' (string) property. + */ +class ObjectList { + + constructor() { + this._items = []; + this._refs = {}; + } + + get() { + return this._items; + } + + get first() { + return this._items[0]; + } + + get last() { + return this._items.length ? this._items[this._items.length - 1] : undefined; + } + + add(item) { + this.addAt(item, this._items.length); + } + + addAt(item, index) { + if (index >= 0 && index <= this._items.length) { + let currentIndex = this._items.indexOf(item); + if (currentIndex === index) { + return item; + } + + if (currentIndex != -1) { + this.setAt(item, index); + } else { + if (item.ref) { + this._refs[item.ref] = item; + } + this._items.splice(index, 0, item); + this.onAdd(item, index); + } + } else { + throw new Error('addAt: The index ' + index + ' is out of bounds ' + this._items.length); + } + } + + replaceByRef(item) { + if (item.ref) { + const existingItem = this.getByRef(item.ref); + if (!existingItem) { + throw new Error('replaceByRef: no item found with reference: ' + item.ref); + } + this.replace(item, existingItem); + } else { + throw new Error('replaceByRef: no ref specified in item'); + } + this.addAt(item, this._items.length); + + } + + replace(item, prevItem) { + const index = this.getIndex(prevItem); + if (index === -1) { + throw new Error('replace: The previous item does not exist'); + } + this.setAt(item, index); + } + + setAt(item, index) { + if (index >= 0 && index <= this._items.length) { + let currentIndex = this._items.indexOf(item); + if (currentIndex != -1) { + if (currentIndex !== index) { + const fromIndex = currentIndex; + if (fromIndex !== index) { + this._items.splice(fromIndex, 1); + this._items.splice(index, 0, item); + this.onMove(item, fromIndex, index); + } + } + } else { + if (index < this._items.length) { + if (this._items[index].ref) { + this._refs[this._items[index].ref] = undefined; + } + } + + const prevItem = this._items[index]; + + // Doesn't exist yet: overwrite current. + this._items[index] = item; + + if (item.ref) { + this._refs[item.ref] = item; + } + + this.onSet(item, index, prevItem); + } + } else { + throw new Error('setAt: The index ' + index + ' is out of bounds ' + this._items.length); + } + } + + getAt(index) { + return this._items[index]; + } + + getIndex(item) { + return this._items.indexOf(item); + } + + remove(item) { + let index = this._items.indexOf(item); + + if (index !== -1) { + this.removeAt(index); + } + }; + + removeAt(index) { + let item = this._items[index]; + + if (item.ref) { + this._refs[item.ref] = undefined; + } + + this._items.splice(index, 1); + + this.onRemove(item, index); + + return item; + }; + + clear() { + let n = this._items.length; + if (n) { + let prev = this._items; + this._items = []; + this._refs = {}; + this.onSync(prev, [], []); + } + }; + + a(o) { + if (Utils.isObjectLiteral(o)) { + let c = this.createItem(o); + c.patch(o); + this.add(c); + return c; + } else if (Array.isArray(o)) { + for (let i = 0, n = o.length; i < n; i++) { + this.a(o[i]); + } + return null; + } else if (this.isItem(o)) { + this.add(o); + return o; + } + }; + + get length() { + return this._items.length; + } + + _getRefs() { + return this._refs; + } + + getByRef(ref) { + return this._refs[ref]; + } + + clearRef(ref) { + delete this._refs[ref]; + } + + setRef(ref, child) { + this._refs[ref] = child; + } + + patch(settings) { + if (Utils.isObjectLiteral(settings)) { + this._setByObject(settings); + } else if (Array.isArray(settings)) { + this._setByArray(settings); + } + } + + _setByObject(settings) { + // Overrule settings of known referenced items. + let refs = this._getRefs(); + let crefs = Object.keys(settings); + for (let i = 0, n = crefs.length; i < n; i++) { + let cref = crefs[i]; + let s = settings[cref]; + + let c = refs[cref]; + if (!c) { + if (this.isItem(s)) { + // Replace previous item; + s.ref = cref; + this.add(s); + } else { + // Create new item. + c = this.createItem(s); + c.ref = cref; + c.patch(s); + this.add(c); + } + } else { + if (this.isItem(s)) { + if (c !== s) { + // Replace previous item; + let idx = this.getIndex(c); + s.ref = cref; + this.setAt(s, idx); + } + } else { + c.patch(s); + } + } + } + } + + _equalsArray(array) { + let same = true; + if (array.length === this._items.length) { + for (let i = 0, n = this._items.length; (i < n) && same; i++) { + same = same && (this._items[i] === array[i]); + } + } else { + same = false; + } + return same; + } + + _setByArray(array) { + // For performance reasons, first check if the arrays match exactly and bail out if they do. + if (this._equalsArray(array)) { + return; + } + + for (let i = 0, n = this._items.length; i < n; i++) { + this._items[i].marker = true; + } + + let refs; + let newItems = []; + for (let i = 0, n = array.length; i < n; i++) { + let s = array[i]; + if (this.isItem(s)) { + s.marker = false; + newItems.push(s); + } else { + let cref = s.ref; + let c; + if (cref) { + if (!refs) refs = this._getRefs(); + c = refs[cref]; + } + + if (!c) { + // Create new item. + c = this.createItem(s); + } else { + c.marker = false; + } + + if (Utils.isObjectLiteral(s)) { + c.patch(s); + } + + newItems.push(c); + } + } + + this._setItems(newItems); + } + + _setItems(newItems) { + let prevItems = this._items; + this._items = newItems; + + // Remove the items. + let removed = prevItems.filter(item => {let m = item.marker; delete item.marker; return m}); + let added = newItems.filter(item => (prevItems.indexOf(item) === -1)); + + if (removed.length || added.length) { + // Recalculate refs. + this._refs = {}; + for (let i = 0, n = this._items.length; i < n; i++) { + let ref = this._items[i].ref; + if (ref) { + this._refs[ref] = this._items[i]; + } + } + } + + this.onSync(removed, added, newItems); + } + + sort(f) { + const items = this._items.slice(); + items.sort(f); + this._setByArray(items); + } + + onAdd(item, index) { + } + + onRemove(item, index) { + } + + onSync(removed, added, order) { + } + + onSet(item, index, prevItem) { + } + + onMove(item, fromIndex, toIndex) { + } + + createItem(object) { + throw new Error("ObjectList.createItem must create and return a new object"); + } + + isItem(object) { + return false; + } + + forEach(f) { + this.get().forEach(f); + } + +} + +/** + * Manages the list of children for an element. + */ + +class ElementChildList extends ObjectList { + + constructor(element) { + super(); + this._element = element; + } + + _connectParent(item) { + const prevParent = item.parent; + if (prevParent && prevParent !== this._element) { + // Cleanup in previous child list, without + const prevChildList = item.parent.childList; + const index = prevChildList.getIndex(item); + + if (item.ref) { + prevChildList._refs[item.ref] = undefined; + } + prevChildList._items.splice(index, 1); + + // Also clean up element core. + prevParent.core.removeChildAt(index); + + } + + item._setParent(this._element); + + // We are expecting the caller to sync it to the core. + } + + onAdd(item, index) { + this._connectParent(item); + this._element.core.addChildAt(index, item.core); + } + + onRemove(item, index) { + item._setParent(null); + this._element.core.removeChildAt(index); + } + + onSync(removed, added, order) { + for (let i = 0, n = removed.length; i < n; i++) { + removed[i]._setParent(null); + } + for (let i = 0, n = added.length; i < n; i++) { + this._connectParent(added[i]); + } + let gc = i => i.core; + this._element.core.syncChildren(removed.map(gc), added.map(gc), order.map(gc)); + } + + onSet(item, index, prevItem) { + prevItem._setParent(null); + + this._connectParent(item); + this._element.core.setChildAt(index, item.core); + } + + onMove(item, fromIndex, toIndex) { + this._element.core.moveChild(fromIndex, toIndex); + } + + createItem(object) { + if (object.type) { + return new object.type(this._element.stage); + } else { + return this._element.stage.createElement(); + } + } + + isItem(object) { + return object.isElement; + } + +} + +/** + * Render tree node. + * Copyright Metrological, 2017 + */ + +class Element { + + constructor(stage) { + this.stage = stage; + + this.__id = Element.id++; + + this.__start(); + + // EventEmitter constructor. + this._hasEventListeners = false; + + this.__core = new ElementCore(this); + + /** + * A reference that can be used while merging trees. + * @type {string} + */ + this.__ref = null; + + /** + * An element is attached if it is a descendant of the stage root. + * @type {boolean} + */ + this.__attached = false; + + /** + * An element is enabled when it is attached and it is visible (worldAlpha > 0). + * @type {boolean} + */ + this.__enabled = false; + + /** + * An element is active when it is enabled and it is within bounds. + * @type {boolean} + */ + this.__active = false; + + /** + * @type {Element} + */ + this.__parent = null; + + /** + * The texture that is currently set. + * @type {Texture} + */ + this.__texture = null; + + /** + * The currently displayed texture. While this.texture is loading, this one may be different. + * @type {Texture} + */ + this.__displayedTexture = null; + + /** + * Tags that can be used to identify/search for a specific element. + * @type {String[]} + */ + this.__tags = null; + + /** + * The tree's tags mapping. + * This contains all elements for all known tags, at all times. + * @type {Map} + */ + this.__treeTags = null; + + /** + * Creates a tag context: tagged elements in this branch will not be reachable from ancestors of this elements. + * @type {boolean} + */ + this.__tagRoot = false; + + /** + * (Lazy-initialised) list of children owned by this elements. + * @type {ElementChildList} + */ + this.__childList = null; + + this._w = 0; + + this._h = 0; + } + + __start() { + } + + get id() { + return this.__id; + } + + set ref(ref) { + if (this.__ref !== ref) { + const charcode = ref.charCodeAt(0); + if (!Utils.isUcChar(charcode)) { + this._throwError("Ref must start with an upper case character: " + ref); + } + if (this.__ref !== null) { + this.removeTag(this.__ref); + if (this.__parent) { + this.__parent.__childList.clearRef(this.__ref); + } + } + + this.__ref = ref; + + if (this.__ref) { + this._addTag(this.__ref); + if (this.__parent) { + this.__parent.__childList.setRef(this.__ref, this); + } + } + } + } + + get ref() { + return this.__ref; + } + + get core() { + return this.__core; + } + + setAsRoot() { + this.__core.setAsRoot(); + this._updateAttachedFlag(); + this._updateEnabledFlag(); + } + + get isRoot() { + return this.__core.isRoot; + } + + _setParent(parent) { + if (this.__parent === parent) return; + + if (this.__parent) { + this._unsetTagsParent(); + } + + this.__parent = parent; + + if (parent) { + this._setTagsParent(); + } + + this._updateAttachedFlag(); + this._updateEnabledFlag(); + + if (this.isRoot && parent) { + this._throwError("Root should not be added as a child! Results are unspecified!"); + } + }; + + getDepth() { + let depth = 0; + + let p = this.__parent; + while(p) { + depth++; + p = p.__parent; + } + + return depth; + }; + + getAncestor(l) { + let p = this; + while (l > 0 && p.__parent) { + p = p.__parent; + l--; + } + return p; + }; + + getAncestorAtDepth(depth) { + let levels = this.getDepth() - depth; + if (levels < 0) { + return null; + } + return this.getAncestor(levels); + }; + + isAncestorOf(c) { + let p = c; + while(p = p.parent) { + if (this === p) { + return true; + } + } + return false; + }; + + getSharedAncestor(c) { + let o1 = this; + let o2 = c; + let l1 = o1.getDepth(); + let l2 = o2.getDepth(); + if (l1 > l2) { + o1 = o1.getAncestor(l1 - l2); + } else if (l2 > l1) { + o2 = o2.getAncestor(l2 - l1); + } + + do { + if (o1 === o2) { + return o1; + } + + o1 = o1.__parent; + o2 = o2.__parent; + } while (o1 && o2); + + return null; + }; + + get attached() { + return this.__attached; + } + + get enabled() { + return this.__enabled; + } + + get active() { + return this.__active; + } + + _isAttached() { + return (this.__parent ? this.__parent.__attached : (this.stage.root === this)); + }; + + _isEnabled() { + return this.__core.visible && (this.__core.alpha > 0) && (this.__parent ? this.__parent.__enabled : (this.stage.root === this)); + }; + + _isActive() { + return this._isEnabled() && this.withinBoundsMargin; + }; + + /** + * Updates the 'attached' flag for this branch. + */ + _updateAttachedFlag() { + let newAttached = this._isAttached(); + if (this.__attached !== newAttached) { + this.__attached = newAttached; + + if (newAttached) { + this._onSetup(); + } + + let children = this._children.get(); + if (children) { + let m = children.length; + if (m > 0) { + for (let i = 0; i < m; i++) { + children[i]._updateAttachedFlag(); + } + } + } + + if (newAttached) { + this._onAttach(); + } else { + this._onDetach(); + } + } + }; + + /** + * Updates the 'enabled' flag for this branch. + */ + _updateEnabledFlag() { + let newEnabled = this._isEnabled(); + if (this.__enabled !== newEnabled) { + if (newEnabled) { + this._onEnabled(); + this._setEnabledFlag(); + } else { + this._onDisabled(); + this._unsetEnabledFlag(); + } + + let children = this._children.get(); + if (children) { + let m = children.length; + if (m > 0) { + for (let i = 0; i < m; i++) { + children[i]._updateEnabledFlag(); + } + } + } + } + }; + + _setEnabledFlag() { + this.__enabled = true; + + // Force re-check of texture because dimensions might have changed (cutting). + this._updateDimensions(); + this._updateTextureCoords(); + + if (this.__texture) { + this.__texture.addElement(this); + } + + if (this.withinBoundsMargin) { + this._setActiveFlag(); + } + + if (this.__core.shader) { + this.__core.shader.addElement(this.__core); + } + + } + + _unsetEnabledFlag() { + if (this.__active) { + this._unsetActiveFlag(); + } + + if (this.__texture) { + this.__texture.removeElement(this); + } + + if (this.__core.shader) { + this.__core.shader.removeElement(this.__core); + } + + if (this._texturizer) { + this.texturizer.filters.forEach(filter => filter.removeElement(this.__core)); + } + + this.__enabled = false; + } + + _setActiveFlag() { + this.__active = true; + + // This must happen before enabling the texture, because it may already be loaded or load directly. + if (this.__texture) { + this.__texture.incActiveCount(); + } + + if (this.__texture) { + this._enableTexture(); + } + this._onActive(); + } + + _unsetActiveFlag() { + if (this.__texture) { + this.__texture.decActiveCount(); + } + + this.__active = false; + if (this.__texture) { + this._disableTexture(); + } + + if (this._hasTexturizer()) { + this.texturizer.deactivate(); + } + + this._onInactive(); + } + + _onSetup() { + } + + _onAttach() { + } + + _onDetach() { + } + + _onEnabled() { + } + + _onDisabled() { + } + + _onActive() { + } + + _onInactive() { + } + + _onResize() { + } + + _getRenderWidth() { + if (this._w) { + return this._w; + } else if (this.__displayedTexture) { + return this.__displayedTexture.getRenderWidth(); + } else if (this.__texture) { + // Texture already loaded, but not yet updated (probably because this element is not active). + return this.__texture.getRenderWidth(); + } else { + return 0; + } + }; + + _getRenderHeight() { + if (this._h) { + return this._h; + } else if (this.__displayedTexture) { + return this.__displayedTexture.getRenderHeight(); + } else if (this.__texture) { + // Texture already loaded, but not yet updated (probably because this element is not active). + return this.__texture.getRenderHeight(); + } else { + return 0; + } + }; + + get renderWidth() { + if (this.__enabled) { + // Render width is only maintained if this element is enabled. + return this.__core.getRenderWidth(); + } else { + return this._getRenderWidth(); + } + } + + get renderHeight() { + if (this.__enabled) { + return this.__core.getRenderHeight(); + } else { + return this._getRenderHeight(); + } + } + + get finalX() { + return this.__core.x; + } + + get finalY() { + return this.__core.y; + } + + get finalW() { + return this.__core.w; + } + + get finalH() { + return this.__core.h; + } + + textureIsLoaded() { + return this.__texture && this.__texture.isLoaded(); + } + + loadTexture() { + if (this.__texture) { + this.__texture.load(); + + if (!this.__texture.isUsed() || !this._isEnabled()) { + // Loading the texture will have no effect on the dimensions of this element. + // Manually update them, so that calcs can be performed immediately in userland. + this._updateDimensions(); + } + } + } + + _enableTextureError() { + // txError event should automatically be re-triggered when a element becomes active. + const loadError = this.__texture.loadError; + if (loadError) { + this.emit('txError', loadError, this.__texture._source); + } + } + + _enableTexture() { + if (this.__texture.isLoaded()) { + this._setDisplayedTexture(this.__texture); + } else { + // We don't want to retain the old 'ghost' image as it wasn't visible anyway. + this._setDisplayedTexture(null); + + this._enableTextureError(); + } + } + + _disableTexture() { + // We disable the displayed texture because, when the texture changes while invisible, we should use that w, h, + // mw, mh for checking within bounds. + this._setDisplayedTexture(null); + } + + get texture() { + return this.__texture; + } + + set texture(v) { + let texture; + if (Utils.isObjectLiteral(v)) { + if (v.type) { + texture = new v.type(this.stage); + } else { + texture = this.texture; + } + + if (texture) { + Base.patchObject(texture, v); + } + } else if (!v) { + texture = null; + } else { + if (v.isTexture) { + texture = v; + } else if (v.isTextureSource) { + texture = new SourceTexture(this.stage); + texture.textureSource = v; + } else { + console.error("Please specify a texture type."); + return; + } + } + + const prevTexture = this.__texture; + if (texture !== prevTexture) { + this.__texture = texture; + + if (this.__texture) { + if (this.__enabled) { + this.__texture.addElement(this); + + if (this.withinBoundsMargin) { + if (this.__texture.isLoaded()) { + this._setDisplayedTexture(this.__texture); + } else { + this._enableTextureError(); + } + } + } + } else { + // Make sure that current texture is cleared when the texture is explicitly set to null. + this._setDisplayedTexture(null); + } + + if (prevTexture && prevTexture !== this.__displayedTexture) { + prevTexture.removeElement(this); + } + + this._updateDimensions(); + } + } + + get displayedTexture() { + return this.__displayedTexture; + } + + _setDisplayedTexture(v) { + let prevTexture = this.__displayedTexture; + + if (prevTexture && (v !== prevTexture)) { + if (this.__texture !== prevTexture) { + // The old displayed texture is deprecated. + prevTexture.removeElement(this); + } + } + + const prevSource = this.__core.displayedTextureSource ? this.__core.displayedTextureSource._source : null; + const sourceChanged = (v ? v._source : null) !== prevSource; + + this.__displayedTexture = v; + this._updateDimensions(); + + if (this.__displayedTexture) { + if (sourceChanged) { + // We don't need to reference the displayed texture because it was already referenced (this.texture === this.displayedTexture). + this._updateTextureCoords(); + this.__core.setDisplayedTextureSource(this.__displayedTexture._source); + } + } else { + this.__core.setDisplayedTextureSource(null); + } + + if (sourceChanged) { + if (this.__displayedTexture) { + this.emit('txLoaded', this.__displayedTexture); + } else { + this.emit('txUnloaded', this.__displayedTexture); + } + } + } + + onTextureSourceLoaded() { + // This function is called when element is enabled, but we only want to set displayed texture for active elements. + if (this.active) { + // We may be dealing with a texture reloading, so we must force update. + this._setDisplayedTexture(this.__texture); + } + }; + + onTextureSourceLoadError(e) { + this.emit('txError', e, this.__texture._source); + }; + + forceRenderUpdate() { + this.__core.setHasRenderUpdates(3); + } + + onDisplayedTextureClippingChanged() { + this._updateDimensions(); + this._updateTextureCoords(); + }; + + onPrecisionChanged() { + this._updateDimensions(); + }; + + onDimensionsChanged(w, h) { + if (this.texture instanceof TextTexture) { + this.texture.w = w; + this.texture.h = h; + this.w = w; + this.h = h; + } + } + + _updateDimensions() { + let w = this._getRenderWidth(); + let h = this._getRenderHeight(); + + let unknownSize = false; + if (!w || !h) { + if (!this.__displayedTexture && this.__texture) { + // We use a 'max width' replacement instead in the ElementCore calcs. + // This makes sure that it is able to determine withinBounds. + w = w || this.__texture.mw; + h = h || this.__texture.mh; + + if ((!w || !h) && this.__texture.isAutosizeTexture()) { + unknownSize = true; + } + } + } + + if (this.__core.setDimensions(w, h, unknownSize)) { + this._onResize(); + } + } + + _updateTextureCoords() { + if (this.displayedTexture && this.displayedTexture._source) { + let displayedTexture = this.displayedTexture; + let displayedTextureSource = this.displayedTexture._source; + + let tx1 = 0, ty1 = 0, tx2 = 1.0, ty2 = 1.0; + if (displayedTexture.clipping) { + // Apply texture clipping. + let w = displayedTextureSource.getRenderWidth(); + let h = displayedTextureSource.getRenderHeight(); + let iw, ih, rw, rh; + iw = 1 / w; + ih = 1 / h; + + if (displayedTexture.pw) { + rw = (displayedTexture.pw) * iw; + } else { + rw = (w - displayedTexture.px) * iw; + } + + if (displayedTexture.ph) { + rh = displayedTexture.ph * ih; + } else { + rh = (h - displayedTexture.py) * ih; + } + + iw *= (displayedTexture.px); + ih *= (displayedTexture.py); + + tx1 = iw; + ty1 = ih; + tx2 = tx2 * rw + iw; + ty2 = ty2 * rh + ih; + + tx1 = Math.max(0, tx1); + ty1 = Math.max(0, ty1); + tx2 = Math.min(1, tx2); + ty2 = Math.min(1, ty2); + } + + if (displayedTextureSource._flipTextureY) { + let tempty = ty2; + ty2 = ty1; + ty1 = tempty; + } + this.__core.setTextureCoords(tx1, ty1, tx2, ty2); + } + } + + getCornerPoints() { + return this.__core.getCornerPoints(); + } + + _unsetTagsParent() { + if (this.__tags) { + this.__tags.forEach((tag) => { + // Remove from treeTags. + let p = this; + while (p = p.__parent) { + let parentTreeTags = p.__treeTags.get(tag); + parentTreeTags.delete(this); + + if (p.__tagRoot) { + break; + } + } + }); + } + + let tags = null; + let n = 0; + if (this.__treeTags) { + if (!this.__tagRoot) { + tags = Utils.iteratorToArray(this.__treeTags.keys()); + n = tags.length; + + if (n > 0) { + for (let i = 0; i < n; i++) { + let tagSet = this.__treeTags.get(tags[i]); + + // Remove from treeTags. + let p = this; + while ((p = p.__parent)) { + let parentTreeTags = p.__treeTags.get(tags[i]); + + tagSet.forEach(function (comp) { + parentTreeTags.delete(comp); + }); + + if (p.__tagRoot) { + break; + } + } + } + } + } + } + }; + + _setTagsParent() { + // Just copy over the 'local' tags. + if (this.__tags) { + this.__tags.forEach((tag) => { + let p = this; + while (p = p.__parent) { + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + s.add(this); + + if (p.__tagRoot) { + break; + } + } + }); + } + + if (this.__treeTags && this.__treeTags.size) { + if (!this.__tagRoot) { + this.__treeTags.forEach((tagSet, tag) => { + let p = this; + while (!p.__tagRoot && (p = p.__parent)) { + if (p.__tagRoot) ; + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + tagSet.forEach(function (comp) { + s.add(comp); + }); + } + }); + } + } + }; + + + _getByTag(tag) { + if (!this.__treeTags) { + return []; + } + let t = this.__treeTags.get(tag); + return t ? Utils.setToArray(t) : []; + }; + + getTags() { + return this.__tags ? this.__tags : []; + }; + + setTags(tags) { + tags = tags.reduce((acc, tag) => { + return acc.concat(tag.split(' ')); + }, []); + + if (this.__ref) { + tags.push(this.__ref); + } + + let i, n = tags.length; + let removes = []; + let adds = []; + for (i = 0; i < n; i++) { + if (!this.hasTag(tags[i])) { + adds.push(tags[i]); + } + } + + let currentTags = this.tags || []; + n = currentTags.length; + for (i = 0; i < n; i++) { + if (tags.indexOf(currentTags[i]) == -1) { + removes.push(currentTags[i]); + } + } + + for (i = 0; i < removes.length; i++) { + this.removeTag(removes[i]); + } + + for (i = 0; i < adds.length; i++) { + this.addTag(adds[i]); + } + } + + addTag(tag) { + if (tag.indexOf(' ') === -1) { + if (Utils.isUcChar(tag.charCodeAt(0))) { + this._throwError("Tag may not start with an upper case character."); + } + + this._addTag(tag); + } else { + const tags = tag.split(' '); + for (let i = 0, m = tags.length; i < m; i++) { + const tag = tags[i]; + + if (Utils.isUcChar(tag.charCodeAt(0))) { + this._throwError("Tag may not start with an upper case character."); + } + + this._addTag(tag); + } + } + } + + _addTag(tag) { + if (!this.__tags) { + this.__tags = []; + } + if (this.__tags.indexOf(tag) === -1) { + this.__tags.push(tag); + + // Add to treeTags hierarchy. + let p = this.__parent; + if (p) { + do { + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + s.add(this); + + } while (!p.__tagRoot && (p = p.__parent)); + } + } + } + + removeTag(tag) { + let i = this.__tags.indexOf(tag); + if (i !== -1) { + this.__tags.splice(i, 1); + + // Remove from treeTags hierarchy. + let p = this.__parent; + if (p) { + do { + let list = p.__treeTags.get(tag); + if (list) { + list.delete(this); + } + } while (!p.__tagRoot && (p = p.__parent)); + } + } + } + + hasTag(tag) { + return (this.__tags && (this.__tags.indexOf(tag) !== -1)); + } + + /** + * Returns one of the elements from the subtree that have this tag. + * @param {string} tag + * @returns {Element} + */ + _tag(tag) { + if (tag.indexOf(".") !== -1) { + return this.mtag(tag)[0]; + } else { + if (this.__treeTags) { + let t = this.__treeTags.get(tag); + if (t) { + const item = t.values().next(); + return item ? item.value : undefined; + } + } + } + }; + + get tag() { + return this._tag; + } + + set tag(t) { + this.tags = t; + } + + /** + * Returns all elements from the subtree that have this tag. + * @param {string} tag + * @returns {Element[]} + */ + mtag(tag) { + let idx = tag.indexOf("."); + if (idx >= 0) { + let parts = tag.split('.'); + let res = this._getByTag(parts[0]); + let level = 1; + let c = parts.length; + while (res.length && level < c) { + let resn = []; + for (let j = 0, n = res.length; j < n; j++) { + resn = resn.concat(res[j]._getByTag(parts[level])); + } + + res = resn; + level++; + } + return res; + } else { + return this._getByTag(tag); + } + }; + + stag(tag, settings) { + let t = this.mtag(tag); + let n = t.length; + for (let i = 0; i < n; i++) { + Base.patchObject(t[i], settings); + } + } + + get tagRoot() { + return this.__tagRoot; + } + + set tagRoot(v) { + if (this.__tagRoot !== v) { + if (!v) { + this._setTagsParent(); + } else { + this._unsetTagsParent(); + } + + this.__tagRoot = v; + } + } + + sel(path) { + const results = this.select(path); + if (results.length) { + return results[0]; + } else { + return undefined; + } + } + + select(path) { + if (path.indexOf(",") !== -1) { + let selectors = path.split(','); + let res = []; + for (let i = 0; i < selectors.length; i++) { + res = res.concat(this._select(selectors[i])); + } + return res; + } else { + return this._select(path); + } + } + + _select(path) { + if (path === "") return [this]; + + + let pointIdx = path.indexOf("."); + let arrowIdx = path.indexOf(">"); + if (pointIdx === -1 && arrowIdx === -1) { + // Quick case. + return this.mtag(path); + } + + // Detect by first char. + let isRef; + if (arrowIdx === 0) { + isRef = true; + path = path.substr(1); + } else if (pointIdx === 0) { + isRef = false; + path = path.substr(1); + } else { + isRef = false; + } + + return this._selectChilds(path, isRef); + } + + _selectChilds(path, isRef) { + const pointIdx = path.indexOf("."); + const arrowIdx = path.indexOf(">"); + + if (pointIdx === -1 && arrowIdx === -1) { + if (isRef) { + const ref = this.getByRef(path); + return ref ? [ref] : []; + } else { + return this.mtag(path); + } + } + + if ((arrowIdx === -1) || (pointIdx !== -1 && pointIdx < arrowIdx)) { + let next; + const str = path.substr(0, pointIdx); + if (isRef) { + const ref = this.getByRef(str); + next = ref ? [ref] : []; + } else { + next = this.mtag(str); + } + let total = []; + const subPath = path.substr(pointIdx + 1); + for (let i = 0, n = next.length; i < n; i++) { + total = total.concat(next[i]._selectChilds(subPath, false)); + } + return total; + } else { + let next; + const str = path.substr(0, arrowIdx); + if (isRef) { + const ref = this.getByRef(str); + next = ref ? [ref] : []; + } else { + next = this.mtag(str); + } + let total = []; + const subPath = path.substr(arrowIdx + 1); + for (let i = 0, n = next.length; i < n; i++) { + total = total.concat(next[i]._selectChilds(subPath, true)); + } + return total; + } + } + + getByRef(ref) { + return this.childList.getByRef(ref); + } + + getLocationString() { + let i; + i = this.__parent ? this.__parent._children.getIndex(this) : "R"; + let localTags = this.getTags(); + let str = this.__parent ? this.__parent.getLocationString(): ""; + if (this.ref) { + str += ":[" + i + "]" + this.ref; + } else if (localTags.length) { + str += ":[" + i + "]" + localTags.join(","); + } else { + str += ":[" + i + "]#" + this.id; + } + return str; + } + + toString() { + let obj = this.getSettings(); + return Element.getPrettyString(obj, ""); + }; + + static getPrettyString(obj, indent) { + let children = obj.children; + delete obj.children; + + + // Convert singular json settings object. + let colorKeys = ["color", "colorUl", "colorUr", "colorBl", "colorBr"]; + let str = JSON.stringify(obj, function (k, v) { + if (colorKeys.indexOf(k) !== -1) { + return "COLOR[" + v.toString(16) + "]"; + } + return v; + }); + str = str.replace(/"COLOR\[([a-f0-9]{1,8})\]"/g, "0x$1"); + + if (children) { + let childStr = ""; + if (Utils.isObjectLiteral(children)) { + let refs = Object.keys(children); + childStr = ""; + for (let i = 0, n = refs.length; i < n; i++) { + childStr += `\n${indent} "${refs[i]}":`; + delete children[refs[i]].ref; + childStr += Element.getPrettyString(children[refs[i]], indent + " ") + (i < n - 1 ? "," : ""); + } + let isEmpty = (str === "{}"); + str = str.substr(0, str.length - 1) + (isEmpty ? "" : ",") + childStr + "\n" + indent + "}"; + } else { + let n = children.length; + childStr = "["; + for (let i = 0; i < n; i++) { + childStr += Element.getPrettyString(children[i], indent + " ") + (i < n - 1 ? "," : "") + "\n"; + } + childStr += indent + "]}"; + let isEmpty = (str === "{}"); + str = str.substr(0, str.length - 1) + (isEmpty ? "" : ",") + "\"children\":\n" + indent + childStr + "}"; + } + + } + + return str; + } + + getSettings() { + let settings = this.getNonDefaults(); + + let children = this._children.get(); + if (children) { + let n = children.length; + if (n) { + const childArray = []; + let missing = false; + for (let i = 0; i < n; i++) { + childArray.push(children[i].getSettings()); + missing = missing || !children[i].ref; + } + + if (!missing) { + settings.children = {}; + childArray.forEach(child => { + settings.children[child.ref] = child; + }); + } else { + settings.children = childArray; + } + } + } + + settings.id = this.id; + + return settings; + } + + getNonDefaults() { + let settings = {}; + + if (this.constructor !== Element) { + settings.type = this.constructor.name; + } + + if (this.__ref) { + settings.ref = this.__ref; + } + + if (this.__tags && this.__tags.length) { + settings.tags = this.__tags; + } + + if (this.x !== 0) settings.x = this.x; + if (this.y !== 0) settings.y = this.y; + if (this.w !== 0) settings.w = this.w; + if (this.h !== 0) settings.h = this.h; + + if (this.scaleX === this.scaleY) { + if (this.scaleX !== 1) settings.scale = this.scaleX; + } else { + if (this.scaleX !== 1) settings.scaleX = this.scaleX; + if (this.scaleY !== 1) settings.scaleY = this.scaleY; + } + + if (this.pivotX === this.pivotY) { + if (this.pivotX !== 0.5) settings.pivot = this.pivotX; + } else { + if (this.pivotX !== 0.5) settings.pivotX = this.pivotX; + if (this.pivotY !== 0.5) settings.pivotY = this.pivotY; + } + + if (this.mountX === this.mountY) { + if (this.mountX !== 0) settings.mount = this.mountX; + } else { + if (this.mountX !== 0) settings.mountX = this.mountX; + if (this.mountY !== 0) settings.mountY = this.mountY; + } + + if (this.alpha !== 1) settings.alpha = this.alpha; + + if (!this.visible) settings.visible = false; + + if (this.rotation !== 0) settings.rotation = this.rotation; + + if (this.colorUl === this.colorUr && this.colorBl === this.colorBr && this.colorUl === this.colorBl) { + if (this.colorUl !== 0xFFFFFFFF) settings.color = this.colorUl.toString(16); + } else { + if (this.colorUl !== 0xFFFFFFFF) settings.colorUl = this.colorUl.toString(16); + if (this.colorUr !== 0xFFFFFFFF) settings.colorUr = this.colorUr.toString(16); + if (this.colorBl !== 0xFFFFFFFF) settings.colorBl = this.colorBl.toString(16); + if (this.colorBr !== 0xFFFFFFFF) settings.colorBr = this.colorBr.toString(16); + } + + if (this.zIndex) settings.zIndex = this.zIndex; + + if (this.forceZIndexContext) settings.forceZIndexContext = true; + + if (this.clipping) settings.clipping = this.clipping; + + if (!this.clipbox) settings.clipbox = this.clipbox; + + if (this.__texture) { + let tnd = this.__texture.getNonDefaults(); + if (Object.keys(tnd).length) { + settings.texture = tnd; + } + } + + if (this.shader) { + let tnd = this.shader.getNonDefaults(); + if (Object.keys(tnd).length) { + settings.shader = tnd; + } + } + + if (this._hasTexturizer()) { + if (this.texturizer.enabled) { + settings.renderToTexture = this.texturizer.enabled; + } + if (this.texturizer.lazy) { + settings.renderToTextureLazy = this.texturizer.lazy; + } + if (this.texturizer.colorize) { + settings.colorizeResultTexture = this.texturizer.colorize; + } + if (this.texturizer.renderOffscreen) { + settings.renderOffscreen = this.texturizer.renderOffscreen; + } + } + + return settings; + }; + + static getGetter(propertyPath) { + let getter = Element.PROP_GETTERS.get(propertyPath); + if (!getter) { + getter = new Function('obj', 'return obj.' + propertyPath); + Element.PROP_GETTERS.set(propertyPath, getter); + } + return getter; + } + + static getSetter(propertyPath) { + let setter = Element.PROP_SETTERS.get(propertyPath); + if (!setter) { + setter = new Function('obj', 'v', 'obj.' + propertyPath + ' = v'); + Element.PROP_SETTERS.set(propertyPath, setter); + } + return setter; + } + + get withinBoundsMargin() { + return this.__core._withinBoundsMargin; + } + + _enableWithinBoundsMargin() { + // Iff enabled, this toggles the active flag. + if (this.__enabled) { + this._setActiveFlag(); + } + } + + _disableWithinBoundsMargin() { + // Iff active, this toggles the active flag. + if (this.__active) { + this._unsetActiveFlag(); + } + } + + set boundsMargin(v) { + if (!Array.isArray(v) && v !== null) { + throw new Error("boundsMargin should be an array of left-top-right-bottom values or null (inherit margin)"); + } + this.__core.boundsMargin = v; + } + + get boundsMargin() { + return this.__core.boundsMargin; + } + + get x() { + return this.__core.offsetX; + } + + set x(v) { + this.__core.offsetX = v; + } + + get y() { + return this.__core.offsetY; + } + + set y(v) { + this.__core.offsetY = v; + } + + get w() { + return this._w; + } + + set w(v) { + if (Utils.isFunction(v)) { + this._w = 0; + this.__core.funcW = v; + } else { + v = Math.max(v, 0); + if (this._w !== v) { + this.__core.disableFuncW(); + this._w = v; + this._updateDimensions(); + } + } + } + + get h() { + return this._h; + } + + set h(v) { + if (Utils.isFunction(v)) { + this._h = 0; + this.__core.funcH = v; + } else { + v = Math.max(v, 0); + if (this._h !== v) { + this.__core.disableFuncH(); + this._h = v; + this._updateDimensions(); + } + } + } + + get scaleX() { + return this.__core.scaleX; + } + + set scaleX(v) { + this.__core.scaleX = v; + } + + get scaleY() { + return this.__core.scaleY; + } + + set scaleY(v) { + this.__core.scaleY = v; + } + + get scale() { + return this.__core.scale; + } + + set scale(v) { + this.__core.scale = v; + } + + get pivotX() { + return this.__core.pivotX; + } + + set pivotX(v) { + this.__core.pivotX = v; + } + + get pivotY() { + return this.__core.pivotY; + } + + set pivotY(v) { + this.__core.pivotY = v; + } + + get pivot() { + return this.__core.pivot; + } + + set pivot(v) { + this.__core.pivot = v; + } + + get mountX() { + return this.__core.mountX; + } + + set mountX(v) { + this.__core.mountX = v; + } + + get mountY() { + return this.__core.mountY; + } + + set mountY(v) { + this.__core.mountY = v; + } + + get mount() { + return this.__core.mount; + } + + set mount(v) { + this.__core.mount = v; + } + + get rotation() { + return this.__core.rotation; + } + + set rotation(v) { + this.__core.rotation = v; + } + + get alpha() { + return this.__core.alpha; + } + + set alpha(v) { + this.__core.alpha = v; + } + + get visible() { + return this.__core.visible; + } + + set visible(v) { + this.__core.visible = v; + } + + get colorUl() { + return this.__core.colorUl; + } + + set colorUl(v) { + this.__core.colorUl = v; + } + + get colorUr() { + return this.__core.colorUr; + } + + set colorUr(v) { + this.__core.colorUr = v; + } + + get colorBl() { + return this.__core.colorBl; + } + + set colorBl(v) { + this.__core.colorBl = v; + } + + get colorBr() { + return this.__core.colorBr; + } + + set colorBr(v) { + this.__core.colorBr = v; + } + + get color() { + return this.__core.colorUl; + } + + set color(v) { + if (this.colorUl !== v || this.colorUr !== v || this.colorBl !== v || this.colorBr !== v) { + this.colorUl = v; + this.colorUr = v; + this.colorBl = v; + this.colorBr = v; + } + } + + get colorTop() { + return this.colorUl; + } + + set colorTop(v) { + if (this.colorUl !== v || this.colorUr !== v) { + this.colorUl = v; + this.colorUr = v; + } + } + + get colorBottom() { + return this.colorBl; + } + + set colorBottom(v) { + if (this.colorBl !== v || this.colorBr !== v) { + this.colorBl = v; + this.colorBr = v; + } + } + + get colorLeft() { + return this.colorUl; + } + + set colorLeft(v) { + if (this.colorUl !== v || this.colorBl !== v) { + this.colorUl = v; + this.colorBl = v; + } + } + + get colorRight() { + return this.colorUr; + } + + set colorRight(v) { + if (this.colorUr !== v || this.colorBr !== v) { + this.colorUr = v; + this.colorBr = v; + } + } + + get zIndex() {return this.__core.zIndex} + set zIndex(v) { + this.__core.zIndex = v; + } + + get forceZIndexContext() {return this.__core.forceZIndexContext} + set forceZIndexContext(v) { + this.__core.forceZIndexContext = v; + } + + get clipping() {return this.__core.clipping} + set clipping(v) { + this.__core.clipping = v; + } + + get clipbox() {return this.__core.clipbox} + set clipbox(v) { + this.__core.clipbox = v; + } + + get tags() { + return this.getTags(); + } + + set tags(v) { + if (!Array.isArray(v)) v = [v]; + this.setTags(v); + } + + set t(v) { + this.tags = v; + } + + get _children() { + if (!this.__childList) { + this.__childList = new ElementChildList(this, false); + } + return this.__childList; + } + + get childList() { + if (!this._allowChildrenAccess()) { + this._throwError("Direct access to children is not allowed in " + this.getLocationString()); + } + return this._children; + } + + hasChildren() { + return this._allowChildrenAccess() && this.__childList && (this.__childList.length > 0); + } + + _allowChildrenAccess() { + return true; + } + + get children() { + return this.childList.get(); + } + + set children(children) { + this.childList.patch(children); + } + + add(o) { + return this.childList.a(o); + } + + get p() { + return this.__parent; + } + + get parent() { + return this.__parent; + } + + get src() { + if (this.texture && this.texture instanceof ImageTexture) { + return this.texture._src; + } else { + return undefined; + } + } + + set src(v) { + const texture = new ImageTexture(this.stage); + texture.src = v; + this.texture = texture; + } + + set mw(v) { + if (this.texture) { + this.texture.mw = v; + this._updateDimensions(); + } else { + this._throwError('Please set mw after setting a texture.'); + } + } + + set mh(v) { + if (this.texture) { + this.texture.mh = v; + this._updateDimensions(); + } else { + this._throwError('Please set mh after setting a texture.'); + } + } + + get rect() { + return (this.texture === this.stage.rectangleTexture); + } + + set rect(v) { + if (v) { + this.texture = this.stage.rectangleTexture; + } else { + this.texture = null; + } + } + + enableTextTexture() { + if (!this.texture || !(this.texture instanceof TextTexture)) { + this.texture = new TextTexture(this.stage); + + if (!this.texture.w && !this.texture.h) { + // Inherit dimensions from element. + // This allows userland to set dimensions of the Element and then later specify the text. + this.texture.w = this.w; + this.texture.h = this.h; + } + } + return this.texture; + } + + get text() { + if (this.texture && (this.texture instanceof TextTexture)) { + return this.texture; + } else { + return null; + } + } + + set text(v) { + if (!this.texture || !(this.texture instanceof TextTexture)) { + this.enableTextTexture(); + } + if (Utils.isString(v)) { + this.texture.text = v; + } else { + this.texture.patch(v); + } + } + + set onUpdate(f) { + this.__core.onUpdate = f; + } + + set onAfterCalcs(f) { + this.__core.onAfterCalcs = f; + } + + set onAfterUpdate(f) { + this.__core.onAfterUpdate = f; + } + + forceUpdate() { + // Make sure that the update loop is run. + this.__core._setHasUpdates(); + } + + get shader() { + return this.__core.shader; + } + + set shader(v) { + if (Utils.isObjectLiteral(v) && !v.type) { + // Setting properties on an existing shader. + if (this.shader) { + this.shader.patch(v); + } + } else { + const shader = Shader.create(this.stage, v); + + if (this.__enabled && this.__core.shader) { + this.__core.shader.removeElement(this.__core); + } + + this.__core.shader = shader; + + if (this.__enabled && this.__core.shader) { + this.__core.shader.addElement(this.__core); + } + } + } + + _hasTexturizer() { + return !!this.__core._texturizer; + } + + get renderToTexture() { + return this.rtt + } + + set renderToTexture(v) { + this.rtt = v; + } + + get rtt() { + return this._hasTexturizer() && this.texturizer.enabled; + } + + set rtt(v) { + this.texturizer.enabled = v; + } + + get rttLazy() { + return this._hasTexturizer() && this.texturizer.lazy; + } + + set rttLazy(v) { + this.texturizer.lazy = v; + } + + get renderOffscreen() { + return this._hasTexturizer() && this.texturizer.renderOffscreen; + } + + set renderOffscreen(v) { + this.texturizer.renderOffscreen = v; + } + + get colorizeResultTexture() { + return this._hasTexturizer() && this.texturizer.colorize; + } + + set colorizeResultTexture(v) { + this.texturizer.colorize = v; + } + + getTexture() { + return this.texturizer._getTextureSource(); + } + + get texturizer() { + return this.__core.texturizer; + } + + patch(settings) { + let paths = Object.keys(settings); + + for (let i = 0, n = paths.length; i < n; i++) { + let path = paths[i]; + const v = settings[path]; + + const firstCharCode = path.charCodeAt(0); + if (Utils.isUcChar(firstCharCode)) { + // Ref. + const child = this.getByRef(path); + if (!child) { + if (v !== undefined) { + // Add to list immediately. + let c; + if (Utils.isObjectLiteral(v)) { + // Catch this case to capture createMode flag. + c = this.childList.createItem(v); + c.patch(v); + } else if (Utils.isObject(v)) { + c = v; + } + if (c.isElement) { + c.ref = path; + } + + this.childList.a(c); + } + } else { + if (v === undefined) { + if (child.parent) { + child.parent.childList.remove(child); + } + } else if (Utils.isObjectLiteral(v)) { + child.patch(v); + } else if (v.isElement) { + // Replace element by new element. + v.ref = path; + this.childList.replace(v, child); + } else { + this._throwError("Unexpected value for path: " + path); + } + } + } else { + // Property. + Base.patchObjectProperty(this, path, v); + } + } + } + + _throwError(message) { + throw new Error(this.constructor.name + " (" + this.getLocationString() + "): " + message); + } + + animation(settings) { + return this.stage.animations.createAnimation(this, settings); + } + + transition(property, settings = null) { + if (settings === null) { + return this._getTransition(property); + } else { + this._setTransition(property, settings); + // We do not create/return the transition, because it would undo the 'lazy transition creation' optimization. + return null; + } + } + + set transitions(object) { + let keys = Object.keys(object); + keys.forEach(property => { + this.transition(property, object[property]); + }); + } + + set smooth(object) { + let keys = Object.keys(object); + keys.forEach(property => { + let value = object[property]; + if (Array.isArray(value)) { + this.setSmooth(property, value[0], value[1]); + } else { + this.setSmooth(property, value); + } + }); + } + + fastForward(property) { + if (this._transitions) { + let t = this._transitions[property]; + if (t && t.isTransition) { + t.finish(); + } + } + } + + _getTransition(property) { + if (!this._transitions) { + this._transitions = {}; + } + let t = this._transitions[property]; + if (!t) { + // Create default transition. + t = new Transition(this.stage.transitions, this.stage.transitions.defaultTransitionSettings, this, property); + } else if (t.isTransitionSettings) { + // Upgrade to 'real' transition. + t = new Transition( + this.stage.transitions, + t, + this, + property + ); + } + this._transitions[property] = t; + return t; + } + + _setTransition(property, settings) { + if (!settings) { + this._removeTransition(property); + } else { + if (Utils.isObjectLiteral(settings)) { + // Convert plain object to proper settings object. + settings = this.stage.transitions.createSettings(settings); + } + + if (!this._transitions) { + this._transitions = {}; + } + + let current = this._transitions[property]; + if (current && current.isTransition) { + // Runtime settings change. + current.settings = settings; + return current; + } else { + // Initially, only set the settings and upgrade to a 'real' transition when it is used. + this._transitions[property] = settings; + } + } + } + + _removeTransition(property) { + if (this._transitions) { + delete this._transitions[property]; + } + } + + getSmooth(property, v) { + let t = this._getTransition(property); + if (t && t.isAttached()) { + return t.targetValue; + } else { + return v; + } + } + + setSmooth(property, v, settings) { + if (settings) { + this._setTransition(property, settings); + } + let t = this._getTransition(property); + t.start(v); + return t; + } + + get flex() { + return this.__core.flex; + } + + set flex(v) { + this.__core.flex = v; + } + + get flexItem() { + return this.__core.flexItem; + } + + set flexItem(v) { + this.__core.flexItem = v; + } + + static isColorProperty(property) { + return property.toLowerCase().indexOf("color") >= 0; + } + + static getMerger(property) { + if (Element.isColorProperty(property)) { + return StageUtils.mergeColors; + } else { + return StageUtils.mergeNumbers; + } + } +} + +// This gives a slight performance benefit compared to extending EventEmitter. +EventEmitter.addAsMixin(Element); + +Element.prototype.isElement = 1; + +Element.id = 1; + +// Getters reused when referencing element (subobject) properties by a property path, as used in a transition or animation ('x', 'texture.x', etc). +Element.PROP_GETTERS = new Map(); + +// Setters reused when referencing element (subobject) properties by a property path, as used in a transition or animation ('x', 'texture.x', etc). +Element.PROP_SETTERS = new Map(); + +class StateMachine { + + constructor() { + StateMachine.setupStateMachine(this); + } + + static setupStateMachine(target) { + const targetConstructor = target.constructor; + const router = StateMachine.create(targetConstructor); + Object.setPrototypeOf(target, router.prototype); + target.constructor = targetConstructor; + target._initStateMachine(); + } + + /** + * Creates a state machine implementation. + * It extends the original type and should be used when creating new instances. + * The original type is available as static property 'original', and it must be used when subclassing as follows: + * const type = StateMachine.create(class YourNewStateMachineClass extends YourBaseStateMachineClass.original { }) + * @param {Class} type + * @returns {StateMachine} + */ + static create(type) { + if (!type.hasOwnProperty('_sm')) { + // Only need to run once. + const stateMachineType = new StateMachineType(type); + type._sm = stateMachineType; + } + + return type._sm.router; + } + + /** + * Calls the specified method if it exists. + * @param {string} event + * @param {*...} args + */ + fire(event, ...args) { + if (this._hasMethod(event)) { + return this[event](...args); + } + } + + /** + * Returns the current state path (for example "Initialized.Loading"). + * @returns {string} + * @protected + */ + _getState() { + return this._state.__path; + } + + /** + * Returns true iff statePath is (an ancestor of) currentStatePath. + * @param {string} statePath + * @param {string} currentStatePath + * @returns {Boolean} + * @protected + */ + _inState(statePath, currentStatePath = this._state.__path) { + const state = this._sm.getStateByPath(statePath); + const currentState = this._sm.getStateByPath(currentStatePath); + const level = state.__level; + const stateAtLevel = StateMachine._getStateAtLevel(currentState, level); + return (stateAtLevel === state); + } + + /** + * Returns true if the specified class member is defined for the currently set state. + * @param {string} name + * @returns {boolean} + * @protected + */ + _hasMember(name) { + return !!this.constructor.prototype[name]; + } + + /** + * Returns true if the specified class member is a method for the currently set state. + * @param {string} name + * @returns {boolean} + * @protected + */ + _hasMethod(name) { + const member = this.constructor.prototype[name]; + return !!member && (typeof member === "function") + } + + /** + * Switches to the specified state. + * @param {string} statePath + * Substates are seperated by a underscores (for example "Initialized.Loading"). + * @param {*[]} [args] + * Args that are supplied in $enter and $exit events. + * @protected + */ + _setState(statePath, args) { + const setStateId = ++this._setStateCounter; + this._setStateId = setStateId; + + if (this._state.__path !== statePath) { + // Performance optimization. + let newState = this._sm._stateMap[statePath]; + if (!newState) { + // Check for super state. + newState = this._sm.getStateByPath(statePath); + } + + const prevState = this._state; + + const hasDifferentEnterMethod = (newState.prototype.$enter !== this._state.prototype.$enter); + const hasDifferentExitMethod = (newState.prototype.$exit !== this._state.prototype.$exit); + if (hasDifferentEnterMethod || hasDifferentExitMethod) { + const sharedState = StateMachine._getSharedState(this._state, newState); + const context = { + newState: newState.__path, + prevState: prevState.__path, + sharedState: sharedState.__path + }; + const sharedLevel = sharedState.__level; + + if (hasDifferentExitMethod) { + const exitStates = StateMachine._getStatesUntilLevel(this._state, sharedLevel); + for (let i = 0, n = exitStates.length; i < n; i++) { + this.__setState(exitStates[i]); + this._callExit(this._state, args, context); + const stateChangeOverridden = (this._setStateId !== setStateId); + if (stateChangeOverridden) { + return; + } + } + } + + if (hasDifferentEnterMethod) { + const enterStates = StateMachine._getStatesUntilLevel(newState, sharedLevel).reverse(); + for (let i = 0, n = enterStates.length; i < n; i++) { + this.__setState(enterStates[i]); + this._callEnter(this._state, args, context); + const stateChangeOverridden = (this._setStateId !== setStateId); + if (stateChangeOverridden) { + return; + } + } + } + + } + + this.__setState(newState); + + if (this._changedState) { + const context = { + newState: newState.__path, + prevState: prevState.__path + }; + + if (args) { + this._changedState(context, ...args); + } else { + this._changedState(context); + } + } + + if (this._onStateChange) { + const context = { + newState: newState.__path, + prevState: prevState.__path + }; + this._onStateChange(context); + } + + } + } + + _callEnter(state, args = [], context) { + const hasParent = !!state.__parent; + if (state.prototype.$enter) { + if (!hasParent || (state.__parent.prototype.$enter !== state.prototype.$enter)) { + state.prototype.$enter.apply(this, [context, ...args]); + } + } + } + + _callExit(state, args = [], context) { + const hasParent = !!state.__parent; + if (state.prototype.$exit) { + if (!hasParent || (state.__parent.prototype.$exit !== state.prototype.$exit)) { + state.prototype.$exit.apply(this, [context, ...args]); + } + } + } + + __setState(state) { + this._state = state; + this._stateIndex = state.__index; + this.constructor = state; + } + + _initStateMachine() { + this._state = null; + this._stateIndex = 0; + this._setStateCounter = 0; + this._sm = this._routedType._sm; + this.__setState(this._sm.getStateByPath("")); + const context = {newState: "", prevState: undefined, sharedState: undefined}; + this._callEnter(this._state, [], context); + this._onStateChange = undefined; + } + + /** + * Between multiple member names, select the one specified in the deepest state. + * If multiple member names are specified in the same deepest state, the first one in the array is returned. + * @param {string[]} memberNames + * @returns {string|undefined} + * @protected + */ + _getMostSpecificHandledMember(memberNames) { + let cur = this._state; + do { + for (let i = 0, n = memberNames.length; i < n; i++) { + const memberName = memberNames[i]; + if (!cur.__parent) { + if (cur.prototype[memberName]) { + return memberName; + } + } else { + const alias = StateMachineType.getStateMemberAlias(cur.__path, memberName); + if (this[alias]) { + return memberName; + } + } + } + cur = cur.__parent; + } while (cur); + } + + static _getStatesUntilLevel(state, level) { + const states = []; + while (state.__level > level) { + states.push(state); + state = state.__parent; + } + return states; + } + + static _getSharedState(state1, state2) { + const state1Array = StateMachine._getAncestorStates(state1); + const state2Array = StateMachine._getAncestorStates(state2); + const n = Math.min(state1Array.length, state2Array.length); + for (let i = 0; i < n; i++) { + if (state1Array[i] !== state2Array[i]) { + return state1Array[i - 1]; + } + } + return state1Array[n - 1]; + } + + static _getAncestorStates(state) { + const result = []; + do { + result.push(state); + } while(state = state.__parent); + return result.reverse(); + } + + static _getStateAtLevel(state, level) { + if (level > state.__level) { + return undefined; + } + + while(level < state.__level) { + state = state.__parent; + } + return state; + } +} + +class StateMachineType { + + constructor(type) { + this._type = type; + this._router = null; + + this.init(); + } + + get router() { + return this._router; + } + + init() { + this._router = this._createRouter(); + + this._stateMap = this._getStateMap(); + + this._addStateMemberDelegatorsToRouter(); + + } + + _createRouter() { + const type = this._type; + + const router = class StateMachineRouter extends type { + constructor() { + super(...arguments); + if (!this.constructor.hasOwnProperty('_isRouter')) { + throw new Error(`You need to extend ${type.name}.original instead of ${type.name}.`); + } + } + }; + router._isRouter = true; + router.prototype._routedType = type; + router.original = type; + + this._mixinStateMachineMethods(router); + + return router; + } + + _mixinStateMachineMethods(router) { + // Mixin the state machine methods, so that we reuse the methods instead of re-creating them. + const names = Object.getOwnPropertyNames(StateMachine.prototype); + for (let i = 0, n = names.length; i < n; i++) { + const name = names[i]; + if (name !== "constructor") { + const descriptor = Object.getOwnPropertyDescriptor(StateMachine.prototype, name); + Object.defineProperty(router.prototype, name, descriptor); + } + } + } + + _addStateMemberDelegatorsToRouter() { + const members = this._getAllMemberNames(); + + members.forEach(member => { + this._addMemberRouter(member); + }); + } + + /** + * @note We are generating code because it yields much better performance. + */ + _addMemberRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + let type = undefined; + descriptors.forEach(descriptor => { + if (descriptor) { + const descType = this._getDescriptorType(descriptor); + if (type && (type !== descType)) { + console.warn(`Member ${member} in ${this._type.name} has inconsistent types.`); + return; + } + type = descType; + } + }); + + switch(type) { + case "method": + this._addMethodRouter(member, descriptors, aliases); + break; + case "getter": + this._addGetterSetterRouters(member); + break; + case "property": + console.warn("Fixed properties are not supported; please use a getter instead!"); + break; + } + } + + _getDescriptor(state, member, isValid = () => true) { + let type = state; + let curState = state; + + do { + const descriptor = Object.getOwnPropertyDescriptor(type.prototype, member); + if (descriptor) { + if (isValid(descriptor)) { + descriptor._source = curState; + return descriptor; + } + } + type = Object.getPrototypeOf(type); + if (type && type.hasOwnProperty('__state')) { + curState = type; + } + } while(type && type.prototype); + return undefined; + } + + _getDescriptorType(descriptor) { + if (descriptor.get || descriptor.set) { + return 'getter'; + } else { + if (typeof descriptor.value === "function") { + return 'method'; + } else { + return 'property'; + } + } + } + + static _supportsSpread() { + if (this.__supportsSpread === undefined) { + this.__supportsSpread = false; + try { + const func = new Function("return [].concat(...arguments);"); + func(); + this.__supportsSpread = true; + } catch(e) {} + } + return this.__supportsSpread; + } + + _addMethodRouter(member, descriptors, aliases) { + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + const supportsSpread = StateMachineType._supportsSpread(); + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + if (supportsSpread) { + code.push(`if (i < ${i}) return this["${cur}"](...arguments); else`); + } else { + code.push(`if (i < ${i}) return this["${cur}"].apply(this, arguments); else`); + } + } else { + code.push(`if (i < ${i}) return ; else`); + } + } + cur = alias; + } + if (cur) { + if (supportsSpread) { + code.push(`return this["${cur}"](...arguments);`); + } else { + code.push(`return this["${cur}"].apply(this, arguments);`); + } + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function([], functionBody); + + const descriptor = {value: router}; + Object.defineProperty(this._router.prototype, member, descriptor); + } + + _addGetterSetterRouters(member) { + const getter = this._getGetterRouter(member); + const setter = this._getSetterRouter(member); + const descriptor = { + get: getter, + set: setter + }; + Object.defineProperty(this._router.prototype, member, descriptor); + } + + _getGetterRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member, (descriptor => descriptor.get)); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + code.push(`if (i < ${i}) return this["${cur}"]; else`); + } else { + code.push(`if (i < ${i}) return ; else`); + } + } + cur = alias; + } + if (cur) { + code.push(`return this["${cur}"];`); + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function([], functionBody); + return router; + } + + _getSetterRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member, (descriptor => descriptor.set)); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + code.push(`if (i < ${i}) this["${cur}"] = arg; else`); + } else { + code.push(`if (i < ${i}) ; else`); + } + } + cur = alias; + } + if (cur) { + code.push(`this["${cur}"] = arg;`); + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function(["arg"], functionBody); + return router; + } + + static getStateMemberAlias(path, member) { + return "$" + (path ? path + "." : "") + member; + } + + _getAllMemberNames() { + const stateMap = this._stateMap; + const map = Object.keys(stateMap); + let members = new Set(); + map.forEach(statePath => { + if (statePath === "") { + // Root state can be skipped: if the method only occurs in the root state, we don't need to re-delegate it based on state. + return; + } + const state = stateMap[statePath]; + const names = this._getStateMemberNames(state); + names.forEach(name => { + members.add(name); + }); + }); + return [...members]; + } + + _getStateMemberNames(state) { + let type = state; + let members = new Set(); + const isRoot = this._type === state; + do { + const names = this._getStateMemberNamesForType(type); + names.forEach(name => { + members.add(name); + }); + + type = Object.getPrototypeOf(type); + } while(type && type.prototype && (!type.hasOwnProperty("__state") || isRoot)); + + return members; + } + + _getStateMemberNamesForType(type) { + const memberNames = Object.getOwnPropertyNames(type.prototype); + return memberNames.filter(memberName => { + return (memberName !== "constructor") && !StateMachineType._isStateLocalMember(memberName); + }); + } + + static _isStateLocalMember(memberName) { + return (memberName === "$enter") || (memberName === "$exit"); + } + + getStateByPath(statePath) { + if (this._stateMap[statePath]) { + return this._stateMap[statePath]; + } + + // Search for closest match. + const parts = statePath.split("."); + while(parts.pop()) { + const statePath = parts.join("."); + if (this._stateMap[statePath]) { + return this._stateMap[statePath]; + } + } + } + + _getStateMap() { + if (!this._stateMap) { + this._stateMap = this._createStateMap(); + } + return this._stateMap; + } + + _createStateMap() { + const stateMap = {}; + this._addState(this._type, null, "", stateMap); + return stateMap; + } + + _addState(state, parentState, name, stateMap) { + state.__state = true; + state.__name = name; + + this._addStaticStateProperty(state, parentState); + + const parentPath = (parentState ? parentState.__path : ""); + let path = (parentPath ? parentPath + "." : "") + name; + state.__path = path; + state.__level = parentState ? parentState.__level + 1 : 0; + state.__parent = parentState; + state.__index = Object.keys(stateMap).length; + stateMap[path] = state; + + const states = state._states; + if (states) { + const isInheritedFromParent = (parentState && parentState._states === states); + if (!isInheritedFromParent) { + const subStates = state._states(); + subStates.forEach(subState => { + const stateName = StateMachineType._getStateName(subState); + this._addState(subState, state, stateName, stateMap); + }); + } + } + } + + static _getStateName(state) { + const name = state.name; + + const index = name.indexOf('$'); + if (index > 0) { + // Strip off rollup name suffix. + return name.substr(0, index); + } + + return name; + } + + _addStaticStateProperty(state, parentState) { + if (parentState) { + const isClassStateLevel = parentState && !parentState.__parent; + if (isClassStateLevel) { + this._router[state.__name] = state; + } else { + parentState[state.__name] = state; + } + } + } + +} + +/** + * @extends StateMachine + */ +class Component extends Element { + + constructor(stage, properties) { + super(stage); + + // Encapsulate tags to prevent leaking. + this.tagRoot = true; + + if (Utils.isObjectLiteral(properties)) { + Object.assign(this, properties); + } + + this.__initialized = false; + this.__firstActive = false; + this.__firstEnable = false; + + this.__signals = undefined; + + this.__passSignals = undefined; + + this.__construct(); + + // Quick-apply template. + const func = this.constructor.getTemplateFunc(); + func.f(this, func.a); + + this._build(); + } + + __start() { + StateMachine.setupStateMachine(this); + this._onStateChange = Component.prototype.__onStateChange; + } + + get state() { + return this._getState(); + } + + __onStateChange() { + /* FIXME: Workaround for case, where application was shut but component still lives */ + if (this.application) { + this.application.updateFocusPath(); + } + } + + _refocus() { + /* FIXME: Workaround for case, where application was shut but component still lives */ + if (this.application) { + this.application.updateFocusPath(); + } + } + + /** + * Returns a high-performance template patcher. + */ + static getTemplateFunc() { + // We need a different template function per patch id. + const name = "_templateFunc"; + + // Be careful with class-based static inheritance. + const hasName = '__has' + name; + if (this[hasName] !== this) { + this[hasName] = this; + this[name] = this.parseTemplate(this._template()); + } + return this[name]; + } + + static parseTemplate(obj) { + const context = { + loc: [], + store: [], + rid: 0 + }; + + this.parseTemplateRec(obj, context, "element"); + + const code = context.loc.join(";\n"); + const f = new Function("element", "store", code); + return {f:f, a:context.store} + } + + static parseTemplateRec(obj, context, cursor) { + const store = context.store; + const loc = context.loc; + const keys = Object.keys(obj); + keys.forEach(key => { + let value = obj[key]; + if (Utils.isUcChar(key.charCodeAt(0))) { + // Value must be expanded as well. + if (Utils.isObjectLiteral(value)) { + // Ref. + const childCursor = `r${key.replace(/[^a-z0-9]/gi, "") + context.rid}`; + let type = value.type ? value.type : Element; + if (type === Element) { + loc.push(`const ${childCursor} = element.stage.createElement()`); + } else { + store.push(type); + loc.push(`const ${childCursor} = new store[${store.length - 1}](${cursor}.stage)`); + } + loc.push(`${childCursor}.ref = "${key}"`); + context.rid++; + + // Enter sub. + this.parseTemplateRec(value, context, childCursor); + + loc.push(`${cursor}.childList.add(${childCursor})`); + } else if (Utils.isObject(value)) { + // Dynamic assignment. + store.push(value); + loc.push(`${cursor}.childList.add(store[${store.length - 1}])`); + } + } else { + if (key === "text") { + const propKey = cursor + "__text"; + loc.push(`const ${propKey} = ${cursor}.enableTextTexture()`); + this.parseTemplatePropRec(value, context, propKey); + } else if (key === "texture" && Utils.isObjectLiteral(value)) { + const propKey = cursor + "__texture"; + const type = value.type; + if (type) { + store.push(type); + loc.push(`const ${propKey} = new store[${store.length - 1}](${cursor}.stage)`); + this.parseTemplatePropRec(value, context, propKey); + loc.push(`${cursor}["${key}"] = ${propKey}`); + } else { + loc.push(`${propKey} = ${cursor}.texture`); + this.parseTemplatePropRec(value, context, propKey); + } + } else { + // Property; + if (Utils.isNumber(value)) { + loc.push(`${cursor}["${key}"] = ${value}`); + } else if (Utils.isBoolean(value)) { + loc.push(`${cursor}["${key}"] = ${value ? "true" : "false"}`); + } else if (Utils.isObject(value) || Array.isArray(value)) { + // Dynamic assignment. + // Because literal objects may contain dynamics, we store the full object. + store.push(value); + loc.push(`${cursor}["${key}"] = store[${store.length - 1}]`); + } else { + // String etc. + loc.push(`${cursor}["${key}"] = ${JSON.stringify(value)}`); + } + } + } + }); + } + + static parseTemplatePropRec(obj, context, cursor) { + const store = context.store; + const loc = context.loc; + const keys = Object.keys(obj); + keys.forEach(key => { + if (key !== "type") { + const value = obj[key]; + if (Utils.isNumber(value)) { + loc.push(`${cursor}["${key}"] = ${value}`); + } else if (Utils.isBoolean(value)) { + loc.push(`${cursor}["${key}"] = ${value ? "true" : "false"}`); + } else if (Utils.isObject(value) || Array.isArray(value)) { + // Dynamic assignment. + // Because literal objects may contain dynamics, we store the full object. + store.push(value); + loc.push(`${cursor}["${key}"] = store[${store.length - 1}]`); + } else { + // String etc. + loc.push(`${cursor}["${key}"] = ${JSON.stringify(value)}`); + } + } + }); + } + + _onSetup() { + if (!this.__initialized) { + this._setup(); + } + } + + _setup() { + } + + _onAttach() { + if (!this.__initialized) { + this.__init(); + this.__initialized = true; + } + + this._attach(); + } + + _attach() { + } + + _onDetach() { + this._detach(); + } + + _detach() { + } + + _onEnabled() { + if (!this.__firstEnable) { + this._firstEnable(); + this.__firstEnable = true; + } + + this._enable(); + } + + _firstEnable() { + } + + _enable() { + } + + _onDisabled() { + this._disable(); + } + + _disable() { + } + + _onActive() { + if (!this.__firstActive) { + this._firstActive(); + this.__firstActive = true; + } + + this._active(); + } + + _firstActive() { + } + + _active() { + } + + _onInactive() { + this._inactive(); + } + + _inactive() { + } + + get application() { + return this.stage.application; + } + + __construct() { + this._construct(); + } + + _construct() { + } + + _build() { + } + + __init() { + this._init(); + } + + _init() { + } + + _focus(newTarget, prevTarget) { + } + + _unfocus(newTarget) { + } + + _focusChange(target, newTarget) { + } + + _getFocused() { + // Override to delegate focus to child components. + return this; + } + + _setFocusSettings(settings) { + // Override to add custom settings. See Application._handleFocusSettings(). + } + + _handleFocusSettings(settings) { + // Override to react on custom settings. See Application._handleFocusSettings(). + } + + static _template() { + return {} + } + + hasFinalFocus() { + let path = this.application._focusPath; + return path && path.length && path[path.length - 1] === this; + } + + hasFocus() { + let path = this.application._focusPath; + return path && (path.indexOf(this) >= 0); + } + + get cparent() { + return Component.getParent(this); + } + + seekAncestorByType(type) { + let c = this.cparent; + while(c) { + if (c.constructor === type) { + return c; + } + c = c.cparent; + } + } + + getSharedAncestorComponent(element) { + let ancestor = this.getSharedAncestor(element); + while(ancestor && !ancestor.isComponent) { + ancestor = ancestor.parent; + } + return ancestor; + } + + get signals() { + return this.__signals; + } + + set signals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + this.__signals = v; + } + + set alterSignals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + if (!this.__signals) { + this.__signals = {}; + } + for (let key in v) { + const d = v[key]; + if (d === undefined) { + delete this.__signals[key]; + } else { + this.__signals[key] = v; + } + } + } + + get passSignals() { + return this.__passSignals || {}; + } + + set passSignals(v) { + this.__passSignals = Object.assign(this.__passSignals || {}, v); + } + + set alterPassSignals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + if (!this.__passSignals) { + this.__passSignals = {}; + } + for (let key in v) { + const d = v[key]; + if (d === undefined) { + delete this.__passSignals[key]; + } else { + this.__passSignals[key] = v; + } + } + } + + /** + * Signals the parent of the specified event. + * A parent/ancestor that wishes to handle the signal should set the 'signals' property on this component. + * @param {string} event + * @param {...*} args + */ + signal(event, ...args) { + return this._signal(event, args); + } + + _signal(event, args) { + const signalParent = this._getParentSignalHandler(); + if (signalParent) { + if (this.__signals) { + let fireEvent = this.__signals[event]; + if (fireEvent === false) { + // Ignore event. + return; + } + if (fireEvent) { + if (fireEvent === true) { + fireEvent = event; + } + + if (signalParent._hasMethod(fireEvent)) { + return signalParent[fireEvent](...args); + } + } + } + + let passSignal = (this.__passSignals && this.__passSignals[event]); + if (passSignal) { + // Bubble up. + if (passSignal && passSignal !== true) { + // Replace signal name. + event = passSignal; + } + + return signalParent._signal(event, args); + } + } + } + + _getParentSignalHandler() { + return this.cparent ? this.cparent._getSignalHandler() : null; + } + + _getSignalHandler() { + if (this._signalProxy) { + return this.cparent ? this.cparent._getSignalHandler() : null; + } + return this; + } + + get _signalProxy() { + return false; + } + + fireAncestors(name, ...args) { + if (!name.startsWith('$')) { + throw new Error("Ancestor event name must be prefixed by dollar sign."); + } + + return this._doFireAncestors(name, args); + } + + _doFireAncestors(name, args) { + if (this._hasMethod(name)) { + return this.fire(name, ...args); + } else { + const signalParent = this._getParentSignalHandler(); + if (signalParent) { + return signalParent._doFireAncestors(name, args); + } + } + } + + static collectSubComponents(subs, element) { + if (element.hasChildren()) { + const childList = element.__childList; + for (let i = 0, n = childList.length; i < n; i++) { + const child = childList.getAt(i); + if (child.isComponent) { + subs.push(child); + } else { + Component.collectSubComponents(subs, child); + } + } + } + } + + static getComponent(element) { + let parent = element; + while (parent && !parent.isComponent) { + parent = parent.parent; + } + return parent; + } + + static getParent(element) { + return Component.getComponent(element.parent); + } + +} + +Component.prototype.isComponent = true; + +class CoreQuadList { + + constructor(ctx) { + + this.ctx = ctx; + + this.quadTextures = []; + + this.quadElements = []; + } + + get length() { + return this.quadTextures.length; + } + + reset() { + this.quadTextures = []; + this.quadElements = []; + this.dataLength = 0; + } + + getElement(index) { + return this.quadElements[index]._element; + } + + getElementCore(index) { + return this.quadElements[index]; + } + + getTexture(index) { + return this.quadTextures[index]; + } + + getTextureWidth(index) { + let nativeTexture = this.quadTextures[index]; + if (nativeTexture.w) { + // Render texture; + return nativeTexture.w; + } else { + return this.quadElements[index]._displayedTextureSource.w; + } + } + + getTextureHeight(index) { + let nativeTexture = this.quadTextures[index]; + if (nativeTexture.h) { + // Render texture; + return nativeTexture.h; + } else { + return this.quadElements[index]._displayedTextureSource.h; + } + } + +} + +class WebGLCoreQuadList extends CoreQuadList { + + constructor(ctx) { + super(ctx); + + // Allocate a fairly big chunk of memory that should be enough to support ~100000 (default) quads. + // We do not (want to) handle memory overflow. + const byteSize = ctx.stage.getOption('bufferMemory'); + + this.dataLength = 0; + + this.data = new ArrayBuffer(byteSize); + this.floats = new Float32Array(this.data); + this.uints = new Uint32Array(this.data); + } + + getAttribsDataByteOffset(index) { + // Where this quad can be found in the attribs buffer. + return index * 80; + } + + getQuadContents() { + // Debug: log contents of quad buffer. + let floats = this.floats; + let uints = this.uints; + let lines = []; + for (let i = 1; i <= this.length; i++) { + let str = 'entry ' + i + ': '; + for (let j = 0; j < 4; j++) { + let b = i * 20 + j * 4; + str += floats[b] + ',' + floats[b+1] + ':' + floats[b+2] + ',' + floats[b+3] + '[' + uints[b+4].toString(16) + '] '; + } + lines.push(str); + } + + return lines; + } + + +} + +class CoreQuadOperation { + + constructor(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + + this.ctx = ctx; + this.shader = shader; + this.shaderOwner = shaderOwner; + this.renderTextureInfo = renderTextureInfo; + this.scissor = scissor; + this.index = index; + this.length = 0; + + } + + get quads() { + return this.ctx.renderState.quads; + } + + getTexture(index) { + return this.quads.getTexture(this.index + index); + } + + getElementCore(index) { + return this.quads.getElementCore(this.index + index); + } + + getElement(index) { + return this.quads.getElement(this.index + index); + } + + getElementWidth(index) { + return this.getElement(index).renderWidth; + } + + getElementHeight(index) { + return this.getElement(index).renderHeight; + } + + getTextureWidth(index) { + return this.quads.getTextureWidth(this.index + index); + } + + getTextureHeight(index) { + return this.quads.getTextureHeight(this.index + index); + } + + getRenderWidth() { + if (this.renderTextureInfo) { + return this.renderTextureInfo.w; + } else { + return this.ctx.stage.w; + } + } + + getRenderHeight() { + if (this.renderTextureInfo) { + return this.renderTextureInfo.h; + } else { + return this.ctx.stage.h; + } + } + +} + +class WebGLCoreQuadOperation extends CoreQuadOperation { + + constructor(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + super(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + + this.extraAttribsDataByteOffset = 0; + } + + getAttribsDataByteOffset(index) { + // Where this quad can be found in the attribs buffer. + return this.quads.getAttribsDataByteOffset(this.index + index); + } + + /** + * Returns the relative pixel coordinates in the shader owner to gl position coordinates in the render texture. + * @param x + * @param y + * @return {number[]} + */ + getNormalRenderTextureCoords(x, y) { + let coords = this.shaderOwner.getRenderTextureCoords(x, y); + coords[0] /= this.getRenderWidth(); + coords[1] /= this.getRenderHeight(); + coords[0] = coords[0] * 2 - 1; + coords[1] = 1 - coords[1] * 2; + return coords; + } + + getProjection() { + if (this.renderTextureInfo === null) { + return this.ctx.renderExec._projection; + } else { + return this.renderTextureInfo.nativeTexture.projection; + } + } + +} + +class CoreRenderExecutor { + + constructor(ctx) { + this.ctx = ctx; + + this.renderState = ctx.renderState; + + this.gl = this.ctx.stage.gl; + } + + destroy() { + } + + _reset() { + this._bindRenderTexture(null); + this._setScissor(null); + this._clearRenderTexture(); + } + + execute() { + this._reset(); + + let qops = this.renderState.quadOperations; + + let i = 0, n = qops.length; + while (i < n) { + this._processQuadOperation(qops[i]); + i++; + } + } + + _processQuadOperation(quadOperation) { + if (quadOperation.renderTextureInfo && quadOperation.renderTextureInfo.ignore) { + // Ignore quad operations when we are 're-using' another texture as the render texture result. + return; + } + + this._setupQuadOperation(quadOperation); + this._execQuadOperation(quadOperation); + + } + + _setupQuadOperation(quadOperation) { + } + + _execQuadOperation(op) { + // Set render texture. + let nativeTexture = op.renderTextureInfo ? op.renderTextureInfo.nativeTexture : null; + + if (this._renderTexture !== nativeTexture) { + this._bindRenderTexture(nativeTexture); + } + + if (op.renderTextureInfo && !op.renderTextureInfo.cleared) { + this._setScissor(null); + this._clearRenderTexture(); + op.renderTextureInfo.cleared = true; + this._setScissor(op.scissor); + } else { + this._setScissor(op.scissor); + } + + this._renderQuadOperation(op); + } + + _renderQuadOperation(op) { + } + + _bindRenderTexture(renderTexture) { + this._renderTexture = renderTexture; + } + + _clearRenderTexture(renderTexture) { + } + + _setScissor(area) { + } + +} + +class WebGLCoreRenderExecutor extends CoreRenderExecutor { + + constructor(ctx) { + super(ctx); + + this.gl = this.ctx.stage.gl; + + this.init(); + } + + init() { + let gl = this.gl; + + // Create new sharable buffer for params. + this._attribsBuffer = gl.createBuffer(); + + let maxQuads = Math.floor(this.renderState.quads.data.byteLength / 80); + + // Init webgl arrays. + let allIndices = new Uint16Array(maxQuads * 6); + + // fill the indices with the quads to draw. + for (let i = 0, j = 0; i < maxQuads; i += 6, j += 4) { + allIndices[i] = j; + allIndices[i + 1] = j + 1; + allIndices[i + 2] = j + 2; + allIndices[i + 3] = j; + allIndices[i + 4] = j + 2; + allIndices[i + 5] = j + 3; + } + + // The quads buffer can be (re)used to draw a range of quads. + this._quadsBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._quadsBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, allIndices, gl.STATIC_DRAW); + + // The matrix that causes the [0,0 - W,H] box to map to [-1,-1 - 1,1] in the end results. + this._projection = new Float32Array([2/this.ctx.stage.coordsWidth, -2/this.ctx.stage.coordsHeight]); + + } + + destroy() { + super.destroy(); + this.gl.deleteBuffer(this._attribsBuffer); + this.gl.deleteBuffer(this._quadsBuffer); + } + + _reset() { + super._reset(); + + let gl = this.gl; + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.enable(gl.BLEND); + gl.disable(gl.DEPTH_TEST); + + this._stopShaderProgram(); + this._setupBuffers(); + } + + _setupBuffers() { + let gl = this.gl; + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._quadsBuffer); + let element = new Float32Array(this.renderState.quads.data, 0, this.renderState.quads.dataLength); + gl.bindBuffer(gl.ARRAY_BUFFER, this._attribsBuffer); + gl.bufferData(gl.ARRAY_BUFFER, element, gl.DYNAMIC_DRAW); + } + + _setupQuadOperation(quadOperation) { + super._setupQuadOperation(quadOperation); + this._useShaderProgram(quadOperation.shader, quadOperation); + } + + _renderQuadOperation(op) { + let shader = op.shader; + + if (op.length || op.shader.addEmpty()) { + shader.beforeDraw(op); + shader.draw(op); + shader.afterDraw(op); + } + } + + /** + * @param {WebGLShader} shader; + * @param {CoreQuadOperation} operation; + */ + _useShaderProgram(shader, operation) { + if (!shader.hasSameProgram(this._currentShaderProgram)) { + if (this._currentShaderProgram) { + this._currentShaderProgram.stopProgram(); + } + shader.useProgram(); + this._currentShaderProgram = shader; + } + shader.setupUniforms(operation); + } + + _stopShaderProgram() { + if (this._currentShaderProgram) { + // The currently used shader program should be stopped gracefully. + this._currentShaderProgram.stopProgram(); + this._currentShaderProgram = null; + } + } + + _bindRenderTexture(renderTexture) { + super._bindRenderTexture(renderTexture); + + let gl = this.gl; + if (!this._renderTexture) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0,0,this.ctx.stage.w,this.ctx.stage.h); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderTexture.framebuffer); + gl.viewport(0,0,this._renderTexture.w, this._renderTexture.h); + } + } + + _clearRenderTexture() { + super._clearRenderTexture(); + let gl = this.gl; + if (!this._renderTexture) { + let glClearColor = this.ctx.stage.getClearColor(); + if (glClearColor) { + gl.clearColor(glClearColor[0] * glClearColor[3], glClearColor[1] * glClearColor[3], glClearColor[2] * glClearColor[3], glClearColor[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } else { + // Clear texture. + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } + + _setScissor(area) { + super._setScissor(area); + + if (this._scissor === area) { + return; + } + this._scissor = area; + + let gl = this.gl; + if (!area) { + gl.disable(gl.SCISSOR_TEST); + } else { + gl.enable(gl.SCISSOR_TEST); + let precision = this.ctx.stage.getRenderPrecision(); + let y = area[1]; + if (this._renderTexture === null) { + // Flip. + y = (this.ctx.stage.h / precision - (area[1] + area[3])); + } + gl.scissor(Math.round(area[0] * precision), Math.round(y * precision), Math.round(area[2] * precision), Math.round(area[3] * precision)); + } + } + +} + +class CoreRenderState { + + constructor(ctx) { + this.ctx = ctx; + + this.stage = ctx.stage; + + this.defaultShader = this.stage.renderer.getDefaultShader(ctx); + + this.renderer = ctx.stage.renderer; + + this.quads = this.renderer.createCoreQuadList(ctx); + + } + + reset() { + this._renderTextureInfo = null; + + this._scissor = null; + + this._shader = null; + + this._shaderOwner = null; + + this._realShader = null; + + this._check = false; + + this.quadOperations = []; + + this._texturizer = null; + + this._texturizerTemporary = false; + + this._quadOperation = null; + + this.quads.reset(); + + this._temporaryTexturizers = []; + + this._isCachingTexturizer = false; + + } + + get length() { + return this.quads.quadTextures.length; + } + + setShader(shader, owner) { + if (this._shaderOwner !== owner || this._realShader !== shader) { + // Same shader owner: active shader is also the same. + // Prevent any shader usage to save performance. + + this._realShader = shader; + + if (shader.useDefault()) { + // Use the default shader when possible to prevent unnecessary program changes. + shader = this.defaultShader; + } + if (this._shader !== shader || this._shaderOwner !== owner) { + this._shader = shader; + this._shaderOwner = owner; + this._check = true; + } + } + } + + get renderTextureInfo() { + return this._renderTextureInfo; + } + + setScissor(area) { + if (this._scissor !== area) { + if (area) { + this._scissor = area; + } else { + this._scissor = null; + } + this._check = true; + } + } + + getScissor() { + return this._scissor; + } + + setRenderTextureInfo(renderTextureInfo) { + if (this._renderTextureInfo !== renderTextureInfo) { + this._renderTextureInfo = renderTextureInfo; + this._scissor = null; + this._check = true; + } + } + + /** + * Sets the texturizer to be drawn during subsequent addQuads. + * @param {ElementTexturizer} texturizer + */ + setTexturizer(texturizer, cache = false) { + this._texturizer = texturizer; + this._cacheTexturizer = cache; + } + + set isCachingTexturizer(v) { + this._isCachingTexturizer = v; + } + + get isCachingTexturizer() { + return this._isCachingTexturizer; + } + + addQuad(elementCore) { + if (!this._quadOperation) { + this._createQuadOperation(); + } else if (this._check && this._hasChanges()) { + this._finishQuadOperation(); + this._check = false; + } + + let nativeTexture = null; + if (this._texturizer) { + nativeTexture = this._texturizer.getResultTexture(); + + if (!this._cacheTexturizer) { + // We can release the temporary texture immediately after finalizing this quad operation. + this._temporaryTexturizers.push(this._texturizer); + } + } + + if (!nativeTexture) { + nativeTexture = elementCore._displayedTextureSource.nativeTexture; + } + + if (this._renderTextureInfo) { + if (this._shader === this.defaultShader && this._renderTextureInfo.empty) { + // The texture might be reusable under some conditions. We will check them in ElementCore.renderer. + this._renderTextureInfo.nativeTexture = nativeTexture; + this._renderTextureInfo.offset = this.length; + } else { + // It is not possible to reuse another texture when there is more than one quad. + this._renderTextureInfo.nativeTexture = null; + } + this._renderTextureInfo.empty = false; + } + + this.quads.quadTextures.push(nativeTexture); + this.quads.quadElements.push(elementCore); + + this._quadOperation.length++; + + this.renderer.addQuad(this, this.quads, this.length - 1); + } + + finishedRenderTexture() { + if (this._renderTextureInfo.nativeTexture) { + // There was only one texture drawn in this render texture. + // Check if we can reuse it so that we can optimize out an unnecessary render texture operation. + // (it should exactly span this render texture). + if (!this._isRenderTextureReusable()) { + this._renderTextureInfo.nativeTexture = null; + } + } + } + + _isRenderTextureReusable() { + const offset = this._renderTextureInfo.offset; + return (this.quads.quadTextures[offset].w === this._renderTextureInfo.w) && + (this.quads.quadTextures[offset].h === this._renderTextureInfo.h) && + this.renderer.isRenderTextureReusable(this, this._renderTextureInfo) + } + + _hasChanges() { + let q = this._quadOperation; + if (this._shader !== q.shader) return true; + if (this._shaderOwner !== q.shaderOwner) return true; + if (this._renderTextureInfo !== q.renderTextureInfo) return true; + if (this._scissor !== q.scissor) { + if ((this._scissor[0] !== q.scissor[0]) || (this._scissor[1] !== q.scissor[1]) || (this._scissor[2] !== q.scissor[2]) || (this._scissor[3] !== q.scissor[3])) { + return true; + } + } + + return false; + } + + _finishQuadOperation(create = true) { + if (this._quadOperation) { + if (this._quadOperation.length || this._shader.addEmpty()) { + if (!this._quadOperation.scissor || ((this._quadOperation.scissor[2] > 0) && (this._quadOperation.scissor[3] > 0))) { + // Ignore empty clipping regions. + this.quadOperations.push(this._quadOperation); + } + } + + if (this._temporaryTexturizers.length) { + for (let i = 0, n = this._temporaryTexturizers.length; i < n; i++) { + // We can now reuse these render-to-textures in subsequent stages. + // Huge performance benefit when filtering (fast blur). + this._temporaryTexturizers[i].releaseRenderTexture(); + } + this._temporaryTexturizers = []; + } + + this._quadOperation = null; + } + + if (create) { + this._createQuadOperation(); + } + } + + _createQuadOperation() { + this._quadOperation = this.renderer.createCoreQuadOperation( + this.ctx, + this._shader, + this._shaderOwner, + this._renderTextureInfo, + this._scissor, + this.length + ); + this._check = false; + } + + finish() { + if (this._quadOperation) { + // Add remaining. + this._finishQuadOperation(false); + } + + this.renderer.finishRenderState(this); + } + +} + +/** + * Base functionality for shader setup/destroy. + */ +class WebGLShaderProgram { + + constructor(vertexShaderSource, fragmentShaderSource) { + + this.vertexShaderSource = vertexShaderSource; + this.fragmentShaderSource = fragmentShaderSource; + + this._program = null; + + this._uniformLocations = new Map(); + this._attributeLocations = new Map(); + + this._currentUniformValues = {}; + } + + compile(gl) { + if (this._program) return; + + this.gl = gl; + + this._program = gl.createProgram(); + + let glVertShader = this._glCompile(gl.VERTEX_SHADER, this.vertexShaderSource); + let glFragShader = this._glCompile(gl.FRAGMENT_SHADER, this.fragmentShaderSource); + + gl.attachShader(this._program, glVertShader); + gl.attachShader(this._program, glFragShader); + gl.linkProgram(this._program); + + // if linking fails, then log and cleanup + if (!gl.getProgramParameter(this._program, gl.LINK_STATUS)) { + console.error('Error: Could not initialize shader.'); + console.error('gl.VALIDATE_STATUS', gl.getProgramParameter(this._program, gl.VALIDATE_STATUS)); + console.error('gl.getError()', gl.getError()); + + // if there is a program info log, log it + if (gl.getProgramInfoLog(this._program) !== '') { + console.warn('Warning: gl.getProgramInfoLog()', gl.getProgramInfoLog(this._program)); + } + + gl.deleteProgram(this._program); + this._program = null; + } + + // clean up some shaders + gl.deleteShader(glVertShader); + gl.deleteShader(glFragShader); + } + + _glCompile(type, src) { + let shader = this.gl.createShader(type); + + this.gl.shaderSource(shader, src); + this.gl.compileShader(shader); + + if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { + console.log(this.constructor.name, 'Type: ' + (type === this.gl.VERTEX_SHADER ? 'vertex shader' : 'fragment shader') ); + console.log(this.gl.getShaderInfoLog(shader)); + let idx = 0; + console.log("========== source ==========\n" + src.split("\n").map(line => "" + (++idx) + ": " + line).join("\n")); + return null; + } + + return shader; + } + + getUniformLocation(name) { + let location = this._uniformLocations.get(name); + if (location === undefined) { + location = this.gl.getUniformLocation(this._program, name); + this._uniformLocations.set(name, location); + } + + return location; + } + + getAttribLocation(name) { + let location = this._attributeLocations.get(name); + if (location === undefined) { + location = this.gl.getAttribLocation(this._program, name); + this._attributeLocations.set(name, location); + } + + return location; + } + + destroy() { + if (this._program) { + this.gl.deleteProgram(this._program); + this._program = null; + } + } + + get glProgram() { + return this._program; + } + + get compiled() { + return !!this._program; + } + + _valueEquals(v1, v2) { + // Uniform value is either a typed array or a numeric value. + if (v1.length && v2.length) { + for (let i = 0, n = v1.length; i < n; i++) { + if (v1[i] !== v2[i]) return false; + } + return true; + } else { + return (v1 === v2); + } + } + + _valueClone(v) { + if (v.length) { + return v.slice(0); + } else { + return v; + } + } + + setUniformValue(name, value, glFunction) { + let v = this._currentUniformValues[name]; + if (v === undefined || !this._valueEquals(v, value)) { + let clonedValue = this._valueClone(value); + this._currentUniformValues[name] = clonedValue; + + let loc = this.getUniformLocation(name); + if (loc) { + let isMatrix = (glFunction === this.gl.uniformMatrix2fv || glFunction === this.gl.uniformMatrix3fv || glFunction === this.gl.uniformMatrix4fv); + if (isMatrix) { + glFunction.call(this.gl, loc, false, clonedValue); + } else { + glFunction.call(this.gl, loc, clonedValue); + } + } + } + } + +} + +class WebGLShader extends Shader { + + constructor(ctx) { + super(ctx); + + const stage = ctx.stage; + + this._program = stage.renderer.shaderPrograms.get(this.constructor); + if (!this._program) { + this._program = new WebGLShaderProgram(this.constructor.vertexShaderSource, this.constructor.fragmentShaderSource); + + // Let the vbo context perform garbage collection. + stage.renderer.shaderPrograms.set(this.constructor, this._program); + } + + this.gl = stage.gl; + } + + get glProgram() { + return this._program.glProgram; + } + + _init() { + if (!this._initialized) { + this.initialize(); + this._initialized = true; + } + } + + initialize() { + this._program.compile(this.gl); + } + + get initialized() { + return this._initialized; + } + + _uniform(name) { + return this._program.getUniformLocation(name); + } + + _attrib(name) { + return this._program.getAttribLocation(name); + } + + _setUniform(name, value, glFunction) { + this._program.setUniformValue(name, value, glFunction); + } + + useProgram() { + this._init(); + this.gl.useProgram(this.glProgram); + this.beforeUsage(); + this.enableAttribs(); + } + + stopProgram() { + this.afterUsage(); + this.disableAttribs(); + } + + hasSameProgram(other) { + // For performance reasons, we first check for identical references. + return (other && ((other === this) || (other._program === this._program))); + } + + beforeUsage() { + // Override to set settings other than the default settings (blend mode etc). + } + + afterUsage() { + // All settings changed in beforeUsage should be reset here. + } + + enableAttribs() { + + } + + disableAttribs() { + + } + + getExtraAttribBytesPerVertex() { + return 0; + } + + getVertexAttribPointerOffset(operation) { + return operation.extraAttribsDataByteOffset - (operation.index + 1) * 4 * this.getExtraAttribBytesPerVertex(); + } + + setExtraAttribsInBuffer(operation) { + // Set extra attrib data in in operation.quads.data/floats/uints, starting from + // operation.extraAttribsBufferByteOffset. + } + + setupUniforms(operation) { + // Set all shader-specific uniforms. + // Notice that all uniforms should be set, even if they have not been changed within this shader instance. + // The uniforms are shared by all shaders that have the same type (and shader program). + } + + _getProjection(operation) { + return operation.getProjection(); + } + + getFlipY(operation) { + return this._getProjection(operation)[1] < 0; + } + + beforeDraw(operation) { + } + + draw(operation) { + } + + afterDraw(operation) { + } + + cleanup() { + this._initialized = false; + // Program takes little resources, so it is only destroyed when the full stage is destroyed. + } + +} + +class DefaultShader extends WebGLShader { + + enableAttribs() { + // Enables the attribs in the shader program. + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aVertexPosition"), 2, gl.FLOAT, false, 20, 0); + gl.enableVertexAttribArray(this._attrib("aVertexPosition")); + + if (this._attrib("aTextureCoord") !== -1) { + gl.vertexAttribPointer(this._attrib("aTextureCoord"), 2, gl.FLOAT, false, 20, 2 * 4); + gl.enableVertexAttribArray(this._attrib("aTextureCoord")); + } + + if (this._attrib("aColor") !== -1) { + // Some shaders may ignore the color. + gl.vertexAttribPointer(this._attrib("aColor"), 4, gl.UNSIGNED_BYTE, true, 20, 4 * 4); + gl.enableVertexAttribArray(this._attrib("aColor")); + } + } + + disableAttribs() { + // Disables the attribs in the shader program. + let gl = this.gl; + gl.disableVertexAttribArray(this._attrib("aVertexPosition")); + + if (this._attrib("aTextureCoord") !== -1) { + gl.disableVertexAttribArray(this._attrib("aTextureCoord")); + } + + if (this._attrib("aColor") !== -1) { + gl.disableVertexAttribArray(this._attrib("aColor")); + } + } + + setupUniforms(operation) { + this._setUniform("projection", this._getProjection(operation), this.gl.uniform2fv, false); + } + + draw(operation) { + let gl = this.gl; + + let length = operation.length; + + if (length) { + let glTexture = operation.getTexture(0); + let pos = 0; + for (let i = 0; i < length; i++) { + let tx = operation.getTexture(i); + if (glTexture !== tx) { + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.drawElements(gl.TRIANGLES, 6 * (i - pos), gl.UNSIGNED_SHORT, (pos + operation.index) * 6 * 2); + glTexture = tx; + pos = i; + } + } + if (pos < length) { + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.drawElements(gl.TRIANGLES, 6 * (length - pos), gl.UNSIGNED_SHORT, (pos + operation.index) * 6 * 2); + } + } + } + +} + +DefaultShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +DefaultShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor; + } +`; + +class Renderer { + + constructor(stage) { + this.stage = stage; + this._defaultShader = undefined; + } + + gc(aggressive) { + } + + destroy() { + } + + getDefaultShader(ctx = this.stage.ctx) { + if (!this._defaultShader) { + this._defaultShader = this._createDefaultShader(ctx); + } + return this._defaultShader; + } + + _createDefaultShader(ctx) { + } + + isValidShaderType(shaderType) { + return (shaderType.prototype instanceof this._getShaderBaseType()); + } + + createShader(ctx, settings) { + const shaderType = settings.type; + // If shader type is not correct, use a different platform. + if (!this.isValidShaderType(shaderType)) { + const convertedShaderType = this._getShaderAlternative(shaderType); + if (!convertedShaderType) { + console.warn("Shader has no implementation for render target: " + shaderType.name); + return this._createDefaultShader(ctx); + } + return new convertedShaderType(ctx); + } else { + const shader = new shaderType(ctx); + Base.patchObject(this, settings); + return shader; + } + } + + _getShaderBaseType() { + } + + _getShaderAlternative(shaderType) { + return this.getDefaultShader(); + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + console.warn('copyRenderTexture not supported by renderer'); + } +} + +class WebGLRenderer extends Renderer { + + constructor(stage) { + super(stage); + this.shaderPrograms = new Map(); + } + + destroy() { + this.shaderPrograms.forEach(shaderProgram => shaderProgram.destroy()); + } + + _createDefaultShader(ctx) { + return new DefaultShader(ctx); + } + + _getShaderBaseType() { + return WebGLShader + } + + _getShaderAlternative(shaderType) { + return shaderType.getWebGL && shaderType.getWebGL(); + } + + createCoreQuadList(ctx) { + return new WebGLCoreQuadList(ctx); + } + + createCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + return new WebGLCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + } + + createCoreRenderExecutor(ctx) { + return new WebGLCoreRenderExecutor(ctx); + } + + createCoreRenderState(ctx) { + return new CoreRenderState(ctx); + } + + createRenderTexture(w, h, pw, ph) { + const gl = this.stage.gl; + const glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, pw, ph, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + glTexture.params = {}; + glTexture.params[gl.TEXTURE_MAG_FILTER] = gl.LINEAR; + glTexture.params[gl.TEXTURE_MIN_FILTER] = gl.LINEAR; + glTexture.params[gl.TEXTURE_WRAP_S] = gl.CLAMP_TO_EDGE; + glTexture.params[gl.TEXTURE_WRAP_T] = gl.CLAMP_TO_EDGE; + glTexture.options = {format: gl.RGBA, internalFormat: gl.RGBA, type: gl.UNSIGNED_BYTE}; + + // We need a specific framebuffer for every render texture. + glTexture.framebuffer = gl.createFramebuffer(); + glTexture.projection = new Float32Array([2/w, 2/h]); + + gl.bindFramebuffer(gl.FRAMEBUFFER, glTexture.framebuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, glTexture, 0); + + return glTexture; + } + + freeRenderTexture(glTexture) { + let gl = this.stage.gl; + gl.deleteFramebuffer(glTexture.framebuffer); + gl.deleteTexture(glTexture); + } + + uploadTextureSource(textureSource, options) { + const gl = this.stage.gl; + + const source = options.source; + + const format = { + premultiplyAlpha: true, + hasAlpha: true + }; + + if (options && options.hasOwnProperty('premultiplyAlpha')) { + format.premultiplyAlpha = options.premultiplyAlpha; + } + + if (options && options.hasOwnProperty('flipBlueRed')) { + format.flipBlueRed = options.flipBlueRed; + } + + if (options && options.hasOwnProperty('hasAlpha')) { + format.hasAlpha = options.hasAlpha; + } + + if (!format.hasAlpha) { + format.premultiplyAlpha = false; + } + + format.texParams = options.texParams || {}; + format.texOptions = options.texOptions || {}; + + let glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, format.premultiplyAlpha); + + if (Utils.isNode) { + gl.pixelStorei(gl.UNPACK_FLIP_BLUE_RED, !!format.flipBlueRed); + } + + const texParams = format.texParams; + if (!texParams[gl.TEXTURE_MAG_FILTER]) texParams[gl.TEXTURE_MAG_FILTER] = gl.LINEAR; + if (!texParams[gl.TEXTURE_MIN_FILTER]) texParams[gl.TEXTURE_MIN_FILTER] = gl.LINEAR; + if (!texParams[gl.TEXTURE_WRAP_S]) texParams[gl.TEXTURE_WRAP_S] = gl.CLAMP_TO_EDGE; + if (!texParams[gl.TEXTURE_WRAP_T]) texParams[gl.TEXTURE_WRAP_T] = gl.CLAMP_TO_EDGE; + + Object.keys(texParams).forEach(key => { + const value = texParams[key]; + gl.texParameteri(gl.TEXTURE_2D, parseInt(key), value); + }); + + const texOptions = format.texOptions; + texOptions.format = texOptions.format || (format.hasAlpha ? gl.RGBA : gl.RGB); + texOptions.type = texOptions.type || gl.UNSIGNED_BYTE; + texOptions.internalFormat = texOptions.internalFormat || texOptions.format; + + this.stage.platform.uploadGlTexture(gl, textureSource, source, texOptions); + + glTexture.params = Utils.cloneObjShallow(texParams); + glTexture.options = Utils.cloneObjShallow(texOptions); + + return glTexture; + } + + freeTextureSource(textureSource) { + this.stage.gl.deleteTexture(textureSource.nativeTexture); + } + + addQuad(renderState, quads, index) { + let offset = (index * 20); + const elementCore = quads.quadElements[index]; + + let r = elementCore._renderContext; + + let floats = renderState.quads.floats; + let uints = renderState.quads.uints; + const mca = StageUtils.mergeColorAlpha; + + if (r.tb !== 0 || r.tc !== 0) { + floats[offset++] = r.px; + floats[offset++] = r.py; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUl, r.alpha); + floats[offset++] = r.px + elementCore._w * r.ta; + floats[offset++] = r.py + elementCore._w * r.tc; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUr, r.alpha); + floats[offset++] = r.px + elementCore._w * r.ta + elementCore._h * r.tb; + floats[offset++] = r.py + elementCore._w * r.tc + elementCore._h * r.td; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._bry; + uints[offset++] = mca(elementCore._colorBr, r.alpha); + floats[offset++] = r.px + elementCore._h * r.tb; + floats[offset++] = r.py + elementCore._h * r.td; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._bry; + uints[offset] = mca(elementCore._colorBl, r.alpha); + } else { + // Simple. + let cx = r.px + elementCore._w * r.ta; + let cy = r.py + elementCore._h * r.td; + + floats[offset++] = r.px; + floats[offset++] = r.py; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUl, r.alpha); + floats[offset++] = cx; + floats[offset++] = r.py; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUr, r.alpha); + floats[offset++] = cx; + floats[offset++] = cy; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._bry; + uints[offset++] = mca(elementCore._colorBr, r.alpha); + floats[offset++] = r.px; + floats[offset++] = cy; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._bry; + uints[offset] = mca(elementCore._colorBl, r.alpha); + } + } + + isRenderTextureReusable(renderState, renderTextureInfo) { + let offset = (renderState._renderTextureInfo.offset * 80) / 4; + let floats = renderState.quads.floats; + let uints = renderState.quads.uints; + return ((floats[offset] === 0) && + (floats[offset + 1] === 0) && + (floats[offset + 2] === 0) && + (floats[offset + 3] === 0) && + (uints[offset + 4] === 0xFFFFFFFF) && + (floats[offset + 5] === renderTextureInfo.w) && + (floats[offset + 6] === 0) && + (floats[offset + 7] === 1) && + (floats[offset + 8] === 0) && + (uints[offset + 9] === 0xFFFFFFFF) && + (floats[offset + 10] === renderTextureInfo.w) && + (floats[offset + 11] === renderTextureInfo.h) && + (floats[offset + 12] === 1) && + (floats[offset + 13] === 1) && + (uints[offset + 14] === 0xFFFFFFFF) && + (floats[offset + 15] === 0) && + (floats[offset + 16] === renderTextureInfo.h) && + (floats[offset + 17] === 0) && + (floats[offset + 18] === 1) && + (uints[offset + 19] === 0xFFFFFFFF)); + } + + finishRenderState(renderState) { + // Set extra shader attribute data. + let offset = renderState.length * 80; + for (let i = 0, n = renderState.quadOperations.length; i < n; i++) { + renderState.quadOperations[i].extraAttribsDataByteOffset = offset; + let extra = renderState.quadOperations[i].shader.getExtraAttribBytesPerVertex() * 4 * renderState.quadOperations[i].length; + offset += extra; + if (extra) { + renderState.quadOperations[i].shader.setExtraAttribsInBuffer(renderState.quadOperations[i], renderState.quads); + } + } + renderState.quads.dataLength = offset; + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + const gl = this.stage.gl; + gl.bindTexture(gl.TEXTURE_2D, nativeTexture); + gl.bindFramebuffer(gl.FRAMEBUFFER, renderTexture.framebuffer); + const precision = renderTexture.precision; + gl.copyTexSubImage2D( + gl.TEXTURE_2D, + 0, + precision * (options.sx || 0), + precision * (options.sy || 0), + precision * (options.x || 0), + precision * (options.y || 0), + precision * (options.w || renderTexture.ow), + precision * (options.h || renderTexture.oh)); + } + +} + +class C2dCoreQuadList extends CoreQuadList { + + constructor(ctx) { + super(ctx); + + this.renderContexts = []; + this.modes = []; + } + + setRenderContext(index, v) { + this.renderContexts[index] = v; + } + + setSimpleTc(index, v) { + if (v) { + this.modes[index] |= 1; + } else { + this.modes[index] -= (this.modes[index] & 1); + } + } + + setWhite(index, v) { + if (v) { + this.modes[index] |= 2; + } else { + this.modes[index] -= (this.modes[index] & 2); + } + } + + getRenderContext(index) { + return this.renderContexts[index]; + } + + getSimpleTc(index) { + return (this.modes[index] & 1); + } + + getWhite(index) { + return (this.modes[index] & 2); + } + +} + +class C2dCoreQuadOperation extends CoreQuadOperation { + + getRenderContext(index) { + return this.quads.getRenderContext(this.index + index); + } + + getSimpleTc(index) { + return this.quads.getSimpleTc(this.index + index); + } + + getWhite(index) { + return this.quads.getWhite(this.index + index); + } + +} + +class C2dCoreRenderExecutor extends CoreRenderExecutor { + + init() { + this._mainRenderTexture = this.ctx.stage.getCanvas(); + } + + _renderQuadOperation(op) { + let shader = op.shader; + + if (op.length || op.shader.addEmpty()) { + const target = this._renderTexture || this._mainRenderTexture; + shader.beforeDraw(op, target); + shader.draw(op, target); + shader.afterDraw(op, target); + } + } + + _clearRenderTexture() { + const ctx = this._getContext(); + + let clearColor = [0, 0, 0, 0]; + if (this._mainRenderTexture.ctx === ctx) { + clearColor = this.ctx.stage.getClearColor(); + } + + const renderTexture = ctx.canvas; + ctx.setTransform(1, 0, 0, 1, 0, 0); + if (!clearColor[0] && !clearColor[1] && !clearColor[2] && !clearColor[3]) { + ctx.clearRect(0, 0, renderTexture.width, renderTexture.height); + } else { + ctx.fillStyle = StageUtils.getRgbaStringFromArray(clearColor); + // Do not use fillRect because it produces artifacts. + ctx.globalCompositeOperation = 'copy'; + ctx.beginPath(); + ctx.rect(0, 0, renderTexture.width, renderTexture.height); + ctx.closePath(); + ctx.fill(); + ctx.globalCompositeOperation = 'source-over'; + } + } + + _getContext() { + if (this._renderTexture) { + return this._renderTexture.ctx; + } else { + return this._mainRenderTexture.ctx; + } + } + + _restoreContext() { + const ctx = this._getContext(); + ctx.restore(); + ctx.save(); + ctx._scissor = null; + } + + _setScissor(area) { + const ctx = this._getContext(); + + if (!C2dCoreRenderExecutor._equalScissorAreas(ctx.canvas, ctx._scissor, area)) { + // Clipping is stored in the canvas context state. + // We can't reset clipping alone so we need to restore the full context. + this._restoreContext(); + + let precision = this.ctx.stage.getRenderPrecision(); + if (area) { + ctx.beginPath(); + ctx.rect(Math.round(area[0] * precision), Math.round(area[1] * precision), Math.round(area[2] * precision), Math.round(area[3] * precision)); + ctx.closePath(); + ctx.clip(); + } + ctx._scissor = area; + } + } + + static _equalScissorAreas(canvas, area, current) { + if (!area) { + area = [0, 0, canvas.width, canvas.height]; + } + if (!current) { + current = [0, 0, canvas.width, canvas.height]; + } + return Utils.equalValues(area, current) + } + +} + +class C2dShader extends Shader { + + beforeDraw(operation) { + } + + draw(operation) { + } + + afterDraw(operation) { + } + +} + +class DefaultShader$1 extends C2dShader { + + constructor(ctx) { + super(ctx); + this._rectangleTexture = ctx.stage.rectangleTexture.source.nativeTexture; + this._tintManager = this.ctx.stage.renderer.tintManager; + } + + draw(operation, target) { + const ctx = target.ctx; + let length = operation.length; + for (let i = 0; i < length; i++) { + const tx = operation.getTexture(i); + const vc = operation.getElementCore(i); + const rc = operation.getRenderContext(i); + const white = operation.getWhite(i); + const stc = operation.getSimpleTc(i); + + //@todo: try to optimize out per-draw transform setting. split translate, transform. + const precision = this.ctx.stage.getRenderPrecision(); + ctx.setTransform(rc.ta * precision, rc.tc * precision, rc.tb * precision, rc.td * precision, rc.px * precision, rc.py * precision); + + const rect = (tx === this._rectangleTexture); + const info = {operation, target, index: i, rect}; + + if (rect) { + // Check for gradient. + if (white) { + ctx.fillStyle = 'white'; + } else { + this._setColorGradient(ctx, vc); + } + + ctx.globalAlpha = rc.alpha; + this._beforeDrawEl(info); + ctx.fillRect(0, 0, vc.w, vc.h); + this._afterDrawEl(info); + ctx.globalAlpha = 1.0; + } else { + // @todo: set image smoothing based on the texture. + + // @todo: optimize by registering whether identity texcoords are used. + ctx.globalAlpha = rc.alpha; + this._beforeDrawEl(info); + + // @todo: test if rounding yields better performance. + + // Notice that simple texture coords can be turned on even though vc._ulx etc are not simple, because + // we are rendering a render-to-texture (texcoords were stashed). Same is true for 'white' color btw. + const sourceX = stc ? 0 : (vc._ulx * tx.w); + const sourceY = stc ? 0 : (vc._uly * tx.h); + const sourceW = (stc ? 1 : (vc._brx - vc._ulx)) * tx.w; + const sourceH = (stc ? 1 : (vc._bry - vc._uly)) * tx.h; + + let colorize = !white; + if (colorize) { + // @todo: cache the tint texture for better performance. + + // Draw to intermediate texture with background color/gradient. + // This prevents us from having to create a lot of render texture canvases. + + // Notice that we don't support (non-rect) gradients, only color tinting for c2d. We'll just take the average color. + let color = vc._colorUl; + if (vc._colorUl !== vc._colorUr || vc._colorUr !== vc._colorBl || vc._colorBr !== vc._colorBl) { + color = StageUtils.mergeMultiColorsEqual([vc._colorUl, vc._colorUr, vc._colorBl, vc._colorBr]); + } + + const alpha = ((color / 16777216) | 0) / 255.0; + ctx.globalAlpha *= alpha; + + const rgb = color & 0x00FFFFFF; + const tintTexture = this._tintManager.getTintTexture(tx, rgb); + + // Actually draw result. + ctx.fillStyle = 'white'; + ctx.drawImage(tintTexture, sourceX, sourceY, sourceW, sourceH, 0, 0, vc.w, vc.h); + } else { + ctx.fillStyle = 'white'; + ctx.drawImage(tx, sourceX, sourceY, sourceW, sourceH, 0, 0, vc.w, vc.h); + } + this._afterDrawEl(info); + ctx.globalAlpha = 1.0; + } + } + } + + _setColorGradient(ctx, vc, w = vc.w, h = vc.h, transparency = true) { + let color = vc._colorUl; + let gradient; + //@todo: quick single color check. + //@todo: cache gradient/fill style (if possible, probably context-specific). + + if (vc._colorUl === vc._colorUr) { + if (vc._colorBl === vc._colorBr) { + if (vc._colorUl === vc.colorBl) ; else { + // Vertical gradient. + gradient = ctx.createLinearGradient(0, 0, 0, h); + if (transparency) { + gradient.addColorStop(0, StageUtils.getRgbaString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbaString(vc._colorBl)); + } else { + gradient.addColorStop(0, StageUtils.getRgbString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbString(vc._colorBl)); + + } + } + } + } else { + if (vc._colorUl === vc._colorBl && vc._colorUr === vc._colorBr) { + // Horizontal gradient. + gradient = ctx.createLinearGradient(0, 0, w, 0); + if (transparency) { + gradient.addColorStop(0, StageUtils.getRgbaString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbaString(vc._colorBr)); + } else { + gradient.addColorStop(0, StageUtils.getRgbString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbString(vc._colorBr)); + } + } + } + + if (gradient) { + ctx.fillStyle = gradient; + } else { + ctx.fillStyle = transparency ? StageUtils.getRgbaString(color) : StageUtils.getRgbString(color); + } + } + + _beforeDrawEl(info) { + } + + _afterDrawEl(info) { + } + +} + +class C2dTextureTintManager { + + constructor(stage) { + this.stage = stage; + this._usedMemory = 0; + this._cachedNativeTextures = new Set(); + } + + destroy() { + this.gc(true); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + + this.stage.addMemoryUsage(delta); + } + + delete(nativeTexture) { + // Should be called when native texture is cleaned up. + if (this._hasCache(nativeTexture)) { + const cache = this._getCache(nativeTexture); + const prevMemUsage = cache.memoryUsage; + cache.clear(); + this._cachedNativeTextures.delete(nativeTexture); + this._addMemoryUsage(cache.memoryUsage - prevMemUsage); + } + } + + getTintTexture(nativeTexture, color) { + const frame = this.stage.frameCounter; + + this._cachedNativeTextures.add(nativeTexture); + + const cache = this._getCache(nativeTexture); + + const item = cache.get(color); + item.lf = frame; + + if (item.tx) { + if (nativeTexture.update > item.u) { + // Native texture was updated in the mean time: renew. + this._tintTexture(item.tx, nativeTexture, color); + } + + return item.tx; + } else { + const before = cache.memoryUsage; + + // Find blanco tint texture. + let target = cache.reuseTexture(frame); + if (target) { + target.ctx.clearRect(0, 0, target.width, target.height); + } else { + // Allocate new. + target = document.createElement('canvas'); + target.width = nativeTexture.w; + target.height = nativeTexture.h; + target.ctx = target.getContext('2d'); + } + + this._tintTexture(target, nativeTexture, color); + cache.set(color, target, frame); + + const after = cache.memoryUsage; + + if (after !== before) { + this._addMemoryUsage(after - before); + } + + return target; + } + } + + _tintTexture(target, source, color) { + let col = color.toString(16); + while (col.length < 6) { + col = "0" + col; + } + target.ctx.fillStyle = '#' + col; + target.ctx.globalCompositeOperation = 'copy'; + target.ctx.fillRect(0, 0, source.w, source.h); + target.ctx.globalCompositeOperation = 'multiply'; + target.ctx.drawImage(source, 0, 0, source.w, source.h, 0, 0, target.width, target.height); + + // Alpha-mix the texture. + target.ctx.globalCompositeOperation = 'destination-in'; + target.ctx.drawImage(source, 0, 0, source.w, source.h, 0, 0, target.width, target.height); + } + + _hasCache(nativeTexture) { + return !!nativeTexture._tintCache; + } + + _getCache(nativeTexture) { + if (!nativeTexture._tintCache) { + nativeTexture._tintCache = new C2dTintCache(nativeTexture); + } + return nativeTexture._tintCache; + } + + gc(aggressive = false) { + const frame = this.stage.frameCounter; + let delta = 0; + this._cachedNativeTextures.forEach(texture => { + const cache = this._getCache(texture); + if (aggressive) { + delta += cache.memoryUsage; + texture.clear(); + } else { + const before = cache.memoryUsage; + cache.cleanup(frame); + cache.releaseBlancoTextures(); + delta += (cache.memoryUsage - before); + } + }); + + if (aggressive) { + this._cachedNativeTextures.clear(); + } + + if (delta) { + this._addMemoryUsage(delta); + } + } + +} + +class C2dTintCache { + + constructor(nativeTexture) { + this._tx = nativeTexture; + this._colors = new Map(); + this._blancoTextures = null; + this._lastCleanupFrame = 0; + this._memTextures = 0; + } + + get memoryUsage() { + return this._memTextures * this._tx.w * this._tx.h; + } + + releaseBlancoTextures() { + this._memTextures -= this._blancoTextures.length; + this._blancoTextures = []; + } + + clear() { + // Dereference the textures. + this._blancoTextures = null; + this._colors.clear(); + this._memTextures = 0; + } + + get(color) { + let item = this._colors.get(color); + if (!item) { + item = {lf: -1, tx: undefined, u: -1}; + this._colors.set(color, item); + } + return item; + } + + set(color, texture, frame) { + const item = this.get(color); + item.lf = frame; + item.tx = texture; + item.u = frame; + this._memTextures++; + } + + cleanup(frame) { + // We only need to clean up once per frame. + if (this._lastCleanupFrame !== frame) { + + // We limit blanco textures reuse to one frame only to prevent memory usage growth. + this._blancoTextures = []; + + this._colors.forEach((item, color) => { + // Clean up entries that were not used last frame. + if (item.lf < frame - 1) { + if (item.tx) { + // Keep as reusable blanco texture. + this._blancoTextures.push(item.tx); + } + this._colors.delete(color); + } + }); + + this._lastCleanupFrame = frame; + } + } + + reuseTexture(frame) { + // Try to reuse textures, because creating them every frame is expensive. + this.cleanup(frame); + if (this._blancoTextures && this._blancoTextures.length) { + this._memTextures--; + return this._blancoTextures.pop(); + } + } + +} + +class C2dRenderer extends Renderer { + + constructor(stage) { + super(stage); + + this.tintManager = new C2dTextureTintManager(stage); + + this.setupC2d(this.stage.c2d.canvas); + } + + destroy() { + this.tintManager.destroy(); + } + + _createDefaultShader(ctx) { + return new DefaultShader$1(ctx); + } + + _getShaderBaseType() { + return C2dShader + } + + _getShaderAlternative(shaderType) { + return shaderType.getC2d && shaderType.getC2d(); + } + + createCoreQuadList(ctx) { + return new C2dCoreQuadList(ctx); + } + + createCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + return new C2dCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + } + + createCoreRenderExecutor(ctx) { + return new C2dCoreRenderExecutor(ctx); + } + + createCoreRenderState(ctx) { + return new CoreRenderState(ctx); + } + + createRenderTexture(w, h, pw, ph) { + const canvas = document.createElement('canvas'); + canvas.width = pw; + canvas.height = ph; + this.setupC2d(canvas); + return canvas; + } + + freeRenderTexture(nativeTexture) { + this.tintManager.delete(nativeTexture); + } + + gc(aggressive) { + this.tintManager.gc(aggressive); + } + + uploadTextureSource(textureSource, options) { + // For canvas, we do not need to upload. + if (options.source.buffer) { + // Convert RGBA buffer to canvas. + const canvas = document.createElement('canvas'); + canvas.width = options.w; + canvas.height = options.h; + + const imageData = new ImageData(new Uint8ClampedArray(options.source.buffer), options.w, options.h); + canvas.getContext('2d').putImageData(imageData, 0, 0); + return canvas; + } + + return options.source; + } + + freeTextureSource(textureSource) { + this.tintManager.delete(textureSource.nativeTexture); + } + + addQuad(renderState, quads, index) { + // Render context changes while traversing so we save it by ref. + const elementCore = quads.quadElements[index]; + quads.setRenderContext(index, elementCore._renderContext); + quads.setWhite(index, elementCore.isWhite()); + quads.setSimpleTc(index, elementCore.hasSimpleTexCoords()); + } + + isRenderTextureReusable(renderState, renderTextureInfo) { + // @todo: check render coords/matrix, maybe move this to core? + return false; + } + + finishRenderState(renderState) { + } + + setupC2d(canvas) { + const ctx = canvas.getContext('2d'); + canvas.ctx = ctx; + + ctx._scissor = null; + + // Save base state so we can restore the defaults later. + canvas.ctx.save(); + } + +} + +class ImageWorker { + + constructor(options = {}) { + this._items = new Map(); + this._id = 0; + + this._initWorker(); + } + + destroy() { + if (this._worker) { + this._worker.terminate(); + } + } + + _initWorker() { + const code = `(${createWorker.toString()})()`; + const blob = new Blob([code.replace('"use strict";', '')]); // firefox adds "use strict"; to any function which might block worker execution so knock it off + const blobURL = (window.URL ? URL : webkitURL).createObjectURL(blob, { + type: 'application/javascript; charset=utf-8' + }); + this._worker = new Worker(blobURL); + + this._worker.postMessage({type: 'config', config: {path: window.location.href}}); + + this._worker.onmessage = (e) => { + if (e.data && e.data.id) { + const id = e.data.id; + const item = this._items.get(id); + if (item) { + if (e.data.type == 'data') { + this.finish(item, e.data.info); + } else { + this.error(item, e.data.info); + } + } + } + }; + } + + create(src) { + const id = ++this._id; + const item = new ImageWorkerImage(this, id, src); + this._items.set(id, item); + this._worker.postMessage({type: "add", id: id, src: src}); + return item; + } + + cancel(image) { + this._worker.postMessage({type: "cancel", id: image.id}); + this._items.delete(image.id); + } + + error(image, info) { + image.error(info); + this._items.delete(image.id); + } + + finish(image, info) { + image.load(info); + this._items.delete(image.id); + } + +} + +class ImageWorkerImage { + + constructor(manager, id, src) { + this._manager = manager; + this._id = id; + this._src = src; + this._onError = null; + this._onLoad = null; + } + + get id() { + return this._id; + } + + get src() { + return this._src; + } + + set onError(f) { + this._onError = f; + } + + set onLoad(f) { + this._onLoad = f; + } + + cancel() { + this._manager.cancel(this); + } + + load(info) { + if (this._onLoad) { + this._onLoad(info); + } + } + + error(info) { + if (this._onError) { + this._onError(info); + } + } + +} + +/** + * Notice that, within the createWorker function, we must only use ES5 code to keep it ES5-valid after babelifying, as + * the converted code of this section is converted to a blob and used as the js of the web worker thread. + */ +const createWorker = function() { + + function ImageWorkerServer() { + + this.items = new Map(); + + var t = this; + onmessage = function(e) { + t._receiveMessage(e); + }; + + } + + ImageWorkerServer.isPathAbsolute = function(path) { + return /^(?:\/|[a-z]+:\/\/)/.test(path); + }; + + ImageWorkerServer.prototype._receiveMessage = function(e) { + if (e.data.type === 'config') { + this.config = e.data.config; + + var base = this.config.path; + var parts = base.split("/"); + parts.pop(); + this._relativeBase = parts.join("/") + "/"; + + } else if (e.data.type === 'add') { + this.add(e.data.id, e.data.src); + } else if (e.data.type === 'cancel') { + this.cancel(e.data.id); + } + }; + + ImageWorkerServer.prototype.add = function(id, src) { + // Convert relative URLs. + if (!ImageWorkerServer.isPathAbsolute(src)) { + src = this._relativeBase + src; + } + + if (src.substr(0,2) === "//") { + // This doesn't work for image workers. + src = "http:" + src; + } + + var item = new ImageWorkerServerItem(id, src); + const t = this; + item.onFinish = function(result) { + t.finish(item, result); + }; + item.onError = function(info) { + t.error(item, info); + }; + this.items.set(id, item); + item.start(); + }; + + ImageWorkerServer.prototype.cancel = function(id) { + var item = this.items.get(id); + if (item) { + item.cancel(); + this.items.delete(id); + } + }; + + ImageWorkerServer.prototype.finish = function(item, {imageBitmap, hasAlphaChannel}) { + postMessage({ + type: "data", + id: item.id, + info: { + imageBitmap, + hasAlphaChannel + } + }, [imageBitmap]); + this.items.delete(item.id); + }; + + ImageWorkerServer.prototype.error = function(item, {type, message}) { + postMessage({ + type: "error", + id: item.id, + info: { + type, + message + } + }); + this.items.delete(item.id); + }; + + ImageWorkerServer.isWPEBrowser = function() { + return (navigator.userAgent.indexOf("WPE") !== -1); + }; + + function ImageWorkerServerItem(id, src) { + + this._onError = undefined; + this._onFinish = undefined; + this._id = id; + this._src = src; + this._xhr = undefined; + this._mimeType = undefined; + this._canceled = false; + + } + + Object.defineProperty(ImageWorkerServerItem.prototype, 'id', { + get: function() { + return this._id; + } + }); + + Object.defineProperty(ImageWorkerServerItem.prototype, 'onFinish', { + get: function() { + return this._onFinish; + }, + set: function(f) { + this._onFinish = f; + } + }); + + Object.defineProperty(ImageWorkerServerItem.prototype, 'onError', { + get: function() { + return this._onError; + }, + set: function(f) { + this._onError = f; + } + }); + + ImageWorkerServerItem.prototype.start = function() { + this._xhr = new XMLHttpRequest(); + this._xhr.open("GET", this._src, true); + this._xhr.responseType = "blob"; + + var t = this; + this._xhr.onerror = function(oEvent) { + t.error({type: "connection", message: "Connection error"}); + }; + + this._xhr.onload = function(oEvent) { + var blob = t._xhr.response; + t._mimeType = blob.type; + + t._createImageBitmap(blob); + }; + + this._xhr.send(); + }; + + ImageWorkerServerItem.prototype._createImageBitmap = function(blob) { + var t = this; + createImageBitmap(blob, {premultiplyAlpha: 'premultiply', colorSpaceConversion: 'none', imageOrientation: 'none'}).then(function(imageBitmap) { + t.finish({ + imageBitmap, + hasAlphaChannel: t._hasAlphaChannel() + }); + }).catch(function(e) { + t.error({type: "parse", message: "Error parsing image data"}); + }); + }; + + ImageWorkerServerItem.prototype._hasAlphaChannel = function() { + if (ImageWorkerServer.isWPEBrowser()) { + // When using unaccelerated rendering image (https://github.com/WebPlatformForEmbedded/WPEWebKit/blob/wpe-20170728/Source/WebCore/html/ImageBitmap.cpp#L52), + // everything including JPG images are in RGBA format. Upload is way faster when using an alpha channel. + // @todo: after hardware acceleration is fixed and re-enabled, JPG should be uploaded in RGB to get the best possible performance and memory usage. + return true; + } else { + return (this._mimeType.indexOf("image/png") !== -1); + } + }; + + ImageWorkerServerItem.prototype.cancel = function() { + if (this._canceled) return; + if (this._xhr) { + this._xhr.abort(); + } + this._canceled = true; + }; + + ImageWorkerServerItem.prototype.error = function(type, message) { + if (!this._canceled && this._onError) { + this._onError({type, message}); + } + }; + + ImageWorkerServerItem.prototype.finish = function(info) { + if (!this._canceled && this._onFinish) { + this._onFinish(info); + } + }; + + var worker = new ImageWorkerServer(); +}; + +/** + * Platform-specific functionality. + * Copyright Metrological, 2017; + */ +class WebPlatform { + + init(stage) { + this.stage = stage; + this._looping = false; + this._awaitingLoop = false; + + if (this.stage.getOption("useImageWorker")) { + if (!window.createImageBitmap || !window.Worker) { + console.warn("Can't use image worker because browser does not have createImageBitmap and Web Worker support"); + } else { + console.log('Using image worker!'); + this._imageWorker = new ImageWorker(); + } + } + } + + destroy() { + if (this._imageWorker) { + this._imageWorker.destroy(); + } + this._removeKeyHandler(); + } + + startLoop() { + this._looping = true; + if (!this._awaitingLoop) { + this.loop(); + } + } + + stopLoop() { + this._looping = false; + } + + loop() { + let self = this; + let lp = function() { + self._awaitingLoop = false; + if (self._looping) { + self.stage.drawFrame(); + requestAnimationFrame(lp); + self._awaitingLoop = true; + } + }; + requestAnimationFrame(lp); + } + + uploadGlTexture(gl, textureSource, source, options) { + if (source instanceof ImageData || source instanceof HTMLImageElement || source instanceof HTMLCanvasElement || source instanceof HTMLVideoElement || (window.ImageBitmap && source instanceof ImageBitmap)) { + // Web-specific data types. + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, options.format, options.type, source); + } else { + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, textureSource.w, textureSource.h, 0, options.format, options.type, source); + } + } + + loadSrcTexture({src, hasAlpha}, cb) { + let cancelCb = undefined; + let isPng = (src.indexOf(".png") >= 0); + if (this._imageWorker) { + // WPE-specific image parser. + const image = this._imageWorker.create(src); + image.onError = function(err) { + return cb("Image load error"); + }; + image.onLoad = function({imageBitmap, hasAlphaChannel}) { + cb(null, { + source: imageBitmap, + renderInfo: {src: src}, + hasAlpha: hasAlphaChannel, + premultiplyAlpha: true + }); + }; + cancelCb = function() { + image.cancel(); + }; + } else { + let image = new Image(); + if (!(src.substr(0,5) == "data:")) { + // Base64. + image.crossOrigin = "Anonymous"; + } + image.onerror = function(err) { + // Ignore error message when cancelled. + if (image.src) { + return cb("Image load error"); + } + }; + image.onload = function() { + cb(null, { + source: image, + renderInfo: {src: src}, + hasAlpha: isPng || hasAlpha + }); + }; + image.src = src; + + cancelCb = function() { + image.onerror = null; + image.onload = null; + image.removeAttribute('src'); + }; + } + + return cancelCb; + } + + createWebGLContext(w, h) { + let canvas = this.stage.getOption('canvas') || document.createElement('canvas'); + + if (w && h) { + canvas.width = w; + canvas.height = h; + } + + let opts = { + alpha: true, + antialias: false, + premultipliedAlpha: true, + stencil: true, + preserveDrawingBuffer: false + }; + + let gl = canvas.getContext('webgl', opts) || canvas.getContext('experimental-webgl', opts); + if (!gl) { + throw new Error('This browser does not support webGL.'); + } + + return gl; + } + + createCanvasContext(w, h) { + let canvas = this.stage.getOption('canvas') || document.createElement('canvas'); + + if (w && h) { + canvas.width = w; + canvas.height = h; + } + + let c2d = canvas.getContext('2d'); + if (!c2d) { + throw new Error('This browser does not support 2d canvas.'); + } + + return c2d; + } + + getHrTime() { + return window.performance ? window.performance.now() : (new Date()).getTime(); + } + + getDrawingCanvas() { + // We can't reuse this canvas because textures may load async. + return document.createElement('canvas'); + } + + getTextureOptionsForDrawingCanvas(canvas) { + let options = {}; + options.source = canvas; + return options; + } + + nextFrame(changes) { + /* WebGL blits automatically */ + } + + registerKeyHandler(keyhandler) { + this._keyListener = e => { + keyhandler(e); + }; + window.addEventListener('keydown', this._keyListener); + } + + _removeKeyHandler() { + if (this._keyListener) { + window.removeEventListener('keydown', this._keyListener); + } + } + +} + +class PlatformLoader { + static load(options) { + if (options.platform) { + return options.platform; + } else { + if (Utils.isWeb) { + return WebPlatform; + } else { + throw new Error("You must specify the platform class to be used."); + } + } + } +} + +class Utils$1 { + + static isFunction(value) { + return typeof value === 'function'; + } + + static isNumber(value) { + return typeof value === 'number'; + } + + static isInteger(value) { + return (typeof value === 'number' && (value % 1) === 0); + } + + static isBoolean(value) { + return value === true || value === false; + } + + static isString(value) { + return typeof value == 'string'; + } + + static isObject(value) { + let type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + static isPlainObject(value) { + let type = typeof value; + return !!value && (type == 'object'); + } + + static isObjectLiteral(value){ + return typeof value === 'object' && value && value.constructor === Object + } + + static getArrayIndex(index, arr) { + return Utils$1.getModuloIndex(index, arr.length); + } + + static equalValues(v1, v2) { + if ((typeof v1) !== (typeof v2)) return false + if (Utils$1.isObjectLiteral(v1)) { + return Utils$1.isObjectLiteral(v2) && Utils$1.equalObjectLiterals(v1, v2) + } else if (Array.isArray(v1)) { + return Array.isArray(v2) && Utils$1.equalArrays(v1, v2) + } else { + return v1 === v2 + } + } + + static equalObjectLiterals(obj1, obj2) { + let keys1 = Object.keys(obj1); + let keys2 = Object.keys(obj2); + if (keys1.length !== keys2.length) { + return false + } + + for (let i = 0, n = keys1.length; i < n; i++) { + const k1 = keys1[i]; + const k2 = keys2[i]; + if (k1 !== k2) { + return false + } + + const v1 = obj1[k1]; + const v2 = obj2[k2]; + + if (!Utils$1.equalValues(v1, v2)) { + return false + } + } + + return true; + } + + static equalArrays(v1, v2) { + if (v1.length !== v2.length) { + return false + } + for (let i = 0, n = v1.length; i < n; i++) { + if (!this.equalValues(v1[i], v2[i])) { + return false + } + } + + return true + } + +} + +/** + * Maintains the state of a WebGLRenderingContext. + */ +class WebGLState { + + constructor(id, gl) { + this._id = id; + this._gl = gl; + this._program = undefined; + this._buffers = new Map(); + this._framebuffers = new Map(); + this._renderbuffers = new Map(); + + // Contains vertex attribute definition arrays (enabled, size, type, normalized, stride, offset). + this._vertexAttribs = new Array(16); + this._nonDefaultFlags = new Set(); + this._settings = new Map(); + this._textures = new Array(8); + this._maxTexture = 0; + this._activeTexture = gl.TEXTURE0; + this._pixelStorei = new Array(5); + } + + _getDefaultFlag(cap) { + return (cap === this._gl.DITHER); + } + + setFlag(cap, v) { + const def = this._getDefaultFlag(cap); + if (v === def) { + return this._nonDefaultFlags.delete(cap); + } else { + if (!this._nonDefaultFlags.has(cap)) { + this._nonDefaultFlags.add(cap); + return true; + } else { + return false; + } + } + } + + setBuffer(target, buffer) { + const change = this._buffers.get(target) !== buffer; + this._buffers.set(target, buffer); + + if (change && (target === this._gl.ARRAY_BUFFER)) { + // When the array buffer is changed all attributes are cleared. + this._vertexAttribs = []; + } + + return change; + } + + setFramebuffer(target, buffer) { + const change = this._framebuffers.get(target) !== buffer; + this._framebuffers.set(target, buffer); + return change; + } + + setRenderbuffer(target, buffer) { + const change = this._renderbuffers.get(target) !== buffer; + this._renderbuffers.set(target, buffer); + return change; + } + + setProgram(program) { + const change = this._program !== program; + this._program = program; + return change + } + + setSetting(func, v) { + const s = this._settings.get(func); + const change = !s || !Utils$1.equalValues(s, v); + this._settings.set(func, v); + return change + } + + disableVertexAttribArray(index) { + const va = this._vertexAttribs[index]; + if (va && va[5]) { + va[5] = false; + return true; + } + return false; + } + + enableVertexAttribArray(index) { + const va = this._vertexAttribs[index]; + if (va) { + if (!va[0]) { + va[0] = true; + return true; + } + } else { + this._vertexAttribs[index] = [0, 0, 0, 0, 0, true]; + return true; + } + return false; + } + + vertexAttribPointer(index, props) { + let va = this._vertexAttribs[index]; + let equal = false; + if (va) { + equal = va[0] === props[0] && + va[1] === props[1] && + va[2] === props[2] && + va[3] === props[3] && + va[4] === props[4]; + } + + if (equal) { + return false; + } else { + props[5] = va ? va[5] : false; + return true; + } + } + + setActiveTexture(texture) { + const changed = this._activeTexture !== texture; + this._activeTexture = texture; + return changed; + } + + bindTexture(target, texture) { + const activeIndex = WebGLState._getTextureIndex(this._activeTexture); + this._maxTexture = Math.max(this._maxTexture, activeIndex + 1); + const current = this._textures[activeIndex]; + const targetIndex = WebGLState._getTextureTargetIndex(target); + if (current) { + if (current[targetIndex] === texture) { + return false; + } + current[targetIndex] = texture; + return true; + } else { + if (texture) { + this._textures[activeIndex] = []; + this._textures[activeIndex][targetIndex] = texture; + return true + } else { + return false + } + } + } + + setPixelStorei(pname, param) { + const i = WebGLState._getPixelStoreiIndex(pname); + const change = !Utils$1.equalValues(this._pixelStorei[i], param); + this._pixelStorei[i] = param; + return change; + } + + migrate(s) { + const t = this; + + // Warning: migrate should call the original prototype methods directly. + + this._migrateFlags(t, s); + + // useProgram + if (s._program !== t._program) { + this._gl._useProgram(s._program); + } + + this._migrateFramebuffers(t, s); + this._migrateRenderbuffers(t, s); + + const buffersChanged = this._migrateBuffers(t, s); + this._migrateAttributes(t, s, buffersChanged); + + this._migrateFlags(t, s); + + this._migrateSettings(t, s); + + this._migratePixelStorei(t, s); + + this._migrateTextures(t, s); + + } + + _migratePixelStorei(t, s) { + for (let i = 0, n = t._pixelStorei.length; i < n; i++) { + if (t._pixelStorei[i] !== s._pixelStorei[i]) { + const value = s._pixelStorei[i] !== undefined ? s._pixelStorei[i] : WebGLState._getDefaultPixelStoreiByIndex(i); + this._gl._pixelStorei(WebGLState._getPixelStoreiByIndex(i), value); + } + } + } + + _migrateTextures(t, s) { + const max = Math.max(t._maxTexture, s._maxTexture); + + let activeTexture = t._activeTexture; + + for (let i = 0; i < max; i++) { + const sTargets = s._textures[i]; + const tTargets = t._textures[i]; + const textureNumb = WebGLState._getTextureByIndex(i); + + const targetMax = Math.max(tTargets ? tTargets.length : 0, sTargets ? sTargets.length : 0); + for (let j = 0, n = targetMax; j < n; j++) { + const target = WebGLState._getTextureTargetByIndex(j); + if (activeTexture !== textureNumb) { + this._gl._activeTexture(textureNumb); + activeTexture = textureNumb; + } + + const texture = (sTargets && sTargets[j]) || null; + this._gl._bindTexture(target, texture); + } + } + + if (s._activeTexture !== activeTexture) { + this._gl._activeTexture(s._activeTexture); + } + } + + _migrateBuffers(t, s) { + s._buffers.forEach((framebuffer, target) => { + if (t._buffers.get(target) !== framebuffer) { + this._gl._bindBuffer(target, framebuffer); + } + }); + + t._buffers.forEach((buffer, target) => { + const b = s._buffers.get(target); + if (b === undefined) { + this._gl._bindBuffer(target, null); + } + }); + return (s._buffers.get(this._gl.ARRAY_BUFFER) !== t._buffers.get(this._gl.ARRAY_BUFFER)) + } + + _migrateFramebuffers(t, s) { + s._framebuffers.forEach((framebuffer, target) => { + if (t._framebuffers.get(target) !== framebuffer) { + this._gl._bindFramebuffer(target, framebuffer); + } + }); + + t._framebuffers.forEach((framebuffer, target) => { + const fb = s._framebuffers.get(target); + if (fb === undefined) { + this._gl._bindFramebuffer(target, null); + } + }); + } + + _migrateRenderbuffers(t, s) { + s._renderbuffers.forEach((renderbuffer, target) => { + if (t._renderbuffers.get(target) !== renderbuffer) { + this._gl._bindRenderbuffer(target, renderbuffer); + } + }); + + t._renderbuffers.forEach((renderbuffer, target) => { + const fb = s._renderbuffers.get(target); + if (fb === undefined) { + this._gl._bindRenderbuffer(target, null); + } + }); + } + + _migrateAttributes(t, s, buffersChanged) { + + if (!buffersChanged) { + t._vertexAttribs.forEach((attrib, index) => { + if (!s._vertexAttribs[index]) { + // We can't 'delete' a vertex attrib so we'll disable it. + this._gl._disableVertexAttribArray(index); + } + }); + + s._vertexAttribs.forEach((attrib, index) => { + this._gl._vertexAttribPointer(index, attrib[0], attrib[1], attrib[2], attrib[4]); + if (attrib[5]) { + this._gl._enableVertexAttribArray(index); + } else { + this._gl._disableVertexAttribArray(index); + } + }); + } else { + // When buffers are changed, previous attributes were reset automatically. + s._vertexAttribs.forEach((attrib, index) => { + if (attrib[0]) { + // Do not set vertex attrib pointer when it was just the default value. + this._gl._vertexAttribPointer(index, attrib[0], attrib[1], attrib[2], attrib[3], attrib[4]); + } + if (attrib[5]) { + this._gl._enableVertexAttribArray(index); + } + }); + } + } + + _migrateSettings(t, s) { + const defaults = this.constructor.getDefaultSettings(); + t._settings.forEach((value, func) => { + const name = func.name || func.xname; + if (!s._settings.has(func)) { + let args = defaults.get(name); + if (Utils$1.isFunction(args)) { + args = args(this._gl); + } + // We are actually setting the setting for optimization purposes. + s._settings.set(func, args); + func.apply(this._gl, args); + } + }); + s._settings.forEach((value, func) => { + const tValue = t._settings.get(func); + if (!tValue || !Utils$1.equalValues(tValue, value)) { + func.apply(this._gl, value); + } + }); + } + + _migrateFlags(t, s) { + t._nonDefaultFlags.forEach(setting => { + if (!s._nonDefaultFlags.has(setting)) { + if (this._getDefaultFlag(setting)) { + this._gl._enable(setting); + } else { + this._gl._disable(setting); + } + } + }); + s._nonDefaultFlags.forEach(setting => { + if (!t._nonDefaultFlags.has(setting)) { + if (this._getDefaultFlag(setting)) { + this._gl._disable(setting); + } else { + this._gl._enable(setting); + } + } + }); + } + + static getDefaultSettings() { + if (!this._defaultSettings) { + this._defaultSettings = new Map(); + const d = this._defaultSettings; + const g = WebGLRenderingContext.prototype; + d.set("viewport", function(gl) {return [0,0,gl.canvas.width, gl.canvas.height]}); + d.set("scissor", function(gl) {return [0,0,gl.canvas.width, gl.canvas.height]}); + d.set("blendColor", [0, 0, 0, 0]); + d.set("blendEquation", [g.FUNC_ADD]); + d.set("blendEquationSeparate", [g.FUNC_ADD, g.FUNC_ADD]); + d.set("blendFunc", [g.ONE, g.ZERO]); + d.set("blendFuncSeparate", [g.ONE, g.ZERO, g.ONE, g.ZERO]); + d.set("clearColor", [0, 0, 0, 0]); + d.set("clearDepth", [1]); + d.set("clearStencil", [0]); + d.set("colorMask", [true, true, true, true]); + d.set("cullFace", [g.BACK]); + d.set("depthFunc", [g.LESS]); + d.set("depthMask", [true]); + d.set("depthRange", [0, 1]); + d.set("frontFace", [g.CCW]); + d.set("lineWidth", [1]); + d.set("polygonOffset", [0, 0]); + d.set("sampleCoverage", [1, false]); + d.set("stencilFunc", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateFront", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateBack", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateFrontAndBack", [g.ALWAYS, 0, 1]); + d.set("stencilMask", [1]); + d.set("_stencilMaskSeparateFront", [1]); + d.set("_stencilMaskSeparateBack", [1]); + d.set("_stencilMaskSeparateFrontAndBack", [1]); + d.set("stencilOp", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateFront", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateBack", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateFrontAndBack", [g.KEEP, g.KEEP, g.KEEP]); + d.set("vertexAttrib1f", []); + d.set("vertexAttrib1fv", []); + d.set("vertexAttrib2f", []); + d.set("vertexAttrib2fv", []); + d.set("vertexAttrib3f", []); + d.set("vertexAttrib3fv", []); + d.set("vertexAttrib4f", []); + d.set("vertexAttrib4fv", []); + } + return this._defaultSettings + } + + static _getTextureTargetIndex(target) { + switch(target) { + case 0x0DE1: + /* TEXTURE_2D */ + return 0; + case 0x8513: + /* TEXTURE_CUBE_MAP */ + return 1; + default: + // Shouldn't happen. + throw new Error('Unknown texture target: ' + target); + } + } + + static _getTextureTargetByIndex(index) { + if (!this._textureTargetIndices) { + this._textureTargetIndices = [0x0DE1, 0x8513]; + } + return this._textureTargetIndices[index] + } + + static _getTextureIndex(index) { + return index - 0x84C0 /* GL_TEXTURE0 */; + } + + static _getTextureByIndex(index) { + return index + 0x84C0; + } + + static _getPixelStoreiIndex(pname) { + switch(pname) { + case 0x0D05: + /* PACK_ALIGNMENT */ + return 0; + case 0x0CF5: + /* UNPACK_ALIGNMENT */ + return 1; + case 0x9240: + /* UNPACK_FLIP_Y_WEBGL */ + return 2; + case 0x9241: + /* UNPACK_PREMULTIPLY_ALPHA_WEBGL */ + return 3; + case 0x9243: + /* UNPACK_COLORSPACE_CONVERSION_WEBGL */ + return 4; + //@todo: support WebGL2 properties, see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/pixelStorei + case 0x9245: + /* UNPACK_FLIP_BLUE_RED */ + return 5; + default: + // Shouldn't happen. + throw new Error('Unknown pixelstorei: ' + pname); + } + } + + static _getPixelStoreiByIndex(index) { + if (!this._pixelStoreiIndices) { + this._pixelStoreiIndices = [0x0D05, 0x0CF5, 0x9240, 0x9241, 0x9243]; + } + return this._pixelStoreiIndices[index] + } + + static _getDefaultPixelStoreiByIndex(index) { + if (!this._pixelStoreiDefaults) { + this._pixelStoreiDefaults = [4, 4, false, false, WebGLRenderingContext.prototype.BROWSER_DEFAULT_WEBGL]; + } + return this._pixelStoreiDefaults[index] + } +} + +class WebGLStateManager { + + _initStateManager(id = "default") { + this._states = {}; + this._state = this._getState(id); + } + + _getState(id) { + if (!this._states[id]) { + this._states[id] = new WebGLState(id, this); + } + return this._states[id]; + } + + switchState(id = "default") { + if (this._state._id !== id) { + const newState = this._getState(id); + this._state.migrate(newState); + this._state = newState; + } + } + + $useProgram(program) { + if (this._state.setProgram(program)) + this._useProgram(program); + } + + $bindBuffer(target, fb) { + if (this._state.setBuffer(target, fb)) + this._bindBuffer(target, fb); + } + + $bindFramebuffer(target, fb) { + if (this._state.setFramebuffer(target, fb)) + this._bindFramebuffer(target, fb); + } + + $bindRenderbuffer(target, fb) { + if (this._state.setRenderbuffer(target, fb)) + this._bindRenderbuffer(target, fb); + } + + $enable(cap) { + if (this._state.setFlag(cap, true)) + this._enable(cap); + } + + $disable(cap) { + if (this._state.setFlag(cap, false)) + this._disable(cap); + } + + $viewport(x, y, w, h) { + if (this._state.setSetting(this._viewport, [x, y, w, h])) + this._viewport(x, y, w, h); + } + + $scissor(x, y, w, h) { + if (this._state.setSetting(this._scissor, [x, y, w, h])) + this._scissor(x, y, w, h); + } + + $disableVertexAttribArray(index) { + if (this._state.disableVertexAttribArray(index)) + this._disableVertexAttribArray(index); + } + + $enableVertexAttribArray(index) { + if (this._state.enableVertexAttribArray(index)) + this._enableVertexAttribArray(index); + } + + $vertexAttribPointer(index, size, type, normalized, stride, offset) { + if (this._state.vertexAttribPointer(index, [size, type, normalized, stride, offset])) + this._vertexAttribPointer(index, size, type, normalized, stride, offset); + } + + $activeTexture(texture) { + if (this._state.setActiveTexture(texture)) + this._activeTexture(texture); + } + + $bindTexture(target, texture) { + if (this._state.bindTexture(target, texture)) + this._bindTexture(target, texture); + } + + $pixelStorei(pname, param) { + if (this._state.setPixelStorei(pname, param)) { + this._pixelStorei(pname, param); + } + } + + $stencilFuncSeparate(face, func, ref, mask) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilFuncSeparateFront; + break; + case this.BACK: + f = this._stencilFuncSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilFuncSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [func, ref, mask])) + f.apply(this, [func, ref, mask]); + } + + _stencilFuncSeparateFront(func, ref, mask) { + this._stencilFuncSeparate(this.FRONT, func, ref, mask); + } + + _stencilFuncSeparateBack(func, ref, mask) { + this._stencilFuncSeparate(this.BACK, func, ref, mask); + } + + _stencilFuncSeparateFrontAndBack(func, ref, mask) { + this._stencilFuncSeparate(this.FRONT_AND_BACK, func, ref, mask); + } + + $stencilMaskSeparate(face, mask) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilMaskSeparateFront; + break; + case this.BACK: + f = this._stencilMaskSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilMaskSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [mask])) + f.apply(this, [mask]); + } + + _stencilMaskSeparateFront(mask) { + this._stencilMaskSeparate(this.FRONT, mask); + } + + _stencilMaskSeparateBack(mask) { + this._stencilMaskSeparate(this.BACK, mask); + } + + _stencilMaskSeparateFrontAndBack(mask) { + this._stencilMaskSeparate(this.FRONT_AND_BACK, mask); + } + + $stencilOpSeparate(face, fail, zfail, zpass) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilOpSeparateFront; + break; + case this.BACK: + f = this._stencilOpSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilOpSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [fail, zfail, zpass])) + f.apply(this, [fail, zfail, zpass]); + } + + _stencilOpSeparateFront(fail, zfail, zpass) { + this._stencilOpSeparate(this.FRONT, fail, zfail, zpass); + } + + _stencilOpSeparateBack(fail, zfail, zpass) { + this._stencilOpSeparate(this.BACK, fail, zfail, zpass); + } + + _stencilOpSeparateFrontAndBack(fail, zfail, zpass) { + this._stencilOpSeparate(this.FRONT_AND_BACK, fail, zfail, zpass); + } + + $blendColor(red, green, blue, alpha) { + if (this._state.setSetting(this._blendColor, [red, green, blue, alpha])) + this._blendColor(red, green, blue, alpha); + } + + $blendEquation(mode) { + if (this._state.setSetting(this._blendEquation, [mode])) + this._blendEquation(mode); + } + + $blendEquationSeparate(modeRGB, modeAlpha) { + if (this._state.setSetting(this._blendEquationSeparate, [modeRGB, modeAlpha])) + this._blendEquationSeparate(modeRGB, modeAlpha); + } + + $blendFunc(sfactor, dfactor) { + if (this._state.setSetting(this._blendFunc, [sfactor, dfactor])) + this._blendFunc(sfactor, dfactor); + } + + $blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha) { + if (this._state.setSetting(this._blendFuncSeparate, [srcRGB, dstRGB, srcAlpha, dstAlpha])) + this._blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + } + + $clearColor(red, green, blue, alpha) { + if (this._state.setSetting(this._clearColor, [red, green, blue, alpha])) + this._clearColor(red, green, blue, alpha); + } + + $clearDepth(depth) { + if (this._state.setSetting(this._clearDepth, [depth])) + this._clearDepth(depth); + } + + $clearStencil(s) { + if (this._state.setSetting(this._clearStencil, [s])) + this._clearStencil(s); + } + + $colorMask(red, green, blue, alpha) { + if (this._state.setSetting(this._colorMask, [red, green, blue, alpha])) + this._colorMask(red, green, blue, alpha); + } + + $cullFace(mode) { + if (this._state.setSetting(this._cullFace, [mode])) + this._cullFace(mode); + } + + $depthFunc(func) { + if (this._state.setSetting(this._depthFunc, [func])) + this._depthFunc(func); + } + + $depthMask(flag) { + if (this._state.setSetting(this._depthMask, [flag])) + this._depthMask(flag); + } + + $depthRange(zNear, zFar) { + if (this._state.setSetting(this._depthRange, [zNear, zFar])) + this._depthRange(zNear, zFar); + } + + $frontFace(mode) { + if (this._state.setSetting(this._frontFace, [mode])) + this._frontFace(mode); + } + + $lineWidth(width) { + if (this._state.setSetting(this._lineWidth, [width])) + this._lineWidth(width); + } + + $polygonOffset(factor, units) { + if (this._state.setSetting(this._polygonOffset, [factor, units])) + this._polygonOffset(factor, units); + } + + $sampleCoverage(value, invert) { + if (this._state.setSetting(this._sampleCoverage, [value, invert])) + this._sampleCoverage(value, invert); + } + + $stencilFunc(func, ref, mask) { + if (this._state.setSetting(this._stencilFunc, [func, ref, mask])) + this._stencilFunc(func, ref, mask); + } + + $stencilMask(mask) { + if (this._state.setSetting(this._stencilMask, [mask])) + this._stencilMask(mask); + } + + $stencilOp(fail, zfail, zpass) { + if (this._state.setSetting(this._stencilOp, [fail, zfail, zpass])) + this._stencilOp(fail, zfail, zpass); + } + + $vertexAttrib1f(indx, x) { + if (this._state.setSetting(this._vertexAttrib1f, [indx, x])) + this._vertexAttrib1f(indx, x); + } + + $vertexAttrib1fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib1fv, [indx, values])) + this._vertexAttrib1fv(indx, values); + } + + $vertexAttrib2f(indx, x, y) { + if (this._state.setSetting(this._vertexAttrib2f, [indx, x, y])) + this._vertexAttrib2f(indx, x, y); + } + + $vertexAttrib2fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib2fv, [indx, values])) + this._vertexAttrib2fv(indx, values); + } + + $vertexAttrib3f(indx, x, y, z) { + if (this._state.setSetting(this._vertexAttrib3f, [indx, x, y, z])) + this._vertexAttrib3f(indx, x, y, z); + } + + $vertexAttrib3fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib3fv, [indx, values])) + this._vertexAttrib3fv(indx, values); + } + + $vertexAttrib4f(indx, x, y, z, w) { + if (this._state.setSetting(this._vertexAttrib4f, [indx, x, y, z, w])) + this._vertexAttrib4f(indx, x, y, z, w); + } + + $vertexAttrib4fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib4fv, [indx, values])) + this._vertexAttrib4fv(indx, values); + } + + /** + * Sets up the rendering context for context sharing. + * @param {WebGLRenderingContext} gl + * @param {string} id + */ + static enable(gl, id = "default") { + const names = Object.getOwnPropertyNames(WebGLStateManager.prototype); + const WebGLRenderingContextProto = gl.__proto__; + names.forEach(name => { + if (name !== "constructor") { + const method = WebGLStateManager.prototype[name]; + if (name.charAt(0) === "$") { + name = name.substr(1); + } + if (gl[name]) { + if (!gl[name].name) { + // We do this for compatibility with the Chrome WebGL Inspector plugin. + gl[name].xname = name; + } + gl['_' + name] = gl[name]; + } + gl[name] = method; + } + }); + + WebGLStateManager.prototype._initStateManager.call(gl, id); + + return gl; + } + +} + +class TextureManager { + + constructor(stage) { + this.stage = stage; + + /** + * The currently used amount of texture memory. + * @type {number} + */ + this._usedMemory = 0; + + /** + * All uploaded texture sources. + * @type {TextureSource[]} + */ + this._uploadedTextureSources = []; + + /** + * The texture source lookup id to texture source hashmap. + * @type {Map} + */ + this.textureSourceHashmap = new Map(); + + } + + get usedMemory() { + return this._usedMemory; + } + + destroy() { + for (let i = 0, n = this._uploadedTextureSources.length; i < n; i++) { + this._nativeFreeTextureSource(this._uploadedTextureSources[i]); + } + + this.textureSourceHashmap.clear(); + this._usedMemory = 0; + } + + getReusableTextureSource(id) { + return this.textureSourceHashmap.get(id); + } + + getTextureSource(func, id) { + // Check if texture source is already known. + let textureSource = id ? this.textureSourceHashmap.get(id) : null; + if (!textureSource) { + // Create new texture source. + textureSource = new TextureSource(this, func); + + if (id) { + textureSource.lookupId = id; + this.textureSourceHashmap.set(id, textureSource); + } + } + + return textureSource; + } + + uploadTextureSource(textureSource, options) { + if (textureSource.isLoaded()) return; + + this._addMemoryUsage(textureSource.w * textureSource.h); + + // Load texture. + const nativeTexture = this._nativeUploadTextureSource(textureSource, options); + + textureSource._nativeTexture = nativeTexture; + + // We attach w and h to native texture (we need it in CoreRenderState._isRenderTextureReusable). + nativeTexture.w = textureSource.w; + nativeTexture.h = textureSource.h; + + nativeTexture.update = this.stage.frameCounter; + + this._uploadedTextureSources.push(textureSource); + + this.addToLookupMap(textureSource); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + this.stage.addMemoryUsage(delta); + } + + addToLookupMap(textureSource) { + const lookupId = textureSource.lookupId; + if (lookupId) { + if (!this.textureSourceHashmap.has(lookupId)) { + this.textureSourceHashmap.set(lookupId, textureSource); + } + } + } + + gc() { + this.freeUnusedTextureSources(); + this._cleanupLookupMap(); + } + + freeUnusedTextureSources() { + let remainingTextureSources = []; + for (let i = 0, n = this._uploadedTextureSources.length; i < n; i++) { + let ts = this._uploadedTextureSources[i]; + if (ts.allowCleanup()) { + this._freeManagedTextureSource(ts); + } else { + remainingTextureSources.push(ts); + } + } + + this._uploadedTextureSources = remainingTextureSources; + + this._cleanupLookupMap(); + } + + _freeManagedTextureSource(textureSource) { + if (textureSource.isLoaded()) { + this._nativeFreeTextureSource(textureSource); + this._addMemoryUsage(-textureSource.w * textureSource.h); + } + + // Should be reloaded. + textureSource.loadingSince = null; + } + + _cleanupLookupMap() { + // We keep those that still have value (are being loaded or already loaded, or are likely to be reused). + this.textureSourceHashmap.forEach((textureSource, lookupId) => { + if (!(textureSource.isLoaded() || textureSource.isLoading()) && !textureSource.isUsed()) { + this.textureSourceHashmap.delete(lookupId); + } + }); + } + + /** + * Externally free texture source. + * @param textureSource + */ + freeTextureSource(textureSource) { + const index = this._uploadedTextureSources.indexOf(textureSource); + const managed = (index !== -1); + + if (textureSource.isLoaded()) { + if (managed) { + this._addMemoryUsage(-textureSource.w * textureSource.h); + this._uploadedTextureSources.splice(index, 1); + } + this._nativeFreeTextureSource(textureSource); + } + + // Should be reloaded. + textureSource.loadingSince = null; + } + + _nativeUploadTextureSource(textureSource, options) { + return this.stage.renderer.uploadTextureSource(textureSource, options); + } + + _nativeFreeTextureSource(textureSource) { + this.stage.renderer.freeTextureSource(textureSource); + textureSource.clearNativeTexture(); + } + +} + +/** + * Allows throttling of loading texture sources, keeping the app responsive. + */ +class TextureThrottler { + + constructor(stage) { + this.stage = stage; + + this.genericCancelCb = (textureSource) => { + this._remove(textureSource); + }; + + this._sources = []; + this._data = []; + } + + destroy() { + this._sources = []; + this._data = []; + } + + processSome() { + if (this._sources.length) { + const start = Date.now(); + do { + this._processItem(); + } while(this._sources.length && (Date.now() - start < TextureThrottler.MAX_UPLOAD_TIME_PER_FRAME)); + } + } + + _processItem() { + const source = this._sources.pop(); + const data = this._data.pop(); + if (source.isLoading()) { + source.processLoadedSource(data); + } + } + + add(textureSource, data) { + this._sources.push(textureSource); + this._data.push(data); + } + + _remove(textureSource) { + const index = this._sources.indexOf(textureSource); + if (index >= 0) { + this._sources.splice(index, 1); + this._data.splice(index, 1); + } + } + +} + +TextureThrottler.MAX_UPLOAD_TIME_PER_FRAME = 10; + +class CoreContext { + + constructor(stage) { + this.stage = stage; + + this.root = null; + + this.updateTreeOrder = 0; + + this.renderState = this.stage.renderer.createCoreRenderState(this); + + this.renderExec = this.stage.renderer.createCoreRenderExecutor(this); + this.renderExec.init(); + + this._usedMemory = 0; + this._renderTexturePool = []; + + this._renderTextureId = 1; + + this._zSorts = []; + } + + get usedMemory() { + return this._usedMemory; + } + + destroy() { + this._renderTexturePool.forEach(texture => this._freeRenderTexture(texture)); + this._usedMemory = 0; + } + + hasRenderUpdates() { + return !!this.root._parent._hasRenderUpdates; + } + + render() { + // Clear flag to identify if anything changes before the next frame. + this.root._parent._hasRenderUpdates = 0; + + this._render(); + } + + update() { + this._update(); + + // Due to the boundsVisibility flag feature (and onAfterUpdate hook), it is possible that other elements were + // changed during the update loop (for example due to the txLoaded event). We process these changes immediately + // (but not recursively to prevent infinite loops). + if (this.root._hasUpdates) { + this._update(); + } + + this._performForcedZSorts(); + } + + /** + * Certain ElementCore items may be forced to zSort to strip out references to prevent memleaks.. + */ + _performForcedZSorts() { + const n = this._zSorts.length; + if (n) { + // Forced z-sorts (ElementCore may force a z-sort in order to free memory/prevent memory leaks). + for (let i = 0, n = this._zSorts.length; i < n; i++) { + if (this._zSorts[i].zSort) { + this._zSorts[i].sortZIndexedChildren(); + } + } + this._zSorts = []; + } + } + + _update() { + this.updateTreeOrder = 0; + + this.root.update(); + } + + _render() { + // Obtain a sequence of the quad operations. + this._fillRenderState(); + + if (this.stage.getOption('readPixelsBeforeDraw')) { + const pixels = new Uint8Array(4); + const gl = this.stage.gl; + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + } + + // Now run them with the render executor. + this._performRender(); + } + + _fillRenderState() { + this.renderState.reset(); + this.root.render(); + this.renderState.finish(); + } + + _performRender() { + this.renderExec.execute(); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + this.stage.addMemoryUsage(delta); + } + + allocateRenderTexture(w, h) { + let prec = this.stage.getRenderPrecision(); + let pw = Math.max(1, Math.round(w * prec)); + let ph = Math.max(1, Math.round(h * prec)); + + // Search last item first, so that last released render texture is preferred (may cause memory cache benefits). + const n = this._renderTexturePool.length; + for (let i = n - 1; i >= 0; i--) { + const texture = this._renderTexturePool[i]; + // We don't want to reuse the same render textures within the same frame because that will create gpu stalls. + if (texture.w === pw && texture.h === ph && (texture.update !== this.stage.frameCounter)) { + texture.f = this.stage.frameCounter; + this._renderTexturePool.splice(i, 1); + return texture; + } + } + + const texture = this._createRenderTexture(w, h, pw, ph); + texture.precision = prec; + return texture; + } + + releaseRenderTexture(texture) { + this._renderTexturePool.push(texture); + } + + freeUnusedRenderTextures(maxAge = 60) { + // Clean up all textures that are no longer used. + // This cache is short-lived because it is really just meant to supply running shaders that are + // updated during a number of frames. + let limit = this.stage.frameCounter - maxAge; + + this._renderTexturePool = this._renderTexturePool.filter(texture => { + if (texture.f <= limit) { + this._freeRenderTexture(texture); + return false; + } + return true; + }); + } + + _createRenderTexture(w, h, pw, ph) { + this._addMemoryUsage(pw * ph); + + const texture = this.stage.renderer.createRenderTexture(w, h, pw, ph); + texture.id = this._renderTextureId++; + texture.f = this.stage.frameCounter; + texture.ow = w; + texture.oh = h; + texture.w = pw; + texture.h = ph; + + return texture; + } + + _freeRenderTexture(nativeTexture) { + this.stage.renderer.freeRenderTexture(nativeTexture); + this._addMemoryUsage(-nativeTexture.w * nativeTexture.h); + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + this.stage.renderer.copyRenderTexture(renderTexture, nativeTexture, options); + } + + forceZSort(elementCore) { + this._zSorts.push(elementCore); + } + +} + +class TransitionSettings { + constructor(stage) { + this.stage = stage; + this._timingFunction = 'ease'; + this._timingFunctionImpl = StageUtils.getTimingFunction(this._timingFunction); + this.delay = 0; + this.duration = 0.2; + this.merger = null; + } + + get timingFunction() { + return this._timingFunction; + } + + set timingFunction(v) { + this._timingFunction = v; + this._timingFunctionImpl = StageUtils.getTimingFunction(v); + } + + get timingFunctionImpl() { + return this._timingFunctionImpl; + } + + patch(settings) { + Base.patchObject(this, settings); + } +} + +TransitionSettings.prototype.isTransitionSettings = true; + +class TransitionManager { + + constructor(stage) { + this.stage = stage; + + this.stage.on('frameStart', () => this.progress()); + + /** + * All transitions that are running and attached. + * (we don't support transitions on un-attached elements to prevent memory leaks) + * @type {Set} + */ + this.active = new Set(); + + this.defaultTransitionSettings = new TransitionSettings(this.stage); + } + + progress() { + if (this.active.size) { + let dt = this.stage.dt; + + let filter = false; + this.active.forEach(function(a) { + a.progress(dt); + if (!a.isRunning()) { + filter = true; + } + }); + + if (filter) { + this.active = new Set([...this.active].filter(t => (t.isRunning()))); + } + } + } + + createSettings(settings) { + const transitionSettings = new TransitionSettings(); + Base.patchObject(transitionSettings, settings); + return transitionSettings; + } + + addActive(transition) { + this.active.add(transition); + } + + removeActive(transition) { + this.active.delete(transition); + } +} + +class MultiSpline { + + constructor() { + this._clear(); + } + + _clear() { + this._p = []; + this._pe = []; + this._idp = []; + this._f = []; + this._v = []; + this._lv = []; + this._sm = []; + this._s = []; + this._ve = []; + this._sme = []; + this._se = []; + + this._length = 0; + } + + parse(rgba, def) { + let i, n; + if (!Utils.isObjectLiteral(def)) { + def = {0: def}; + } + + let defaultSmoothness = 0.5; + + let items = []; + for (let key in def) { + if (def.hasOwnProperty(key)) { + let obj = def[key]; + if (!Utils.isObjectLiteral(obj)) { + obj = {v: obj}; + } + + let p = parseFloat(key); + + if (key === "sm") { + defaultSmoothness = obj.v; + } else if (!isNaN(p) && p >= 0 && p <= 2) { + obj.p = p; + + obj.f = Utils.isFunction(obj.v); + obj.lv = obj.f ? obj.v(0, 0) : obj.v; + + items.push(obj); + } + } + } + + // Sort by progress value. + items = items.sort(function(a, b) {return a.p - b.p}); + + n = items.length; + + for (i = 0; i < n; i++) { + let last = (i === n - 1); + if (!items[i].hasOwnProperty('pe')) { + // Progress. + items[i].pe = last ? (items[i].p <= 1 ? 1 : 2 /* support onetotwo stop */) : items[i + 1].p; + } else { + // Prevent multiple items at the same time. + const max = i < n - 1 ? items[i + 1].p : 1; + if (items[i].pe > max) { + items[i].pe = max; + } + } + if (items[i].pe === items[i].p) { + items[i].idp = 0; + } else { + items[i].idp = 1 / (items[i].pe - items[i].p); + } + } + + // Color merger: we need to split/combine RGBA components. + + // Calculate bezier helper values.; + for (i = 0; i < n; i++) { + if (!items[i].hasOwnProperty('sm')) { + // Smoothness.; + items[i].sm = defaultSmoothness; + } + if (!items[i].hasOwnProperty('s')) { + // Slope.; + if (i === 0 || i === n - 1 || (items[i].p === 1 /* for onetotwo */)) { + // Horizontal slope at start and end.; + items[i].s = rgba ? [0, 0, 0, 0] : 0; + } else { + const pi = items[i - 1]; + const ni = items[i + 1]; + if (pi.p === ni.p) { + items[i].s = rgba ? [0, 0, 0, 0] : 0; + } else { + if (rgba) { + const nc = MultiSpline.getRgbaComponents(ni.lv); + const pc = MultiSpline.getRgbaComponents(pi.lv); + const d = 1 / (ni.p - pi.p); + items[i].s = [ + d * (nc[0] - pc[0]), + d * (nc[1] - pc[1]), + d * (nc[2] - pc[2]), + d * (nc[3] - pc[3]) + ]; + } else { + items[i].s = (ni.lv - pi.lv) / (ni.p - pi.p); + } + } + } + } + } + + for (i = 0; i < n - 1; i++) { + // Calculate value function.; + if (!items[i].f) { + + let last = (i === n - 1); + if (!items[i].hasOwnProperty('ve')) { + items[i].ve = last ? items[i].lv : items[i + 1].lv; + } + + // We can only interpolate on numeric values. Non-numeric values are set literally when reached time. + if (Utils.isNumber(items[i].v) && Utils.isNumber(items[i].lv)) { + if (!items[i].hasOwnProperty('sme')) { + items[i].sme = last ? defaultSmoothness : items[i + 1].sm; + } + if (!items[i].hasOwnProperty('se')) { + items[i].se = last ? (rgba ? [0, 0, 0, 0] : 0) : items[i + 1].s; + } + + // Generate spline.; + if (rgba) { + items[i].v = MultiSpline.getSplineRgbaValueFunction(items[i].v, items[i].ve, items[i].p, items[i].pe, items[i].sm, items[i].sme, items[i].s, items[i].se); + } else { + items[i].v = MultiSpline.getSplineValueFunction(items[i].v, items[i].ve, items[i].p, items[i].pe, items[i].sm, items[i].sme, items[i].s, items[i].se); + } + + items[i].f = true; + } + } + } + + if (this.length) { + this._clear(); + } + + for (i = 0, n = items.length; i < n; i++) { + this._add(items[i]); + } + } + + _add(item) { + this._p.push(item.p || 0); + this._pe.push(item.pe || 0); + this._idp.push(item.idp || 0); + this._f.push(item.f || false); + this._v.push(item.hasOwnProperty('v') ? item.v : 0 /* v might be false or null */ ); + this._lv.push(item.lv || 0); + this._sm.push(item.sm || 0); + this._s.push(item.s || 0); + this._ve.push(item.ve || 0); + this._sme.push(item.sme || 0); + this._se.push(item.se || 0); + this._length++; + } + + _getItem(p) { + const n = this._length; + if (!n) { + return -1; + } + + if (p < this._p[0]) { + return 0; + } + + for (let i = 0; i < n; i++) { + if (this._p[i] <= p && p < this._pe[i]) { + return i; + } + } + + return n - 1; + } + + getValue(p) { + const i = this._getItem(p); + if (i === -1) { + return undefined; + } else { + if (this._f[i]) { + const o = Math.min(1, Math.max(0, (p - this._p[i]) * this._idp[i])); + return this._v[i](o); + } else { + return this._v[i]; + } + } + } + + get length() { + return this._length; + } + + static getRgbaComponents(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r, g, b, a]; + }; + + static getSplineValueFunction(v1, v2, p1, p2, o1, i2, s1, s2) { + // Normalize slopes because we use a spline that goes from 0 to 1. + let dp = p2 - p1; + s1 *= dp; + s2 *= dp; + + let helpers = MultiSpline.getSplineHelpers(v1, v2, o1, i2, s1, s2); + if (!helpers) { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + + return v2 * p + v1 * (1 - p); + }; + } else { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + return MultiSpline.calculateSpline(helpers, p); + }; + } + }; + + static getSplineRgbaValueFunction(v1, v2, p1, p2, o1, i2, s1, s2) { + // Normalize slopes because we use a spline that goes from 0 to 1. + let dp = p2 - p1; + s1[0] *= dp; + s1[1] *= dp; + s1[2] *= dp; + s1[3] *= dp; + s2[0] *= dp; + s2[1] *= dp; + s2[2] *= dp; + s2[3] *= dp; + + let cv1 = MultiSpline.getRgbaComponents(v1); + let cv2 = MultiSpline.getRgbaComponents(v2); + + let helpers = [ + MultiSpline.getSplineHelpers(cv1[0], cv2[0], o1, i2, s1[0], s2[0]), + MultiSpline.getSplineHelpers(cv1[1], cv2[1], o1, i2, s1[1], s2[1]), + MultiSpline.getSplineHelpers(cv1[2], cv2[2], o1, i2, s1[2], s2[2]), + MultiSpline.getSplineHelpers(cv1[3], cv2[3], o1, i2, s1[3], s2[3]) + ]; + + if (!helpers[0]) { + return function (p) { + // Linear. + if (p === 0) return v1; + if (p === 1) return v2; + + return MultiSpline.mergeColors(v2, v1, p); + }; + } else { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + + return MultiSpline.getArgbNumber([ + Math.min(255, MultiSpline.calculateSpline(helpers[0], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[1], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[2], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[3], p)) + ]); + }; + } + + }; + + /** + * Creates helpers to be used in the spline function. + * @param {number} v1 + * From value. + * @param {number} v2 + * To value. + * @param {number} o1 + * From smoothness (0 = linear, 1 = smooth). + * @param {number} s1 + * From slope (0 = horizontal, infinite = vertical). + * @param {number} i2 + * To smoothness. + * @param {number} s2 + * To slope. + * @returns {Number[]} + * The helper values to be supplied to the spline function. + * If the configuration is actually linear, null is returned. + */ + static getSplineHelpers(v1, v2, o1, i2, s1, s2) { + if (!o1 && !i2) { + // Linear. + return null; + } + + // Cubic bezier points. + // http://cubic-bezier.com/ + let csx = o1; + let csy = v1 + s1 * o1; + let cex = 1 - i2; + let cey = v2 - s2 * i2; + + let xa = 3 * csx - 3 * cex + 1; + let xb = -6 * csx + 3 * cex; + let xc = 3 * csx; + + let ya = 3 * csy - 3 * cey + v2 - v1; + let yb = 3 * (cey + v1) - 6 * csy; + let yc = 3 * (csy - v1); + let yd = v1; + + return [xa, xb, xc, ya, yb, yc, yd]; + }; + + /** + * Calculates the intermediate spline value based on the specified helpers. + * @param {number[]} helpers + * Obtained from getSplineHelpers. + * @param {number} p + * @return {number} + */ + static calculateSpline(helpers, p) { + let xa = helpers[0]; + let xb = helpers[1]; + let xc = helpers[2]; + let ya = helpers[3]; + let yb = helpers[4]; + let yc = helpers[5]; + let yd = helpers[6]; + + if (xa === -2 && ya === -2 && xc === 0 && yc === 0) { + // Linear. + return p; + } + + // Find t for p. + let t = 0.5, cbx, dx; + + for (let it = 0; it < 20; it++) { + // Cubic bezier function: f(t)=t*(t*(t*a+b)+c). + cbx = t * (t * (t * xa + xb) + xc); + + dx = p - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc) + yd; + } + + // Cubic bezier derivative function: f'(t)=t*(t*(3*a)+2*b)+c + let cbxd = t * (t * (3 * xa) + 2 * xb) + xc; + + if (cbxd > 1e-10 && cbxd < 1e-10) { + // Problematic. Fall back to binary search method. + break; + } + + t += dx / cbxd; + } + + // Fallback: binary search method. This is more reliable when there are near-0 slopes. + let minT = 0; + let maxT = 1; + for (let it = 0; it < 20; it++) { + t = 0.5 * (minT + maxT); + + // Cubic bezier function: f(t)=t*(t*(t*a+b)+c)+d. + cbx = t * (t * (t * xa + xb) + xc); + + dx = p - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc) + yd; + } + + if (dx < 0) { + maxT = t; + } else { + minT = t; + } + } + + return t; + }; + + static mergeColors(c1, c2, p) { + let r1 = ((c1 / 65536) | 0) % 256; + let g1 = ((c1 / 256) | 0) % 256; + let b1 = c1 % 256; + let a1 = ((c1 / 16777216) | 0); + + let r2 = ((c2 / 65536) | 0) % 256; + let g2 = ((c2 / 256) | 0) % 256; + let b2 = c2 % 256; + let a2 = ((c2 / 16777216) | 0); + + let r = r1 * p + r2 * (1 - p); + let g = g1 * p + g2 * (1 - p); + let b = b1 * p + b2 * (1 - p); + let a = a1 * p + a2 * (1 - p); + + return Math.round(a) * 16777216 + Math.round(r) * 65536 + Math.round(g) * 256 + Math.round(b); + }; + + static getArgbNumber(rgba) { + rgba[0] = Math.max(0, Math.min(255, rgba[0])); + rgba[1] = Math.max(0, Math.min(255, rgba[1])); + rgba[2] = Math.max(0, Math.min(255, rgba[2])); + rgba[3] = Math.max(0, Math.min(255, rgba[3])); + let v = ((rgba[3] | 0) << 24) + ((rgba[0] | 0) << 16) + ((rgba[1] | 0) << 8) + (rgba[2] | 0); + if (v < 0) { + v = 0xFFFFFFFF + v + 1; + } + return v; + }; +} + +class AnimationActionSettings { + + constructor(animationSettings) { + + this.animationSettings = animationSettings; + + /** + * The selector that selects the elements. + * @type {string} + */ + this._selector = ""; + + /** + * The value items, ordered by progress offset. + * @type {MultiSpline} + * @private; + */ + this._items = new MultiSpline(); + + /** + * The affected properties (paths). + * @private; + */ + this._props = []; + + /** + * Property setters, indexed according to props. + * @private; + */ + this._propSetters = []; + + this._resetValue = undefined; + this._hasResetValue = false; + + this._hasColorProperty = undefined; + } + + getResetValue() { + if (this._hasResetValue) { + return this._resetValue; + } else { + return this._items.getValue(0); + } + } + + apply(element, p, factor) { + const elements = this.getAnimatedElements(element); + + let v = this._items.getValue(p); + + if (v === undefined || !elements.length) { + return; + } + + if (factor !== 1) { + // Stop factor.; + let sv = this.getResetValue(); + + if (Utils.isNumber(v) && Utils.isNumber(sv)) { + if (this.hasColorProperty()) { + v = StageUtils.mergeColors(v, sv, factor); + } else { + v = StageUtils.mergeNumbers(v, sv, factor); + } + } + } + + // Apply transformation to all components.; + const n = this._propSetters.length; + + const m = elements.length; + for (let j = 0; j < m; j++) { + for (let i = 0; i < n; i++) { + this._propSetters[i](elements[j], v); + } + } + } + + getAnimatedElements(element) { + return element.select(this._selector); + } + + reset(element) { + const elements = this.getAnimatedElements(element); + + let v = this.getResetValue(); + + if (v === undefined || !elements.length) { + return; + } + + // Apply transformation to all components. + const n = this._propSetters.length; + + const m = elements.length; + for (let j = 0; j < m; j++) { + for (let i = 0; i < n; i++) { + this._propSetters[i](elements[j], v); + } + } + } + + set selector(v) { + this._selector = v; + } + + set t(v) { + this.selector = v; + } + + get resetValue() { + return this._resetValue; + } + + set resetValue(v) { + this._resetValue = v; + this._hasResetValue = (v !== undefined); + } + + set rv(v) { + this.resetValue = v; + } + + set value(v) { + this._items.parse(this.hasColorProperty(), v); + } + + set v(v) { + this.value = v; + } + + set properties(v) { + if (!Array.isArray(v)) { + v = [v]; + } + + this._props = []; + + v.forEach((prop) => { + this._props.push(prop); + this._propSetters.push(Element.getSetter(prop)); + }); + } + + set property(v) { + this._hasColorProperty = undefined; + this.properties = v; + } + + set p(v) { + this.properties = v; + } + + patch(settings) { + Base.patchObject(this, settings); + } + + hasColorProperty() { + if (this._hasColorProperty === undefined) { + this._hasColorProperty = this._props.length ? Element.isColorProperty(this._props[0]) : false; + } + return this._hasColorProperty; + } +} + +AnimationActionSettings.prototype.isAnimationActionSettings = true; + +class AnimationSettings { + constructor() { + /** + * @type {AnimationActionSettings[]} + */ + this._actions = []; + + this.delay = 0; + this.duration = 1; + + this.repeat = 0; + this.repeatOffset = 0; + this.repeatDelay = 0; + + this.autostop = false; + + this.stopMethod = AnimationSettings.STOP_METHODS.FADE; + this._stopTimingFunction = 'ease'; + this._stopTimingFunctionImpl = StageUtils.getTimingFunction(this._stopTimingFunction); + this.stopDuration = 0; + this.stopDelay = 0; + } + + get actions() { + return this._actions; + } + + set actions(v) { + this._actions = []; + for (let i = 0, n = v.length; i < n; i++) { + const e = v[i]; + if (!e.isAnimationActionSettings) { + const aas = new AnimationActionSettings(this); + aas.patch(e); + this._actions.push(aas); + } else { + this._actions.push(e); + } + } + } + + /** + * Applies the animation to the specified element, for the specified progress between 0 and 1. + * @param {Element} element; + * @param {number} p; + * @param {number} factor; + */ + apply(element, p, factor = 1) { + this._actions.forEach(function(action) { + action.apply(element, p, factor); + }); + } + + /** + * Resets the animation to the reset values. + * @param {Element} element; + */ + reset(element) { + this._actions.forEach(function(action) { + action.reset(element); + }); + } + + get stopTimingFunction() { + return this._stopTimingFunction; + } + + set stopTimingFunction(v) { + this._stopTimingFunction = v; + this._stopTimingFunctionImpl = StageUtils.getTimingFunction(v); + } + + get stopTimingFunctionImpl() { + return this._stopTimingFunctionImpl; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +AnimationSettings.STOP_METHODS = { + FADE: 'fade', + REVERSE: 'reverse', + FORWARD: 'forward', + IMMEDIATE: 'immediate', + ONETOTWO: 'onetotwo' +}; + +class Animation extends EventEmitter { + + constructor(manager, settings, element) { + super(); + + this.manager = manager; + + this._settings = settings; + + this._element = element; + + this._state = Animation.STATES.IDLE; + + this._p = 0; + this._delayLeft = 0; + this._repeatsLeft = 0; + + this._stopDelayLeft = 0; + this._stopP = 0; + } + + start() { + if (this._element && this._element.attached) { + this._p = 0; + this._delayLeft = this.settings.delay; + this._repeatsLeft = this.settings.repeat; + this._state = Animation.STATES.PLAYING; + this.emit('start'); + this.checkActive(); + } else { + console.warn("Element must be attached before starting animation"); + } + } + + play() { + if (this._state === Animation.STATES.PAUSED) { + // Continue.; + this._state = Animation.STATES.PLAYING; + this.checkActive(); + this.emit('resume'); + } else if (this._state == Animation.STATES.STOPPING && this.settings.stopMethod == AnimationSettings.STOP_METHODS.REVERSE) { + // Continue.; + this._state = Animation.STATES.PLAYING; + this.emit('stopContinue'); + } else if (this._state != Animation.STATES.PLAYING && this._state != Animation.STATES.FINISHED) { + // Restart.; + this.start(); + } + } + + pause() { + if (this._state === Animation.STATES.PLAYING) { + this._state = Animation.STATES.PAUSED; + this.emit('pause'); + } + } + + replay() { + if (this._state == Animation.STATES.FINISHED) { + this.start(); + } else { + this.play(); + } + } + + skipDelay() { + this._delayLeft = 0; + this._stopDelayLeft = 0; + } + + finish() { + if (this._state === Animation.STATES.PLAYING) { + this._delayLeft = 0; + this._p = 1; + } else if (this._state === Animation.STATES.STOPPING) { + this._stopDelayLeft = 0; + this._p = 0; + } + } + + stop() { + if (this._state === Animation.STATES.STOPPED || this._state === Animation.STATES.IDLE) return; + + this._stopDelayLeft = this.settings.stopDelay || 0; + + if (((this.settings.stopMethod === AnimationSettings.STOP_METHODS.IMMEDIATE) && !this._stopDelayLeft) || this._delayLeft > 0) { + // Stop upon next progress.; + this._state = Animation.STATES.STOPPING; + this.emit('stop'); + } else { + if (this.settings.stopMethod === AnimationSettings.STOP_METHODS.FADE) { + this._stopP = 0; + } + + this._state = Animation.STATES.STOPPING; + this.emit('stop'); + } + + this.checkActive(); + } + + stopNow() { + if (this._state !== Animation.STATES.STOPPED || this._state !== Animation.STATES.IDLE) { + this._state = Animation.STATES.STOPPING; + this._p = 0; + this.emit('stop'); + this.reset(); + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } + + isPaused() { + return this._state === Animation.STATES.PAUSED; + } + + isPlaying() { + return this._state === Animation.STATES.PLAYING; + } + + isStopping() { + return this._state === Animation.STATES.STOPPING; + } + + isFinished() { + return this._state === Animation.STATES.FINISHED; + } + + checkActive() { + if (this.isActive()) { + this.manager.addActive(this); + } + } + + isActive() { + return (this._state == Animation.STATES.PLAYING || this._state == Animation.STATES.STOPPING) && this._element && this._element.attached; + } + + progress(dt) { + if (!this._element) return; + this._progress(dt); + this.apply(); + } + + _progress(dt) { + if (this._state == Animation.STATES.STOPPING) { + this._stopProgress(dt); + return; + } + + if (this._state != Animation.STATES.PLAYING) { + return; + } + + if (this._delayLeft > 0) { + this._delayLeft -= dt; + + if (this._delayLeft < 0) { + dt = -this._delayLeft; + this._delayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + if (this.settings.duration === 0) { + this._p = 1; + } else if (this.settings.duration > 0) { + this._p += dt / this.settings.duration; + } + if (this._p >= 1) { + // Finished!; + if (this.settings.repeat == -1 || this._repeatsLeft > 0) { + if (this._repeatsLeft > 0) { + this._repeatsLeft--; + } + this._p = this.settings.repeatOffset; + + if (this.settings.repeatDelay) { + this._delayLeft = this.settings.repeatDelay; + } + + this.emit('repeat', this._repeatsLeft); + } else { + this._p = 1; + this._state = Animation.STATES.FINISHED; + this.emit('finish'); + if (this.settings.autostop) { + this.stop(); + } + } + } else { + this.emit('progress', this._p); + } + } + + _stopProgress(dt) { + let duration = this._getStopDuration(); + + if (this._stopDelayLeft > 0) { + this._stopDelayLeft -= dt; + + if (this._stopDelayLeft < 0) { + dt = -this._stopDelayLeft; + this._stopDelayLeft = 0; + + this.emit('stopDelayEnd'); + } else { + return; + } + } + if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.IMMEDIATE) { + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.REVERSE) { + if (duration === 0) { + this._p = 0; + } else if (duration > 0) { + this._p -= dt / duration; + } + + if (this._p <= 0) { + this._p = 0; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FADE) { + this._progressStopTransition(dt); + if (this._stopP >= 1) { + this._p = 0; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.ONETOTWO) { + if (this._p < 2) { + if (duration === 0) { + this._p = 2; + } else if (duration > 0) { + if (this._p < 1) { + this._p += dt / this.settings.duration; + } else { + this._p += dt / duration; + } + } + if (this._p >= 2) { + this._p = 2; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else { + this.emit('progress', this._p); + } + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FORWARD) { + if (this._p < 1) { + if (this.settings.duration == 0) { + this._p = 1; + } else { + this._p += dt / this.settings.duration; + } + if (this._p >= 1) { + if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FORWARD) { + this._p = 1; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else { + if (this._repeatsLeft > 0) { + this._repeatsLeft--; + this._p = 0; + this.emit('repeat', this._repeatsLeft); + } else { + this._p = 1; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } + } else { + this.emit('progress', this._p); + } + } + } + + } + + _progressStopTransition(dt) { + if (this._stopP < 1) { + if (this._stopDelayLeft > 0) { + this._stopDelayLeft -= dt; + + if (this._stopDelayLeft < 0) { + dt = -this._stopDelayLeft; + this._stopDelayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + const duration = this._getStopDuration(); + + if (duration == 0) { + this._stopP = 1; + } else { + this._stopP += dt / duration; + } + if (this._stopP >= 1) { + // Finished!; + this._stopP = 1; + } + } + } + + _getStopDuration() { + return this.settings.stopDuration || this.settings.duration; + } + + apply() { + if (this._state === Animation.STATES.STOPPED) { + this.reset(); + } else { + let factor = 1; + if (this._state === Animation.STATES.STOPPING && this.settings.stopMethod === AnimationSettings.STOP_METHODS.FADE) { + factor = (1 - this.settings.stopTimingFunctionImpl(this._stopP)); + } + this._settings.apply(this._element, this._p, factor); + } + } + + reset() { + this._settings.reset(this._element); + } + + get state() { + return this._state; + } + + get p() { + return this._p; + } + + get delayLeft() { + return this._delayLeft; + } + + get element() { + return this._element; + } + + get frame() { + return Math.round(this._p * this._settings.duration * 60); + } + + get settings() { + return this._settings; + } + +} + +Animation.STATES = { + IDLE: 0, + PLAYING: 1, + STOPPING: 2, + STOPPED: 3, + FINISHED: 4, + PAUSED: 5 +}; + +class AnimationManager { + + constructor(stage) { + this.stage = stage; + + this.stage.on('frameStart', () => this.progress()); + + /** + * All running animations on attached subjects. + * @type {Set} + */ + this.active = new Set(); + } + + progress() { + if (this.active.size) { + let dt = this.stage.dt; + + let filter = false; + this.active.forEach(function(a) { + if (a.isActive()) { + a.progress(dt); + } else { + filter = true; + } + }); + + if (filter) { + this.active = new Set([...this.active].filter(t => t.isActive())); + } + } + } + + createAnimation(element, settings) { + if (Utils.isObjectLiteral(settings)) { + // Convert plain object to proper settings object. + settings = this.createSettings(settings); + } + + return new Animation( + this, + settings, + element + ); + } + + createSettings(settings) { + const animationSettings = new AnimationSettings(); + Base.patchObject(animationSettings, settings); + return animationSettings; + } + + addActive(transition) { + this.active.add(transition); + } +} + +class RectangleTexture extends Texture { + + _getLookupId() { + return '__whitepix'; + } + + _getSourceLoader() { + return function(cb) { + var whitePixel = new Uint8Array([255, 255, 255, 255]); + cb(null, {source: whitePixel, w: 1, h: 1, permanent: true}); + } + } + + isAutosizeTexture() { + return false; + } +} + +/** + * Application render tree. + * Copyright Metrological, 2017; + */ + +class Stage extends EventEmitter { + + constructor(options = {}) { + super(); + this._setOptions(options); + + this._usedMemory = 0; + this._lastGcFrame = 0; + + const platformType = Stage.platform ? Stage.platform : PlatformLoader.load(options); + this.platform = new platformType(); + + if (this.platform.init) { + this.platform.init(this); + } + + this.gl = null; + this.c2d = null; + + const context = this.getOption('context'); + if (context) { + if (context.useProgram) { + this.gl = context; + } else { + this.c2d = context; + } + } else { + if (Utils.isWeb && (!Stage.isWebglSupported() || this.getOption('canvas2d'))) { + console.log('Using canvas2d renderer'); + this.c2d = this.platform.createCanvasContext(this.getOption('w'), this.getOption('h')); + } else { + this.gl = this.platform.createWebGLContext(this.getOption('w'), this.getOption('h')); + } + } + + if (this.gl) { + // Wrap in WebGLStateManager. + // This prevents unnecessary double WebGL commands from being executed, and allows context switching. + // Context switching is necessary when reusing the same context for Three.js. + // Note that the user must make sure that the WebGL context is untouched before creating the application, + // when manually passing over a canvas or context in the options. + WebGLStateManager.enable(this.gl, "lightning"); + } + + this._mode = this.gl ? 0 : 1; + + // Override width and height. + if (this.getCanvas()) { + this._options.w = this.getCanvas().width; + this._options.h = this.getCanvas().height; + } + + if (this._mode === 0) { + this._renderer = new WebGLRenderer(this); + } else { + this._renderer = new C2dRenderer(this); + } + + this.setClearColor(this.getOption('clearColor')); + + this.frameCounter = 0; + + this.transitions = new TransitionManager(this); + this.animations = new AnimationManager(this); + + this.textureManager = new TextureManager(this); + this.textureThrottler = new TextureThrottler(this); + + this.startTime = 0; + this.currentTime = 0; + this.dt = 0; + + // Preload rectangle texture, so that we can skip some border checks for loading textures. + this.rectangleTexture = new RectangleTexture(this); + this.rectangleTexture.load(); + + // Never clean up because we use it all the time. + this.rectangleTexture.source.permanent = true; + + this.ctx = new CoreContext(this); + + this._updateSourceTextures = new Set(); + } + + get renderer() { + return this._renderer; + } + + static isWebglSupported() { + if (Utils.isNode) { + return true; + } + + try { + return !!window.WebGLRenderingContext; + } catch(e) { + return false; + } + } + + /** + * Returns the rendering mode. + * @returns {number} + * 0: WebGL + * 1: Canvas2d + */ + get mode() { + return this._mode; + } + + isWebgl() { + return this.mode === 0; + } + + isC2d() { + return this.mode === 1; + } + + getOption(name) { + return this._options[name]; + } + + _setOptions(o) { + this._options = {}; + + let opt = (name, def) => { + let value = o[name]; + + if (value === undefined) { + this._options[name] = def; + } else { + this._options[name] = value; + } + }; + + opt('canvas', null); + opt('context', null); + opt('w', 1920); + opt('h', 1080); + opt('srcBasePath', null); + opt('memoryPressure', 24e6); + opt('bufferMemory', 2e6); + opt('textRenderIssueMargin', 0); + opt('clearColor', [0, 0, 0, 0]); + opt('defaultFontFace', 'sans-serif'); + opt('fixedDt', 0); + opt('useImageWorker', true); + opt('autostart', true); + opt('precision', 1); + opt('canvas2d', false); + opt('platform', null); + opt('readPixelsBeforeDraw', false); + } + + setApplication(app) { + this.application = app; + } + + init() { + this.application.setAsRoot(); + if (this.getOption('autostart')) { + this.platform.startLoop(); + } + } + + destroy() { + this.platform.stopLoop(); + this.platform.destroy(); + this.ctx.destroy(); + this.textureManager.destroy(); + this._renderer.destroy(); + } + + stop() { + this.platform.stopLoop(); + } + + resume() { + this.platform.startLoop(); + } + + get root() { + return this.application; + } + + getCanvas() { + return this._mode ? this.c2d.canvas : this.gl.canvas; + } + + getRenderPrecision() { + return this._options.precision; + } + + /** + * Marks a texture for updating it's source upon the next drawFrame. + * @param texture + */ + addUpdateSourceTexture(texture) { + if (this._updatingFrame) { + // When called from the upload loop, we must immediately load the texture in order to avoid a 'flash'. + texture._performUpdateSource(); + } else { + this._updateSourceTextures.add(texture); + } + } + + removeUpdateSourceTexture(texture) { + if (this._updateSourceTextures) { + this._updateSourceTextures.delete(texture); + } + } + + hasUpdateSourceTexture(texture) { + return (this._updateSourceTextures && this._updateSourceTextures.has(texture)); + } + + drawFrame() { + this.startTime = this.currentTime; + this.currentTime = this.platform.getHrTime(); + + if (this._options.fixedDt) { + this.dt = this._options.fixedDt; + } else { + this.dt = (!this.startTime) ? .02 : .001 * (this.currentTime - this.startTime); + } + + this.emit('frameStart'); + + if (this._updateSourceTextures.size) { + this._updateSourceTextures.forEach(texture => { + texture._performUpdateSource(); + }); + this._updateSourceTextures = new Set(); + } + + this.emit('update'); + + const changes = this.ctx.hasRenderUpdates(); + + // Update may cause textures to be loaded in sync, so by processing them here we may be able to show them + // during the current frame already. + this.textureThrottler.processSome(); + + if (changes) { + this._updatingFrame = true; + this.ctx.update(); + this.ctx.render(); + this._updatingFrame = false; + } + + this.platform.nextFrame(changes); + + this.emit('frameEnd'); + + this.frameCounter++; + } + + isUpdatingFrame() { + return this._updatingFrame; + } + + renderFrame() { + this.ctx.frame(); + } + + forceRenderUpdate() { + // Enforce re-rendering. + if (this.root) { + this.root.core._parent.setHasRenderUpdates(1); + } + } + + setClearColor(clearColor) { + this.forceRenderUpdate(); + if (clearColor === null) { + // Do not clear. + this._clearColor = null; + } else if (Array.isArray(clearColor)) { + this._clearColor = clearColor; + } else { + this._clearColor = StageUtils.getRgbaComponentsNormalized(clearColor); + } + } + + getClearColor() { + return this._clearColor; + } + + createElement(settings) { + if (settings) { + return this.element(settings); + } else { + return new Element(this); + } + } + + createShader(settings) { + return Shader.create(this, settings); + } + + element(settings) { + if (settings.isElement) return settings; + + let element; + if (settings.type) { + element = new settings.type(this); + } else { + element = new Element(this); + } + + element.patch(settings); + + return element; + } + + c(settings) { + return this.element(settings); + } + + get w() { + return this._options.w; + } + + get h() { + return this._options.h; + } + + get coordsWidth() { + return this.w / this._options.precision; + } + + get coordsHeight() { + return this.h / this._options.precision; + } + + addMemoryUsage(delta) { + this._usedMemory += delta; + if (this._lastGcFrame !== this.frameCounter) { + if (this._usedMemory > this.getOption('memoryPressure')) { + this.gc(false); + if (this._usedMemory > this.getOption('memoryPressure') - 2e6) { + // Too few released. Aggressive cleanup. + this.gc(true); + } + } + } + } + + get usedMemory() { + return this._usedMemory; + } + + gc(aggressive) { + if (this._lastGcFrame !== this.frameCounter) { + this._lastGcFrame = this.frameCounter; + const memoryUsageBefore = this._usedMemory; + this.gcTextureMemory(aggressive); + this.gcRenderTextureMemory(aggressive); + this.renderer.gc(aggressive); + + console.log(`GC${aggressive ? "[aggressive]" : ""}! Frame ${this._lastGcFrame} Freed ${((memoryUsageBefore - this._usedMemory) / 1e6).toFixed(2)}MP from GPU memory. Remaining: ${(this._usedMemory / 1e6).toFixed(2)}MP`); + const other = this._usedMemory - this.textureManager.usedMemory - this.ctx.usedMemory; + console.log(` Textures: ${(this.textureManager.usedMemory / 1e6).toFixed(2)}MP, Render Textures: ${(this.ctx.usedMemory / 1e6).toFixed(2)}MP, Renderer caches: ${(other / 1e6).toFixed(2)}MP`); + } + } + + gcTextureMemory(aggressive = false) { + if (aggressive && this.ctx.root.visible) { + // Make sure that ALL textures are cleaned; + this.ctx.root.visible = false; + this.textureManager.gc(); + this.ctx.root.visible = true; + } else { + this.textureManager.gc(); + } + } + + gcRenderTextureMemory(aggressive = false) { + if (aggressive && this.root.visible) { + // Make sure that ALL render textures are cleaned; + this.root.visible = false; + this.ctx.freeUnusedRenderTextures(0); + this.root.visible = true; + } else { + this.ctx.freeUnusedRenderTextures(0); + } + } + + getDrawingCanvas() { + return this.platform.getDrawingCanvas(); + } + + update() { + this.ctx.update(); + } + +} + +class Application extends Component { + + constructor(options = {}, properties) { + // Save options temporarily to avoid having to pass it through the constructor. + Application._temp_options = options; + + // Booting flag is used to postpone updateFocusSettings; + Application.booting = true; + const stage = new Stage(options.stage); + super(stage, properties); + Application.booting = false; + + this.__updateFocusCounter = 0; + + // We must construct while the application is not yet attached. + // That's why we 'init' the stage later (which actually emits the attach event). + this.stage.init(); + + // Initially, the focus settings are updated after both the stage and application are constructed. + this.updateFocusSettings(); + + this.__keymap = this.getOption('keys'); + if (this.__keymap) { + this.stage.platform.registerKeyHandler((e) => { + this._receiveKeydown(e); + }); + } + } + + getOption(name) { + return this.__options[name]; + } + + _setOptions(o) { + this.__options = {}; + + let opt = (name, def) => { + let value = o[name]; + + if (value === undefined) { + this.__options[name] = def; + } else { + this.__options[name] = value; + } + }; + + opt('debug', false); + opt('keys', { + 38: "Up", + 40: "Down", + 37: "Left", + 39: "Right", + 13: "Enter", + 8: "Back", + 27: "Exit" + }); + } + + __construct() { + this.stage.setApplication(this); + + this._setOptions(Application._temp_options); + delete Application._temp_options; + + super.__construct(); + } + + __init() { + super.__init(); + this.__updateFocus(); + } + + updateFocusPath() { + this.__updateFocus(); + } + + __updateFocus() { + const notOverridden = this.__updateFocusRec(); + + if (!Application.booting && notOverridden) { + this.updateFocusSettings(); + } + } + + __updateFocusRec() { + const updateFocusId = ++this.__updateFocusCounter; + this.__updateFocusId = updateFocusId; + + const newFocusPath = this.__getFocusPath(); + const newFocusedComponent = newFocusPath[newFocusPath.length - 1]; + const prevFocusedComponent = this._focusPath ? this._focusPath[this._focusPath.length - 1] : undefined; + + if (!prevFocusedComponent) { + // Focus events. + this._focusPath = []; + for (let i = 0, n = newFocusPath.length; i < n; i++) { + this._focusPath.push(newFocusPath[i]); + this._focusPath[i]._focus(newFocusedComponent, undefined); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + return true; + } else { + let m = Math.min(this._focusPath.length, newFocusPath.length); + let index; + for (index = 0; index < m; index++) { + if (this._focusPath[index] !== newFocusPath[index]) { + break; + } + } + + if (this._focusPath.length !== newFocusPath.length || index !== newFocusPath.length) { + if (this.__options.debug) { + console.log('FOCUS ' + newFocusedComponent.getLocationString()); + } + // Unfocus events. + for (let i = this._focusPath.length - 1; i >= index; i--) { + const unfocusedElement = this._focusPath.pop(); + unfocusedElement._unfocus(newFocusedComponent, prevFocusedComponent); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + + // Focus events. + for (let i = index, n = newFocusPath.length; i < n; i++) { + this._focusPath.push(newFocusPath[i]); + this._focusPath[i]._focus(newFocusedComponent, prevFocusedComponent); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + + // Focus changed events. + for (let i = 0; i < index; i++) { + this._focusPath[i]._focusChange(newFocusedComponent, prevFocusedComponent); + } + } + } + + return true; + } + + updateFocusSettings() { + const focusedComponent = this._focusPath[this._focusPath.length - 1]; + + // Get focus settings. These can be used for dynamic application-wide settings that depend on the + // focus directly (such as the application background). + const focusSettings = {}; + const defaultSetFocusSettings = Component.prototype._setFocusSettings; + for (let i = 0, n = this._focusPath.length; i < n; i++) { + if (this._focusPath[i]._setFocusSettings !== defaultSetFocusSettings) { + this._focusPath[i]._setFocusSettings(focusSettings); + } + } + + const defaultHandleFocusSettings = Component.prototype._handleFocusSettings; + for (let i = 0, n = this._focusPath.length; i < n; i++) { + if (this._focusPath[i]._handleFocusSettings !== defaultHandleFocusSettings) { + this._focusPath[i]._handleFocusSettings(focusSettings, this.__prevFocusSettings, focusedComponent); + } + } + + this.__prevFocusSettings = focusSettings; + } + + _handleFocusSettings(settings, prevSettings, focused, prevFocused) { + // Override to handle focus-based settings. + } + + __getFocusPath() { + const path = [this]; + let current = this; + do { + const nextFocus = current._getFocused(); + if (!nextFocus || (nextFocus === current)) { + // Found!; + break; + } + + + let ptr = nextFocus.cparent; + if (ptr === current) { + path.push(nextFocus); + } else { + // Not an immediate child: include full path to descendant. + const newParts = [nextFocus]; + do { + if (!ptr) { + current._throwError("Return value for _getFocused must be an attached descendant component but its '" + nextFocus.getLocationString() + "'"); + } + newParts.push(ptr); + ptr = ptr.cparent; + } while (ptr !== current); + + // Add them reversed. + for (let i = 0, n = newParts.length; i < n; i++) { + path.push(newParts[n - i - 1]); + } + } + + current = nextFocus; + } while(true); + + return path; + } + + get focusPath() { + return this._focusPath; + } + + /** + * Injects an event in the state machines, top-down from application to focused component. + */ + focusTopDownEvent(events, ...args) { + const path = this.focusPath; + const n = path.length; + + // Multiple events. + for (let i = 0; i < n; i++) { + const event = path[i]._getMostSpecificHandledMember(events); + if (event !== undefined) { + const returnValue = path[i][event](...args); + if (returnValue !== false) { + return true; + } + } + } + + return false; + } + + /** + * Injects an event in the state machines, bottom-up from focused component to application. + */ + focusBottomUpEvent(events, ...args) { + const path = this.focusPath; + const n = path.length; + + // Multiple events. + for (let i = n - 1; i >= 0; i--) { + const event = path[i]._getMostSpecificHandledMember(events); + if (event !== undefined) { + const returnValue = path[i][event](...args); + if (returnValue !== false) { + return true; + } + } + } + + return false; + } + + _receiveKeydown(e) { + const obj = e; + if (this.__keymap[e.keyCode]) { + if (!this.stage.application.focusTopDownEvent(["_capture" + this.__keymap[e.keyCode], "_captureKey"], obj)) { + this.stage.application.focusBottomUpEvent(["_handle" + this.__keymap[e.keyCode], "_handleKey"], obj); + } + } else { + if (!this.stage.application.focusTopDownEvent(["_captureKey"], obj)) { + this.stage.application.focusBottomUpEvent(["_handleKey"], obj); + } + } + this.updateFocusPath(); + } + + destroy() { + if (!this._destroyed) { + this._destroy(); + this.stage.destroy(); + this._destroyed = true; + } + } + + _destroy() { + // This forces the _detach, _disabled and _active events to be called. + this.stage.setApplication(undefined); + this._updateAttachedFlag(); + this._updateEnabledFlag(); + } + + getCanvas() { + return this.stage.getCanvas(); + } + +} + +class StaticCanvasTexture extends Texture { + + constructor(stage) { + super(stage); + this._factory = undefined; + this._lookupId = undefined; + } + + set content({factory, lookupId = undefined}) { + this._factory = factory; + this._lookupId = lookupId; + this._changed(); + } + + _getIsValid() { + return !!this._factory; + } + + _getLookupId() { + return this._lookupId; + } + + _getSourceLoader() { + const f = this._factory; + return (cb) => { + return f((err, canvas) => { + if (err) { + return cb(err); + } + cb(null, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas)); + }, this.stage); + } + } + +} + +class Tools { + + static getCanvasTexture(canvasFactory, lookupId) { + return {type: StaticCanvasTexture, content: {factory: canvasFactory, lookupId: lookupId}} + } + + static getRoundRect(w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (!Array.isArray(radius)){ + // upper-left, upper-right, bottom-right, bottom-left. + radius = [radius, radius, radius, radius]; + } + + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createRoundRect(cb, stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor); + } else { + cb(null, this.createRoundRect(stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor)); + } + }; + let id = 'rect' + [w, h, strokeWidth, strokeColor, fill ? 1 : 0, fillColor].concat(radius).join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createRoundRect(stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (fill === undefined) fill = true; + if (strokeWidth === undefined) strokeWidth = 0; + + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + canvas.width = w + strokeWidth + 2; + canvas.height = h + strokeWidth + 2; + + ctx.beginPath(); + let x = 0.5 * strokeWidth + 1, y = 0.5 * strokeWidth + 1; + + ctx.moveTo(x + radius[0], y); + ctx.lineTo(x + w - radius[1], y); + ctx.arcTo(x + w, y, x + w, y + radius[1], radius[1]); + ctx.lineTo(x + w, y + h - radius[2]); + ctx.arcTo(x + w, y + h, x + w - radius[2], y + h, radius[2]); + ctx.lineTo(x + radius[3], y + h); + ctx.arcTo(x, y + h, x, y + h - radius[3], radius[3]); + ctx.lineTo(x, y + radius[0]); + ctx.arcTo(x, y, x + radius[0], y, radius[0]); + ctx.closePath(); + + if (fill) { + if (Utils.isNumber(fillColor)) { + ctx.fillStyle = StageUtils.getRgbaString(fillColor); + } else { + ctx.fillStyle = "white"; + } + ctx.fill(); + } + + if (strokeWidth) { + if (Utils.isNumber(strokeColor)) { + ctx.strokeStyle = StageUtils.getRgbaString(strokeColor); + } else { + ctx.strokeStyle = "white"; + } + ctx.lineWidth = strokeWidth; + ctx.stroke(); + } + + return canvas; + } + + static getShadowRect(w, h, radius = 0, blur = 5, margin = blur * 2) { + if (!Array.isArray(radius)){ + // upper-left, upper-right, bottom-right, bottom-left. + radius = [radius, radius, radius, radius]; + } + + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createShadowRect(cb, stage, w, h, radius, blur, margin); + } else { + cb(null, this.createShadowRect(stage, w, h, radius, blur, margin)); + } + }; + let id = 'shadow' + [w, h, blur, margin].concat(radius).join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createShadowRect(stage, w, h, radius, blur, margin) { + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + canvas.width = w + margin * 2; + canvas.height = h + margin * 2; + + // WpeWebKit bug: we experienced problems without this with shadows in noncompositedwebgl mode. + ctx.globalAlpha = 0.01; + ctx.fillRect(0, 0, 0.01, 0.01); + ctx.globalAlpha = 1.0; + + ctx.shadowColor = StageUtils.getRgbaString(0xFFFFFFFF); + ctx.fillStyle = StageUtils.getRgbaString(0xFFFFFFFF); + ctx.shadowBlur = blur; + ctx.shadowOffsetX = (w + 10) + margin; + ctx.shadowOffsetY = margin; + + ctx.beginPath(); + const x = -(w + 10); + const y = 0; + + ctx.moveTo(x + radius[0], y); + ctx.lineTo(x + w - radius[1], y); + ctx.arcTo(x + w, y, x + w, y + radius[1], radius[1]); + ctx.lineTo(x + w, y + h - radius[2]); + ctx.arcTo(x + w, y + h, x + w - radius[2], y + h, radius[2]); + ctx.lineTo(x + radius[3], y + h); + ctx.arcTo(x, y + h, x, y + h - radius[3], radius[3]); + ctx.lineTo(x, y + radius[0]); + ctx.arcTo(x, y, x + radius[0], y, radius[0]); + ctx.closePath(); + ctx.fill(); + + return canvas; + } + + static getSvgTexture(url, w, h) { + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createSvg(cb, stage, url, w, h); + } else { + this.createSvg(cb, stage, url, w, h); + } + }; + let id = 'svg' + [w, h, url].join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createSvg(cb, stage, url, w, h) { + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + let img = new Image(); + img.onload = () => { + canvas.width = w; + canvas.height = h; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + cb(null, canvas); + }; + img.onError = (err) => { + cb(err); + }; + img.src = url; + } + +} + +class ObjMerger { + + static isMf(f) { + return Utils.isFunction(f) && f.__mf; + } + + static mf(f) { + // Set as merge function. + f.__mf = true; + return f; + } + + static merge(a, b) { + const aks = Object.keys(a); + const bks = Object.keys(b); + + if (!bks.length) { + return a; + } + + // Create index array for all elements. + const ai = {}; + const bi = {}; + for (let i = 0, n = bks.length; i < n; i++) { + const key = bks[i]; + ai[key] = -1; + bi[key] = i; + } + for (let i = 0, n = aks.length; i < n; i++) { + const key = aks[i]; + ai[key] = i; + if (bi[key] === undefined) { + bi[key] = -1; + } + } + + const aksl = aks.length; + + const result = {}; + for (let i = 0, n = bks.length; i < n; i++) { + const key = bks[i]; + + // Prepend all items in a that are not in b - before the now added b attribute. + const aIndex = ai[key]; + let curIndex = aIndex; + while(--curIndex >= 0) { + const akey = aks[curIndex]; + if (bi[akey] !== -1) { + // Already found? Stop processing. + // Not yet found but exists in b? Also stop processing: wait until we find it in b. + break; + } + } + while(++curIndex < aIndex) { + const akey = aks[curIndex]; + result[akey] = a[akey]; + } + + const bv = b[key]; + const av = a[key]; + let r; + if (this.isMf(bv)) { + r = bv(av); + } else { + if (!Utils.isObjectLiteral(av) || !Utils.isObjectLiteral(bv)) { + r = bv; + } else { + r = ObjMerger.merge(av, bv); + } + } + + // When marked as undefined, property is deleted. + if (r !== undefined) { + result[key] = r; + } + } + + // Append remaining final items in a. + let curIndex = aksl; + while(--curIndex >= 0) { + const akey = aks[curIndex]; + if (bi[akey] !== -1) { + break; + } + } + while(++curIndex < aksl) { + const akey = aks[curIndex]; + result[akey] = a[akey]; + } + + return result; + } + +} + +/** + * Manages the list of children for an element. + */ + +class ObjectListProxy extends ObjectList { + + constructor(target) { + super(); + this._target = target; + } + + onAdd(item, index) { + this._target.addAt(item, index); + } + + onRemove(item, index) { + this._target.removeAt(index); + } + + onSync(removed, added, order) { + this._target._setByArray(order); + } + + onSet(item, index) { + this._target.setAt(item, index); + } + + onMove(item, fromIndex, toIndex) { + this._target.setAt(item, toIndex); + } + + createItem(object) { + return this._target.createItem(object); + } + + isItem(object) { + return this._target.isItem(object); + } + +} + +/** + * Manages the list of children for an element. + */ + +class ObjectListWrapper extends ObjectListProxy { + + constructor(target, wrap) { + super(target); + this._wrap = wrap; + } + + wrap(item) { + let wrapper = this._wrap(item); + item._wrapper = wrapper; + return wrapper; + } + + onAdd(item, index) { + item = this.wrap(item); + super.onAdd(item, index); + } + + onRemove(item, index) { + super.onRemove(item, index); + } + + onSync(removed, added, order) { + added.forEach(a => this.wrap(a)); + order = order.map(a => a._wrapper); + super.onSync(removed, added, order); + } + + onSet(item, index) { + item = this.wrap(item); + super.onSet(item, index); + } + + onMove(item, fromIndex, toIndex) { + super.onMove(item, fromIndex, toIndex); + } + +} + +class NoiseTexture extends Texture { + + _getLookupId() { + return '__noise'; + } + + _getSourceLoader() { + const gl = this.stage.gl; + return function(cb) { + const noise = new Uint8Array(128 * 128 * 4); + for (let i = 0; i < 128 * 128 * 4; i+=4) { + const v = Math.floor(Math.random() * 256); + noise[i] = v; + noise[i+1] = v; + noise[i+2] = v; + noise[i+3] = 255; + } + const texParams = {}; + + if (gl) { + texParams[gl.TEXTURE_WRAP_S] = gl.REPEAT; + texParams[gl.TEXTURE_WRAP_T] = gl.REPEAT; + texParams[gl.TEXTURE_MIN_FILTER] = gl.NEAREST; + texParams[gl.TEXTURE_MAG_FILTER] = gl.NEAREST; + } + + cb(null, {source: noise, w: 128, h: 128, texParams: texParams}); + } + } + +} + +class HtmlTexture extends Texture { + + constructor(stage) { + super(stage); + this._htmlElement = undefined; + this._scale = 1; + } + + set htmlElement(v) { + this._htmlElement = v; + this._changed(); + } + + get htmlElement() { + return this._htmlElement; + } + + set scale(v) { + this._scale = v; + this._changed(); + } + + get scale() { + return this._scale; + } + + set html(v) { + if (!v) { + this.htmlElement = undefined; + } else { + const d = document.createElement('div'); + d.innerHTML = "
" + v + "
"; + this.htmlElement = d.firstElementChild; + } + } + + get html() { + return this._htmlElement.innerHTML; + } + + _getIsValid() { + return this.htmlElement; + } + + _getLookupId() { + return this._scale + ":" + this._htmlElement.innerHTML; + } + + _getSourceLoader() { + const htmlElement = this._htmlElement; + const scale = this._scale; + return function(cb) { + if (!window.html2canvas) { + return cb(new Error("Please include html2canvas (https://html2canvas.hertzen.com/)")); + } + + const area = HtmlTexture.getPreloadArea(); + area.appendChild(htmlElement); + + html2canvas(htmlElement, {backgroundColor: null, scale: scale}).then(function(canvas) { + area.removeChild(htmlElement); + if (canvas.height === 0) { + return cb(new Error("Canvas height is 0")); + } + cb(null, {source: canvas, width: canvas.width, height: canvas.height}); + }).catch(e => { + console.error(e); + }); + } + } + + static getPreloadArea() { + if (!this._preloadArea) { + // Preload area must be included in document body and must be visible to trigger html element rendering. + this._preloadArea = document.createElement('div'); + if (this._preloadArea.attachShadow) { + // Use a shadow DOM if possible to prevent styling from interfering. + this._preloadArea.attachShadow({mode: 'closed'}); + } + this._preloadArea.style.opacity = 0; + this._preloadArea.style.pointerEvents = 'none'; + this._preloadArea.style.position = 'fixed'; + this._preloadArea.style.display = 'block'; + this._preloadArea.style.top = '100vh'; + this._preloadArea.style.overflow = 'hidden'; + document.body.appendChild(this._preloadArea); + } + return this._preloadArea; + } +} + +class StaticTexture extends Texture { + + constructor(stage, options) { + super(stage); + + this._options = options; + } + + set options(v) { + if (this._options !== v) { + this._options = v; + this._changed(); + } + } + + get options() { + return this._options; + } + + _getIsValid() { + return !!this._options; + } + + _getSourceLoader() { + return (cb) => { + cb(null, this._options); + } + } +} + +class ListComponent extends Component { + + constructor(stage) { + super(stage); + + this._wrapper = super._children.a({}); + + this._reloadVisibleElements = false; + + this._visibleItems = new Set(); + + this._index = 0; + + this._started = false; + + /** + * The transition definition that is being used when scrolling the items. + * @type TransitionSettings + */ + this._scrollTransitionSettings = this.stage.transitions.createSettings({}); + + /** + * The scroll area size in pixels per item. + */ + this._itemSize = 100; + + this._viewportScrollOffset = 0; + + this._itemScrollOffset = 0; + + /** + * Should the list jump when scrolling between end to start, or should it be continuous, like a carrousel? + */ + this._roll = false; + + /** + * Allows restricting the start scroll position. + */ + this._rollMin = 0; + + /** + * Allows restricting the end scroll position. + */ + this._rollMax = 0; + + /** + * Definition for a custom animation that is applied when an item is (partially) selected. + * @type AnimationSettings + */ + this._progressAnimation = null; + + /** + * Inverts the scrolling direction. + * @type {boolean} + * @private + */ + this._invertDirection = false; + + /** + * Layout the items horizontally or vertically? + * @type {boolean} + * @private + */ + this._horizontal = true; + + this.itemList = new ListItems(this); + } + + _allowChildrenAccess() { + return false; + } + + get items() { + return this.itemList.get(); + } + + set items(children) { + this.itemList.patch(children); + } + + start() { + this._wrapper.transition(this.property, this._scrollTransitionSettings); + this._scrollTransition = this._wrapper.transition(this.property); + this._scrollTransition.on('progress', p => this.update()); + + this.setIndex(0, true, true); + + this._started = true; + + this.update(); + } + + setIndex(index, immediate = false, closest = false) { + let nElements = this.length; + if (!nElements) return; + + this.emit('unfocus', this.getElement(this.realIndex), this._index, this.realIndex); + + if (closest) { + // Scroll to same offset closest to the index. + let offset = Utils.getModuloIndex(index, nElements); + let o = Utils.getModuloIndex(this.index, nElements); + let diff = offset - o; + if (diff > 0.5 * nElements) { + diff -= nElements; + } else if (diff < -0.5 * nElements) { + diff += nElements; + } + this._index += diff; + } else { + this._index = index; + } + + if (this._roll || (this.viewportSize > this._itemSize * nElements)) { + this._index = Utils.getModuloIndex(this._index, nElements); + } + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + let value = direction * this._index * this._itemSize; + + if (this._roll) { + let min, max, scrollDelta; + if (direction == 1) { + max = (nElements - 1) * this._itemSize; + scrollDelta = this._viewportScrollOffset * this.viewportSize - this._itemScrollOffset * this._itemSize; + + max -= scrollDelta; + + min = this.viewportSize - (this._itemSize + scrollDelta); + + if (this._rollMin) min -= this._rollMin; + if (this._rollMax) max += this._rollMax; + + value = Math.max(Math.min(value, max), min); + } else { + max = (nElements * this._itemSize - this.viewportSize); + scrollDelta = this._viewportScrollOffset * this.viewportSize - this._itemScrollOffset * this._itemSize; + + max += scrollDelta; + + let min = scrollDelta; + + if (this._rollMin) min -= this._rollMin; + if (this._rollMax) max += this._rollMax; + + value = Math.min(Math.max(-max, value), -min); + } + } + + this._scrollTransition.start(value); + + if (immediate) { + this._scrollTransition.finish(); + } + + this.emit('focus', this.getElement(this.realIndex), this._index, this.realIndex); + } + + getAxisPosition() { + let target = -this._scrollTransition._targetValue; + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + let value = -direction * this._index * this._itemSize; + + return this._viewportScrollOffset * this.viewportSize + (value - target); + } + + update() { + if (!this._started) return; + + let nElements = this.length; + if (!nElements) return; + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + + // Map position to index value. + let v = (this._horizontal ? this._wrapper.x : this._wrapper.y); + + let viewportSize = this.viewportSize; + let scrollDelta = this._viewportScrollOffset * viewportSize - this._itemScrollOffset * this._itemSize; + v += scrollDelta; + + let s, e, ps, pe; + if (direction == -1) { + s = Math.floor(-v / this._itemSize); + ps = 1 - ((-v / this._itemSize) - s); + e = Math.floor((viewportSize - v) / this._itemSize); + pe = (((viewportSize - v) / this._itemSize) - e); + } else { + s = Math.ceil(v / this._itemSize); + ps = 1 + (v / this._itemSize) - s; + e = Math.ceil((v - viewportSize) / this._itemSize); + pe = e - ((v - viewportSize) / this._itemSize); + } + if (this._roll || (viewportSize > this._itemSize * nElements)) { + // Don't show additional items. + if (e >= nElements) { + e = nElements - 1; + pe = 1; + } + if (s >= nElements) { + s = nElements - 1; + ps = 1; + } + if (e <= -1) { + e = 0; + pe = 1; + } + if (s <= -1) { + s = 0; + ps = 1; + } + } + + let offset = -direction * s * this._itemSize; + + let item; + for (let index = s; (direction == -1 ? index <= e : index >= e); (direction == -1 ? index++ : index--)) { + let realIndex = Utils.getModuloIndex(index, nElements); + + let element = this.getElement(realIndex); + item = element.parent; + this._visibleItems.delete(item); + if (this._horizontal) { + item.x = offset + scrollDelta; + } else { + item.y = offset + scrollDelta; + } + + let wasVisible = item.visible; + item.visible = true; + + if (!wasVisible || this._reloadVisibleElements) { + // Turned visible. + this.emit('visible', index, realIndex); + } + + + + if (this._progressAnimation) { + let p = 1; + if (index == s) { + p = ps; + } else if (index == e) { + p = pe; + } + + // Use animation to progress. + this._progressAnimation.apply(element, p); + } + + offset += this._itemSize; + } + + // Handle item visibility. + let self = this; + this._visibleItems.forEach(function(invisibleItem) { + invisibleItem.visible = false; + self._visibleItems.delete(invisibleItem); + }); + + for (let index = s; (direction == -1 ? index <= e : index >= e); (direction == -1 ? index++ : index--)) { + let realIndex = Utils.getModuloIndex(index, nElements); + this._visibleItems.add(this.getWrapper(realIndex)); + } + + this._reloadVisibleElements = false; + } + + setPrevious() { + this.setIndex(this._index - 1); + } + + setNext() { + this.setIndex(this._index + 1); + } + + getWrapper(index) { + return this._wrapper.children[index]; + } + + getElement(index) { + let e = this._wrapper.children[index]; + return e ? e.children[0] : null; + } + + reload() { + this._reloadVisibleElements = true; + this.update(); + } + + get element() { + let e = this._wrapper.children[this.realIndex]; + return e ? e.children[0] : null; + } + + get length() { + return this._wrapper.children.length; + } + + get property() { + return this._horizontal ? 'x' : 'y'; + } + + get viewportSize() { + return this._horizontal ? this.w : this.h; + } + + get index() { + return this._index; + } + + get realIndex() { + return Utils.getModuloIndex(this._index, this.length); + } + + get itemSize() { + return this._itemSize; + } + + set itemSize(v) { + this._itemSize = v; + this.update(); + } + + get viewportScrollOffset() { + return this._viewportScrollOffset; + } + + set viewportScrollOffset(v) { + this._viewportScrollOffset = v; + this.update(); + } + + get itemScrollOffset() { + return this._itemScrollOffset; + } + + set itemScrollOffset(v) { + this._itemScrollOffset = v; + this.update(); + } + + get scrollTransitionSettings() { + return this._scrollTransitionSettings; + } + + set scrollTransitionSettings(v) { + this._scrollTransitionSettings.patch(v); + } + + set scrollTransition(v) { + this._scrollTransitionSettings.patch(v); + } + + get scrollTransition() { + return this._scrollTransition; + } + + get progressAnimation() { + return this._progressAnimation; + } + + set progressAnimation(v) { + if (Utils.isObjectLiteral(v)) { + this._progressAnimation = this.stage.animations.createSettings(v); + } else { + this._progressAnimation = v; + } + this.update(); + } + + get roll() { + return this._roll; + } + + set roll(v) { + this._roll = v; + this.update(); + } + + get rollMin() { + return this._rollMin; + } + + set rollMin(v) { + this._rollMin = v; + this.update(); + } + + get rollMax() { + return this._rollMax; + } + + set rollMax(v) { + this._rollMax = v; + this.update(); + } + + get invertDirection() { + return this._invertDirection; + } + + set invertDirection(v) { + if (!this._started) { + this._invertDirection = v; + } + } + + get horizontal() { + return this._horizontal; + } + + set horizontal(v) { + if (v !== this._horizontal) { + if (!this._started) { + this._horizontal = v; + } + } + } + +} +class ListItems extends ObjectListWrapper { + constructor(list) { + let wrap = (item => { + let parent = item.stage.createElement(); + parent.add(item); + parent.visible = false; + return parent; + }); + + super(list._wrapper._children, wrap); + this.list = list; + } + + onAdd(item, index) { + super.onAdd(item, index); + this.checkStarted(index); + } + + checkStarted(index) { + this.list._reloadVisibleElements = true; + if (!this.list._started) { + this.list.start(); + } else { + if (this.list.length === 1) { + this.list.setIndex(0, true, true); + } else { + if (this.list._index >= this.list.length) { + this.list.setIndex(0); + } + } + this.list.update(); + } + } + + onRemove(item, index) { + super.onRemove(item, index); + let ri = this.list.realIndex; + if (ri === index) { + if (ri === this.list.length) { + ri--; + } + if (ri >= 0) { + this.list.setIndex(ri); + } + } else if (ri > index) { + this.list.setIndex(ri - 1); + } + + this.list._reloadVisibleElements = true; + } + + onSet(item, index) { + super.onSet(item, index); + this.checkStarted(index); + } + + onSync(removed, added, order) { + super.onSync(removed, added, order); + this.checkStarted(0); + } + + get _signalProxy() { + return true; + } + +} + +class LinearBlurShader extends DefaultShader { + + constructor(context) { + super(context); + + this._direction = new Float32Array([1, 0]); + this._kernelRadius = 1; + } + + get x() { + return this._direction[0]; + } + + set x(v) { + this._direction[0] = v; + this.redraw(); + } + + get y() { + return this._direction[1]; + } + + set y(v) { + this._direction[1] = v; + this.redraw(); + } + + get kernelRadius() { + return this._kernelRadius; + } + + set kernelRadius(v) { + this._kernelRadius = v; + this.redraw(); + } + + + useDefault() { + return (this._kernelRadius === 0); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("direction", this._direction, this.gl.uniform2fv); + this._setUniform("kernelRadius", this._kernelRadius, this.gl.uniform1i); + + const w = operation.getRenderWidth(); + const h = operation.getRenderHeight(); + this._setUniform("resolution", new Float32Array([w, h]), this.gl.uniform2fv); + } +} + +LinearBlurShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + uniform vec2 resolution; + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform vec2 direction; + uniform int kernelRadius; + + vec4 blur1(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.3333333333333333) * direction; + color += texture2D(image, uv) * 0.29411764705882354; + color += texture2D(image, uv + (off1 / resolution)) * 0.35294117647058826; + color += texture2D(image, uv - (off1 / resolution)) * 0.35294117647058826; + return color; + } + + vec4 blur2(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.3846153846) * direction; + vec2 off2 = vec2(3.2307692308) * direction; + color += texture2D(image, uv) * 0.2270270270; + color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162; + color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162; + color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703; + color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703; + return color; + } + + vec4 blur3(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.411764705882353) * direction; + vec2 off2 = vec2(3.2941176470588234) * direction; + vec2 off3 = vec2(5.176470588235294) * direction; + color += texture2D(image, uv) * 0.1964825501511404; + color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344; + color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344; + color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732; + color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732; + color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057; + color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057; + return color; + } + + void main(void){ + if (kernelRadius == 1) { + gl_FragColor = blur1(uSampler, vTextureCoord, resolution, direction) * vColor; + } else if (kernelRadius == 2) { + gl_FragColor = blur2(uSampler, vTextureCoord, resolution, direction) * vColor; + } else { + gl_FragColor = blur3(uSampler, vTextureCoord, resolution, direction) * vColor; + } + } +`; + +/** + * 4x4 box blur shader which works in conjunction with a 50% rescale. + */ +class BoxBlurShader extends DefaultShader { + + setupUniforms(operation) { + super.setupUniforms(operation); + const dx = 1.0 / operation.getTextureWidth(0); + const dy = 1.0 / operation.getTextureHeight(0); + this._setUniform("stepTextureCoord", new Float32Array([dx, dy]), this.gl.uniform2fv); + } + +} + +BoxBlurShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + uniform vec2 stepTextureCoord; + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec4 vColor; + varying vec2 vTextureCoordUl; + varying vec2 vTextureCoordUr; + varying vec2 vTextureCoordBl; + varying vec2 vTextureCoordBr; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoordUl = aTextureCoord - stepTextureCoord; + vTextureCoordBr = aTextureCoord + stepTextureCoord; + vTextureCoordUr = vec2(vTextureCoordBr.x, vTextureCoordUl.y); + vTextureCoordBl = vec2(vTextureCoordUl.x, vTextureCoordBr.y); + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +BoxBlurShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoordUl; + varying vec2 vTextureCoordUr; + varying vec2 vTextureCoordBl; + varying vec2 vTextureCoordBr; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + vec4 color = 0.25 * (texture2D(uSampler, vTextureCoordUl) + texture2D(uSampler, vTextureCoordUr) + texture2D(uSampler, vTextureCoordBl) + texture2D(uSampler, vTextureCoordBr)); + gl_FragColor = color * vColor; + } +`; + +class BlurShader extends DefaultShader$1 { + + constructor(context) { + super(context); + this._kernelRadius = 1; + } + + get kernelRadius() { + return this._kernelRadius; + } + + set kernelRadius(v) { + this._kernelRadius = v; + this.redraw(); + } + + useDefault() { + return this._amount === 0; + } + + _beforeDrawEl({target}) { + target.ctx.filter = "blur(" + this._kernelRadius + "px)"; + } + + _afterDrawEl({target}) { + target.ctx.filter = "none"; + } + +} + +class FastBlurComponent extends Component { + static _template() { + return {} + } + + get wrap() { + return this.tag("Wrap"); + } + + set content(v) { + return this.wrap.content = v; + } + + get content() { + return this.wrap.content; + } + + set padding(v) { + this.wrap._paddingX = v; + this.wrap._paddingY = v; + this.wrap._updateBlurSize(); + } + + set paddingX(v) { + this.wrap._paddingX = v; + this.wrap._updateBlurSize(); + } + + set paddingY(v) { + this.wrap._paddingY = v; + this.wrap._updateBlurSize(); + } + + set amount(v) { + return this.wrap.amount = v; + } + + get amount() { + return this.wrap.amount; + } + + _onResize() { + this.wrap.w = this.renderWidth; + this.wrap.h = this.renderHeight; + } + + get _signalProxy() { + return true; + } + + _build() { + this.patch({ + Wrap: {type: this.stage.gl ? WebGLFastBlurComponent : C2dFastBlurComponent} + }); + } + +} + + +class C2dFastBlurComponent extends Component { + + static _template() { + return { + forceZIndexContext: true, + rtt: true, + Textwrap: {shader: {type: BlurShader}, Content: {}} + } + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap>Content"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + + } + + static getSpline() { + if (!this._multiSpline) { + this._multiSpline = new MultiSpline(); + this._multiSpline.parse(false, {0: 0, 0.25: 1.5, 0.5: 5.5, 0.75: 18, 1: 39}); + } + return this._multiSpline; + } + + get content() { + return this.sel('Textwrap>Content'); + } + + set content(v) { + this.sel('Textwrap>Content').patch(v, true); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + this._wrapper.x = paddingX; + this._textwrap.x = -paddingX; + + this._wrapper.y = paddingY; + this._textwrap.y = -paddingY; + + this._textwrap.w = w + paddingX * 2; + this._textwrap.h = h + paddingY * 2; + } + + get amount() { + return this._amount; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._textwrap.shader.kernelRadius = C2dFastBlurComponent._amountToKernelRadius(v); + } + + static _amountToKernelRadius(v) { + return C2dFastBlurComponent.getSpline().getValue(Math.min(1, v * 0.25)); + } + + get _signalProxy() { + return true; + } + +} + +class WebGLFastBlurComponent extends Component { + + static _template() { + const onUpdate = function(element, elementCore) { + if ((elementCore._recalc & (2 + 128))) { + const w = elementCore.w; + const h = elementCore.h; + let cur = elementCore; + do { + cur = cur._children[0]; + cur._element.w = w; + cur._element.h = h; + } while(cur._children); + } + }; + + return { + Textwrap: {rtt: true, forceZIndexContext: true, renderOffscreen: true, Content: {}}, + Layers: { + L0: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L1: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L2: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L3: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}} + }, + Result: {shader: {type: FastBlurOutputShader}, visible: false} + } + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap>Content"); + this._layers = this.sel("Layers"); + this._output = this.sel("Result"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + } + + _buildLayers() { + const filterShaderSettings = [{x:1,y:0,kernelRadius:1},{x:0,y:1,kernelRadius:1},{x:1.5,y:0,kernelRadius:1},{x:0,y:1.5,kernelRadius:1}]; + const filterShaders = filterShaderSettings.map(s => { + const shader = Shader.create(this.stage, Object.assign({type: LinearBlurShader}, s)); + return shader; + }); + + this._setLayerTexture(this.getLayerContents(0), this._textwrap.getTexture(), []); + this._setLayerTexture(this.getLayerContents(1), this.getLayer(0).getTexture(), [filterShaders[0], filterShaders[1]]); + + // Notice that 1.5 filters should be applied before 1.0 filters. + this._setLayerTexture(this.getLayerContents(2), this.getLayer(1).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + this._setLayerTexture(this.getLayerContents(3), this.getLayer(2).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + } + + _setLayerTexture(element, texture, steps) { + if (!steps.length) { + element.texture = texture; + } else { + const step = steps.pop(); + const child = element.stage.c({rtt: true, shader: step}); + + // Recurse. + this._setLayerTexture(child, texture, steps); + + element.childList.add(child); + } + return element; + } + + get content() { + return this.sel('Textwrap>Content'); + } + + set content(v) { + this.sel('Textwrap>Content').patch(v, true); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + getLayer(i) { + return this._layers.sel("L" + i); + } + + getLayerContents(i) { + return this.getLayer(i).sel("Content"); + } + + _onResize() { + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + let fw = w + paddingX * 2; + let fh = h + paddingY * 2; + this._textwrap.w = fw; + this._wrapper.x = paddingX; + this.getLayer(0).w = this.getLayerContents(0).w = fw / 2; + this.getLayer(1).w = this.getLayerContents(1).w = fw / 4; + this.getLayer(2).w = this.getLayerContents(2).w = fw / 8; + this.getLayer(3).w = this.getLayerContents(3).w = fw / 16; + this._output.x = -paddingX; + this._textwrap.x = -paddingX; + this._output.w = fw; + + this._textwrap.h = fh; + this._wrapper.y = paddingY; + this.getLayer(0).h = this.getLayerContents(0).h = fh / 2; + this.getLayer(1).h = this.getLayerContents(1).h = fh / 4; + this.getLayer(2).h = this.getLayerContents(2).h = fh / 8; + this.getLayer(3).h = this.getLayerContents(3).h = fh / 16; + this._output.y = -paddingY; + this._textwrap.y = -paddingY; + this._output.h = fh; + + this.w = w; + this.h = h; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._update(); + } + + get amount() { + return this._amount; + } + + _update() { + let v = Math.min(4, Math.max(0, this._amount)); + if (v === 0) { + this._textwrap.renderToTexture = false; + this._output.shader.otherTextureSource = null; + this._output.visible = false; + } else { + this._textwrap.renderToTexture = true; + this._output.visible = true; + + this.getLayer(0).visible = (v > 0); + this.getLayer(1).visible = (v > 1); + this.getLayer(2).visible = (v > 2); + this.getLayer(3).visible = (v > 3); + + if (v <= 1) { + this._output.texture = this._textwrap.getTexture(); + this._output.shader.otherTextureSource = this.getLayer(0).getTexture(); + this._output.shader.a = v; + } else if (v <= 2) { + this._output.texture = this.getLayer(0).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(1).getTexture(); + this._output.shader.a = v - 1; + } else if (v <= 3) { + this._output.texture = this.getLayer(1).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(2).getTexture(); + this._output.shader.a = v - 2; + } else if (v <= 4) { + this._output.texture = this.getLayer(2).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(3).getTexture(); + this._output.shader.a = v - 3; + } + } + } + + set shader(s) { + super.shader = s; + if (!this.renderToTexture) { + console.warn("Please enable renderToTexture to use with a shader."); + } + } + + _firstActive() { + this._buildLayers(); + } + +} + +/** + * Shader that combines two textures into one output. + */ +class FastBlurOutputShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._a = 0; + this._otherTextureSource = null; + } + + get a() { + return this._a; + } + + set a(v) { + this._a = v; + this.redraw(); + } + + set otherTextureSource(v) { + this._otherTextureSource = v; + this.redraw(); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("a", this._a, this.gl.uniform1f); + this._setUniform("uSampler2", 1, this.gl.uniform1i); + } + + beforeDraw(operation) { + let glTexture = this._otherTextureSource ? this._otherTextureSource.nativeTexture : null; + + let gl = this.gl; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.activeTexture(gl.TEXTURE0); + } +} + +FastBlurOutputShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform sampler2D uSampler2; + uniform float a; + void main(void){ + if (a == 1.0) { + gl_FragColor = texture2D(uSampler2, vTextureCoord) * vColor; + } else { + gl_FragColor = ((1.0 - a) * texture2D(uSampler, vTextureCoord) + (a * texture2D(uSampler2, vTextureCoord))) * vColor; + } + } +`; + +class BloomComponent extends Component { + + static _template() { + const onUpdate = function(element, elementCore) { + if ((elementCore._recalc & (2 + 128))) { + const w = elementCore.w; + const h = elementCore.h; + let cur = elementCore; + do { + cur = cur._children[0]; + cur._element.w = w; + cur._element.h = h; + } while(cur._children); + } + }; + + return { + Textwrap: {rtt: true, forceZIndexContext: true, renderOffscreen: true, + BloomBase: {shader: {type: BloomBaseShader}, + Content: {} + } + }, + Layers: { + L0: {rtt: true, onUpdate: onUpdate, scale: 2, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L1: {rtt: true, onUpdate: onUpdate, scale: 4, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L2: {rtt: true, onUpdate: onUpdate, scale: 8, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L3: {rtt: true, onUpdate: onUpdate, scale: 16, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}} + } + } + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap.Content"); + this._layers = this.sel("Layers"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + } + + _build() { + const filterShaderSettings = [{x:1,y:0,kernelRadius:3},{x:0,y:1,kernelRadius:3},{x:1.5,y:0,kernelRadius:3},{x:0,y:1.5,kernelRadius:3}]; + const filterShaders = filterShaderSettings.map(s => { + const shader = this.stage.createShader(Object.assign({type: LinearBlurShader}, s)); + return shader; + }); + + this._setLayerTexture(this.getLayerContents(0), this._textwrap.getTexture(), []); + this._setLayerTexture(this.getLayerContents(1), this.getLayer(0).getTexture(), [filterShaders[0], filterShaders[1]]); + + // Notice that 1.5 filters should be applied before 1.0 filters. + this._setLayerTexture(this.getLayerContents(2), this.getLayer(1).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + this._setLayerTexture(this.getLayerContents(3), this.getLayer(2).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + } + + _setLayerTexture(element, texture, steps) { + if (!steps.length) { + element.texture = texture; + } else { + const step = steps.pop(); + const child = element.stage.c({rtt: true, shader: step}); + + // Recurse. + this._setLayerTexture(child, texture, steps); + + element.childList.add(child); + } + return element; + } + + get content() { + return this.sel('Textwrap.Content'); + } + + set content(v) { + this.sel('Textwrap.Content').patch(v); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + getLayer(i) { + return this._layers.sel("L" + i); + } + + getLayerContents(i) { + return this.getLayer(i).sel("Content"); + } + + _onResize() { + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + let fw = w + paddingX * 2; + let fh = h + paddingY * 2; + this._textwrap.w = fw; + this._wrapper.x = paddingX; + this.getLayer(0).w = this.getLayerContents(0).w = fw / 2; + this.getLayer(1).w = this.getLayerContents(1).w = fw / 4; + this.getLayer(2).w = this.getLayerContents(2).w = fw / 8; + this.getLayer(3).w = this.getLayerContents(3).w = fw / 16; + this._textwrap.x = -paddingX; + + this._textwrap.h = fh; + this._wrapper.y = paddingY; + this.getLayer(0).h = this.getLayerContents(0).h = fh / 2; + this.getLayer(1).h = this.getLayerContents(1).h = fh / 4; + this.getLayer(2).h = this.getLayerContents(2).h = fh / 8; + this.getLayer(3).h = this.getLayerContents(3).h = fh / 16; + this._textwrap.y = -paddingY; + + this.w = w; + this.h = h; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._update(); + } + + get amount() { + return this._amount; + } + + _update() { + let v = Math.min(4, Math.max(0, this._amount)); + if (v > 0) { + this.getLayer(0).visible = (v > 0); + this.getLayer(1).visible = (v > 1); + this.getLayer(2).visible = (v > 2); + this.getLayer(3).visible = (v > 3); + } + } + + set shader(s) { + super.shader = s; + if (!this.renderToTexture) { + console.warn("Please enable renderToTexture to use with a shader."); + } + } + + _firstActive() { + this._build(); + } + +} + +class BloomBaseShader extends DefaultShader { +} + +BloomBaseShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + float m = max(max(color.r, color.g), color.b); + float c = max(0.0, (m - 0.80)) * 5.0; + color = color * c; + gl_FragColor = color; + } +`; + +class SmoothScaleComponent extends Component { + + static _template() { + return { + ContentWrap: {renderOffscreen: true, forceZIndexContext: true, onAfterUpdate: SmoothScaleComponent._updateDimensions, + Content: {} + }, + Scale: {visible: false} + } + } + + constructor(stage) { + super(stage); + + this._smoothScale = 1; + this._iterations = 0; + } + + get content() { + return this.tag('Content'); + } + + set content(v) { + this.tag('Content').patch(v, true); + } + + get smoothScale() { + return this._smoothScale; + } + + set smoothScale(v) { + if (this._smoothScale !== v) { + let its = 0; + while(v < 0.5 && its < 12) { + its++; + v = v * 2; + } + + this.scale = v; + this._setIterations(its); + + this._smoothScale = v; + } + } + + _setIterations(its) { + if (this._iterations !== its) { + const scalers = this.sel("Scale").childList; + const content = this.sel("ContentWrap"); + while (scalers.length < its) { + const first = scalers.length === 0; + const texture = (first ? content.getTexture() : scalers.last.getTexture()); + scalers.a({rtt: true, renderOffscreen: true, texture: texture}); + } + + SmoothScaleComponent._updateDimensions(this.tag("ContentWrap"), true); + + const useScalers = (its > 0); + this.patch({ + ContentWrap: {renderToTexture: useScalers}, + Scale: {visible: useScalers} + }); + + for (let i = 0, n = scalers.length; i < n; i++) { + scalers.getAt(i).patch({ + visible: i < its, + renderOffscreen: i !== its - 1 + }); + } + this._iterations = its; + } + } + + static _updateDimensions(contentWrap, force) { + const content = contentWrap.children[0]; + let w = content.renderWidth; + let h = content.renderHeight; + if (w !== contentWrap.w || h !== contentWrap.h || force) { + contentWrap.w = w; + contentWrap.h = h; + + const scalers = contentWrap.parent.tag("Scale").children; + for (let i = 0, n = scalers.length; i < n; i++) { + w = w * 0.5; + h = h * 0.5; + scalers[i].w = w; + scalers[i].h = h; + } + } + } + + get _signalProxy() { + return true; + } + +} + +class BorderComponent extends Component { + + static _template() { + return { + Content: {}, + Borders: { + Top: {rect: true, visible: false, mountY: 1}, + Right: {rect: true, visible: false}, + Bottom: {rect: true, visible: false}, + Left: {rect: true, visible: false, mountX: 1} + } + }; + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + + this._borderTop = this.tag("Top"); + this._borderRight = this.tag("Right"); + this._borderBottom = this.tag("Bottom"); + this._borderLeft = this.tag("Left"); + + this.onAfterUpdate = function (element) { + const content = element.childList.first; + let w = element.core.w || content.renderWidth; + let h = element.core.h || content.renderHeight; + element._borderTop.w = w; + element._borderBottom.y = h; + element._borderBottom.w = w; + element._borderLeft.h = h + element._borderTop.h + element._borderBottom.h; + element._borderLeft.y = -element._borderTop.h; + element._borderRight.x = w; + element._borderRight.h = h + element._borderTop.h + element._borderBottom.h; + element._borderRight.y = -element._borderTop.h; + }; + + this.borderWidth = 1; + } + + get content() { + return this.sel('Content'); + } + + set content(v) { + this.sel('Content').patch(v, true); + } + + get borderWidth() { + return this.borderWidthTop; + } + + get borderWidthTop() { + return this._borderTop.h; + } + + get borderWidthRight() { + return this._borderRight.w; + } + + get borderWidthBottom() { + return this._borderBottom.h; + } + + get borderWidthLeft() { + return this._borderLeft.w; + } + + set borderWidth(v) { + this.borderWidthTop = v; + this.borderWidthRight = v; + this.borderWidthBottom = v; + this.borderWidthLeft = v; + } + + set borderWidthTop(v) { + this._borderTop.h = v; + this._borderTop.visible = (v > 0); + } + + set borderWidthRight(v) { + this._borderRight.w = v; + this._borderRight.visible = (v > 0); + } + + set borderWidthBottom(v) { + this._borderBottom.h = v; + this._borderBottom.visible = (v > 0); + } + + set borderWidthLeft(v) { + this._borderLeft.w = v; + this._borderLeft.visible = (v > 0); + } + + get colorBorder() { + return this.colorBorderTop; + } + + get colorBorderTop() { + return this._borderTop.color; + } + + get colorBorderRight() { + return this._borderRight.color; + } + + get colorBorderBottom() { + return this._borderBottom.color; + } + + get colorBorderLeft() { + return this._borderLeft.color; + } + + set colorBorder(v) { + this.colorBorderTop = v; + this.colorBorderRight = v; + this.colorBorderBottom = v; + this.colorBorderLeft = v; + } + + set colorBorderTop(v) { + this._borderTop.color = v; + } + + set colorBorderRight(v) { + this._borderRight.color = v; + } + + set colorBorderBottom(v) { + this._borderBottom.color = v; + } + + set colorBorderLeft(v) { + this._borderLeft.color = v; + } + + get borderTop() { + return this._borderTop; + } + + set borderTop(settings) { + this.borderTop.patch(settings); + } + + get borderRight() { + return this._borderRight; + } + + set borderRight(settings) { + this.borderRight.patch(settings); + } + + get borderBottom() { + return this._borderBottom; + } + + set borderBottom(settings) { + this.borderBottom.patch(settings); + } + + get borderLeft() { + return this._borderLeft; + } + + set borderLeft(settings) { + this.borderLeft.patch(settings); + } + + set borders(settings) { + this.borderTop = settings; + this.borderLeft = settings; + this.borderBottom = settings; + this.borderRight = settings; + } + +} + +class GrayscaleShader extends DefaultShader$1 { + + constructor(context) { + super(context); + this._amount = 1; + } + + static getWebGL() { + return GrayscaleShader$1; + } + + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + _beforeDrawEl({target}) { + target.ctx.filter = "grayscale(" + this._amount + ")"; + } + + _afterDrawEl({target}) { + target.ctx.filter = "none"; + } + +} + +class GrayscaleShader$1 extends DefaultShader { + + constructor(context) { + super(context); + this._amount = 1; + } + + static getC2d() { + return GrayscaleShader; + } + + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("amount", this._amount, this.gl.uniform1f); + } + +} + +GrayscaleShader$1.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float amount; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + float grayness = 0.2 * color.r + 0.6 * color.g + 0.2 * color.b; + gl_FragColor = vec4(amount * vec3(grayness, grayness, grayness) + (1.0 - amount) * color.rgb, color.a); + } +`; + +/** + * This shader can be used to fix a problem that is known as 'gradient banding'. + */ +class DitheringShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._noiseTexture = new NoiseTexture(ctx.stage); + + this._graining = 1/256; + + this._random = false; + } + + set graining(v) { + this._graining = v; + this.redraw(); + } + + set random(v) { + this._random = v; + this.redraw(); + } + + setExtraAttribsInBuffer(operation) { + // Make sure that the noise texture is uploaded to the GPU. + this._noiseTexture.load(); + + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + + for (let i = 0; i < length; i++) { + + // Calculate noise texture coordinates so that it spans the full element. + let brx = operation.getElementWidth(i) / this._noiseTexture.getRenderWidth(); + let bry = operation.getElementHeight(i) / this._noiseTexture.getRenderHeight(); + + let ulx = 0; + let uly = 0; + if (this._random) { + ulx = Math.random(); + uly = Math.random(); + + brx += ulx; + bry += uly; + + if (Math.random() < 0.5) { + // Flip for more randomness. + const t = ulx; + ulx = brx; + brx = t; + } + + if (Math.random() < 0.5) { + // Flip for more randomness. + const t = uly; + uly = bry; + bry = t; + } + } + + // Specify all corner points. + floats[offset] = ulx; + floats[offset + 1] = uly; + + floats[offset + 2] = brx; + floats[offset + 3] = uly; + + floats[offset + 4] = brx; + floats[offset + 5] = bry; + + floats[offset + 6] = ulx; + floats[offset + 7] = bry; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aNoiseTextureCoord"), 2, gl.FLOAT, false, 8, this.getVertexAttribPointerOffset(operation)); + + let glTexture = this._noiseTexture.source.nativeTexture; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.activeTexture(gl.TEXTURE0); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("uNoiseSampler", 1, this.gl.uniform1i); + this._setUniform("graining", 2 * this._graining, this.gl.uniform1f); + } + + enableAttribs() { + super.enableAttribs(); + let gl = this.gl; + gl.enableVertexAttribArray(this._attrib("aNoiseTextureCoord")); + } + + disableAttribs() { + super.disableAttribs(); + let gl = this.gl; + gl.disableVertexAttribArray(this._attrib("aNoiseTextureCoord")); + } + + useDefault() { + return this._graining === 0; + } + + afterDraw(operation) { + if (this._random) { + this.redraw(); + } + } + +} + +DitheringShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec2 aNoiseTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec2 vNoiseTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vNoiseTextureCoord = aNoiseTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +DitheringShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec2 vNoiseTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform sampler2D uNoiseSampler; + uniform float graining; + void main(void){ + vec4 noise = texture2D(uNoiseSampler, vNoiseTextureCoord); + vec4 color = texture2D(uSampler, vTextureCoord); + gl_FragColor = (color * vColor) + graining * (noise.r - 0.5); + } +`; + +class CircularPushShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._inputValue = 0; + + this._maxDerivative = 0.01; + + this._normalizedValue = 0; + + // The offset between buckets. A value between 0 and 1. + this._offset = 0; + + this._amount = 0.1; + + this._aspectRatio = 1; + + this._offsetX = 0; + + this._offsetY = 0; + + this.buckets = 100; + } + + get aspectRatio() { + return this._aspectRatio; + } + + set aspectRatio(v) { + this._aspectRatio = v; + this.redraw(); + } + + get offsetX() { + return this._offsetX; + } + + set offsetX(v) { + this._offsetX = v; + this.redraw(); + } + + get offsetY() { + return this._offsetY; + } + + set offsetY(v) { + this._offsetY = v; + this.redraw(); + } + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + set inputValue(v) { + this._inputValue = v; + } + + get inputValue() { + return this._inputValue; + } + + set maxDerivative(v) { + this._maxDerivative = v; + } + + get maxDerivative() { + return this._maxDerivative; + } + + set buckets(v) { + if (v > 100) { + console.warn("CircularPushShader: supports max 100 buckets"); + v = 100; + } + + // This should be set before starting. + this._buckets = v; + + // Init values array in the correct length. + this._values = new Uint8Array(this._getValues(v)); + + this.redraw(); + } + + get buckets() { + return this._buckets; + } + + _getValues(n) { + const v = []; + for (let i = 0; i < n; i++) { + v.push(this._inputValue); + } + return v; + } + + /** + * Progresses the shader with the specified (fractional) number of buckets. + * @param {number} o; + * A number from 0 to 1 (1 = all buckets). + */ + progress(o) { + this._offset += o * this._buckets; + const full = Math.floor(this._offset); + this._offset -= full; + this._shiftBuckets(full); + this.redraw(); + } + + _shiftBuckets(n) { + for (let i = this._buckets - 1; i >= 0; i--) { + const targetIndex = i - n; + if (targetIndex < 0) { + this._normalizedValue = Math.min(this._normalizedValue + this._maxDerivative, Math.max(this._normalizedValue - this._maxDerivative, this._inputValue)); + this._values[i] = 255 * this._normalizedValue; + } else { + this._values[i] = this._values[targetIndex]; + } + } + } + + set offset(v) { + this._offset = v; + this.redraw(); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("aspectRatio", this._aspectRatio, this.gl.uniform1f); + this._setUniform("offsetX", this._offsetX, this.gl.uniform1f); + this._setUniform("offsetY", this._offsetY, this.gl.uniform1f); + this._setUniform("amount", this._amount, this.gl.uniform1f); + this._setUniform("offset", this._offset, this.gl.uniform1f); + this._setUniform("buckets", this._buckets, this.gl.uniform1f); + this._setUniform("uValueSampler", 1, this.gl.uniform1i); + } + + useDefault() { + return this._amount === 0; + } + + beforeDraw(operation) { + const gl = this.gl; + gl.activeTexture(gl.TEXTURE1); + if (!this._valuesTexture) { + this._valuesTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._valuesTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (Utils.isNode) { + gl.pixelStorei(gl.UNPACK_FLIP_BLUE_RED, false); + } + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + } else { + gl.bindTexture(gl.TEXTURE_2D, this._valuesTexture); + } + + // Upload new values. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this._buckets, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this._values); + gl.activeTexture(gl.TEXTURE0); + } + + cleanup() { + if (this._valuesTexture) { + this.gl.deleteTexture(this._valuesTexture); + } + } + + +} + +CircularPushShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + uniform float offsetX; + uniform float offsetY; + uniform float aspectRatio; + varying vec2 vTextureCoord; + varying vec2 vPos; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vPos = vTextureCoord * 2.0 - 1.0; + vPos.y = vPos.y * aspectRatio; + vPos.y = vPos.y + offsetY; + vPos.x = vPos.x + offsetX; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +CircularPushShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vPos; + uniform float amount; + uniform float offset; + uniform float values[100]; + uniform float buckets; + uniform sampler2D uSampler; + uniform sampler2D uValueSampler; + void main(void){ + float l = length(vPos); + float m = (l * buckets * 0.678 - offset) / buckets; + float f = texture2D(uValueSampler, vec2(m, 0.0)).a * amount; + vec2 unit = vPos / l; + gl_FragColor = texture2D(uSampler, vTextureCoord - f * unit) * vColor; + } +`; + +class InversionShader extends DefaultShader { + + constructor(context) { + super(context); + this._amount = 1; + } + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("amount", this._amount, this.gl.uniform1f); + } + +} + +InversionShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float amount; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord); + color.rgb = color.rgb * (1.0 - amount) + amount * (1.0 * color.a - color.rgb); + gl_FragColor = color * vColor; + } +`; + +class OutlineShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + this._width = 5; + this._col = 0xFFFFFFFF; + this._color = [1,1,1,1]; + } + + set width(v) { + this._width = v; + this.redraw(); + } + + get color() { + return this._col; + } + + set color(v) { + if (this._col !== v) { + const col = StageUtils.getRgbaComponentsNormalized(v); + col[0] = col[0] * col[3]; + col[1] = col[1] * col[3]; + col[2] = col[2] * col[3]; + + this._color = col; + + this.redraw(); + + this._col = v; + } + } + + useDefault() { + return (this._width === 0 || this._col[3] === 0); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + let gl = this.gl; + this._setUniform("color", new Float32Array(this._color), gl.uniform4fv); + } + + enableAttribs() { + super.enableAttribs(); + this.gl.enableVertexAttribArray(this._attrib("aCorner")); + } + + disableAttribs() { + super.disableAttribs(); + this.gl.disableVertexAttribArray(this._attrib("aCorner")); + } + + setExtraAttribsInBuffer(operation) { + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + + for (let i = 0; i < length; i++) { + + const elementCore = operation.getElementCore(i); + + // We are setting attributes such that if the value is < 0 or > 1, a border should be drawn. + const ddw = this._width / elementCore.w; + const dw = ddw / (1 - 2 * ddw); + const ddh = this._width / elementCore.h; + const dh = ddh / (1 - 2 * ddh); + + // Specify all corner points. + floats[offset] = -dw; + floats[offset + 1] = -dh; + + floats[offset + 2] = 1 + dw; + floats[offset + 3] = -dh; + + floats[offset + 4] = 1 + dw; + floats[offset + 5] = 1 + dh; + + floats[offset + 6] = -dw; + floats[offset + 7] = 1 + dh; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aCorner"), 2, gl.FLOAT, false, 8, this.getVertexAttribPointerOffset(operation)); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + +} + +OutlineShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + attribute vec2 aCorner; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec2 vCorner; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vCorner = aCorner; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +OutlineShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vCorner; + uniform vec4 color; + uniform sampler2D uSampler; + void main(void){ + vec2 m = min(vCorner, 1.0 - vCorner); + float value = step(0.0, min(m.x, m.y)); + gl_FragColor = mix(color, texture2D(uSampler, vTextureCoord) * vColor, value); + } +`; + +/** + * @see https://github.com/pixijs/pixi-filters/tree/master/filters/pixelate/src + */ +class PixelateShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._size = new Float32Array([4, 4]); + } + + get x() { + return this._size[0]; + } + + set x(v) { + this._size[0] = v; + this.redraw(); + } + + get y() { + return this._size[1]; + } + + set y(v) { + this._size[1] = v; + this.redraw(); + } + + get size() { + return this._size[0]; + } + + set size(v) { + this._size[0] = v; + this._size[1] = v; + this.redraw(); + } + + useDefault() { + return ((this._size[0] === 0) && (this._size[1] === 0)); + } + + static getWebGLImpl() { + return WebGLPixelateShaderImpl; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + let gl = this.gl; + this._setUniform("size", new Float32Array(this._size), gl.uniform2fv); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + + enableAttribs() { + super.enableAttribs(); + this.gl.enableVertexAttribArray(this._attrib("aTextureRes")); + } + + disableAttribs() { + super.disableAttribs(); + this.gl.disableVertexAttribArray(this._attrib("aTextureRes")); + } + + setExtraAttribsInBuffer(operation) { + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + for (let i = 0; i < length; i++) { + let w = operation.quads.getTextureWidth(operation.index + i); + let h = operation.quads.getTextureHeight(operation.index + i); + + floats[offset] = w; + floats[offset + 1] = h; + floats[offset + 2] = w; + floats[offset + 3] = h; + floats[offset + 4] = w; + floats[offset + 5] = h; + floats[offset + 6] = w; + floats[offset + 7] = h; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aTextureRes"), 2, gl.FLOAT, false, this.getExtraAttribBytesPerVertex(), this.getVertexAttribPointerOffset(operation)); + } +} + +PixelateShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + attribute vec2 aTextureRes; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vTextureRes; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + vTextureRes = aTextureRes; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +PixelateShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vTextureRes; + + uniform vec2 size; + uniform sampler2D uSampler; + + vec2 mapCoord( vec2 coord ) + { + coord *= vTextureRes.xy; + return coord; + } + + vec2 unmapCoord( vec2 coord ) + { + coord /= vTextureRes.xy; + return coord; + } + + vec2 pixelate(vec2 coord, vec2 size) + { + return floor( coord / size ) * size; + } + + void main(void) + { + vec2 coord = mapCoord(vTextureCoord); + coord = pixelate(coord, size); + coord = unmapCoord(coord); + gl_FragColor = texture2D(uSampler, coord) * vColor; + } +`; + +class RadialFilterShader extends DefaultShader { + constructor(context) { + super(context); + this._radius = 0; + this._cutoff = 1; + } + + set radius(v) { + this._radius = v; + this.redraw(); + } + + get radius() { + return this._radius; + } + + set cutoff(v) { + this._cutoff = v; + this.redraw(); + } + + get cutoff() { + return this._cutoff; + } + + useDefault() { + return this._radius === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + // We substract half a pixel to get a better cutoff effect. + this._setUniform("radius", 2 * (this._radius - 0.5) / operation.getRenderWidth(), this.gl.uniform1f); + this._setUniform("cutoff", 0.5 * operation.getRenderWidth() / this._cutoff, this.gl.uniform1f); + } + +} + +RadialFilterShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 pos; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + pos = gl_Position.xy; + } +`; + +RadialFilterShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec2 pos; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float radius; + uniform float cutoff; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord); + float f = max(0.0, min(1.0, 1.0 - (length(pos) - radius) * cutoff)); + gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor * f; + } +`; + +class RadialGradientShader extends DefaultShader { + + constructor(context) { + super(context); + + this._x = 0; + this._y = 0; + + this.color = 0xFFFF0000; + + this._radiusX = 100; + this._radiusY = 100; + } + + set x(v) { + this._x = v; + this.redraw(); + } + + set y(v) { + this._y = v; + this.redraw(); + } + + set radiusX(v) { + this._radiusX = v; + this.redraw(); + } + + get radiusX() { + return this._radiusX; + } + + set radiusY(v) { + this._radiusY = v; + this.redraw(); + } + + get radiusY() { + return this._radiusY; + } + + set radius(v) { + this.radiusX = v; + this.radiusY = v; + } + + get color() { + return this._color; + } + + set color(v) { + if (this._color !== v) { + const col = StageUtils.getRgbaComponentsNormalized(v); + col[0] = col[0] * col[3]; + col[1] = col[1] * col[3]; + col[2] = col[2] * col[3]; + + this._rawColor = new Float32Array(col); + + this.redraw(); + + this._color = v; + } + } + + setupUniforms(operation) { + super.setupUniforms(operation); + // We substract half a pixel to get a better cutoff effect. + const rtc = operation.getNormalRenderTextureCoords(this._x, this._y); + this._setUniform("center", new Float32Array(rtc), this.gl.uniform2fv); + + this._setUniform("radius", 2 * this._radiusX / operation.getRenderWidth(), this.gl.uniform1f); + + + // Radial gradient shader is expected to be used on a single element. That element's alpha is used. + this._setUniform("alpha", operation.getElementCore(0).renderContext.alpha, this.gl.uniform1f); + + this._setUniform("color", this._rawColor, this.gl.uniform4fv); + this._setUniform("aspectRatio", (this._radiusX/this._radiusY) * operation.getRenderHeight()/operation.getRenderWidth(), this.gl.uniform1f); + } + +} + +RadialGradientShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + uniform vec2 center; + uniform float aspectRatio; + varying vec2 pos; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + pos = gl_Position.xy - center; + pos.y = pos.y * aspectRatio; + } +`; + +RadialGradientShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 pos; + uniform sampler2D uSampler; + uniform float radius; + uniform vec4 color; + uniform float alpha; + void main(void){ + float dist = length(pos); + gl_FragColor = mix(color * alpha, texture2D(uSampler, vTextureCoord) * vColor, min(1.0, dist / radius)); + } +`; + +class Light3dShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._strength = 0.5; + this._ambient = 0.5; + this._fudge = 0.4; + + this._rx = 0; + this._ry = 0; + + this._z = 0; + this._pivotX = NaN; + this._pivotY = NaN; + this._pivotZ = 0; + + this._lightY = 0; + this._lightZ = 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + + let vr = operation.shaderOwner; + let element = vr.element; + + let pivotX = isNaN(this._pivotX) ? element.pivotX * vr.w : this._pivotX; + let pivotY = isNaN(this._pivotY) ? element.pivotY * vr.h : this._pivotY; + let coords = vr.getRenderTextureCoords(pivotX, pivotY); + + // Counter normal rotation. + + let rz = -Math.atan2(vr._renderContext.tc, vr._renderContext.ta); + + let gl = this.gl; + this._setUniform("pivot", new Float32Array([coords[0], coords[1], this._pivotZ]), gl.uniform3fv); + this._setUniform("rot", new Float32Array([this._rx, this._ry, rz]), gl.uniform3fv); + + this._setUniform("z", this._z, gl.uniform1f); + this._setUniform("lightY", this.lightY, gl.uniform1f); + this._setUniform("lightZ", this.lightZ, gl.uniform1f); + this._setUniform("strength", this._strength, gl.uniform1f); + this._setUniform("ambient", this._ambient, gl.uniform1f); + this._setUniform("fudge", this._fudge, gl.uniform1f); + } + + set strength(v) { + this._strength = v; + this.redraw(); + } + + get strength() { + return this._strength; + } + + set ambient(v) { + this._ambient = v; + this.redraw(); + } + + get ambient() { + return this._ambient; + } + + set fudge(v) { + this._fudge = v; + this.redraw(); + } + + get fudge() { + return this._fudge; + } + + get rx() { + return this._rx; + } + + set rx(v) { + this._rx = v; + this.redraw(); + } + + get ry() { + return this._ry; + } + + set ry(v) { + this._ry = v; + this.redraw(); + } + + get z() { + return this._z; + } + + set z(v) { + this._z = v; + this.redraw(); + } + + get pivotX() { + return this._pivotX; + } + + set pivotX(v) { + this._pivotX = v + 1; + this.redraw(); + } + + get pivotY() { + return this._pivotY; + } + + set pivotY(v) { + this._pivotY = v + 1; + this.redraw(); + } + + get lightY() { + return this._lightY; + } + + set lightY(v) { + this._lightY = v; + this.redraw(); + } + + get pivotZ() { + return this._pivotZ; + } + + set pivotZ(v) { + this._pivotZ = v; + this.redraw(); + } + + get lightZ() { + return this._lightZ; + } + + set lightZ(v) { + this._lightZ = v; + this.redraw(); + } + + useDefault() { + return (this._rx === 0 && this._ry === 0 && this._z === 0 && this._strength === 0 && this._ambient === 1); + } + +} + +Light3dShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + + uniform float fudge; + uniform float strength; + uniform float ambient; + uniform float z; + uniform float lightY; + uniform float lightZ; + uniform vec3 pivot; + uniform vec3 rot; + varying vec3 pos; + + void main(void) { + pos = vec3(aVertexPosition.xy, z); + + pos -= pivot; + + // Undo XY rotation + mat2 iRotXy = mat2( cos(rot.z), sin(rot.z), + -sin(rot.z), cos(rot.z)); + pos.xy = iRotXy * pos.xy; + + // Perform 3d rotations + gl_Position.x = cos(rot.x) * pos.x - sin(rot.x) * pos.z; + gl_Position.y = pos.y; + gl_Position.z = sin(rot.x) * pos.x + cos(rot.x) * pos.z; + + pos.x = gl_Position.x; + pos.y = cos(rot.y) * gl_Position.y - sin(rot.y) * gl_Position.z; + pos.z = sin(rot.y) * gl_Position.y + cos(rot.y) * gl_Position.z; + + // Redo XY rotation + iRotXy[0][1] = -iRotXy[0][1]; + iRotXy[1][0] = -iRotXy[1][0]; + pos.xy = iRotXy * pos.xy; + + // Undo translate to pivot position + pos.xyz += pivot; + + pos = vec3(pos.x * projection.x - 1.0, pos.y * -abs(projection.y) + 1.0, pos.z * projection.x); + + // Set depth perspective + float perspective = 1.0 + fudge * pos.z; + + pos.z += lightZ * projection.x; + + // Map coords to gl coordinate space. + // Set z to 0 because we don't want to perform z-clipping + gl_Position = vec4(pos.xy, 0.0, perspective); + + // Correct light source position. + pos.y += lightY * abs(projection.y); + + vTextureCoord = aTextureCoord; + vColor = aColor; + + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +Light3dShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec3 pos; + uniform sampler2D uSampler; + uniform float ambient; + uniform float strength; + void main(void){ + vec4 rgba = texture2D(uSampler, vTextureCoord); + float d = length(pos); + float n = 1.0 / max(0.1, d); + rgba.rgb = rgba.rgb * (strength * n + ambient); + gl_FragColor = rgba * vColor; + } +`; + +const lightning = { + Application, + Component, + Base, + Utils, + StageUtils, + Element, + Tools, + Stage, + ElementCore, + ElementTexturizer, + Texture, + EventEmitter, + shaders: { + Grayscale: GrayscaleShader$1, + BoxBlur: BoxBlurShader, + Dithering: DitheringShader, + CircularPush: CircularPushShader, + Inversion: InversionShader, + LinearBlur: LinearBlurShader, + Outline: OutlineShader, + Pixelate: PixelateShader, + RadialFilter: RadialFilterShader, + RadialGradient: RadialGradientShader, + Light3d: Light3dShader, + WebGLShader, + WebGLDefaultShader: DefaultShader, + C2dShader, + C2dDefaultShader: DefaultShader$1, + c2d: { + Grayscale: GrayscaleShader, + Blur: BlurShader + } + }, + textures: { + RectangleTexture, + NoiseTexture, + TextTexture, + ImageTexture, + HtmlTexture, + StaticTexture, + StaticCanvasTexture, + SourceTexture + }, + components: { + FastBlurComponent, + BloomComponent, + SmoothScaleComponent, + BorderComponent, + ListComponent + }, + tools: { + ObjMerger, + ObjectListProxy, + ObjectListWrapper + } +}; + +if (Utils.isWeb) { + window.lng = lightning; +} + +class SparkMediaplayer extends lightning.Component { + + _construct(){ + this._skipRenderToTexture = false; + } + + static _supportedEvents() + { + return ['onProgressUpdate', 'onEndOfStream']; + } + + static _template() { + return { + Video: { + VideoWrap: { + VideoTexture: { + visible: false, + pivot: 0.5, + texture: {type: lightning.textures.StaticTexture, options: {}} + } + } + } + }; + } + + set skipRenderToTexture (v) { + this._skipRenderToTexture = v; + } + + set textureMode(v) { + return this._textureMode = v; + } + + get textureMode() { + return this._textureMode; + } + + get videoView() { + return this.tag("Video"); + } + + _init() { + + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + + this.videoEl = sparkscene.create({ + t: "video", + id: "video-player", + autoPlay: "false", + proxy:proxyServer + }); + + var _this = this; + sparkscene.on('onClose' , function(e) { + _this.close(); + }); + + this.eventHandlers = []; + } + + _registerListeners() { + SparkMediaplayer._supportedEvents().forEach(event => { + const handler = (e) => { + this.fire(event, {videoElement: this.videoEl, event: e}); + }; + this.eventHandlers.push(handler); + this.videoEl.on(event, handler); + }); + } + + _deregisterListeners() { + SparkMediaplayer._supportedEvents().forEach((event, index) => { + this.videoEl.delListener(event, this.eventHandlers[index]); + }); + this.eventHandlers = []; + } + + _attach() { + this._registerListeners(); + } + + _detach() { + this._deregisterListeners(); + } + + updateSettings(settings = {}) { + // The Component that 'consumes' the media player. + this._consumer = settings.consumer; + + if (this._consumer && this._consumer.getMediaplayerSettings) { + // Allow consumer to add settings. + settings = Object.assign(settings, this._consumer.getMediaplayerSettings()); + } + + if (!lightning.Utils.equalValues(this._stream, settings.stream)) { + if (settings.stream && settings.stream.keySystem) { + navigator.requestMediaKeySystemAccess(settings.stream.keySystem.id, settings.stream.keySystem.config).then((keySystemAccess) => { + return keySystemAccess.createMediaKeys(); + }).then((createdMediaKeys) => { + return this.videoEl.setMediaKeys(createdMediaKeys); + }).then(() => { + if (settings.stream && settings.stream.src) + this.open(settings.stream.src); + }).catch(() => { + console.error('Failed to set up MediaKeys'); + }); + } else if (settings.stream && settings.stream.src) { + this.open(settings.stream.src); + this._setHide(settings.hide); + this._setVideoArea(settings.videoPos); + this.doPlay(); + } else { + this.close(); + } + this._stream = settings.stream; + } + } + + _setHide(hide) { + this.videoEl.a = hide ? 0 : 1; + } + + open(url) { + console.log('Playing stream', url); + if (this.application.noVideo) { + console.log('noVideo option set, so ignoring: ' + url); + return; + } + if (this.videoEl.url === url) return this.reload(); + this.videoEl.url = url; + } + + close() { + this.videoEl.stop(); + this._clearSrc(); + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + this.videoEl.play(); + } + + doPause() { + this.videoEl.pause(); + } + + reload() { + var url = this.videoEl.url; + this.close(); + this.videoEl.url = url; + } + + getPosition() { + return Promise.resolve(this.videoEl.position); + } + + setPosition(pos) { + this.videoEl.position = pos; + } + + getDuration() { + return Promise.resolve(this.videoEl.duration); + } + + seek(time, absolute = false) { + if(absolute) { + this.videoEl.position = time; + } + else { + this.videoEl.setPositionRelative(time); + } + } + + _setVideoArea(videoPos) { + if (lightning.Utils.equalValues(this._videoPos, videoPos)) { + return; + } + + this._videoPos = videoPos; + + if (this.textureMode) { + this.videoTextureView.patch({ + smooth: { + x: videoPos[0], + y: videoPos[1], + w: videoPos[2] - videoPos[0], + h: videoPos[3] - videoPos[1] + } + }); + } else { + const precision = this.stage.getRenderPrecision(); + this.videoEl.x = Math.round(videoPos[0] * precision) + 'px'; + this.videoEl.y = Math.round(videoPos[1] * precision) + 'px'; + this.videoEl.w = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px'; + this.videoEl.h = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px'; + } + } + + _fireConsumer(event, args) { + if (this._consumer) { + this._consumer.fire(event, args); + } + } + + _equalInitData(buf1, buf2) { + if (!buf1 || !buf2) return false; + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Int8Array(buf1); + const dv2 = new Int8Array(buf2); + for (let i = 0 ; i != buf1.byteLength ; i++) + if (dv1[i] != dv2[i]) return false; + return true; + } + + error(args) { + this._fireConsumer('$mediaplayerError', args); + this._setState(""); + return ""; + } + + loadeddata(args) { + this._fireConsumer('$mediaplayerLoadedData', args); + } + + play(args) { + this._fireConsumer('$mediaplayerPlay', args); + } + + playing(args) { + this._fireConsumer('$mediaplayerPlaying', args); + this._setState("Playing"); + } + + canplay(args) { + this.videoEl.play(); + this._fireConsumer('$mediaplayerStart', args); + } + + loadstart(args) { + this._fireConsumer('$mediaplayerLoad', args); + } + + seeked(args) { + this._fireConsumer('$mediaplayerSeeked', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + seeking(args) { + this._fireConsumer('$mediaplayerSeeking', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + onEndOfStream(args) { + this._fireConsumer('$mediaplayerEnded', args); + this._setState(""); + } + + onProgressUpdate(args) { + this._fireConsumer('$mediaplayerProgress', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + durationchange(args) { + this._fireConsumer('$mediaplayerDurationChange', args); + } + + encrypted(args) { + const video = args.videoElement; + const event = args.event; + // FIXME: Double encrypted events need to be properly filtered by Gstreamer + if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { + this._previousInitData = event.initData; + this._fireConsumer('$mediaplayerEncrypted', args); + } + } + + static _states() { + return [ + class Playing extends this { + $enter() { + this._startUpdatingVideoTexture(); + } + $exit() { + this._stopUpdatingVideoTexture(); + } + pause(args) { + this._fireConsumer('$mediaplayerPause', args); + this._setState("Playing.Paused"); + } + _clearSrc() { + this._fireConsumer('$mediaplayerStop', {}); + this._setState(""); + } + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } +} + +class SparkPlatform { + + init(stage) { + this.stage = stage; + this._looping = false; + this._awaitingLoop = false; + this._sparkCanvas = null; + } + + destroy() { + } + + startLoop() { + this._looping = true; + if (!this._awaitingLoop) { + this.loop(); + } + } + + stopLoop() { + this._looping = false; + } + + loop() { + let self = this; + let lp = function() { + self._awaitingLoop = false; + if (self._looping) { + self.stage.drawFrame(); + if (self.changes) { + // We depend on blit to limit to 60fps. + setImmediate(lp); + } else { + setTimeout(lp, 32); + } + self._awaitingLoop = true; + } + }; + setTimeout(lp, 32); + } + + uploadGlTexture(gl, textureSource, source, options) { + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, textureSource.w, textureSource.h, 0, options.format, options.type, source); + } + + loadSrcTexture({src}, cb) { + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + let imageResource = sparkscene.create({t:"imageResource", url:src, proxy:proxyServer}); + imageResource.ready.then(function(res) { + let sparkImage = sparkscene.create({t:"image", resource:res}); + const sparkGl = this.stage.gl; + sparkImage.ready.then( function(obj) { + let texture = sparkImage.texture(); + cb(null, {source: sparkGl.createWebGLTexture(texture), w: sparkImage.resource.w, h: sparkImage.resource.h, premultiplyAlpha: false, flipBlueRed: false, imageRef: sparkImage, flipTextureY:true}); + }); + }.bind(this)); + } + + createRoundRect(cb, stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (fill === undefined) fill = true; + if (strokeWidth === undefined) strokeWidth = 0; + if (fillColor === undefined) fillColor = 0; + + fillColor = fill ? fillColor : 0; + fillColor = fillColor.toString(16); + let opacity = 1; + if (fillColor.length >= 8) + { + let alpha = fillColor.substring(0,2); + let red = fillColor.substring(2,4); + let green = fillColor.substring(4,6); + let blue = fillColor.substring(6); + fillColor = "#" + red + green + blue; + opacity = "0x"+alpha; + opacity = parseInt(opacity, 16) / 255; + } + let boundW = w+strokeWidth; + let boundH = h+strokeWidth; + let data = "data:image/svg,"+ + `` + + `` + + ''; + + let imageObj = sparkscene.create({ t: "image", url:data}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }); + } + + createShadowRect(cb, stage, w, h, radius, blur, margin) { + let boundW = w + margin * 2; + let boundH = h + margin * 2; + let data = "data:image/svg,"+ + ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + let imageObj = sparkscene.create({ t: "image", url:data}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }); + } + + createSvg(cb, stage, url, w, h) { + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + let imageResource = sparkscene.create({t:"imageResource", url:src, proxy:proxyServer}); + let imageObj = sparkscene.create({ t: "image", resource:imageResource}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }, function(obj) { + let canvas = {}; + canvas.internal = imageObj; + cb(null, canvas); }); + } + + createWebGLContext(w, h) { + let options = {width: w, height: h, title: "WebGL"}; + const windowOptions = this.stage.getOption('window'); + if (windowOptions) { + options = Object.assign(options, windowOptions); + } + let gl = sparkgles2.init(options); + return gl; + } + + getWebGLCanvas() { + return; + } + + getTextureOptionsForDrawingCanvas(canvas) { + let options = {}; + + if (canvas && canvas.internal) + { + options.source = this.stage.gl.createWebGLTexture(canvas.internal.texture()); + options.w = canvas.width; + options.h = canvas.height; + options.imageRef = canvas.internal; + if (canvas.flipTextureY) { + options.flipTextureY = true; + } + } + options.premultiplyAlpha = false; + options.flipBlueRed = false; + return options; + } + + getHrTime() { + let hrTime = process.hrtime(); + return 1e3 * hrTime[0] + (hrTime[1] / 1e6); + } + + getDrawingCanvas() { + let sparkCanvas; + { + this._sparkCanvas = null; + } + if (this._sparkCanvas === null) { + sparkCanvas = {}; + sparkCanvas.internal = sparkscene.create({t: "textCanvas"}); + sparkCanvas.internal.colorMode = "ARGB"; + this._sparkCanvas = sparkCanvas; + this._sparkCanvas.getContext = function() { + return sparkCanvas.internal; + }; + } + return this._sparkCanvas; + } + + nextFrame(changes) { + this.changes = changes; + if (this.stage && this.stage.gl) { + this.stage.gl.scissor(0,0,0,0); + } + //gles2.nextFrame(changes); + } + + registerKeyHandler(keyhandler) { + console.warn("No support for key handling"); + } + + drawText(textTextureRenderer) { + let canvasInternal = textTextureRenderer._canvas.internal; // _canvas.internal is a pxTextCanvas object created in getDrawingCanvas() + let drawPromise = new Promise((resolve, reject) => { + canvasInternal.ready.then( () => { // waiting for the empty scene + canvasInternal.parent = sparkscene.root; + textTextureRenderer.setFontProperties(); + canvasInternal.font.ready.then(() => { // the font might have been coerced + canvasInternal.pixelSize = textTextureRenderer._settings.fontSize * textTextureRenderer.getPrecision(); + // Original Lightining code with some changes begins here + // Changes to the original code are: + // Replaced: `this.` => `textTextureRenderer.` + // Replaced `StageUtils.getRgbaString(color)` => `color` + // Replaced `this._canvas.width` => `canvasInternal.width` and `this._canvas.height` => `canvasInternal.height` after the line: // Add extra margin to prevent issue with clipped text when scaling. + // setFontProperties() calls are commented out as redundant + // Setting canvas label to faciliatate debugging (this is optional and can be removed): + // canvasInternal.label = textTextureRenderer._settings.text.slice(0, 10) + '..'; + let renderInfo = {}; + const precision = textTextureRenderer.getPrecision(); + let paddingLeft = textTextureRenderer._settings.paddingLeft * precision; + let paddingRight = textTextureRenderer._settings.paddingRight * precision; + const fontSize = textTextureRenderer._settings.fontSize * precision; + let offsetY = textTextureRenderer._settings.offsetY === null ? null : (textTextureRenderer._settings.offsetY * precision); + let lineHeight = textTextureRenderer._settings.lineHeight * precision; + const w = textTextureRenderer._settings.w * precision; + const h = textTextureRenderer._settings.h * precision; + let wordWrapWidth = textTextureRenderer._settings.wordWrapWidth * precision; + const cutSx = textTextureRenderer._settings.cutSx * precision; + const cutEx = textTextureRenderer._settings.cutEx * precision; + const cutSy = textTextureRenderer._settings.cutSy * precision; + const cutEy = textTextureRenderer._settings.cutEy * precision; + + canvasInternal.label = textTextureRenderer._settings.text.slice(0, 10) + '..'; // allows to distinguish different canvases by label, useful for debugging + // Set font properties. + // textTextureRenderer.setFontProperties(); + // Total width. + let width = w || (2048 / textTextureRenderer.getPrecision()); + // Inner width. + let innerWidth = width - (paddingLeft); + if (innerWidth < 10) { + width += (10 - innerWidth); + innerWidth += (10 - innerWidth); + } + if (!wordWrapWidth) { + wordWrapWidth = innerWidth; + } + // word wrap + // preserve original text + let linesInfo; + if (textTextureRenderer._settings.wordWrap) { + linesInfo = textTextureRenderer.wrapText(textTextureRenderer._settings.text, wordWrapWidth); + } else { + linesInfo = {l: textTextureRenderer._settings.text.split(/(?:\r\n|\r|\n)/), n: []}; + let n = linesInfo.l.length; + for (let i = 0; i < n - 1; i++) { + linesInfo.n.push(i); + } + } + let lines = linesInfo.l; + if (textTextureRenderer._settings.maxLines && lines.length > textTextureRenderer._settings.maxLines) { + let usedLines = lines.slice(0, textTextureRenderer._settings.maxLines); + let otherLines = null; + if (textTextureRenderer._settings.maxLinesSuffix) { + // Wrap again with max lines suffix enabled. + let w = textTextureRenderer._settings.maxLinesSuffix ? textTextureRenderer._context.measureText(textTextureRenderer._settings.maxLinesSuffix).width : 0; + let al = textTextureRenderer.wrapText(usedLines[usedLines.length - 1], wordWrapWidth - w); + usedLines[usedLines.length - 1] = al.l[0] + textTextureRenderer._settings.maxLinesSuffix; + otherLines = [al.l.length > 1 ? al.l[1] : '']; + } else { + otherLines = ['']; + } + // Re-assemble the remaining text. + let i, n = lines.length; + let j = 0; + let m = linesInfo.n.length; + for (i = textTextureRenderer._settings.maxLines; i < n; i++) { + otherLines[j] += (otherLines[j] ? " " : "") + lines[i]; + if (i + 1 < m && linesInfo.n[i + 1]) { + j++; + } + } + renderInfo.remainingText = otherLines.join("\n"); + renderInfo.moreTextLines = true; + lines = usedLines; + } else { + renderInfo.moreTextLines = false; + renderInfo.remainingText = ""; + } + // calculate text width + let maxLineWidth = 0; + let lineWidths = []; + for (let i = 0; i < lines.length; i++) { + let lineWidth = textTextureRenderer._context.measureText(lines[i]).width; + lineWidths.push(lineWidth); + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + renderInfo.lineWidths = lineWidths; + if (!w) { + // Auto-set width to max text length. + width = maxLineWidth + paddingLeft + paddingRight; + innerWidth = maxLineWidth; + } + // calculate text height + lineHeight = lineHeight || fontSize; + let height; + if (h) { + height = h; + } else { + height = lineHeight * (lines.length - 1) + 0.5 * fontSize + Math.max(lineHeight, fontSize) + offsetY; + } + if (offsetY === null) { + offsetY = fontSize; + } + renderInfo.w = width; + renderInfo.h = height; + renderInfo.lines = lines; + renderInfo.precision = precision; + if (!width) { + // To prevent canvas errors. + width = 1; + } + if (!height) { + // To prevent canvas errors. + height = 1; + } + if (cutSx || cutEx) { + width = Math.min(width, cutEx - cutSx); + } + if (cutSy || cutEy) { + height = Math.min(height, cutEy - cutSy); + } + // Add extra margin to prevent issue with clipped text when scaling. + canvasInternal.width = Math.ceil(width + textTextureRenderer._stage.getOption('textRenderIssueMargin')); + canvasInternal.height = Math.ceil(height); + // Canvas context has been reset. + // textTextureRenderer.setFontProperties(); + if (fontSize >= 128) { + // WpeWebKit bug: must force compositing because cairo-traps-compositor will not work with text first. + textTextureRenderer._context.globalAlpha = 0.01; + textTextureRenderer._context.fillRect(0, 0, 0.01, 0.01); + textTextureRenderer._context.globalAlpha = 1.0; + } + if (cutSx || cutSy) { + textTextureRenderer._context.translate(-cutSx, -cutSy); + } + let linePositionX; + let linePositionY; + let drawLines = []; + // Draw lines line by line. + for (let i = 0, n = lines.length; i < n; i++) { + linePositionX = 0; + linePositionY = (i * lineHeight) + offsetY; + if (textTextureRenderer._settings.textAlign === 'right') { + linePositionX += (innerWidth - lineWidths[i]); + } else if (textTextureRenderer._settings.textAlign === 'center') { + linePositionX += ((innerWidth - lineWidths[i]) / 2); + } + linePositionX += paddingLeft; + drawLines.push({text: lines[i], x: linePositionX, y: linePositionY, w: lineWidths[i]}); + } + // Highlight. + if (textTextureRenderer._settings.highlight) { + let color = textTextureRenderer._settings.highlightColor || 0x00000000; + let hlHeight = (textTextureRenderer._settings.highlightHeight * precision || fontSize * 1.5); + let offset = (textTextureRenderer._settings.highlightOffset !== null ? textTextureRenderer._settings.highlightOffset * precision : -0.5 * fontSize); + const hlPaddingLeft = (textTextureRenderer._settings.highlightPaddingLeft !== null ? textTextureRenderer._settings.highlightPaddingLeft * precision : paddingLeft); + const hlPaddingRight = (textTextureRenderer._settings.highlightPaddingRight !== null ? textTextureRenderer._settings.highlightPaddingRight * precision : paddingRight); + + textTextureRenderer._context.fillStyle = color; + for (let i = 0; i < drawLines.length; i++) { + let drawLine = drawLines[i]; + textTextureRenderer._context.fillRect((drawLine.x - hlPaddingLeft), (drawLine.y + offset), (drawLine.w + hlPaddingRight + hlPaddingLeft), hlHeight); + } + } + // Text shadow. + let prevShadowSettings = null; + if (textTextureRenderer._settings.shadow) { + prevShadowSettings = [textTextureRenderer._context.shadowColor, textTextureRenderer._context.shadowOffsetX, textTextureRenderer._context.shadowOffsetY, textTextureRenderer._context.shadowBlur]; + textTextureRenderer._context.shadowColor = textTextureRenderer._settings.shadowColor; + textTextureRenderer._context.shadowOffsetX = textTextureRenderer._settings.shadowOffsetX * precision; + textTextureRenderer._context.shadowOffsetY = textTextureRenderer._settings.shadowOffsetY * precision; + textTextureRenderer._context.shadowBlur = textTextureRenderer._settings.shadowBlur * precision; + } + textTextureRenderer._context.fillStyle = textTextureRenderer._settings.textColor; + for (let i = 0, n = drawLines.length; i < n; i++) { + let drawLine = drawLines[i]; + textTextureRenderer._context.fillText(drawLine.text, drawLine.x, drawLine.y); + } + + if (prevShadowSettings) { + textTextureRenderer._context.shadowColor = prevShadowSettings[0]; + textTextureRenderer._context.shadowOffsetX = prevShadowSettings[1]; + textTextureRenderer._context.shadowOffsetY = prevShadowSettings[2]; + textTextureRenderer._context.shadowBlur = prevShadowSettings[3]; + } + + if (cutSx || cutSy) { + textTextureRenderer._context.translate(cutSx, cutSy); + } + // Original Lightining code ends here + canvasInternal.ready.then(() => { // everything is drawn + renderInfo.w = canvasInternal.w; + renderInfo.h = canvasInternal.h; + textTextureRenderer._canvas.width = canvasInternal.w; + textTextureRenderer._canvas.height = canvasInternal.h; + textTextureRenderer.renderInfo = renderInfo; + resolve(); + }); + }); + }); + }); + return drawPromise; + } + + loadFonts(fonts) { + let promises = []; + let fontResources = new Map(); + for (let font of fonts) { + let fontResource = sparkscene.create({t: "fontResource", url: font.url}); + promises.push(fontResource.ready); + fontResources.set(font.family, fontResource); + } + + // load fonts and then store a + // reference to them so they can be used + // in getFontSetting calls + Promise.all(promises) + .then(() => this._fontResources = fontResources); + + // continue to return promise/font object + // to maintain compatibility with SDK client + return { + promises: promises, + fontResources: fontResources + }; + } + + getFontSetting(textTextureRenderer) { + let fontResource = textTextureRenderer._context.font; + let fontFace = textTextureRenderer._settings.fontFace; + let fontStyle = textTextureRenderer._settings.fontStyle.toLowerCase(); + + if (this._fontResources !== undefined && this._fontResources.has(fontFace)) { + fontResource = this._fontResources.get(fontFace); + if (fontResource.needsStyleCoercion(fontStyle)) { + let url = fontResource.url; + fontResource = sparkscene.create({t: "fontResource", url: url, fontStyle: fontStyle}); + } + } + return fontResource; + } + + + static createMediaPlayer() + { + return SparkMediaplayer; + } +} + +const lightning$1 = lightning; + +lightning$1.Stage.platform = SparkPlatform; + +const Headers = fetch.Headers; + + +const events = ['timeupdate', 'error', 'ended', 'loadeddata', 'canplay', 'play', 'playing', 'pause', 'loadstart', 'seeking', 'seeked', 'encrypted']; + +class Mediaplayer extends lightning$1.Component { + + _construct(){ + this._skipRenderToTexture = false; + } + + static _template() { + return { + Video: { + VideoWrap: { + VideoTexture: { + visible: false, + pivot: 0.5, + texture: {type: lightning$1.textures.StaticTexture, options: {}} + } + } + } + }; + } + + set skipRenderToTexture (v) { + this._skipRenderToTexture = v; + } + + set textureMode(v) { + return this._textureMode = v; + } + + get textureMode() { + return this._textureMode; + } + + get videoView() { + return this.tag("Video"); + } + + _init() { + //re-use videotag if already there + const videoEls = document.getElementsByTagName('video'); + if (videoEls && videoEls.length > 0) + this.videoEl = videoEls[0]; + else { + this.videoEl = document.createElement('video'); + this.videoEl.setAttribute('id', 'video-player'); + this.videoEl.style.position = 'absolute'; + this.videoEl.style.zIndex = '1'; + this.videoEl.style.display = 'none'; + this.videoEl.setAttribute('width', '100%'); + this.videoEl.setAttribute('height', '100%'); + + this.videoEl.style.visibility = (this.textureMode) ? 'hidden' : 'visible'; + document.body.appendChild(this.videoEl); + } + if (this.textureMode && !this._skipRenderToTexture) { + this._createVideoTexture(); + } + + this.eventHandlers = []; + } + + _registerListeners() { + events.forEach(event => { + const handler = (e) => { + this.fire(event, {videoElement: this.videoEl, event: e}); + }; + this.eventHandlers.push(handler); + this.videoEl.addEventListener(event, handler); + }); + } + + _deregisterListeners() { + events.forEach((event, index) => { + this.videoEl.removeEventListener(event, this.eventHandlers[index]); + }); + this.eventHandlers = []; + } + + _attach() { + this._registerListeners(); + } + + _detach() { + this._deregisterListeners(); + } + + _createVideoTexture() { + const stage = this.stage; + + const gl = stage.gl; + const glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + this.videoTexture.options = {source: glTexture, w: this.videoEl.width, h: this.videoEl.height}; + } + + _startUpdatingVideoTexture() { + if (this.textureMode && !this._skipRenderToTexture) { + const stage = this.stage; + if (!this._updateVideoTexture) { + this._updateVideoTexture = () => { + if (this.videoTexture.options.source && this.videoEl.videoWidth && this.active) { + const gl = stage.gl; + + const currentTime = (new Date()).getTime(); + + // When BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DEBUGUTILS is not set in WPE, webkitDecodedFrameCount will not be available. + // We'll fallback to fixed 30fps in this case. + const frameCount = this.videoEl.webkitDecodedFrameCount; + + const mustUpdate = (frameCount ? (this._lastFrame !== frameCount) : (this._lastTime < currentTime - 30)); + + if (mustUpdate) { + this._lastTime = currentTime; + this._lastFrame = frameCount; + try { + gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.videoEl); + this._lastFrame = this.videoEl.webkitDecodedFrameCount; + this.videoTextureView.visible = true; + + this.videoTexture.options.w = this.videoEl.videoWidth; + this.videoTexture.options.h = this.videoEl.videoHeight; + const expectedAspectRatio = this.videoTextureView.w / this.videoTextureView.h; + const realAspectRatio = this.videoEl.videoWidth / this.videoEl.videoHeight; + if (expectedAspectRatio > realAspectRatio) { + this.videoTextureView.scaleX = (realAspectRatio / expectedAspectRatio); + this.videoTextureView.scaleY = 1; + } else { + this.videoTextureView.scaleY = expectedAspectRatio / realAspectRatio; + this.videoTextureView.scaleX = 1; + } + } catch (e) { + console.error('texImage2d video', e); + this._stopUpdatingVideoTexture(); + this.videoTextureView.visible = false; + } + this.videoTexture.source.forceRenderUpdate(); + } + } + }; + } + if (!this._updatingVideoTexture) { + stage.on('frameStart', this._updateVideoTexture); + this._updatingVideoTexture = true; + } + } + } + + _stopUpdatingVideoTexture() { + if (this.textureMode) { + const stage = this.stage; + stage.removeListener('frameStart', this._updateVideoTexture); + this._updatingVideoTexture = false; + this.videoTextureView.visible = false; + + if (this.videoTexture.options.source) { + const gl = stage.gl; + gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } + } + + updateSettings(settings = {}) { + // The Component that 'consumes' the media player. + this._consumer = settings.consumer; + + if (this._consumer && this._consumer.getMediaplayerSettings) { + // Allow consumer to add settings. + settings = Object.assign(settings, this._consumer.getMediaplayerSettings()); + } + + if (!lightning$1.Utils.equalValues(this._stream, settings.stream)) { + if (settings.stream && settings.stream.keySystem) { + navigator.requestMediaKeySystemAccess(settings.stream.keySystem.id, settings.stream.keySystem.config).then((keySystemAccess) => { + return keySystemAccess.createMediaKeys(); + }).then((createdMediaKeys) => { + return this.videoEl.setMediaKeys(createdMediaKeys); + }).then(() => { + if (settings.stream && settings.stream.src) + this.open(settings.stream.src); + }).catch(() => { + console.error('Failed to set up MediaKeys'); + }); + } else if (settings.stream && settings.stream.src) { + if(!window.Hls){ + window.Hls = class Hls{ + static isSupported(){ + console.warn("hls-light not included"); + return false; + } + }; + } + if (ux.Ui.hasOption("hls") && Hls.isSupported()) { + if (!this._hls) this._hls = new Hls({liveDurationInfinity: true}); + this._hls.loadSource(settings.stream.src); + this._hls.attachMedia(this.videoEl); + this.videoEl.style.display = "block"; + } else { + this.open(settings.stream.src); + } + } else { + this.close(); + } + this._stream = settings.stream; + } + + this._setHide(settings.hide); + this._setVideoArea(settings.videoPos); + } + + _setHide(hide) { + if (this.textureMode) { + this.tag("Video").setSmooth('alpha', hide ? 0 : 1); + } else { + this.videoEl.style.visibility = hide ? 'hidden' : 'visible'; + } + } + + open(url) { + console.log('Playing stream', url); + if (this.application.noVideo) { + console.log('noVideo option set, so ignoring: ' + url); + return; + } + if (this.videoEl.getAttribute('src') === url) return this.reload(); + this.videoEl.setAttribute('src', url); + + this.videoEl.style.display = 'block'; + } + + close() { + // We need to pause first in order to stop sound. + this.videoEl.pause(); + this.videoEl.removeAttribute('src'); + + // force load to reset everything without errors + this.videoEl.load(); + + this._clearSrc(); + + this.videoEl.style.display = 'none'; + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + this.videoEl.play(); + } + + doPause() { + this.videoEl.pause(); + } + + reload() { + var url = this.videoEl.getAttribute('src'); + this.close(); + this.videoEl.src = url; + } + + getPosition() { + return Promise.resolve(this.videoEl.currentTime); + } + + setPosition(pos) { + this.videoEl.currentTime = pos; + } + + getDuration() { + return Promise.resolve(this.videoEl.duration); + } + + seek(time, absolute = false) { + if(absolute) { + this.videoEl.currentTime = time; + } + else { + this.videoEl.currentTime += time; + } + } + + get videoTextureView() { + return this.tag("Video").tag("VideoTexture"); + } + + get videoTexture() { + return this.videoTextureView.texture; + } + + _setVideoArea(videoPos) { + if (lightning$1.Utils.equalValues(this._videoPos, videoPos)) { + return; + } + + this._videoPos = videoPos; + + if (this.textureMode) { + this.videoTextureView.patch({ + smooth: { + x: videoPos[0], + y: videoPos[1], + w: videoPos[2] - videoPos[0], + h: videoPos[3] - videoPos[1] + } + }); + } else { + const precision = this.stage.getRenderPrecision(); + this.videoEl.style.left = Math.round(videoPos[0] * precision) + 'px'; + this.videoEl.style.top = Math.round(videoPos[1] * precision) + 'px'; + this.videoEl.style.width = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px'; + this.videoEl.style.height = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px'; + } + } + + _fireConsumer(event, args) { + if (this._consumer) { + this._consumer.fire(event, args); + } + } + + _equalInitData(buf1, buf2) { + if (!buf1 || !buf2) return false; + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Int8Array(buf1); + const dv2 = new Int8Array(buf2); + for (let i = 0 ; i != buf1.byteLength ; i++) + if (dv1[i] != dv2[i]) return false; + return true; + } + + error(args) { + this._fireConsumer('$mediaplayerError', args); + this._setState(""); + return ""; + } + + loadeddata(args) { + this._fireConsumer('$mediaplayerLoadedData', args); + } + + play(args) { + this._fireConsumer('$mediaplayerPlay', args); + } + + playing(args) { + this._fireConsumer('$mediaplayerPlaying', args); + this._setState("Playing"); + } + + canplay(args) { + this.videoEl.play(); + this._fireConsumer('$mediaplayerStart', args); + } + + loadstart(args) { + this._fireConsumer('$mediaplayerLoad', args); + } + + seeked(args) { + this._fireConsumer('$mediaplayerSeeked', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + + seeking(args) { + this._fireConsumer('$mediaplayerSeeking', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + + durationchange(args) { + this._fireConsumer('$mediaplayerDurationChange', args); + } + + encrypted(args) { + const video = args.videoElement; + const event = args.event; + // FIXME: Double encrypted events need to be properly filtered by Gstreamer + if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { + this._previousInitData = event.initData; + this._fireConsumer('$mediaplayerEncrypted', args); + } + } + + static _states() { + return [ + class Playing extends this { + $enter() { + this._startUpdatingVideoTexture(); + } + $exit() { + this._stopUpdatingVideoTexture(); + } + timeupdate() { + this._fireConsumer('$mediaplayerProgress', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + ended(args) { + this._fireConsumer('$mediaplayerEnded', args); + this._setState(""); + } + pause(args) { + this._fireConsumer('$mediaplayerPause', args); + this._setState("Playing.Paused"); + } + _clearSrc() { + this._fireConsumer('$mediaplayerStop', {}); + this._setState(""); + } + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } + +} + +class NoopMediaplayer extends lightning$1.Component { + + static _template() { + return { + Video: { + w: 1920, h: 1080 + } + }; + } + + open(url) { + console.log('Playing stream', url); + } + + close() { + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + } + + doPause() { + } + + reload() { + } + + getPosition() { + return Promise.resolve(0); + } + + setPosition(pos) { + } + + getDuration() { + return Promise.resolve(0); + } + + seek(time, absolute = false) { + } + + updateSettings(settings = {}) { + } + + static _states() { + return [ + class Playing extends this { + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } + +} + +class ScaledImageTexture extends lightning$1.textures.ImageTexture { + + constructor(stage) { + super(stage); + + this._scalingOptions = undefined; + this.precision = 1; + } + + set scalingOptions(options) { + if (!lightning$1.Utils.equalValues(this._scalingOptions, options)) { + this._scalingOptions = options; + this._changed(); + } + } + + _getLookupId() { + const opts = this._scalingOptions; + return `${this._src}-${opts.type}-${opts.width}-${opts.height}`; + } + + _getSourceLoader() { + let src = this._src; + if (this.stage.getOption('srcBasePath')) { + var fc = src.charCodeAt(0); + if ((src.indexOf("//") === -1) && ((fc >= 65 && fc <= 90) || (fc >= 97 && fc <= 122) || fc == 46)) { + // Alphabetical or dot: prepend base path. + src = this.stage.getOption('srcBasePath') + src; + } + } + + if (this.stage.application.useImageServer) { + src = this._getImageServerSrc(src); + } else { + this.resizeMode = ScaledImageTexture._convertScalingOptions(this._scalingOptions); + } + + const platform = this.stage.platform; + return function(cb) { + return platform.loadSrcTexture({src: src, hasAlpha: this._hasAlpha}, cb); + } + } + + static _convertScalingOptions(options) { + const opts = lightning$1.Utils.clone(options); + switch(options.type) { + case "crop": + opts.type = "cover"; + break; + case "fit": + case "parent": + case "exact": + case "height": + case "portrait": + case "width": + case "landscape": + case "auto": + default: + opts.type = "contain"; + break; + } + opts.w = opts.w || opts.width || 0; + opts.h = opts.h || opts.height || 0; + return opts; + } + + get precision() { + return this._customPrecision; + } + + set precision(v) { + this._customPrecision = v; + super.precision = this.stage.getRenderPrecision() * this._customPrecision; + } + + _getImageServerSrc(src) { + if (this._scalingOptions && (this._precision !== 1)) { + const opts = lightning$1.Utils.clone(this._scalingOptions); + if (opts.width) { + opts.width = Math.round(opts.width * this._precision); + } + + if (opts.height) { + opts.height = Math.round(opts.height * this._precision); + } + src = ScaledImageTexture.getImageUrl(src, opts); + } else { + src = ScaledImageTexture.getImageUrl(src, this._scalingOptions); + } + return src; + } + + static getImageUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/image" + this.getQueryString(url, opts); + } + + static _getCdnProtocol() { + return lightning$1.Utils.isWeb && location.protocol === "https:" ? "https" : "http"; + } + + static getQueryString(url, opts, key = "url") { + let str = `?operator=${encodeURIComponent('metrological')}`; + const keys = Object.keys(opts); + keys.forEach(key => { + str += "&" + encodeURIComponent(key) + "=" + encodeURIComponent("" + opts[key]); + }); + str += `&${key}=${encodeURIComponent(url)}`; + return str; + } + + getNonDefaults() { + const obj = super.getNonDefaults(); + if (this._src) { + obj.src = this._src; + } + return obj; + } + +} + +class Ui extends lightning$1.Application { + + constructor(options) { + options.defaultFontFace = options.defaultFontFace || "RobotoRegular"; + super(options); + this._options = options; + } + + static _template() { + let mediaPlayerType = NoopMediaplayer; + if (lightning$1.Utils.isWeb) { + mediaPlayerType = Mediaplayer; + } +/* + else if (lightning$1.Utils.isSpark) { + mediaPlayerType = lightning$1.Stage.platform.createMediaPlayer(); + } +*/ + return { + Mediaplayer: {type: mediaPlayerType, textureMode: Ui.hasOption('texture')}, + AppWrapper: {} + }; + } + + static set staticFilesPath(path) { + this._staticFilesPath = path; + } + + get useImageServer() { + return !Ui.hasOption("noImageServer"); + } + + get mediaplayer() { + return this.tag("Mediaplayer"); + } + + _active() { + this.tag('Mediaplayer').skipRenderToTexture = this._options.skipRenderToTexture; + } + + startApp(appClass) { + this._setState("App.Loading", [appClass]); + } + + stopApp() { + } + + _handleBack() { + if (lightning$1.Utils.isWeb) { + window.close(); + } + } + + loadPlatformFonts(fonts) { + if (lightning$1.Utils.isNode && !lightning$1.Utils.isSpark) { + // Font loading not supported. Fonts should be installed in Linux system and then they can be picked up by cairo. + return Promise.resolve(); + } + + if (lightning$1.Utils.isSpark) { + let ret = this.stage.platform.loadFonts(fonts); + return Promise.all(ret.promises).then(() => {return ret.fontResources}); + } + } + + static loadFonts(fonts) { + const fontFaces = fonts.map(({family, url, descriptors}) => new FontFace(family, `url(${url})`, descriptors)); + fontFaces.forEach(fontFace => { + document.fonts.add(fontFace); + }); + return Promise.all(fontFaces.map(ff => ff.load())).then(() => {return fontFaces}); + } + + static getPath(relPath) { + return this._staticFilesPath + "static-ux/" + relPath; + } + + static getFonts() { + return [ + {family: 'RobotoRegular', url: Ui.getPath('fonts/roboto-regular.ttf'), descriptors: {}}, + {family: 'Material-Icons', url: Ui.getPath('fonts/Material-Icons.ttf'), descriptors: {}} + ] + } + + static _states() { + return [ + class App extends this { + stopApp() { + this._setState(""); + } + static _states() { + return [ + class Loading extends this { + $enter(context, appClass) { + this._startApp(appClass); + } + _startApp(appClass) { + this._currentApp = { + type: appClass, + fontFaces: [] + }; + + // Preload fonts. + const fonts = this._currentApp.type.getFonts().concat(Ui.getFonts()); + let fn = lightning$1.Utils.isWeb ? Ui.loadFonts(fonts): this.loadPlatformFonts(fonts); + fn.then((fontFaces) => { + this._currentApp.fontFaces = fontFaces; + }).catch((e) => { + console.warn('Font loading issues: ' + e); + }).finally(()=>{ + this._done(); + }); + } + _done() { + this._setState("App.Started"); + } + }, + class Started extends this { + $enter() { + this.tag("AppWrapper").children = [{ref: "App", type: this._currentApp.type}]; + } + $exit() { + this.tag("AppWrapper").children = []; + } + } + ] + } + } + ] + } + + _getFocused() { + return this.tag("App"); + } + + _setFocusSettings(settings) { + settings.clearColor = this.stage.getOption('clearColor'); + settings.mediaplayer = { + consumer: null, + stream: null, + hide: false, + videoPos: [0, 0, 1920, 1080] + }; + } + + _handleFocusSettings(settings) { + if (this._clearColor !== settings.clearColor) { + this._clearColor = settings.clearColor; + this.stage.setClearColor(settings.clearColor); + } + + if (this.tag("Mediaplayer").attached) { + this.tag("Mediaplayer").updateSettings(settings.mediaplayer); + } + } + + static getProxyUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/proxy" + this.getQueryString(url, opts); + } + + static getImage(url, opts = {}) { + return {type: ScaledImageTexture, src: url, scalingOptions: opts}; + } + + static getImageUrl(url, opts = {}) { + throw new Error("{src: Ui.getImageUrl(...)} is deprecated. Please use {texture: Ui.getImage(...)} instead."); + } + + static getQrUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/qr" + this.getQueryString(url, opts, "q"); + } + + static _getCdnProtocol() { + return lightning$1.Utils.isWeb && location.protocol === "https:" ? "https" : "http"; + } + + static hasOption(name) { + if (lightning$1.Utils.isNode) { + return false; + } + + return new URL(document.location.href).searchParams.has(name); + } + + static getOption(name) { + if (lightning$1.Utils.isNode) { + return undefined; + } + + return new URL(document.location.href).searchParams.get(name); + } + + static getQueryString(url, opts, key = "url") { + let str = `?operator=${encodeURIComponent(this.getOption('operator') || 'metrological')}`; + const keys = Object.keys(opts); + keys.forEach(key => { + str += "&" + encodeURIComponent(key) + "=" + encodeURIComponent("" + opts[key]); + }); + str += `&${key}=${encodeURIComponent(url)}`; + return str; + } + + +} + +Ui._staticFilesPath = "./"; + +class App extends lightning$1.Component { + + static g(c) { + return c.seekAncestorByType(this); + } + + /** + * Returns all fonts to be preloaded before entering this app. + * @returns {{family: string, url: string, descriptors: {}}[]} + */ + static getFonts() { + return []; + } + + getPath(relPath) { + return App.getPath(this.constructor, relPath); + } + + static getPath(relPath) { + return Ui._staticFilesPath + "static/" + relPath; + } + + static get identifier() { + throw new Error("Please supply an identifier in the App definition file."); + } + +} + +class PlayerButton extends lightning$1.Component { + + static _template() { + const o = this.options; + return { + w: o.w, h: o.h, + Background: {x: -1, y: -1, texture: lightning$1.Tools.getRoundRect(o.w, o.h, 4, 0, 0, true), color: o.colors.deselected}, + Icon: {x: o.w/2, y: o.h/2, mount: 0.5, color: o.colors.selected} + }; + } + + set icon(source) { + this.tag("Icon").src = Ui.getPath(`tools/player/img/${source}`); + } + + set active(v) { + this.alpha = v ? 1 : 0.3; + } + + get active() { + return this.alpha === 1; + } + + static _states() { + return [ + class Selected extends this { + $enter() { + this.tag("Background").color = this.constructor.options.colors.selected; + this.tag("Icon").color = this.constructor.options.colors.deselected; + } + $exit() { + this.tag("Background").color = this.constructor.options.colors.deselected; + this.tag("Icon").color = this.constructor.options.colors.selected; + } + } + ] + } + + _focus() { + this._setState("Selected"); + } + + _unfocus() { + this._setState(""); + } + + static get options() { + if (!this._options) { + this._options = this._buildOptions(); + } + return this._options; + } + + static _buildOptions() { + return { + colors: { + selected: 0xFFFFFFFF, + deselected: 0xFF606060 + }, + w: 60, + h: 60 + }; + } + +} + +class PlayerControls extends lightning$1.Component { + + static _template() { + return { + Buttons: { + Previous: {type: this.PlayerButton, icon: "prev.png"}, + Play: {type: this.PlayerButton, icon: "play.png"}, + Next: {type: this.PlayerButton, icon: "next.png"} + }, + Title: {text: {fontSize: 46, lineHeight: 56, maxLines: 1, shadow: true}, y: 2} + }; + } + + static get PlayerButton() { + return PlayerButton; + } + + showButtons(previous, next) { + const o = this.constructor.options; + let buttons = []; + if (previous) buttons.push("Previous"); + buttons = buttons.concat(o.buttons); + if (next) buttons.push("Next"); + this._setActiveButtons(buttons); + } + + set title(title) { + this.tag("Title").text = title || ""; + } + + get _activeButtonIndex() { + let button = this.tag("Buttons").getByRef(this._getState()); + if (!button.active) { + button = this.tag("Play"); + } + return this._activeButtons.indexOf(button); + } + + get _activeButton() { + return this._activeButtons[this._activeButtonIndex]; + } + + _setActiveButtons(buttons) { + const o = this.constructor.options; + + let x = 0; + this._activeButtons = []; + this.tag("Buttons").children.map(button => { + button.active = (buttons.indexOf(button.ref) !== -1); + button.x = x; + if (button.active) { + this._activeButtons.push(button); + } + x += button.renderWidth + o.margin; + }); + this.tag("Title").x = x + 20; + + + this._checkActiveButton(); + } + + _setup() { + this._setState("Play"); + } + + _init() { + this.showButtons(false, false); + this._setState("Play"); + } + + _checkActiveButton() { + // After changing the active buttons, make sure that an active button is selected. + let index = this._activeButtonIndex; + if (index === -1) { + if (this._index >= this._activeButtons.length) { + this._index = this._activeButtons.length - 1; + } + } + this._setState(this._activeButtons[index].ref); + } + + _handleLeft() { + let index = this._activeButtonIndex; + if (index > 0) { + index--; + } + this._setState(this._activeButtons[index].ref); + } + + _handleRight() { + let index = this._activeButtonIndex; + if (index < this._activeButtons.length - 1) { + index++; + } + this._setState(this._activeButtons[index].ref); + } + + _handleEnter() { + this.signal('press' + this._activeButton.ref); + } + + + set paused(v) { + this.tag("Play").icon = v ? "play.png" : "pause.png"; + } + + static _states() { + return [ + class Previous extends this { + }, + class Play extends this { + }, + class Next extends this { + } + ] + } + + _getFocused() { + return this.tag(this._getState()); + } + + static get options() { + if (!this._options) { + this._options = this._buildOptions(); + } + return this._options; + } + + static _buildOptions() { + return { + buttons: ["Play"], + margin: 10 + }; + } + +} + +class PlayerProgress extends lightning$1.Component { + + static _template() { + return { + Progress: { + forceZIndexContext: true, + Total: { + x: -1, y: -1, texture: lightning$1.Tools.getRoundRect(1720, 10, 4), color: 0xFF606060, + Scroller: { + x: 0, y: 6, mount: 0.5, w: 16, h: 16, zIndex: 2, + Shadow: { + texture: lightning$1.Tools.getShadowRect(16, 16, 8), + mount: 0.5, + x: 8, + y: 8, + color: 0xFF000000 + }, + Main: {texture: lightning$1.Tools.getRoundRect(16, 16, 8), mount: 0.5, x: 8, y: 8, color: 0xFFF1F1F1} + } + }, + Active: {x: -1, y: -1, color: 0xFFF1F1F1}, + CurrentTime: { + x: 0, + y: 21, + text: {fontSize: 28, lineHeight: 34, maxLines: 1, shadow: true, text: "00:00"} + }, + Duration: { + x: 1720, + mountX: 1, + y: 21, + text: {fontSize: 28, lineHeight: 34, maxLines: 1, shadow: true, text: "00:00"} + } + } + }; + } + + set _progress(v) { + const now = Date.now(); + let estimation = 0; + if (!this._last || (this._last < now - 1000)) { + estimation = 500; + } else { + estimation = now - this._last; + } + this._last = now; + const x = v * 1720; + + estimation *= 0.001; + this.tag("Total").setSmooth('x', x, {timingFunction: 'linear', duration: estimation}); + this.tag("Total").setSmooth('texture.x', x, {timingFunction: 'linear', duration: estimation}); + this.tag("Active").setSmooth('texture.w', Math.max(x, 0.0001) /* force clipping */, { + timingFunction: 'linear', + duration: estimation + }); + } + + setProgress(currentTime, duration) { + this._progress = currentTime / Math.max(duration, 1); + this.tag("CurrentTime").text = Player.formatTime(currentTime); + this.tag("Duration").text = Player.formatTime(duration); + } + + static formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + seconds = Math.floor(seconds); + const parts = []; + if (hours) parts.push(hours); + parts.push(minutes); + parts.push(seconds); + return parts.map(number => (number < 10 ? "0" + number : "" + number)).join(":"); + } + + _alter() { + } + + _setup() { + this._alter(); + } + + _init() { + this.tag("Active").texture = { + type: lightning$1.textures.SourceTexture, + textureSource: this.tag("Total").texture.source + }; + } + +} + +class Player extends lightning$1.Component { + + static _template() { + return { + Gradient: { + x: 0, + y: 1080, + h: 300, + w: 1920, + mountY: 1, + colorTop: 0x00101010, + colorBottom: 0xE6101010, + rect: true + }, + Controls: { + x: 99, + y: 890, + type: this.PlayerControls, + signals: {pressPlay: true, pressPrevious: true, pressNext: "_pressNext"} + }, + Progress: {x: 99, y: 970, type: this.PlayerProgress} + }; + } + + static get PlayerControls() { + return PlayerControls; + } + + static get PlayerProgress() { + return PlayerProgress; + } + + _setItem(item) { + this.tag("Progress").setProgress(0, 0); + this._item = item; + this._stream = item.stream; + this.tag("Controls").title = item.title; + + this._index = this._items.indexOf(item); + this.tag("Controls").showButtons(this._index > 0, this._index < this._items.length - 1); + + this.application.updateFocusSettings(); + } + + static formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + seconds = Math.floor(seconds); + const parts = []; + if (hours) parts.push(hours); + parts.push(minutes); + parts.push(seconds); + return parts.map(number => (number < 10 ? "0" + number : "" + number)).join(":"); + } + + _setInterfaceTimeout() { + if (this._timeout) { + clearTimeout(this._timeout); + } + this._timeout = setTimeout(() => { + this._hide(); + }, 8000); + } + + _init() { + this._setState("Controls"); + } + + _focus() { + this._setInterfaceTimeout(); + } + + _unfocus() { + clearTimeout(this._timeout); + } + + $mediaplayerEnded() { + this._pressNext(); + } + + play({item, items = [item]}) { + this._items = items; + this._setItem(item); + return !!this._stream; + } + + pressPrevious() { + const index = this._index - 1; + if (index < 0) { + this._index = this._items.length - 1; + } + this._setItem(this._items[index]); + } + + _pressNext() { + if (!this._items.length) { + return this.signal('playerStop'); + } + const index = (this._index + 1) % this._items.length; + this._setItem(this._items[index]); + } + + pressPlay() { + this.application.mediaplayer.playPause(); + } + + $mediaplayerPause() { + this.tag("Controls").paused = true; + } + + $mediaplayerPlay() { + this.tag("Controls").paused = false; + } + + $mediaplayerStop() { + this.signal('playerStop'); + } + + $mediaplayerProgress({currentTime, duration}) { + this.tag("Progress").setProgress(currentTime, duration); + } + + _captureKey() { + this._setInterfaceTimeout(); + return false; + } + + _hide() { + this._setState("Hidden"); + } + + static _states() { + return [ + class Hidden extends this { + $enter({prevState}) { + this._prevState = prevState; + this.setSmooth('alpha', 0); + } + $exit() { + this._setInterfaceTimeout(); + this.setSmooth('alpha', 1); + } + _captureKey() { + this._setState(this._prevState); + } + }, + class Controls extends this { + } + ]; + } + + _getFocused() { + return this.tag("Controls"); + } + + _setFocusSettings(settings) { + settings.mediaplayer.consumer = this; + } + + getMediaplayerSettings() { + if (this._stream.link) { + // Backwards compatibility. + this._stream.src = this._stream.link; + } + + return { + stream: this._stream + }; + } + + +} + +const obj = { + Player, + PlayerButton, + PlayerControls, + PlayerProgress +}; + +class Light3dComponent extends lightning$1.Component { + + constructor(stage) { + super(stage); + + this.patch({ + __create: true, + Main: { + x: -1, + y: -1, + shader: {type: lightning$1.shaders.Light3d, fudge: 0.3}, + renderToTexture: true, + Wrapper: { + x: 1, + y: 1, + clipping: true, + Content: {} + } + } + }); + + this._shaderZ = 0; + this._shaderZ0 = 0; + this._shaderZ1 = 0; + + this._shaderRx = 0; + this._shaderRx0 = 0; + this._shaderRx1 = 0; + + this._shaderRy = 0; + this._shaderRy0 = 0; + this._shaderRy1 = 0; + + this._focusedZ = -150; + this._createAnimations(); + + this.transition('lightShader.strength', {duration: 0.2}); + this.transition('lightShader.ambient', {duration: 0.2}); + } + + get focusedZ() { + return this._focusedZ; + } + + set focusedZ(v) { + this._focusedZ = v; + this._createAnimations(); + } + + _createAnimations() { + this._anims = { + neutral: this.animation({ + duration: 0.4, actions: [ + {p: 'shaderZ0', merger: lightning$1.StageUtils.mergeNumbers, v: {0: 0, 0.5: -140, 1: -150}} + ] + }), + left: this._createAnimation('x', -1, 0), + right: this._createAnimation('x', 1, 1), + up: this._createAnimation('y', -1, 0), + down: this._createAnimation('y', 1, 0) + }; + } + + _createAnimation(axis, sign, idx) { + return this.animation({ + duration: 0.4, stopDuration: 0.2, actions: [ + {p: 'shaderR' + axis + idx, merger: lightning$1.StageUtils.mergeNumbers, v: {0: 0, 0.3: -0.20 * sign, 1: 0}}, + { + p: 'shaderZ' + idx, + merger: lightning$1.StageUtils.mergeNumbers, + v: {0: 0, 0.5: this._focusedZ + 10, 1: this._focusedZ} + } + ] + }); + } + + set w(v) { + this.tag('Main').w = v + 2; + this.tag('Wrapper').w = v; + } + + set h(v) { + this.tag('Main').h = v + 2; + this.tag('Wrapper').h = v; + } + + get lightShader() { + return this.tag('Main').shader; + } + + set lightShader(v) { + this.tag('Main').shader = v; + } + + get content() { + return this.tag('Content'); + } + + set content(v) { + this.tag('Content').patch(v, true); + } + + _recalc() { + this.tag('Main').shader.rx = this._shaderRx0 + this._shaderRx1 + this._shaderRx; + this.tag('Main').shader.ry = this._shaderRy0 + this._shaderRy1 + this._shaderRy; + this.tag('Main').shader.z = this._shaderZ0 + this._shaderZ1 + this._shaderZ; + this.tag('Main').shader.pivotZ = this._shaderZ0 + this._shaderZ1 + this._shaderZ; + } + + get shaderZ() { + return this._shaderZ; + } + + set shaderZ(v) { + this._shaderZ = v; + this._recalc(); + } + + get shaderZ0() { + return this._shaderZ0; + } + + set shaderZ0(v) { + this._shaderZ0 = v; + this._recalc(); + } + + get shaderZ1() { + return this._shaderZ1; + } + + set shaderZ1(v) { + this._shaderZ1 = v; + this._recalc(); + } + + get shaderRx() { + return this._shaderRx; + } + + set shaderRx(v) { + this._shaderRx = v; + this._recalc(); + } + + get shaderRx0() { + return this._shaderRx0; + } + + set shaderRx0(v) { + this._shaderRx0 = v; + this._recalc(); + } + + get shaderRx1() { + return this._shaderRx1; + } + + set shaderRx1(v) { + this._shaderRx1 = v; + this._recalc(); + } + + get shaderRy() { + return this._shaderRy; + } + + set shaderRy(v) { + this._shaderRy = v; + this._recalc(); + } + + get shaderRy0() { + return this._shaderRy0; + } + + set shaderRy0(v) { + this._shaderRy0 = v; + this._recalc(); + } + + get shaderRy1() { + return this._shaderRy1; + } + + set shaderRy1(v) { + this._shaderRy1 = v; + this._recalc(); + } + + leftEnter() { + this._anims['left'].start(); + this._enable3dShader(); + } + + leftExit() { + this.neutralExit(); + } + + rightEnter() { + this._anims['right'].start(); + this._enable3dShader(); + } + + rightExit() { + this.neutralExit(); + } + + upEnter() { + this._anims['up'].start(); + this._enable3dShader(); + } + + upExit() { + this.neutralExit(); + } + + downEnter() { + this._anims['down'].start(); + this._enable3dShader(); + } + + downExit() { + this.neutralExit(); + } + + neutralEnter() { + this._anims['neutral'].start(); + this._enable3dShader(); + } + + neutralExit() { + this._anims['up'].stop(); + this._anims['down'].stop(); + this._anims['left'].stop(); + this._anims['right'].stop(); + this._anims['neutral'].stop(); + this._disable3dShader(); + } + + _enable3dShader() { + this.patch({smooth: {'lightShader.strength': 0.4, 'lightShader.ambient': 0.6}}); + } + + _disable3dShader() { + this.patch({smooth: {'lightShader.strength': 0, 'lightShader.ambient': 1}}); + } + + +} + +const obj$1 = { + Light3dComponent +}; + +const template = { + keyWidth: 74, + keyHeight: 74, + horizontalSpacing: 8, + verticalSpacing: 12, + layouts: { + 'ABC': { + rows: [ + { + keys: [ + {c: 'A'}, + {c: 'B'}, + {c: 'C'}, + {c: 'D'}, + {c: 'E'}, + {c: 'F'}, + {c: 'G'}, + {action: 'backspace', w: 148, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {c: 'H'}, + {c: 'I'}, + {c: 'J'}, + {c: 'K'}, + {c: 'L'}, + {c: 'M'}, + {c: 'N'}, + {action: 'toggleToLayout', toLayout: '#123', w: 148, c: '#123'} + ] + }, + { + keys: [ + {c: 'O'}, + {c: 'P'}, + {c: 'Q'}, + {c: 'R'}, + {c: 'S'}, + {c: 'T'}, + {c: 'U'} + ] + }, + { + keys: [ + {c: 'V'}, + {c: 'W'}, + {c: 'X'}, + {c: 'Y'}, + {c: 'Z'}, + {c: '-'}, + {c: '\''} + ] + }, + { + keys: [ + {action: 'space', c: 'space', w: 183}, + {action: 'delete', c: 'delete', w: 183}, + {action: 'ok', c: 'ok', w: 183} + ] + } + ] + }, + '#123': { + rows: [ + { + keys: [ + {c: '1'}, + {c: '2'}, + {c: '3'}, + {c: '&'}, + {c: '#'}, + {c: '('}, + {c: ')'}, + {action: 'backspace', w: 148, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {c: '4'}, + {c: '5'}, + {c: '6'}, + {c: '@'}, + {c: '!'}, + {c: '?'}, + {c: ':'}, + {action: 'toggleToLayout', toLayout: 'ABC', w: 148, c: 'ABC'} + ] + }, + { + keys: [ + {c: '7'}, + {c: '8'}, + {c: '9'}, + {c: '0'}, + {c: '.'}, + {c: '_'}, + {c: '\"'} + ] + }, + { + keys: [ + {action: 'space', c: 'space', w: 183}, + {action: 'delete', c: 'delete', w: 183}, + {action: 'ok', c: 'ok', w: 183} + ] + } + ] + } + } +}; + +class KeyboardButton extends lightning$1.Component { + static _template() { + return { + Background: {colorTop: 0x80e8e8e8, colorBottom: 0x80d1d1d1}, + Content: {} + }; + } + + set action(v) { + this._action = v; + } + + get action() { + return this._action; + } + + get c() { + return this.key.c; + } + + set key(v) { + this._key = v; + if(this.active) { + this._update(); + } + } + + _update() { + this.patch(this._getPatch(this._key)); + } + + _getPatch(key) { + let content = key.patch || {text: {text: key.c, fontFace: 'RobotoRegular', textAlign: 'center', fontSize: 36}}; + return { + Background: {texture: lightning$1.Tools.getRoundRect(this.w, this.h, 7, 0, 0xffffffff, true, 0xffffffff)}, + Content: {mountX: 0.5, mountY: 0.4, x: this.w/2, y: this.h/2, ...content} + }; + } + + get key() { + return this._key; + } + + _focus() { + this.patch({ + Background: {smooth: {colorTop: 0xff3777ee, colorBottom: 0xff2654a8}} + }); + } + + _unfocus() { + this.patch({ + Background: {smooth: {colorTop: 0x80e8e8e8, colorBottom: 0x80d1d1d1}} + }); + } + + _firstActive() { + this._update(); + } +} + +class Keyboard extends lightning$1.Component { + static _template() { + return { + + }; + } + + _construct() { + this._template = template; + } + + set template(v) { + this._template = v; + } + + get keyboardTemplate() { + return this._template; + } + + get keyboardButton() { + return KeyboardButton; + } + + get maxCharacters() { + return 40; + } + + set value(v) { + if(v.length < this.maxCharacters) { + this._value = v; + this.signal('valueChanged', {value: v}); + } + } + + get value() { + return this._value; + } + + get rows() { + return this.children; + } + + get rowLength() { + return this.rows[this.rowIndex].children.length; + } + + get currentKey() { + return this.children[this.rowIndex].children[this.colIndex] || null; + } + + set layout(layout) { + this._layout = layout; + this._update(); + } + + get layout() { + return this._layout; + } + + _getFocused() { + return this.currentKey; + } + + _navigate(dir, value) { + dir = (dir === 'up' || dir === 'down') ? 'vertical' : 'horizontal'; + if(dir === 'horizontal' && this.colIndex + value < this.rowLength && this.colIndex + value > -1) { + this.previous = null; + return this.colIndex += value; + } + else if(dir === 'vertical' && this.rowIndex + value < this.rows.length && this.rowIndex + value > -1) { + const currentColIndex = this.colIndex; + const targetRow = this.rowIndex + value; + if(this.previous && this.previous.row === targetRow) { + const tmp = this.previous.col; + this.previous.col = this.colIndex; + this.colIndex = tmp; + } + else { + const targetRow = this.children[(this.rowIndex + value)]; + const targetItems = targetRow.children; + const ck = this.currentKey; + let target = 0; + for(let i = 0; i < targetItems.length; i++) { + const ckx = this.children[this.rowIndex].x + ck.x; + const tix = targetRow.x + targetItems[i].x; + target = i; + if((ckx >= tix && ckx <= tix + targetItems[i].w) || (tix >= ckx && tix <= ckx + ck.w)) { + break; + } + } + this.colIndex = target; + } + this.previous = {col: currentColIndex, row: this.rowIndex}; + return this.rowIndex += value; + } + return false; + } + + _update() { + if(this._layout && this.keyboardTemplate.layouts[this._layout] === undefined) { + console.error(`Configured layout "${this.layout}" does not exist. Reverting to "${Object.keys(this.keyboardTemplate.layouts)[0]}"`); + this._layout = null; + } + if(!this._layout) { + this._layout = Object.keys(this.keyboardTemplate.layouts)[0]; + } + const {keyWidth, keyHeight, horizontalSpacing = 0, verticalSpacing = 0, layouts} = this.keyboardTemplate; + + this.children = layouts[this._layout].rows.map((row, rowIndex) => { + let keyOffset = 0; + const {x = 0, rowVerticalSpacing = verticalSpacing, rowHorizontalSpacing = horizontalSpacing, keys = []} = row; + return {y: keyHeight * rowIndex + (rowIndex * rowVerticalSpacing), x, + children: keys.map((key) => { + key = Object.assign({action: 'input'}, key); + const prevOffset = keyOffset; + const {w = keyWidth, h = keyHeight, action, toLayout} = key; + keyOffset += w + rowHorizontalSpacing; + return {key, action, toLayout, x: prevOffset, w, h, type: this.keyboardButton} + }) + }; + }); + } + + reset() { + this.colIndex = 0; + this.rowIndex = 0; + this._value = ''; + this.previous = null; + } + + _init() { + this.reset(); + this._update(); + } + + _handleRight() { + return this._navigate('right', 1); + } + + _handleLeft() { + return this._navigate('left', -1); + } + + _handleUp() { + return this._navigate('up', -1); + } + + _handleDown() { + return this._navigate('down', 1); + } + + _handleEnter() { + const key = this.currentKey; + switch(key.action) { + case 'input': + this.value += key.c; + break; + case 'backspace': + this.value = this.value.slice(0, -1); + break + case 'space': + if(this.value.length > 0){ + this.value += ' '; + } + break; + case 'delete': + this.value = ''; + break; + case 'toggleToLayout': + this.layout = key.toLayout; + break; + default: + this.signal(key.action, key); + break; + } + } +} + +const template$1 = { + keyWidth: 64, + keyHeight: 84, + horizontalSpacing: 8, + verticalSpacing: 12, + layouts: { + 'ABC': { + rows: [ + { + keys: [ + {c: 'Q'}, + {c: 'W'}, + {c: 'E'}, + {c: 'R'}, + {c: 'T'}, + {c: 'Y'}, + {c: 'U'}, + {c: 'I'}, + {c: 'O'}, + {c: 'P'} + ] + }, + { + x: 34, + keys: [ + {c: 'A'}, + {c: 'S'}, + {c: 'D'}, + {c: 'F'}, + {c: 'G'}, + {c: 'H'}, + {c: 'J'}, + {c: 'K'}, + {c: 'L'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'abc', c: 'Aa', w: 98}, + {c: 'Z'}, + {c: 'X'}, + {c: 'C'}, + {c: 'V'}, + {c: 'B'}, + {c: 'N'}, + {c: 'M'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', w: 136, c: '#123'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + 'abc': { + rows: [ + { + keys: [ + {c: 'q'}, + {c: 'w'}, + {c: 'e'}, + {c: 'r'}, + {c: 't'}, + {c: 'y'}, + {c: 'u'}, + {c: 'i'}, + {c: 'o'}, + {c: 'p'} + ] + }, + { + x: 34, + keys: [ + {c: 'a'}, + {c: 's'}, + {c: 'd'}, + {c: 'f'}, + {c: 'g'}, + {c: 'h'}, + {c: 'j'}, + {c: 'k'}, + {c: 'l'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', c: 'aA', w: 98}, + {c: 'z'}, + {c: 'x'}, + {c: 'c'}, + {c: 'v'}, + {c: 'b'}, + {c: 'n'}, + {c: 'm'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', w: 136, c: '#123'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + '#123': { + rows: [ + { + keys: [ + {c: '1'}, + {c: '2'}, + {c: '3'}, + {c: '4'}, + {c: '5'}, + {c: '6'}, + {c: '7'}, + {c: '8'}, + {c: '9'}, + {c: '0'} + ] + }, + { + x: 34, + keys: [ + {c: '@'}, + {c: '#'}, + {c: '€'}, + {c: '_'}, + {c: '&'}, + {c: '-'}, + {c: '+'}, + {c: '('}, + {c: ')'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '{&=', c: '{&=', w: 98}, + {c: '*'}, + {c: '\"'}, + {c: '\''}, + {c: ':'}, + {c: ';'}, + {c: '!'}, + {c: '?'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', w: 136, c: 'ABC'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + '{&=': { + rows: [ + { + keys: [ + {c: '~'}, + {c: '\`'}, + {c: '|'}, + {c: '\u2022'}, + {c: '√'}, + {c: 'π'}, + {c: '\u00f7'}, + {c: '\u00d7'}, + {c: '¶'}, + {c: '∆'} + ] + }, + { + keys: [ + {c: '£'}, + {c: '¥'}, + {c: '€'}, + {c: '¢'}, + {c: '^'}, + {c: '°'}, + {c: '='}, + {c: '{'}, + {c: '}'}, + {c: 'a'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', c: '#123', w: 98}, + {c: '%'}, + {c: '©'}, + {c: '®'}, + {c: '™'}, + {c: '\u2713'}, + {c: '['}, + {c: ']'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', w: 136, c: 'ABC'}, + {c: '<'}, + {action: 'space', c: '', w: 276}, + {c: '>'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + } + } +}; + +const obj$2 = { + Keyboard, + KeyboardButton, + SimpleKeyboardTemplate: template, + AdvancedKeyboardTemplate: template$1 +}; + +class ItemList extends lightning$1.Component { + static _template() { + return { + Wrapper: { + flex: {direction: 'row'} + } + }; + } + + set items(items) { + this.tag('Wrapper').children = items; + this._index = 0; + if(items.length > 0) { + this._setState('Filled'); + } + else { + this._setState('Empty'); + } + } + + get items() { + return this.tag('Wrapper').children + } + + get currentItem() { + return this.items[this._index]; + } + + get length() { + return this.items.length; + } + + set orientation(v) { + this._orientation = v; + if(v === 'horizontal') { + this.tag('Wrapper').patch({flex: {direction: 'row'}}); + } + else { + this.tag('Wrapper').patch({flex: {direction: 'column'}}); + } + } + + get orientation() { + return this._orientation || 'horizontal'; + } + + set jump(bool) { + this._jump = bool; + } + + get jump() { + return this._jump || false; + } + + set jumpToStart(bool) { + this._jumpToStart = bool; + } + + get jumpToStart() { + return this._jumpToStart !== undefined ? this._jumpToStart : this.jump; + } + + set jumpToEnd(bool) { + this._jumpToEnd = bool; + } + + get jumpToEnd() { + return this._jumpToEnd !== undefined ? this._jumpToEnd : this.jump; + } + + _navigate(dir) { + const ori = this.orientation; + if(((dir === 'right' || dir === 'left') && ori === 'horizontal') || ((dir === 'up' || dir === 'down') && ori === 'vertical')) { + const length = this.items.length; + const currentIndex = this._index; + let targetIndex = currentIndex + 1; + if(dir === 'left' || dir === 'up') { + targetIndex = currentIndex - 1; + } + + if(targetIndex > -1 && targetIndex < length) { + this._index = targetIndex; + } + else if(this.jump || (this.jumpToStart || this.jumpToEnd)) { + if(targetIndex < 0 && this.jumpToEnd) { + this._index = targetIndex + length; + } + else if(targetIndex === length && this.jumpToStart){ + this._index = 0; + } + } + else { + return false; + } + + if(currentIndex !== this._index) { + this.indexChanged({index: this._index, previousIndex: currentIndex}); + } + } + return false; + } + + setIndex(targetIndex) { + if(targetIndex > -1 && targetIndex < this.items.length) { + const currentIndex = this._index; + this._index = targetIndex; + this.indexChanged({index: this._index, previousIndex: currentIndex}); + } + } + + indexChanged(event) { + this.signal('indexChanged', event); + } + + _getFocused() { + return this; + } + + _construct() { + this._index = 0; + } + + _init() { + this._setState('Empty'); + } + + static _states() { + return [ + class Empty extends this { + }, + class Filled extends this { + _getFocused() { + return this.currentItem; + } + _handleRight() { + return this._navigate('right'); + } + + _handleLeft() { + return this._navigate('left'); + } + + _handleUp() { + return this._navigate('up'); + } + + _handleDown() { + return this._navigate('down'); + } + } + ] + } +} + +const obj$3 = { + ItemList +}; + +class Slider extends lightning$1.Component { + static _template() { + return { + Wrapper: { + flex: {direction: 'row'} + } + } + } + + set items(items) { + this._reset(); + this.tag('Wrapper').children = items; + this.scrollToFocus(true); + if(items.length > 0) { + this._setState('Filled'); + } + else { + this._setState('Empty'); + } + } + + get items() { + return this.tag('Wrapper').children; + } + + get currentItem() { + return this.items[this._index]; + } + + get index() { + return this._index; + } + + set orientation(v) { + this._orientation = v; + if(v === 'horizontal') { + this.tag('Wrapper').patch({flex: {direction: 'row'}}); + } + else { + this.tag('Wrapper').patch({flex: {direction: 'column'}}); + } + } + + get orientation() { + return this._orientation || 'horizontal'; + } + + set margin(v) { + this._margin = v; + } + + get margin() { + return this._margin || 0; + } + + set marginStart(v) { + this._marginStart = v; + } + + get marginStart() { + return this._marginStart || this.margin; + } + + set marginEnd(v) { + this._marginEnd = v; + } + + get marginEnd() { + return this._marginEnd || this.margin; + } + + set jump(bool) { + this._jump = bool; + } + + get jump() { + return this._jump || false; + } + + set jumpToStart(bool) { + this._jumpToStart = bool; + } + + get jumpToStart() { + return this._jumpToStart !== undefined ? this._jumpToStart : this.jump; + } + + set jumpToEnd(bool) { + this._jumpToEnd = bool; + } + + get jumpToEnd() { + return this._jumpToEnd !== undefined ? this._jumpToEnd : this.jump; + } + + get scrollTransitionSettings() { + return this._scrollTransitionSettings; + } + + set scrollTransition(v) { + this._scrollTransitionSettings.patch(v); + } + + get scrollTransition() { + return this._scrollTransition; + } + + get viewportSize() { + return this.orientation === 'horizontal' ? this.w : this.h; + } + + _getItemCenterPosition(item) { + if(this.orientation === 'horizontal') { + return item.finalX + (item.finalW * 0.5); + } + return item.finalY + (item.finalH * 0.5); + } + + _getScrollPosition(position) { + const s = this._fullSize; + + const viewportSize = this.viewportSize; + const marginStart = this.marginStart; + const marginEnd = this.marginEnd; + + const maxDistanceStart = 0.5 * viewportSize - marginStart; + const maxDistanceEnd = 0.5 * viewportSize - marginEnd; + if((position < maxDistanceStart) || (s < viewportSize - (marginStart + marginEnd))) { + position = maxDistanceStart; + } + else if(position > s - maxDistanceEnd) { + position = s - maxDistanceEnd; + } + return position - 0.5 * viewportSize; + } + + _navigate(dir) { + const ori = this.orientation; + if(((dir === 'right' || dir === 'left') && ori === 'horizontal') || ((dir === 'up' || dir === 'down') && ori === 'vertical')) { + const length = this.items.length; + const currentIndex = this._index; + let targetIndex = currentIndex + 1; + if(dir === 'left' || dir === 'up') { + targetIndex = currentIndex - 1; + } + + if(targetIndex > -1 && targetIndex < length) { + this._index = targetIndex; + } + else if(this.jump || (this.jumpToStart || this.jumpToEnd)) { + if(targetIndex < 0 && this.jumpToEnd) { + this._index = targetIndex + length; + } + else if(targetIndex === length && this.jumpToStart){ + this._index = 0; + } + } + + if(currentIndex !== this._index) { + this.indexChanged({index: this._index, previousIndex: currentIndex, length: this.items.length}); + } + this.scrollToFocus(); + } + return false; + } + + scrollToFocus(immediate) { + if(this.currentItem) { + const focusPosition = this._getItemCenterPosition(this.currentItem); + const scrollPosition = this._getScrollPosition(focusPosition); + if(this._scrollTransition.isRunning()) { + this._scrollTransition.reset(-scrollPosition, 0.1); + } + else { + this._scrollTransition.start(-scrollPosition); + } + if(immediate) { + this._scrollTransition.finish(); + } + } + } + + setIndex(targetIndex, immediate = false) { + if(targetIndex > -1 && targetIndex < this.items.length) { + const currentIndex = this._index; + this._index = targetIndex; + this.indexChanged({index: this._index, previousIndex: currentIndex, immediate}); + this.scrollToFocus(immediate); + } + } + + indexChanged(event) { + this.signal('indexChanged', event); + } + + _getFocused() { + return this; + } + + _reset() { + this._index = 0; + } + + _construct() { + this._index = 0; + this._scrollTransitionSettings = this.stage.transitions.createSettings({}); + } + + _init() { + const wrapper = this.tag('Wrapper'); + const or = this.orientation === 'horizontal' ? 'x' : 'y'; + wrapper.transition(or, this._scrollTransitionSettings); + this._scrollTransition = wrapper.transition(or); + wrapper.onAfterUpdate = () => { + if(this.orientation === 'horizontal') { + this._fullSize = wrapper.finalW; + } + else { + this._fullSize = wrapper.finalH; + } + }; + this._setState('Empty'); + } + + static _states() { + return [ + class Empty extends this { + }, + class Filled extends this { + _getFocused() { + return this.currentItem; + } + _handleRight() { + return this._navigate('right'); + } + + _handleLeft() { + return this._navigate('left'); + } + + _handleUp() { + return this._navigate('up'); + } + + _handleDown() { + return this._navigate('down'); + } + } + ] + } +} + +const obj$4 = { + Slider +}; + +const tools = { + player: obj, + effects: obj$1, + keyboard: obj$2, + itemlist: obj$3, + slider: obj$4 +}; + +// Exposes the ux namespace for apps. + +const ux$1 = { + Ui, + App, + tools +}; + +if (typeof window !== "undefined") { + window.ux = ux$1; +} + +class DevLauncher { + + constructor() { + } + + launch(appType, lightningOptions, options = {}) { + this._appType = appType; + this._options = options; + return this._start(lightningOptions); + } + + _handleKey(event) { + this._ui._receiveKeydown(event); + } + + _start(lightningOptions = {}) { + this._openFirewall(); + this._lightningOptions = this._getLightningOptions(lightningOptions); + return this._startApp(); + } + + _startApp() { + ux$1.Ui.staticFilesPath = __dirname + "/"; + + this._ui = new ux$1.Ui(this._lightningOptions); + this._ui.startApp(this._appType); + } + + _loadInspector() { + if (this._options.useInspector) { + /* Attach the inspector to create a fake DOM that shows where lightning elements can be found. */ + return this.loadScript(DevLauncher._uxPath + "../wpe-lightning/devtools/lightning-inspect.js"); + } else { + return Promise.resolve(); + } + } + + _openFirewall() { + // Fetch app store to ensure that proxy/image servers firewall is opened. + //fetch(`http://widgets.metrological.com/${encodeURIComponent(ux.Ui.getOption('operator') || 'metrological')}/nl/test`).then(() => {}); + } + + _getLightningOptions(customOptions = {}) { + let options = {stage: {w: 1920, h: 1080}, debug: false, keys: this._getNavigationKeys()}; + + const config = options.stage; + if (customOptions.h === 720) { + config['w'] = 1280; + config['h'] = 720; + config['precision'] = 0.6666666667; + } else { + config['w'] = 1920; + config['h'] = 1080; + + config.useImageWorker = true; + } + + options = lightning$1.tools.ObjMerger.merge(options, customOptions); + + return options; + } + + _getNavigationKeys() { + return { + 8: "Back", + 13: "Enter", + 27: "Menu", + 37: "Left", + 38: "Up", + 39: "Right", + 40: "Down", + 174: "ChannelDown", + 175: "ChannelUp", + 178: "Stop", + 250: "PlayPause", + 191: "Search", // Use "/" for keyboard + 409: "Search" + }; + } +} + +const Headers$1 = fetch.Headers; + +class MyApp extends ux$1.App { + static _template() { + return { + shader: {type: lightning$1.shaders.Grayscale, amount: 0.9}, + LilLightning:{ x: 100, y: 50, src: MyApp.getPath("ll_image.png"), scaleX:1.5, scaleY:1.5}, + Header: { + rect: true, w: 1920, h: 1280, color: 0xff005500, + SubLilLightning: { + x: 400, y: 50, src: MyApp.getPath("ll_image.png"),scaleX:1.5, scaleY:1.5, + shader: null // Reset shader to default. + }, + SubLilLightning2: { + x: 400, y: 500, src: MyApp.getPath("ll_image.png"), + shader: {type: lightning$1.shaders.Inversion} // Reset shader to other. + }, + Scaler: {type: lightning$1.components.SmoothScaleComponent, smoothScale: 1, content: { + text: {color: 0xff005500, text: "Scaled text", fontSize: 200} + }}, + } + } + } + + _handleUp(){ + this.tag("SubLilLightning2").shader = null; + } + _handleDown(){ + this.tag("SubLilLightning2").shader = {'type': lightning$1.shaders.Inversion} + } +} + +MyApp.COLORS = { + BACKGROUND: 0xff282e32 +}; + +const launcher = new DevLauncher(); + +sparkview.on('onKeyDown', function(e) { + console.log('webgl onKeyDown keyCode:', e.keyCode); + launcher._handleKey(e); +}); + +var keymaps = { + 49: "Up", + 50: "Down" +}; +launcher.launch(MyApp, {debug:false, h:sparkscene.h, keys:keymaps}, {useInspector: false}); diff --git a/simpleImageApp.js b/simpleImageApp.js new file mode 100644 index 0000000..e81d92e --- /dev/null +++ b/simpleImageApp.js @@ -0,0 +1,23761 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +require('fs'); +require('http'); +require('https'); +var fetch = _interopDefault(require('node-fetch')); + +class StageUtils { + + static mergeNumbers(v1, v2, p) { + return v1 * p + v2 * (1 - p); + }; + + static rgb(r, g, b) { + return (r << 16) + (g << 8) + b + (255 * 16777216); + }; + + static rgba(r, g, b, a) { + return (r << 16) + (g << 8) + b + (((a * 255) | 0) * 16777216); + }; + + static getRgbString(color) { + let r = ((color / 65536) | 0) % 256; + let g = ((color / 256) | 0) % 256; + let b = color % 256; + return 'rgb(' + r + ',' + g + ',' + b + ')'; + }; + + static getRgbaString(color) { + let r = ((color / 65536) | 0) % 256; + let g = ((color / 256) | 0) % 256; + let b = color % 256; + let a = ((color / 16777216) | 0) / 255; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(4) + ')'; + }; + + static getRgbaStringFromArray(color) { + let r = Math.floor(color[0] * 255); + let g = Math.floor(color[1] * 255); + let b = Math.floor(color[2] * 255); + let a = Math.floor(color[3] * 255) / 255; + return 'rgba(' + r + ',' + g + ',' + b + ',' + a.toFixed(4) + ')'; + }; + + static getRgbaComponentsNormalized(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r / 255, g / 255, b / 255, a / 255]; + }; + + static getRgbComponentsNormalized(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + return [r / 255, g / 255, b / 255]; + }; + + static getRgbaComponents(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r, g, b, a]; + }; + + static getArgbNumber(rgba) { + rgba[0] = Math.max(0, Math.min(255, rgba[0])); + rgba[1] = Math.max(0, Math.min(255, rgba[1])); + rgba[2] = Math.max(0, Math.min(255, rgba[2])); + rgba[3] = Math.max(0, Math.min(255, rgba[3])); + let v = ((rgba[3] | 0) << 24) + ((rgba[0] | 0) << 16) + ((rgba[1] | 0) << 8) + (rgba[2] | 0); + if (v < 0) { + v = 0xFFFFFFFF + v + 1; + } + return v; + }; + + static mergeColors(c1, c2, p) { + let r1 = ((c1 / 65536) | 0) % 256; + let g1 = ((c1 / 256) | 0) % 256; + let b1 = c1 % 256; + let a1 = ((c1 / 16777216) | 0); + + let r2 = ((c2 / 65536) | 0) % 256; + let g2 = ((c2 / 256) | 0) % 256; + let b2 = c2 % 256; + let a2 = ((c2 / 16777216) | 0); + + let r = r1 * p + r2 * (1 - p); + let g = g1 * p + g2 * (1 - p); + let b = b1 * p + b2 * (1 - p); + let a = a1 * p + a2 * (1 - p); + + return Math.round(a) * 16777216 + Math.round(r) * 65536 + Math.round(g) * 256 + Math.round(b); + }; + + static mergeMultiColors(c, p) { + let r = 0, g = 0, b = 0, a = 0, t = 0; + let n = c.length; + for (let i = 0; i < n; i++) { + let r1 = ((c[i] / 65536) | 0) % 256; + let g1 = ((c[i] / 256) | 0) % 256; + let b1 = c[i] % 256; + let a1 = ((c[i] / 16777216) | 0); + r += r1 * p[i]; + g += g1 * p[i]; + b += b1 * p[i]; + a += a1 * p[i]; + t += p[i]; + } + + t = 1 / t; + return Math.round(a * t) * 16777216 + Math.round(r * t) * 65536 + Math.round(g * t) * 256 + Math.round(b * t); + }; + + static mergeMultiColorsEqual(c) { + let r = 0, g = 0, b = 0, a = 0, t = 0; + let n = c.length; + for (let i = 0; i < n; i++) { + let r1 = ((c[i] / 65536) | 0) % 256; + let g1 = ((c[i] / 256) | 0) % 256; + let b1 = c[i] % 256; + let a1 = ((c[i] / 16777216) | 0); + r += r1; + g += g1; + b += b1; + a += a1; + t += 1.0; + } + + t = 1 / t; + return Math.round(a * t) * 16777216 + Math.round(r * t) * 65536 + Math.round(g * t) * 256 + Math.round(b * t); + }; + + static mergeColorAlpha(c, alpha) { + let a = ((c / 16777216 | 0) * alpha) | 0; + return (((((c >> 16) & 0xff) * a) / 255) & 0xff) + + ((((c & 0xff00) * a) / 255) & 0xff00) + + (((((c & 0xff) << 16) * a) / 255) & 0xff0000) + + (a << 24); + }; + + static rad(deg) { + return deg * (Math.PI / 180); + }; + + static getTimingBezier(a, b, c, d) { + let xc = 3.0 * a; + let xb = 3.0 * (c - a) - xc; + let xa = 1.0 - xc - xb; + let yc = 3.0 * b; + let yb = 3.0 * (d - b) - yc; + let ya = 1.0 - yc - yb; + + return function (time) { + if (time >= 1.0) { + return 1; + } + if (time <= 0) { + return 0; + } + + let t = 0.5, cbx, cbxd, dx; + + for (let it = 0; it < 20; it++) { + cbx = t * (t * (t * xa + xb) + xc); + dx = time - cbx; + if (dx > -1e-8 && dx < 1e-8) { + return t * (t * (t * ya + yb) + yc); + } + + // Cubic bezier derivative. + cbxd = t * (t * (3 * xa) + 2 * xb) + xc; + + if (cbxd > 1e-10 && cbxd < 1e-10) { + // Problematic. Fall back to binary search method. + break; + } + + t += dx / cbxd; + } + + // Fallback: binary search method. This is more reliable when there are near-0 slopes. + let minT = 0; + let maxT = 1; + for (let it = 0; it < 20; it++) { + t = 0.5 * (minT + maxT); + + cbx = t * (t * (t * xa + xb) + xc); + + dx = time - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc); + } + + if (dx < 0) { + maxT = t; + } else { + minT = t; + } + } + + }; + }; + + static getTimingFunction(str) { + switch (str) { + case "linear": + return function (time) { + return time + }; + case "ease": + return StageUtils.getTimingBezier(0.25, 0.1, 0.25, 1.0); + case "ease-in": + return StageUtils.getTimingBezier(0.42, 0, 1.0, 1.0); + case "ease-out": + return StageUtils.getTimingBezier(0, 0, 0.58, 1.0); + case "ease-in-out": + return StageUtils.getTimingBezier(0.42, 0, 0.58, 1.0); + case "step-start": + return function () { + return 1 + }; + case "step-end": + return function (time) { + return time === 1 ? 1 : 0; + }; + default: + let s = "cubic-bezier("; + if (str && str.indexOf(s) === 0) { + let parts = str.substr(s.length, str.length - s.length - 1).split(","); + if (parts.length !== 4) { + console.warn("Unknown timing function: " + str); + + // Fallback: use linear. + return function (time) { + return time + }; + } + let a = parseFloat(parts[0]); + let b = parseFloat(parts[1]); + let c = parseFloat(parts[2]); + let d = parseFloat(parts[3]); + if (isNaN(a) || isNaN(b) || isNaN(c) || isNaN(d)) { + console.warn("Unknown timing function: " + str); + // Fallback: use linear. + return function (time) { + return time + }; + } + + return StageUtils.getTimingBezier(a, b, c, d); + } else { + console.warn("Unknown timing function: " + str); + // Fallback: use linear. + return function (time) { + return time + }; + } + } + }; + +} + +class Utils { + + static isFunction(value) { + return typeof value === 'function'; + } + + static isNumber(value) { + return typeof value === 'number'; + } + + static isInteger(value) { + return (typeof value === 'number' && (value % 1) === 0); + } + + static isBoolean(value) { + return value === true || value === false; + } + + static isString(value) { + return typeof value === 'string'; + } + + static clone(v) { + if (Utils.isObjectLiteral(v) || Array.isArray(v)) { + return Utils.getDeepClone(v); + } else { + // Copy by value. + return v; + } + } + + static cloneObjShallow(obj) { + let keys = Object.keys(obj); + let clone = {}; + for (let i = 0; i < keys.length; i++) { + clone[keys[i]] = obj[keys[i]]; + } + return clone; + } + + static merge(obj1, obj2) { + let keys = Object.keys(obj2); + for (let i = 0; i < keys.length; i++) { + obj1[keys[i]] = obj2[keys[i]]; + } + return obj1; + } + + static isObject(value) { + let type = typeof value; + return !!value && (type === 'object' || type === 'function'); + } + + static isPlainObject(value) { + let type = typeof value; + return !!value && (type === 'object'); + } + + static isObjectLiteral(value){ + return typeof value === 'object' && value && value.constructor === Object; + } + + static getArrayIndex(index, arr) { + return Utils.getModuloIndex(index, arr.length); + } + + static getModuloIndex(index, len) { + if (len === 0) return index; + while (index < 0) { + index += Math.ceil(-index / len) * len; + } + index = index % len; + return index; + } + + static getDeepClone(obj) { + let i, c; + if (Utils.isFunction(obj)) { + // Copy functions by reference. + return obj; + } + if (Array.isArray(obj)) { + c = []; + let keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + c[keys[i]] = Utils.getDeepClone(obj[keys[i]]); + } + return c; + } else if (Utils.isObject(obj)) { + c = {}; + let keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + c[keys[i]] = Utils.getDeepClone(obj[keys[i]]); + } + return c; + } else { + return obj; + } + } + + static equalValues(v1, v2) { + if ((typeof v1) !== (typeof v2)) return false; + if (Utils.isObjectLiteral(v1)) { + return Utils.isObjectLiteral(v2) && Utils.equalObjectLiterals(v1, v2); + } else if (Array.isArray(v1)) { + return Array.isArray(v2) && Utils.equalArrays(v1, v2); + } else { + return v1 === v2; + } + } + + static equalObjectLiterals(obj1, obj2) { + let keys1 = Object.keys(obj1); + let keys2 = Object.keys(obj2); + if (keys1.length !== keys2.length) { + return false; + } + + for (let i = 0, n = keys1.length; i < n; i++) { + const k1 = keys1[i]; + const k2 = keys2[i]; + if (k1 !== k2) { + return false; + } + + const v1 = obj1[k1]; + const v2 = obj2[k2]; + + if (!Utils.equalValues(v1, v2)) { + return false; + } + } + + return true; + } + + static equalArrays(v1, v2) { + if (v1.length !== v2.length) { + return false; + } + for (let i = 0, n = v1.length; i < n; i++) { + if (!this.equalValues(v1[i], v2[i])) { + return false; + } + } + + return true; + } + + static setToArray(s) { + let result = []; + s.forEach(function (value) { + result.push(value); + }); + return result; + } + + static iteratorToArray(iterator) { + let result = []; + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + result.push(iteratorResult.value); + iteratorResult = iterator.next(); + } + return result; + } + + static isUcChar(charcode) { + return charcode >= 65 && charcode <= 90; + } + +} + +Utils.isNode = (typeof window === "undefined"); +Utils.isWeb = (typeof window !== "undefined"); +Utils.isWPE = Utils.isWeb && (navigator.userAgent.indexOf("WPE") !== -1); +Utils.isSpark = (typeof window === "undefined") && (typeof sparkscene !== "undefined"); + +class Base { + + static defaultSetter(obj, name, value) { + obj[name] = value; + } + + static patchObject(obj, settings) { + if (!Utils.isObjectLiteral(settings)) { + console.error("Settings must be object literal"); + } else { + let names = Object.keys(settings); + for (let i = 0, n = names.length; i < n; i++) { + let name = names[i]; + + this.patchObjectProperty(obj, name, settings[name]); + } + } + } + + static patchObjectProperty(obj, name, value) { + let setter = obj.setSetting || Base.defaultSetter; + + if (name.charAt(0) === "_") { + // Disallow patching private variables. + if (name !== "__create") { + console.error("Patch of private property '" + name + "' is not allowed"); + } + } else if (name !== "type") { + // Type is a reserved keyword to specify the class type on creation. + if (Utils.isFunction(value) && value.__local) { + // Local function (Base.local(s => s.something)) + value = value.__local(obj); + } + + setter(obj, name, value); + } + } + + static local(func) { + // This function can be used as an object setting, which is called with the target object. + func.__local = true; + } + + +} + +class SpacingCalculator { + + static getSpacing(mode, numberOfItems, remainingSpace) { + const itemGaps = (numberOfItems - 1); + let spacePerGap; + + let spacingBefore, spacingBetween; + + switch(mode) { + case "flex-start": + spacingBefore = 0; + spacingBetween = 0; + break; + case "flex-end": + spacingBefore = remainingSpace; + spacingBetween = 0; + break; + case "center": + spacingBefore = remainingSpace / 2; + spacingBetween = 0; + break; + case "space-between": + spacingBefore = 0; + spacingBetween = Math.max(0, remainingSpace) / itemGaps; + break; + case "space-around": + if (remainingSpace < 0) { + return this.getSpacing("center", numberOfItems, remainingSpace); + } else { + spacePerGap = remainingSpace / (itemGaps + 1); + spacingBefore = 0.5 * spacePerGap; + spacingBetween = spacePerGap; + } + break; + case "space-evenly": + if (remainingSpace < 0) { + return this.getSpacing("center", numberOfItems, remainingSpace); + } else { + spacePerGap = remainingSpace / (itemGaps + 2); + spacingBefore = spacePerGap; + spacingBetween = spacePerGap; + } + break; + case "stretch": + spacingBefore = 0; + spacingBetween = 0; + break; + default: + throw new Error("Unknown mode: " + mode); + } + + return {spacingBefore, spacingBetween} + } + +} + +class ContentAligner { + + constructor(layout) { + this._layout = layout; + this._totalCrossAxisSize = 0; + } + + get _lines() { + return this._layout._lines; + } + + init() { + this._totalCrossAxisSize = this._getTotalCrossAxisSize(); + } + + align() { + const crossAxisSize = this._layout.crossAxisSize; + const remainingSpace = crossAxisSize - this._totalCrossAxisSize; + + const {spacingBefore, spacingBetween} = this._getSpacing(remainingSpace); + + const lines = this._lines; + + const mode = this._layout._flexContainer.alignContent; + let growSize = 0; + if (mode === "stretch" && lines.length && (remainingSpace > 0)) { + growSize = remainingSpace / lines.length; + } + + let currentPos = spacingBefore; + for (let i = 0, n = lines.length; i < n; i++) { + const crossAxisLayoutOffset = currentPos; + const aligner = lines[i].createItemAligner(); + + let finalCrossAxisLayoutSize = lines[i].crossAxisLayoutSize + growSize; + + aligner.setCrossAxisLayoutSize(finalCrossAxisLayoutSize); + aligner.setCrossAxisLayoutOffset(crossAxisLayoutOffset); + + aligner.align(); + + if (aligner.recursiveResizeOccured) { + lines[i].setItemPositions(); + } + + currentPos += finalCrossAxisLayoutSize; + currentPos += spacingBetween; + } + } + + get totalCrossAxisSize() { + return this._totalCrossAxisSize; + } + + _getTotalCrossAxisSize() { + const lines = this._lines; + let total = 0; + for (let i = 0, n = lines.length; i < n; i++) { + const line = lines[i]; + total += line.crossAxisLayoutSize; + } + return total; + } + + _getSpacing(remainingSpace) { + const mode = this._layout._flexContainer.alignContent; + const numberOfItems = this._lines.length; + return SpacingCalculator.getSpacing(mode, numberOfItems, remainingSpace); + } + +} + +class FlexUtils { + + static getParentAxisSizeWithPadding(item, horizontal) { + const target = item.target; + const parent = target.getParent(); + if (!parent) { + return 0; + } else { + const flexParent = item.flexParent; + if (flexParent) { + // Use pending layout size. + return this.getAxisLayoutSize(flexParent, horizontal) + this.getTotalPadding(flexParent, horizontal); + } else { + // Use 'absolute' size. + return horizontal ? parent.w : parent.h; + } + } + } + + static getRelAxisSize(item, horizontal) { + if (horizontal) { + if (item.funcW) { + if (this._allowRelAxisSizeFunction(item, true)) { + return item.funcW(this.getParentAxisSizeWithPadding(item, true)); + } else { + return 0; + } + } else { + return item.originalWidth; + } + } else { + if (item.funcH) { + if (this._allowRelAxisSizeFunction(item, false)) { + return item.funcH(this.getParentAxisSizeWithPadding(item, false)); + } else { + return 0; + } + } else { + return item.originalHeight; + } + } + } + + static _allowRelAxisSizeFunction(item, horizontal) { + const flexParent = item.flexParent; + if (flexParent && flexParent._flex._layout.isAxisFitToContents(horizontal)) { + // We don't allow relative width on fit-to-contents because it leads to conflicts. + return false; + } + return true; + } + + static isZeroAxisSize(item, horizontal) { + if (horizontal) { + return !item.originalWidth && !item.funcW; + } else { + return !item.originalHeight && !item.funcH; + } + } + + static getAxisLayoutPos(item, horizontal) { + return horizontal ? item.x : item.y; + } + + static getAxisLayoutSize(item, horizontal) { + return horizontal ? item.w : item.h; + } + + static setAxisLayoutPos(item, horizontal, pos) { + if (horizontal) { + item.x = pos; + } else { + item.y = pos; + } + } + + static setAxisLayoutSize(item, horizontal, size) { + if (horizontal) { + item.w = size; + } else { + item.h = size; + } + } + + static getAxisMinSize(item, horizontal) { + let minSize = this.getPlainAxisMinSize(item, horizontal); + + let flexItemMinSize = 0; + if (item.isFlexItemEnabled()) { + flexItemMinSize = item._flexItem._getMinSizeSetting(horizontal); + } + + const hasLimitedMinSize = (flexItemMinSize > 0); + if (hasLimitedMinSize) { + minSize = Math.max(minSize, flexItemMinSize); + } + return minSize; + } + + static getPlainAxisMinSize(item, horizontal) { + if (item.isFlexEnabled()) { + return item._flex._layout.getAxisMinSize(horizontal); + } else { + const isShrinkable = (item.flexItem.shrink !== 0); + if (isShrinkable) { + return 0; + } else { + return this.getRelAxisSize(item, horizontal); + } + } + } + + static resizeAxis(item, horizontal, size) { + if (item.isFlexEnabled()) { + const isMainAxis = (item._flex._horizontal === horizontal); + if (isMainAxis) { + item._flex._layout.resizeMainAxis(size); + } else { + item._flex._layout.resizeCrossAxis(size); + } + } else { + this.setAxisLayoutSize(item, horizontal, size); + } + } + + + static getPaddingOffset(item, horizontal) { + if (item.isFlexEnabled()) { + const flex = item._flex; + if (horizontal) { + return flex.paddingLeft; + } else { + return flex.paddingTop; + } + } else { + return 0; + } + } + + static getTotalPadding(item, horizontal) { + if (item.isFlexEnabled()) { + const flex = item._flex; + if (horizontal) { + return flex.paddingRight + flex.paddingLeft; + } else { + return flex.paddingTop + flex.paddingBottom; + } + } else { + return 0; + } + } + + static getMarginOffset(item, horizontal) { + const flexItem = item.flexItem; + if (flexItem) { + if (horizontal) { + return flexItem.marginLeft; + } else { + return flexItem.marginTop; + } + } else { + return 0; + } + } + + static getTotalMargin(item, horizontal) { + const flexItem = item.flexItem; + if (flexItem) { + if (horizontal) { + return flexItem.marginRight + flexItem.marginLeft; + } else { + return flexItem.marginTop + flexItem.marginBottom; + } + } else { + return 0; + } + } + +} + +class SizeShrinker { + + constructor(line) { + this._line = line; + this._amountRemaining = 0; + this._shrunkSize = 0; + } + + shrink(amount) { + this._shrunkSize = 0; + + this._amountRemaining = amount; + let totalShrinkAmount = this._getTotalShrinkAmount(); + if (totalShrinkAmount) { + const items = this._line.items; + do { + let amountPerShrink = this._amountRemaining / totalShrinkAmount; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + const shrinkAmount = flexItem.shrink; + const isShrinkableItem = (shrinkAmount > 0); + if (isShrinkableItem) { + let shrink = shrinkAmount * amountPerShrink; + const minSize = flexItem._getMainAxisMinSize(); + const size = flexItem._getMainAxisLayoutSize(); + if (size > minSize) { + const maxShrink = size - minSize; + const isFullyShrunk = (shrink >= maxShrink); + if (isFullyShrunk) { + shrink = maxShrink; + + // Destribute remaining amount over the other flex items. + totalShrinkAmount -= shrinkAmount; + } + + const finalSize = size - shrink; + flexItem._resizeMainAxis(finalSize); + + this._shrunkSize += shrink; + this._amountRemaining -= shrink; + + if (Math.abs(this._amountRemaining) < 10e-6) { + return; + } + } + } + } + } while(totalShrinkAmount && (Math.abs(this._amountRemaining) > 10e-6)); + } + } + + _getTotalShrinkAmount() { + let total = 0; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + + if (flexItem.shrink) { + const minSize = flexItem._getMainAxisMinSize(); + const size = flexItem._getMainAxisLayoutSize(); + + // Exclude those already fully shrunk. + if (size > minSize) { + total += flexItem.shrink; + } + } + } + return total; + } + + getShrunkSize() { + return this._shrunkSize; + } + +} + +class SizeGrower { + + constructor(line) { + this._line = line; + this._amountRemaining = 0; + this._grownSize = 0; + } + + grow(amount) { + this._grownSize = 0; + + this._amountRemaining = amount; + let totalGrowAmount = this._getTotalGrowAmount(); + if (totalGrowAmount) { + const items = this._line.items; + do { + let amountPerGrow = this._amountRemaining / totalGrowAmount; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + const growAmount = flexItem.grow; + const isGrowableItem = (growAmount > 0); + if (isGrowableItem) { + let grow = growAmount * amountPerGrow; + const maxSize = flexItem._getMainAxisMaxSizeSetting(); + const size = flexItem._getMainAxisLayoutSize(); + if (maxSize > 0) { + if (size >= maxSize) { + // Already fully grown. + grow = 0; + } else { + const maxGrow = maxSize - size; + const isFullyGrown = (grow >= maxGrow); + if (isFullyGrown) { + grow = maxGrow; + + // Destribute remaining amount over the other flex items. + totalGrowAmount -= growAmount; + } + } + } + + if (grow > 0) { + const finalSize = size + grow; + flexItem._resizeMainAxis(finalSize); + + this._grownSize += grow; + this._amountRemaining -= grow; + + if (Math.abs(this._amountRemaining) < 10e-6) { + return; + } + } + } + } + } while(totalGrowAmount && (Math.abs(this._amountRemaining) > 10e-6)); + } + } + + _getTotalGrowAmount() { + let total = 0; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + const flexItem = item.flexItem; + + if (flexItem.grow) { + const maxSize = flexItem._getMainAxisMaxSizeSetting(); + const size = flexItem._getMainAxisLayoutSize(); + + // Exclude those already fully grown. + if (maxSize === 0 || size < maxSize) { + total += flexItem.grow; + } + } + } + return total; + } + + getGrownSize() { + return this._grownSize; + } + +} + +class ItemPositioner { + + constructor(lineLayout) { + this._line = lineLayout; + } + + get _layout() { + return this._line._layout; + } + + position() { + const {spacingBefore, spacingBetween} = this._getSpacing(); + + let currentPos = spacingBefore; + + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + + item.flexItem._setMainAxisLayoutPos(currentPos); + currentPos += item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin(); + currentPos += spacingBetween; + } + } + + _getSpacing() { + const remainingSpace = this._line._availableSpace; + let mode = this._layout._flexContainer.justifyContent; + const numberOfItems = this._line.numberOfItems; + + return SpacingCalculator.getSpacing(mode, numberOfItems, remainingSpace); + } + +} + +class ItemAligner { + + constructor(line) { + this._line = line; + this._crossAxisLayoutSize = 0; + this._crossAxisLayoutOffset = 0; + this._alignItemsSetting = null; + this._recursiveResizeOccured = false; + this._isCrossAxisFitToContents = false; + } + + get _layout() { + return this._line._layout; + } + + get _flexContainer() { + return this._layout._flexContainer; + } + + setCrossAxisLayoutSize(size) { + this._crossAxisLayoutSize = size; + } + + setCrossAxisLayoutOffset(offset) { + this._crossAxisLayoutOffset = offset; + } + + align() { + this._alignItemsSetting = this._flexContainer.alignItems; + + this._isCrossAxisFitToContents = this._layout.isAxisFitToContents(!this._flexContainer._horizontal); + + this._recursiveResizeOccured = false; + const items = this._line.items; + for (let i = this._line.startIndex; i <= this._line.endIndex; i++) { + const item = items[i]; + this._alignItem(item); + } + } + + get recursiveResizeOccured() { + return this._recursiveResizeOccured; + } + + _alignItem(item) { + const flexItem = item.flexItem; + let align = flexItem.alignSelf || this._alignItemsSetting; + + if (align === "stretch" && this._preventStretch(flexItem)) { + align = "flex-start"; + } + + if (align !== "stretch" && !this._isCrossAxisFitToContents) { + if (flexItem._hasRelCrossAxisSize()) { + // As cross axis size might have changed, we need to recalc the relative flex item's size. + flexItem._resetCrossAxisLayoutSize(); + } + } + + switch(align) { + case "flex-start": + this._alignItemFlexStart(flexItem); + break; + case "flex-end": + this._alignItemFlexEnd(flexItem); + break; + case "center": + this._alignItemFlexCenter(flexItem); + break; + case "stretch": + this._alignItemStretch(flexItem); + break; + } + } + + _alignItemFlexStart(flexItem) { + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset); + } + + _alignItemFlexEnd(flexItem) { + const itemCrossAxisSize = flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin(); + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset + (this._crossAxisLayoutSize - itemCrossAxisSize)); + } + + _alignItemFlexCenter(flexItem) { + const itemCrossAxisSize = flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin(); + const center = (this._crossAxisLayoutSize - itemCrossAxisSize) / 2; + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset + center); + } + + _alignItemStretch(flexItem) { + flexItem._setCrossAxisLayoutPos(this._crossAxisLayoutOffset); + + const mainAxisLayoutSizeBeforeResize = flexItem._getMainAxisLayoutSize(); + let size = this._crossAxisLayoutSize - flexItem._getCrossAxisMargin() - flexItem._getCrossAxisPadding(); + + const crossAxisMinSizeSetting = flexItem._getCrossAxisMinSizeSetting(); + if (crossAxisMinSizeSetting > 0) { + size = Math.max(size, crossAxisMinSizeSetting); + } + + const crossAxisMaxSizeSetting = flexItem._getCrossAxisMaxSizeSetting(); + const crossAxisMaxSizeSettingEnabled = (crossAxisMaxSizeSetting > 0); + if (crossAxisMaxSizeSettingEnabled) { + size = Math.min(size, crossAxisMaxSizeSetting); + } + + flexItem._resizeCrossAxis(size); + const mainAxisLayoutSizeAfterResize = flexItem._getMainAxisLayoutSize(); + + const recursiveResize = (mainAxisLayoutSizeAfterResize !== mainAxisLayoutSizeBeforeResize); + if (recursiveResize) { + // Recursive resize can happen when this flex item has the opposite direction than the container + // and is wrapping and auto-sizing. Due to item/content stretching the main axis size of the flex + // item may decrease. If it does so, we must re-justify-content the complete line. + // Notice that we don't account for changes to the (if autosized) main axis size caused by recursive + // resize, which may cause the container's main axis to not shrink to the contents properly. + // This is by design, because if we had re-run the main axis layout, we could run into issues such + // as slow layout or endless loops. + this._recursiveResizeOccured = true; + } + } + + _preventStretch(flexItem) { + const hasFixedCrossAxisSize = flexItem._hasFixedCrossAxisSize(); + const forceStretch = (flexItem.alignSelf === "stretch"); + return hasFixedCrossAxisSize && !forceStretch; + } + +} + +class LineLayout { + + constructor(layout, startIndex, endIndex, availableSpace) { + this._layout = layout; + this.items = layout.items; + this.startIndex = startIndex; + this.endIndex = endIndex; + this._availableSpace = availableSpace; + } + + performLayout() { + this._setItemSizes(); + this.setItemPositions(); + this._calcLayoutInfo(); + } + + _setItemSizes() { + if (this._availableSpace > 0) { + this._growItemSizes(this._availableSpace); + } else if (this._availableSpace < 0) { + this._shrinkItemSizes(-this._availableSpace); + } + } + + _growItemSizes(amount) { + const grower = new SizeGrower(this); + grower.grow(amount); + this._availableSpace -= grower.getGrownSize(); + } + + _shrinkItemSizes(amount) { + const shrinker = new SizeShrinker(this); + shrinker.shrink(amount); + this._availableSpace += shrinker.getShrunkSize(); + } + + setItemPositions() { + const positioner = new ItemPositioner(this); + positioner.position(); + } + + createItemAligner() { + return new ItemAligner(this); + } + + _calcLayoutInfo() { + this._calcCrossAxisMaxLayoutSize(); + } + + getMainAxisMinSize() { + let mainAxisMinSize = 0; + for (let i = this.startIndex; i <= this.endIndex; i++) { + const item = this.items[i]; + mainAxisMinSize += item.flexItem._getMainAxisMinSizeWithPaddingAndMargin(); + } + return mainAxisMinSize; + } + + get numberOfItems() { + return this.endIndex - this.startIndex + 1; + } + + get crossAxisLayoutSize() { + const noSpecifiedCrossAxisSize = (this._layout.isCrossAxisFitToContents() && !this._layout.resizingCrossAxis); + const shouldFitToContents = (this._layout.isWrapping() || noSpecifiedCrossAxisSize); + if (shouldFitToContents) { + return this._crossAxisMaxLayoutSize; + } else { + return this._layout.crossAxisSize; + } + } + + _calcCrossAxisMaxLayoutSize() { + this._crossAxisMaxLayoutSize = this._getCrossAxisMaxLayoutSize(); + } + + _getCrossAxisMaxLayoutSize() { + let crossAxisMaxSize = 0; + for (let i = this.startIndex; i <= this.endIndex; i++) { + const item = this.items[i]; + crossAxisMaxSize = Math.max(crossAxisMaxSize, item.flexItem._getCrossAxisLayoutSizeWithPaddingAndMargin()); + } + return crossAxisMaxSize; + } + + +} + +/** + * Distributes items over layout lines. + */ +class LineLayouter { + + constructor(layout) { + this._layout = layout; + this._mainAxisMinSize = -1; + this._crossAxisMinSize = -1; + this._mainAxisContentSize = 0; + } + + get lines() { + return this._lines; + } + + get mainAxisMinSize() { + if (this._mainAxisMinSize === -1) { + this._mainAxisMinSize = this._getMainAxisMinSize(); + } + return this._mainAxisMinSize; + } + + get crossAxisMinSize() { + if (this._crossAxisMinSize === -1) { + this._crossAxisMinSize = this._getCrossAxisMinSize(); + } + return this._crossAxisMinSize; + } + + get mainAxisContentSize() { + return this._mainAxisContentSize; + } + + layoutLines() { + this._setup(); + const items = this._layout.items; + const wrap = this._layout.isWrapping(); + + let startIndex = 0; + let i; + const n = items.length; + for (i = 0; i < n; i++) { + const item = items[i]; + + this._layoutFlexItem(item); + + // Get predicted main axis size. + const itemMainAxisSize = item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin(); + + if (wrap && (i > startIndex)) { + const isOverflowing = (this._curMainAxisPos + itemMainAxisSize > this._mainAxisSize); + if (isOverflowing) { + this._layoutLine(startIndex, i - 1); + this._curMainAxisPos = 0; + startIndex = i; + } + } + + this._addToMainAxisPos(itemMainAxisSize); + } + + if (startIndex < i) { + this._layoutLine(startIndex, i - 1); + } + } + + _layoutFlexItem(item) { + if (item.isFlexEnabled()) { + item.flexLayout.updateTreeLayout(); + } else { + item.flexItem._resetLayoutSize(); + } + } + + _setup() { + this._mainAxisSize = this._layout.mainAxisSize; + this._curMainAxisPos = 0; + this._maxMainAxisPos = 0; + this._lines = []; + + this._mainAxisMinSize = -1; + this._crossAxisMinSize = -1; + this._mainAxisContentSize = 0; + } + + _addToMainAxisPos(itemMainAxisSize) { + this._curMainAxisPos += itemMainAxisSize; + if (this._curMainAxisPos > this._maxMainAxisPos) { + this._maxMainAxisPos = this._curMainAxisPos; + } + } + + _layoutLine(startIndex, endIndex) { + const availableSpace = this._getAvailableMainAxisLayoutSpace(); + const line = new LineLayout(this._layout, startIndex, endIndex, availableSpace); + line.performLayout(); + this._lines.push(line); + + if (this._mainAxisContentSize === 0 || (this._curMainAxisPos > this._mainAxisContentSize)) { + this._mainAxisContentSize = this._curMainAxisPos; + } + } + + _getAvailableMainAxisLayoutSpace() { + if (!this._layout.resizingMainAxis && this._layout.isMainAxisFitToContents()) { + return 0; + } else { + return this._mainAxisSize - this._curMainAxisPos; + } + } + + _getCrossAxisMinSize() { + let crossAxisMinSize = 0; + const items = this._layout.items; + for (let i = 0, n = items.length; i < n; i++) { + const item = items[i]; + const itemCrossAxisMinSize = item.flexItem._getCrossAxisMinSizeWithPaddingAndMargin(); + crossAxisMinSize = Math.max(crossAxisMinSize, itemCrossAxisMinSize); + } + return crossAxisMinSize; + } + + _getMainAxisMinSize() { + if (this._lines.length === 1) { + return this._lines[0].getMainAxisMinSize(); + } else { + // Wrapping lines: specified width is used as min width (in accordance to W3C flexbox). + return this._layout.mainAxisSize; + } + } + +} + +class ItemCoordinatesUpdater { + + constructor(layout) { + this._layout = layout; + this._isReverse = this._flexContainer._reverse; + this._horizontalPaddingOffset = this._layout._getHorizontalPaddingOffset(); + this._verticalPaddingOffset = this._layout._getVerticalPaddingOffset(); + } + + get _flexContainer() { + return this._layout._flexContainer; + } + + finalize() { + const parentFlex = this._layout.getParentFlexContainer(); + if (parentFlex) { + // We must update it from the parent to set padding offsets and reverse position. + const updater = new ItemCoordinatesUpdater(parentFlex._layout); + updater._finalizeItemAndChildren(this._flexContainer.item); + } else { + this._finalizeRoot(); + this._finalizeItems(); + } + } + + _finalizeRoot() { + const item = this._flexContainer.item; + let x = FlexUtils.getAxisLayoutPos(item, true); + let y = FlexUtils.getAxisLayoutPos(item, false); + let w = FlexUtils.getAxisLayoutSize(item, true); + let h = FlexUtils.getAxisLayoutSize(item, false); + + w += this._layout._getHorizontalPadding(); + h += this._layout._getVerticalPadding(); + + item.clearRecalcFlag(); + + item.setLayout(x, y, w, h); + } + + _finalizeItems() { + const items = this._layout.items; + for (let i = 0, n = items.length; i < n; i++) { + const item = items[i]; + const validCache = this._validateItemCache(item); + + // Notice that we must also finalize a cached items, as it's coordinates may have changed. + this._finalizeItem(item); + + if (!validCache) { + this._finalizeItemChildren(item); + } + } + } + + _validateItemCache(item) { + if (item.recalc === 0) { + if (item.isFlexEnabled()) { + const layout = item._flex._layout; + + const dimensionsMatchPreviousResult = (item.w === item.target.w && item.h === item.target.h); + if (dimensionsMatchPreviousResult) { + // Cache is valid. + return true; + } else { + const crossAxisSize = layout.crossAxisSize; + layout.performResizeMainAxis(layout.mainAxisSize); + layout.performResizeCrossAxis(crossAxisSize); + } + } + } + return false; + } + + _finalizeItemAndChildren(item) { + this._finalizeItem(item); + this._finalizeItemChildren(item); + } + + _finalizeItem(item) { + if (this._isReverse) { + this._reverseMainAxisLayoutPos(item); + } + + let x = FlexUtils.getAxisLayoutPos(item, true); + let y = FlexUtils.getAxisLayoutPos(item, false); + let w = FlexUtils.getAxisLayoutSize(item, true); + let h = FlexUtils.getAxisLayoutSize(item, false); + + x += this._horizontalPaddingOffset; + y += this._verticalPaddingOffset; + + const flex = item.flex; + if (flex) { + w += item._flex._layout._getHorizontalPadding(); + h += item._flex._layout._getVerticalPadding(); + } + + const flexItem = item.flexItem; + if (flexItem) { + x += flexItem._getHorizontalMarginOffset(); + y += flexItem._getVerticalMarginOffset(); + } + + item.clearRecalcFlag(); + item.setLayout(x, y, w, h); + } + + _finalizeItemChildren(item) { + const flex = item._flex; + if (flex) { + const updater = new ItemCoordinatesUpdater(flex._layout); + updater._finalizeItems(); + } + } + + _reverseMainAxisLayoutPos(item) { + const endPos = (item.flexItem._getMainAxisLayoutPos() + item.flexItem._getMainAxisLayoutSizeWithPaddingAndMargin()); + const reversedPos = this._layout.mainAxisSize - endPos; + item.flexItem._setMainAxisLayoutPos(reversedPos); + } + +} + +/** + * Layouts a flex container (and descendants). + */ +class FlexLayout { + + constructor(flexContainer) { + this._flexContainer = flexContainer; + + this._lineLayouter = new LineLayouter(this); + + this._resizingMainAxis = false; + this._resizingCrossAxis = false; + + this._cachedMainAxisSizeAfterLayout = 0; + this._cachedCrossAxisSizeAfterLayout = 0; + + this._shrunk = false; + } + + get shrunk() { + return this._shrunk; + } + + get recalc() { + return this.item.recalc; + } + + layoutTree() { + const isSubTree = (this.item.flexParent !== null); + if (isSubTree) { + // Use the dimensions set by the parent flex tree. + this._updateSubTreeLayout(); + } else { + this.updateTreeLayout(); + } + this.updateItemCoords(); + } + + updateTreeLayout() { + if (this.recalc) { + this._performUpdateLayoutTree(); + } else { + this._performUpdateLayoutTreeFromCache(); + } + } + + _performUpdateLayoutTree() { + this._setInitialAxisSizes(); + this._layoutAxes(); + this._refreshLayoutCache(); + } + + _refreshLayoutCache() { + this._cachedMainAxisSizeAfterLayout = this.mainAxisSize; + this._cachedCrossAxisSizeAfterLayout = this.crossAxisSize; + } + + _performUpdateLayoutTreeFromCache() { + const sizeMightHaveChanged = (this.item.funcW || this.item.funcH); + if (sizeMightHaveChanged) { + // Update after all. + this.item.enableLocalRecalcFlag(); + this._performUpdateLayoutTree(); + } else { + this.mainAxisSize = this._cachedMainAxisSizeAfterLayout; + this.crossAxisSize = this._cachedCrossAxisSizeAfterLayout; + } + } + + updateItemCoords() { + const updater = new ItemCoordinatesUpdater(this); + updater.finalize(); + } + + _updateSubTreeLayout() { + // The dimensions of this container are guaranteed not to have changed. + // That's why we can safely 'reuse' those and re-layout the contents. + const crossAxisSize = this.crossAxisSize; + this._layoutMainAxis(); + this.performResizeCrossAxis(crossAxisSize); + } + + _setInitialAxisSizes() { + if (this.item.isFlexItemEnabled()) { + this.item.flexItem._resetLayoutSize(); + } else { + this.mainAxisSize = this._getMainAxisBasis(); + this.crossAxisSize = this._getCrossAxisBasis(); + } + this._resizingMainAxis = false; + this._resizingCrossAxis = false; + this._shrunk = false; + } + + _layoutAxes() { + this._layoutMainAxis(); + this._layoutCrossAxis(); + } + + /** + * @pre mainAxisSize should exclude padding. + */ + _layoutMainAxis() { + this._layoutLines(); + this._fitMainAxisSizeToContents(); + } + + _layoutLines() { + this._lineLayouter.layoutLines(); + } + + get _lines() { + return this._lineLayouter.lines; + } + + _fitMainAxisSizeToContents() { + if (!this._resizingMainAxis) { + if (this.isMainAxisFitToContents()) { + this.mainAxisSize = this._lineLayouter.mainAxisContentSize; + } + } + } + + /** + * @pre crossAxisSize should exclude padding. + */ + _layoutCrossAxis() { + const aligner = new ContentAligner(this); + aligner.init(); + this._totalCrossAxisSize = aligner.totalCrossAxisSize; + this._fitCrossAxisSizeToContents(); + aligner.align(); + } + + _fitCrossAxisSizeToContents() { + if (!this._resizingCrossAxis) { + if (this.isCrossAxisFitToContents()) { + this.crossAxisSize = this._totalCrossAxisSize; + } + } + } + + isWrapping() { + return this._flexContainer.wrap; + } + + isAxisFitToContents(horizontal) { + if (this._horizontal === horizontal) { + return this.isMainAxisFitToContents(); + } else { + return this.isCrossAxisFitToContents(); + } + } + + isMainAxisFitToContents() { + return !this.isWrapping() && !this._hasFixedMainAxisBasis(); + } + + isCrossAxisFitToContents() { + return !this._hasFixedCrossAxisBasis(); + } + + _hasFixedMainAxisBasis() { + return !FlexUtils.isZeroAxisSize(this.item, this._horizontal); + } + + _hasFixedCrossAxisBasis() { + return !FlexUtils.isZeroAxisSize(this.item, !this._horizontal); + } + + getAxisMinSize(horizontal) { + if (this._horizontal === horizontal) { + return this._getMainAxisMinSize(); + } else { + return this._getCrossAxisMinSize(); + } + } + + _getMainAxisMinSize() { + return this._lineLayouter.mainAxisMinSize; + } + + _getCrossAxisMinSize() { + return this._lineLayouter.crossAxisMinSize; + } + + resizeMainAxis(size) { + if (this.mainAxisSize !== size) { + if (this.recalc > 0) { + this.performResizeMainAxis(size); + } else { + if (this._checkValidCacheMainAxisResize()) { + this.mainAxisSize = size; + this._fitCrossAxisSizeToContents(); + } else { + // Cache miss. + this.item.enableLocalRecalcFlag(); + this.performResizeMainAxis(size); + } + } + } + } + + _checkValidCacheMainAxisResize(size) { + const isFinalMainAxisSize = (size === this.targetMainAxisSize); + if (isFinalMainAxisSize) { + return true; + } + const canIgnoreCacheMiss = !this.isCrossAxisFitToContents(); + if (canIgnoreCacheMiss) { + // Allow other main axis resizes and check if final resize matches the target main axis size + // (ItemCoordinatesUpdater). + return true; + } + return false; + } + + performResizeMainAxis(size) { + const isShrinking = (size < this.mainAxisSize); + this._shrunk = isShrinking; + + this.mainAxisSize = size; + + this._resizingMainAxis = true; + this._layoutAxes(); + this._resizingMainAxis = false; + } + + resizeCrossAxis(size) { + if (this.crossAxisSize !== size) { + if (this.recalc > 0) { + this.performResizeCrossAxis(size); + } else { + this.crossAxisSize = size; + } + } + } + + performResizeCrossAxis(size) { + this.crossAxisSize = size; + + this._resizingCrossAxis = true; + this._layoutCrossAxis(); + this._resizingCrossAxis = false; + } + + get targetMainAxisSize() { + return this._horizontal ? this.item.target.w : this.item.target.h; + } + + get targetCrossAxisSize() { + return this._horizontal ? this.item.target.h : this.item.target.w; + } + + getParentFlexContainer() { + return this.item.isFlexItemEnabled() ? this.item.flexItem.ctr : null; + } + + _getHorizontalPadding() { + return FlexUtils.getTotalPadding(this.item, true); + } + + _getVerticalPadding() { + return FlexUtils.getTotalPadding(this.item, false); + } + + _getHorizontalPaddingOffset() { + return FlexUtils.getPaddingOffset(this.item, true); + } + + _getVerticalPaddingOffset() { + return FlexUtils.getPaddingOffset(this.item, false); + } + + _getMainAxisBasis() { + return FlexUtils.getRelAxisSize(this.item, this._horizontal); + } + + _getCrossAxisBasis() { + return FlexUtils.getRelAxisSize(this.item, !this._horizontal); + } + + get _horizontal() { + return this._flexContainer._horizontal; + } + + get _reverse() { + return this._flexContainer._reverse; + } + + get item() { + return this._flexContainer.item; + } + + get items() { + return this.item.items; + } + + get resizingMainAxis() { + return this._resizingMainAxis; + } + + get resizingCrossAxis() { + return this._resizingCrossAxis; + } + + get numberOfItems() { + return this.items.length; + } + + get mainAxisSize() { + return FlexUtils.getAxisLayoutSize(this.item, this._horizontal); + } + + get crossAxisSize() { + return FlexUtils.getAxisLayoutSize(this.item, !this._horizontal); + } + + set mainAxisSize(v) { + FlexUtils.setAxisLayoutSize(this.item, this._horizontal, v); + } + + set crossAxisSize(v) { + FlexUtils.setAxisLayoutSize(this.item, !this._horizontal, v); + } + +} + +class FlexContainer { + + + constructor(item) { + this._item = item; + + this._layout = new FlexLayout(this); + this._horizontal = true; + this._reverse = false; + this._wrap = false; + this._alignItems = 'stretch'; + this._justifyContent = 'flex-start'; + this._alignContent = 'flex-start'; + + this._paddingLeft = 0; + this._paddingTop = 0; + this._paddingRight = 0; + this._paddingBottom = 0; + } + + get item() { + return this._item; + } + + _changedDimensions() { + this._item.changedDimensions(); + } + + _changedContents() { + this._item.changedContents(); + } + + get direction() { + return (this._horizontal ? "row" : "column") + (this._reverse ? "-reverse" : ""); + } + + set direction(f) { + if (this.direction === f) return; + + this._horizontal = (f === 'row' || f === 'row-reverse'); + this._reverse = (f === 'row-reverse' || f === 'column-reverse'); + + this._changedContents(); + } + + set wrap(v) { + this._wrap = v; + this._changedContents(); + } + + get wrap() { + return this._wrap; + } + + get alignItems() { + return this._alignItems; + } + + set alignItems(v) { + if (this._alignItems === v) return; + if (FlexContainer.ALIGN_ITEMS.indexOf(v) === -1) { + throw new Error("Unknown alignItems, options: " + FlexContainer.ALIGN_ITEMS.join(",")); + } + this._alignItems = v; + + this._changedContents(); + } + + get alignContent() { + return this._alignContent; + } + + set alignContent(v) { + if (this._alignContent === v) return; + if (FlexContainer.ALIGN_CONTENT.indexOf(v) === -1) { + throw new Error("Unknown alignContent, options: " + FlexContainer.ALIGN_CONTENT.join(",")); + } + this._alignContent = v; + + this._changedContents(); + } + + get justifyContent() { + return this._justifyContent; + } + + set justifyContent(v) { + if (this._justifyContent === v) return; + + if (FlexContainer.JUSTIFY_CONTENT.indexOf(v) === -1) { + throw new Error("Unknown justifyContent, options: " + FlexContainer.JUSTIFY_CONTENT.join(",")); + } + this._justifyContent = v; + + this._changedContents(); + } + + set padding(v) { + this.paddingLeft = v; + this.paddingTop = v; + this.paddingRight = v; + this.paddingBottom = v; + } + + get padding() { + return this.paddingLeft; + } + + set paddingLeft(v) { + this._paddingLeft = v; + this._changedDimensions(); + } + + get paddingLeft() { + return this._paddingLeft; + } + + set paddingTop(v) { + this._paddingTop = v; + this._changedDimensions(); + } + + get paddingTop() { + return this._paddingTop; + } + + set paddingRight(v) { + this._paddingRight = v; + this._changedDimensions(); + } + + get paddingRight() { + return this._paddingRight; + } + + set paddingBottom(v) { + this._paddingBottom = v; + this._changedDimensions(); + } + + get paddingBottom() { + return this._paddingBottom; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +FlexContainer.ALIGN_ITEMS = ["flex-start", "flex-end", "center", "stretch"]; +FlexContainer.ALIGN_CONTENT = ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly", "stretch"]; +FlexContainer.JUSTIFY_CONTENT = ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]; + +class FlexItem { + + constructor(item) { + this._ctr = null; + this._item = item; + this._grow = 0; + this._shrink = FlexItem.SHRINK_AUTO; + this._alignSelf = undefined; + this._minWidth = 0; + this._minHeight = 0; + this._maxWidth = 0; + this._maxHeight = 0; + + this._marginLeft = 0; + this._marginTop = 0; + this._marginRight = 0; + this._marginBottom = 0; + } + + get item() { + return this._item; + } + + get grow() { + return this._grow; + } + + set grow(v) { + if (this._grow === v) return; + + this._grow = parseInt(v) || 0; + + this._changed(); + } + + get shrink() { + if (this._shrink === FlexItem.SHRINK_AUTO) { + return this._getDefaultShrink(); + } + return this._shrink; + } + + _getDefaultShrink() { + if (this.item.isFlexEnabled()) { + return 1; + } else { + // All non-flex containers are absolutely positioned items with fixed dimensions, and by default not shrinkable. + return 0; + } + } + + set shrink(v) { + if (this._shrink === v) return; + + this._shrink = parseInt(v) || 0; + + this._changed(); + } + + get alignSelf() { + return this._alignSelf; + } + + set alignSelf(v) { + if (this._alignSelf === v) return; + + if (v === undefined) { + this._alignSelf = undefined; + } else { + if (FlexContainer.ALIGN_ITEMS.indexOf(v) === -1) { + throw new Error("Unknown alignSelf, options: " + FlexContainer.ALIGN_ITEMS.join(",")); + } + this._alignSelf = v; + } + + this._changed(); + } + + get minWidth() { + return this._minWidth; + } + + set minWidth(v) { + this._minWidth = Math.max(0, v); + this._item.changedDimensions(true, false); + } + + get minHeight() { + return this._minHeight; + } + + set minHeight(v) { + this._minHeight = Math.max(0, v); + this._item.changedDimensions(false, true); + } + + get maxWidth() { + return this._maxWidth; + } + + set maxWidth(v) { + this._maxWidth = Math.max(0, v); + this._item.changedDimensions(true, false); + } + + get maxHeight() { + return this._maxHeight; + } + + set maxHeight(v) { + this._maxHeight = Math.max(0, v); + this._item.changedDimensions(false, true); + } + + /** + * @note margins behave slightly different than in HTML with regard to shrinking. + * In HTML, (outer) margins can be removed when shrinking. In this engine, they will not shrink at all. + */ + set margin(v) { + this.marginLeft = v; + this.marginTop = v; + this.marginRight = v; + this.marginBottom = v; + } + + get margin() { + return this.marginLeft; + } + + set marginLeft(v) { + this._marginLeft = v; + this._changed(); + } + + get marginLeft() { + return this._marginLeft; + } + + set marginTop(v) { + this._marginTop = v; + this._changed(); + } + + get marginTop() { + return this._marginTop; + } + + set marginRight(v) { + this._marginRight = v; + this._changed(); + } + + get marginRight() { + return this._marginRight; + } + + set marginBottom(v) { + this._marginBottom = v; + this._changed(); + } + + get marginBottom() { + return this._marginBottom; + } + + _changed() { + if (this.ctr) this.ctr._changedContents(); + } + + set ctr(v) { + this._ctr = v; + } + + get ctr() { + return this._ctr; + } + + patch(settings) { + Base.patchObject(this, settings); + } + + _resetLayoutSize() { + this._resetHorizontalAxisLayoutSize(); + this._resetVerticalAxisLayoutSize(); + } + + _resetCrossAxisLayoutSize() { + if (this.ctr._horizontal) { + this._resetVerticalAxisLayoutSize(); + } else { + this._resetHorizontalAxisLayoutSize(); + } + } + + _resetHorizontalAxisLayoutSize() { + let w = FlexUtils.getRelAxisSize(this.item, true); + if (this._minWidth) { + w = Math.max(this._minWidth, w); + } + if (this._maxWidth) { + w = Math.min(this._maxWidth, w); + } + FlexUtils.setAxisLayoutSize(this.item, true, w); + } + + _resetVerticalAxisLayoutSize() { + let h = FlexUtils.getRelAxisSize(this.item, false); + if (this._minHeight) { + h = Math.max(this._minHeight, h); + } + if (this._maxHeight) { + h = Math.min(this._maxHeight, h); + } + FlexUtils.setAxisLayoutSize(this.item, false, h); + } + + _getCrossAxisMinSizeSetting() { + return this._getMinSizeSetting(!this.ctr._horizontal); + } + + _getCrossAxisMaxSizeSetting() { + return this._getMaxSizeSetting(!this.ctr._horizontal); + } + + _getMainAxisMaxSizeSetting() { + return this._getMaxSizeSetting(this.ctr._horizontal); + } + + _getMinSizeSetting(horizontal) { + if (horizontal) { + return this._minWidth; + } else { + return this._minHeight; + } + } + + _getMaxSizeSetting(horizontal) { + if (horizontal) { + return this._maxWidth; + } else { + return this._maxHeight; + } + } + + _getMainAxisMinSize() { + return FlexUtils.getAxisMinSize(this.item, this.ctr._horizontal); + } + + _getCrossAxisMinSize() { + return FlexUtils.getAxisMinSize(this.item, !this.ctr._horizontal); + } + + _getMainAxisLayoutSize() { + return FlexUtils.getAxisLayoutSize(this.item, this.ctr._horizontal); + } + + _getMainAxisLayoutPos() { + return FlexUtils.getAxisLayoutPos(this.item, this.ctr._horizontal); + } + + _setMainAxisLayoutPos(pos) { + return FlexUtils.setAxisLayoutPos(this.item, this.ctr._horizontal, pos); + } + + _setCrossAxisLayoutPos(pos) { + return FlexUtils.setAxisLayoutPos(this.item, !this.ctr._horizontal, pos); + } + + _getCrossAxisLayoutSize() { + return FlexUtils.getAxisLayoutSize(this.item, !this.ctr._horizontal); + } + + _resizeCrossAxis(size) { + return FlexUtils.resizeAxis(this.item, !this.ctr._horizontal, size); + } + + _resizeMainAxis(size) { + return FlexUtils.resizeAxis(this.item, this.ctr._horizontal, size); + } + + _getMainAxisPadding() { + return FlexUtils.getTotalPadding(this.item, this.ctr._horizontal); + } + + _getCrossAxisPadding() { + return FlexUtils.getTotalPadding(this.item, !this.ctr._horizontal); + } + + _getMainAxisMargin() { + return FlexUtils.getTotalMargin(this.item, this.ctr._horizontal); + } + + _getCrossAxisMargin() { + return FlexUtils.getTotalMargin(this.item, !this.ctr._horizontal); + } + + _getHorizontalMarginOffset() { + return FlexUtils.getMarginOffset(this.item, true); + } + + _getVerticalMarginOffset() { + return FlexUtils.getMarginOffset(this.item, false); + } + + _getMainAxisMinSizeWithPaddingAndMargin() { + return this._getMainAxisMinSize() + this._getMainAxisPadding() + this._getMainAxisMargin(); + } + + _getCrossAxisMinSizeWithPaddingAndMargin() { + return this._getCrossAxisMinSize() + this._getCrossAxisPadding() + this._getCrossAxisMargin(); + } + + _getMainAxisLayoutSizeWithPaddingAndMargin() { + return this._getMainAxisLayoutSize() + this._getMainAxisPadding() + this._getMainAxisMargin(); + } + + _getCrossAxisLayoutSizeWithPaddingAndMargin() { + return this._getCrossAxisLayoutSize() + this._getCrossAxisPadding() + this._getCrossAxisMargin(); + } + + _hasFixedCrossAxisSize() { + return !FlexUtils.isZeroAxisSize(this.item, !this.ctr._horizontal); + } + + _hasRelCrossAxisSize() { + return !!(this.ctr._horizontal ? this.item.funcH : this.item.funcW); + } + +} + + +FlexItem.SHRINK_AUTO = -1; + +/** + * This is the connection between the render tree with the layout tree of this flex container/item. + */ +class FlexTarget { + + constructor(target) { + this._target = target; + + /** + * Possible values (only in case of container): + * bit 0: has changed or contains items with changes + * bit 1: width changed + * bit 2: height changed + */ + this._recalc = 0; + + this._enabled = false; + + this.x = 0; + this.y = 0; + this.w = 0; + this.h = 0; + + this._originalX = 0; + this._originalY = 0; + this._originalWidth = 0; + this._originalHeight = 0; + + this._flex = null; + this._flexItem = null; + this._flexItemDisabled = false; + + this._items = null; + } + + get flexLayout() { + return this.flex ? this.flex._layout : null; + } + + layoutFlexTree() { + if (this.isFlexEnabled() && this.isChanged()) { + this.flexLayout.layoutTree(); + } + } + + get target() { + return this._target; + } + + get flex() { + return this._flex; + } + + set flex(v) { + if (!v) { + if (this.isFlexEnabled()) { + this._disableFlex(); + } + } else { + if (!this.isFlexEnabled()) { + this._enableFlex(); + } + this._flex.patch(v); + } + } + + get flexItem() { + if (this._flexItemDisabled) { + return false; + } + this._ensureFlexItem(); + return this._flexItem; + } + + set flexItem(v) { + if (v === false) { + if (!this._flexItemDisabled) { + const parent = this.flexParent; + this._flexItemDisabled = true; + this._checkEnabled(); + if (parent) { + parent._clearFlexItemsCache(); + parent.changedContents(); + } + } + } else { + this._ensureFlexItem(); + + this._flexItem.patch(v); + + if (this._flexItemDisabled) { + this._flexItemDisabled = false; + this._checkEnabled(); + const parent = this.flexParent; + if (parent) { + parent._clearFlexItemsCache(); + parent.changedContents(); + } + } + } + } + + _enableFlex() { + this._flex = new FlexContainer(this); + this._checkEnabled(); + this.changedDimensions(); + this._enableChildrenAsFlexItems(); + } + + _disableFlex() { + this.changedDimensions(); + this._flex = null; + this._checkEnabled(); + this._disableChildrenAsFlexItems(); + } + + _enableChildrenAsFlexItems() { + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + child.layout._enableFlexItem(); + } + } + } + + _disableChildrenAsFlexItems() { + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + child.layout._disableFlexItem(); + } + } + } + + _enableFlexItem() { + this._ensureFlexItem(); + const flexParent = this._target._parent._layout; + this._flexItem.ctr = flexParent._flex; + flexParent.changedContents(); + this._checkEnabled(); + } + + _disableFlexItem() { + if (this._flexItem) { + this._flexItem.ctr = null; + } + + // We keep the flexItem object because it may contain custom settings. + this._checkEnabled(); + + // Offsets have been changed. We can't recover them, so we'll just clear them instead. + this._resetOffsets(); + } + + _resetOffsets() { + this.x = 0; + this.y = 0; + } + + _ensureFlexItem() { + if (!this._flexItem) { + this._flexItem = new FlexItem(this); + } + } + + _checkEnabled() { + const enabled = this.isEnabled(); + if (this._enabled !== enabled) { + if (enabled) { + this._enable(); + } else { + this._disable(); + } + this._enabled = enabled; + } + } + + _enable() { + this._setupTargetForFlex(); + this._target.enableFlexLayout(); + } + + _disable() { + this._restoreTargetToNonFlex(); + this._target.disableFlexLayout(); + } + + isEnabled() { + return this.isFlexEnabled() || this.isFlexItemEnabled(); + } + + isFlexEnabled() { + return this._flex !== null; + } + + isFlexItemEnabled() { + return this.flexParent !== null; + } + + _restoreTargetToNonFlex() { + const target = this._target; + target.x = this._originalX; + target.y = this._originalY; + target.setDimensions(this._originalWidth, this._originalHeight); + } + + _setupTargetForFlex() { + const target = this._target; + this._originalX = target._x; + this._originalY = target._y; + this._originalWidth = target._w; + this._originalHeight = target._h; + } + + setParent(from, to) { + if (from && from.isFlexContainer()) { + from._layout._changedChildren(); + } + + if (to && to.isFlexContainer()) { + this._enableFlexItem(); + to._layout._changedChildren(); + } + this._checkEnabled(); + } + + get flexParent() { + if (this._flexItemDisabled) { + return null; + } + + const parent = this._target._parent; + if (parent && parent.isFlexContainer()) { + return parent._layout; + } + return null; + } + + setVisible(v) { + const parent = this.flexParent; + if (parent) { + parent._changedChildren(); + } + } + + get items() { + if (!this._items) { + this._items = this._getFlexItems(); + } + return this._items; + } + + _getFlexItems() { + const items = []; + const children = this._target._children; + if (children) { + for (let i = 0, n = children.length; i < n; i++) { + const item = children[i]; + if (item.visible) { + if (item.isFlexItem()) { + items.push(item.layout); + } + } + } + } + return items; + } + + _changedChildren() { + this._clearFlexItemsCache(); + this.changedContents(); + } + + _clearFlexItemsCache() { + this._items = null; + } + + setLayout(x, y, w, h) { + let originalX = this._originalX; + let originalY = this._originalY; + if (this.funcX) { + originalX = this.funcX(FlexUtils.getParentAxisSizeWithPadding(this, true)); + } + if (this.funcY) { + originalY = this.funcY(FlexUtils.getParentAxisSizeWithPadding(this, false)); + } + + if (this.isFlexItemEnabled()) { + this.target.setLayout(x + originalX, y + originalY, w, h); + } else { + // Reuse the x,y 'settings'. + this.target.setLayout(originalX, originalY, w, h); + } + } + + changedDimensions(changeWidth = true, changeHeight = true) { + this._updateRecalc(changeWidth, changeHeight); + } + + changedContents() { + this._updateRecalc(); + } + + forceLayout() { + this._updateRecalc(); + } + + isChanged() { + return this._recalc > 0; + } + + _updateRecalc(changeExternalWidth = false, changeExternalHeight = false) { + if (this.isFlexEnabled()) { + const layout = this._flex._layout; + + // When something internal changes, it can have effect on the external dimensions. + changeExternalWidth = changeExternalWidth || layout.isAxisFitToContents(true); + changeExternalHeight = changeExternalHeight || layout.isAxisFitToContents(false); + } + + const recalc = 1 + (changeExternalWidth ? 2 : 0) + (changeExternalHeight ? 4 : 0); + const newRecalcFlags = this.getNewRecalcFlags(recalc); + this._recalc |= recalc; + if (newRecalcFlags > 1) { + if (this.flexParent) { + this.flexParent._updateRecalcBottomUp(recalc); + } else { + this._target.triggerLayout(); + } + } else { + this._target.triggerLayout(); + } + } + + getNewRecalcFlags(flags) { + return (7 - this._recalc) & flags; + } + + _updateRecalcBottomUp(childRecalc) { + const newRecalc = this._getRecalcFromChangedChildRecalc(childRecalc); + const newRecalcFlags = this.getNewRecalcFlags(newRecalc); + this._recalc |= newRecalc; + if (newRecalcFlags > 1) { + const flexParent = this.flexParent; + if (flexParent) { + flexParent._updateRecalcBottomUp(newRecalc); + } else { + this._target.triggerLayout(); + } + } else { + this._target.triggerLayout(); + } + } + + _getRecalcFromChangedChildRecalc(childRecalc) { + const layout = this._flex._layout; + + const mainAxisRecalcFlag = layout._horizontal ? 1 : 2; + const crossAxisRecalcFlag = layout._horizontal ? 2 : 1; + + const crossAxisDimensionsChangedInChild = (childRecalc & crossAxisRecalcFlag); + if (!crossAxisDimensionsChangedInChild) { + const mainAxisDimensionsChangedInChild = (childRecalc & mainAxisRecalcFlag); + if (mainAxisDimensionsChangedInChild) { + const mainAxisIsWrapping = layout.isWrapping(); + if (mainAxisIsWrapping) { + const crossAxisIsFitToContents = layout.isCrossAxisFitToContents(); + if (crossAxisIsFitToContents) { + // Special case: due to wrapping, the cross axis size may be changed. + childRecalc += crossAxisRecalcFlag; + } + } + } + } + + let isWidthDynamic = layout.isAxisFitToContents(true); + let isHeightDynamic = layout.isAxisFitToContents(false); + + if (layout.shrunk) { + // If during previous layout this container was 'shrunk', any changes may change the 'min axis size' of the + // contents, leading to a different axis size on this container even when it was not 'fit to contents'. + if (layout._horizontal) { + isWidthDynamic = true; + } else { + isHeightDynamic = true; + } + } + + const localRecalc = 1 + (isWidthDynamic ? 2 : 0) + (isHeightDynamic ? 4 : 0); + + const combinedRecalc = childRecalc & localRecalc; + return combinedRecalc; + } + + get recalc() { + return this._recalc; + } + + clearRecalcFlag() { + this._recalc = 0; + } + + enableLocalRecalcFlag() { + this._recalc = 1; + } + + get originalX() { + return this._originalX; + } + + setOriginalXWithoutUpdatingLayout(v) { + this._originalX = v; + } + + get originalY() { + return this._originalY; + } + + setOriginalYWithoutUpdatingLayout(v) { + this._originalY = v; + } + + get originalWidth() { + return this._originalWidth; + } + + set originalWidth(v) { + if (this._originalWidth !== v) { + this._originalWidth = v; + this.changedDimensions(true, false); + } + } + + get originalHeight() { + return this._originalHeight; + } + + set originalHeight(v) { + if (this._originalHeight !== v) { + this._originalHeight = v; + this.changedDimensions(false, true); + } + } + + get funcX() { + return this._target.funcX; + } + + get funcY() { + return this._target.funcY; + } + + get funcW() { + return this._target.funcW; + } + + get funcH() { + return this._target.funcH; + } +} + +class TextureSource { + + constructor(manager, loader = null) { + this.id = TextureSource.id++; + + this.manager = manager; + + this.stage = manager.stage; + + /** + * All enabled textures (textures that are used by visible elements). + * @type {Set} + */ + this.textures = new Set(); + + /** + * The number of active textures (textures that have at least one active element). + * @type {number} + * @private + */ + this._activeTextureCount = 0; + + /** + * The factory for the source of this texture. + * @type {Function} + */ + this.loader = loader; + + /** + * Identifier for reuse. + * @type {String} + */ + this.lookupId = null; + + /** + * If set, this.is called when the texture source is no longer displayed (this.components.size becomes 0). + * @type {Function} + */ + this._cancelCb = null; + + /** + * Loading since timestamp in millis. + * @type {number} + */ + this.loadingSince = 0; + + this.w = 0; + this.h = 0; + + this._nativeTexture = null; + + /** + * If true, then this.texture source is never freed from memory during garbage collection. + * @type {boolean} + */ + this.permanent = false; + + /** + * Sub-object with texture-specific rendering information. + * For images, contains the src property, for texts, contains handy rendering information. + * @type {Object} + */ + this.renderInfo = null; + + /** + * Generated for 'renderToTexture'. + * @type {boolean} + * @private + */ + this._isResultTexture = !this.loader; + + /** + * Contains the load error, if the texture source could previously not be loaded. + * @type {object} + * @private + */ + this._loadError = null; + + /** + * Hold a reference to the javascript variable which contains the texture, this is not required for WebGL in WebBrowsers but is required for Spark runtime. + * @type {object} + * @private + */ + this._imageRef = null; + + } + + get loadError() { + return this._loadError; + } + + addTexture(v) { + if (!this.textures.has(v)) { + this.textures.add(v); + } + } + + removeTexture(v) { + this.textures.delete(v); + } + + incActiveTextureCount() { + this._activeTextureCount++; + if (this._activeTextureCount === 1) { + this.becomesUsed(); + } + } + + decActiveTextureCount() { + this._activeTextureCount--; + if (this._activeTextureCount === 0) { + this.becomesUnused(); + } + } + + get isResultTexture() { + return this._isResultTexture; + } + + set isResultTexture(v) { + this._isResultTexture = v; + } + + forEachEnabledElement(cb) { + this.textures.forEach(texture => { + texture.elements.forEach(cb); + }); + } + + hasEnabledElements() { + return this.textures.size > 0; + } + + forEachActiveElement(cb) { + this.textures.forEach(texture => { + texture.elements.forEach(element => { + if (element.active) { + cb(element); + } + }); + }); + } + + getRenderWidth() { + return this.w; + } + + getRenderHeight() { + return this.h; + } + + allowCleanup() { + return !this.permanent && !this.isUsed(); + } + + becomesUsed() { + // Even while the texture is being loaded, make sure it is on the lookup map so that others can reuse it. + this.load(); + } + + becomesUnused() { + this.cancel(); + } + + cancel() { + if (this.isLoading()) { + if (this._cancelCb) { + this._cancelCb(this); + + // Clear callback to avoid memory leaks. + this._cancelCb = null; + } + this.loadingSince = 0; + } + } + + isLoaded() { + return !!this._nativeTexture; + } + + isLoading() { + return (this.loadingSince > 0); + } + + isError() { + return !!this._loadError; + } + + reload() { + this.free(); + if (this.isUsed()) { + this.load(); + } + } + + load(forceSync = false) { + // From the moment of loading (when a texture source becomes used by active elements) + if (this.isResultTexture) { + // Element result texture source, for which the loading is managed by the core. + return; + } + + if (!this._nativeTexture && !this.isLoading()) { + this.loadingSince = (new Date()).getTime(); + this._cancelCb = this.loader((err, options) => { + // Ignore loads that come in after a cancel. + if (this.isLoading()) { + // Clear callback to avoid memory leaks. + this._cancelCb = null; + + if (this.manager.stage.destroyed) { + // Ignore async load when stage is destroyed. + return; + } + if (err) { + // Emit txError. + this.onError(err); + } else if (options && options.source) { + if (!this.stage.isUpdatingFrame() && !forceSync && (options.throttle !== false)) { + const textureThrottler = this.stage.textureThrottler; + this._cancelCb = textureThrottler.genericCancelCb; + textureThrottler.add(this, options); + } else { + this.processLoadedSource(options); + } + } + } + }, this); + } + } + + processLoadedSource(options) { + this.loadingSince = 0; + this.setSource(options); + } + + setSource(options) { + const source = options.source; + + this.w = source.width || (options && options.w) || 0; + this.h = source.height || (options && options.h) || 0; + + if (options && options.renderInfo) { + // Assign to id in cache so that it can be reused. + this.renderInfo = options.renderInfo; + } + + this.permanent = !!options.permanent; + + if (options && options.imageRef) + this._imageRef = options.imageRef; + if (options && options.flipTextureY) { + this._flipTextureY = options.flipTextureY; + } else { + this._flipTextureY = false; + } + + if (this._isNativeTexture(source)) { + // Texture managed by caller. + this._nativeTexture = source; + + this.w = this.w || source.w; + this.h = this.h || source.h; + + // WebGLTexture objects are by default; + this.permanent = options.hasOwnProperty('permanent') ? options.permanent : true; + } else { + this.manager.uploadTextureSource(this, options); + } + + // Must be cleared when reload is succesful. + this._loadError = null; + + this.onLoad(); + } + + isUsed() { + return this._activeTextureCount > 0; + } + + onLoad() { + if (this.isUsed()) { + this.textures.forEach(texture => { + texture.onLoad(); + }); + } + } + + forceRenderUpdate() { + // Userland should call this method after changing the nativeTexture manually outside of the framework + // (using tex[Sub]Image2d for example). + + if (this._nativeTexture) { + // Change 'update' flag. This is currently not used by the framework but is handy in userland. + this._nativeTexture.update = this.stage.frameCounter; + } + + this.forEachActiveElement(function (element) { + element.forceRenderUpdate(); + }); + + } + + forceUpdateRenderCoords() { + this.forEachActiveElement(function (element) { + element._updateTextureCoords(); + }); + } + + get nativeTexture() { + return this._nativeTexture; + } + + clearNativeTexture() { + this._nativeTexture = null; + //also clear the reference to the texture variable. + this._imageRef = null; + } + + /** + * Used for result textures. + */ + replaceNativeTexture(newNativeTexture, w, h) { + let prevNativeTexture = this._nativeTexture; + // Loaded by core. + this._nativeTexture = newNativeTexture; + this.w = w; + this.h = h; + + if (!prevNativeTexture && this._nativeTexture) { + this.forEachActiveElement(element => element.onTextureSourceLoaded()); + } + + if (!this._nativeTexture) { + this.forEachActiveElement(element => element._setDisplayedTexture(null)); + } + + // Dimensions must be updated also on enabled elements, as it may force it to go within bounds. + this.forEachEnabledElement(element => element._updateDimensions()); + + // Notice that the sprite map must never contain render textures. + } + + onError(e) { + this._loadError = e; + this.loadingSince = 0; + console.error('texture load error', e, this.lookupId); + this.forEachActiveElement(element => element.onTextureSourceLoadError(e)); + } + + free() { + if (this.isLoaded()) { + this.manager.freeTextureSource(this); + } + } + + _isNativeTexture(source) { + return ((Utils.isNode ? source.constructor.name === "WebGLTexture" : source instanceof WebGLTexture)); + } + +} + +TextureSource.prototype.isTextureSource = true; + +TextureSource.id = 1; + +class ElementTexturizer { + + constructor(elementCore) { + + this._element = elementCore.element; + this._core = elementCore; + + this.ctx = this._core.ctx; + + this._enabled = false; + this.lazy = false; + this._colorize = false; + + this._renderTexture = null; + + this._renderTextureReused = false; + + this._resultTextureSource = null; + + this._renderOffscreen = false; + + this.empty = false; + } + + get enabled() { + return this._enabled; + } + + set enabled(v) { + this._enabled = v; + this._core.updateRenderToTextureEnabled(); + } + + get renderOffscreen() { + return this._renderOffscreen; + } + + set renderOffscreen(v) { + this._renderOffscreen = v; + this._core.setHasRenderUpdates(1); + + // This enforces rechecking the 'within bounds'. + this._core._setRecalc(6); + } + + get colorize() { + return this._colorize; + } + + set colorize(v) { + if (this._colorize !== v) { + this._colorize = v; + + // Only affects the finally drawn quad. + this._core.setHasRenderUpdates(1); + } + } + + _getTextureSource() { + if (!this._resultTextureSource) { + this._resultTextureSource = new TextureSource(this._element.stage.textureManager); + this.updateResultTexture(); + } + return this._resultTextureSource; + } + + hasResultTexture() { + return !!this._resultTextureSource; + } + + resultTextureInUse() { + return this._resultTextureSource && this._resultTextureSource.hasEnabledElements(); + } + + updateResultTexture() { + let resultTexture = this.getResultTexture(); + if (this._resultTextureSource) { + if (this._resultTextureSource.nativeTexture !== resultTexture) { + let w = resultTexture ? resultTexture.w : 0; + let h = resultTexture ? resultTexture.h : 0; + this._resultTextureSource.replaceNativeTexture(resultTexture, w, h); + } + + // Texture will be updated: all elements using the source need to be updated as well. + this._resultTextureSource.forEachEnabledElement(element => { + element._updateDimensions(); + element.core.setHasRenderUpdates(3); + }); + } + } + + mustRenderToTexture() { + // Check if we must really render as texture. + if (this._enabled && !this.lazy) { + return true; + } else if (this._enabled && this.lazy && this._core._hasRenderUpdates < 3) { + // Static-only: if renderToTexture did not need to update during last drawn frame, generate it as a cache. + return true; + } + return false; + } + + deactivate() { + this.release(); + } + + get renderTextureReused() { + return this._renderTextureReused; + } + + release() { + this.releaseRenderTexture(); + } + + releaseRenderTexture() { + if (this._renderTexture) { + if (!this._renderTextureReused) { + this.ctx.releaseRenderTexture(this._renderTexture); + } + this._renderTexture = null; + this._renderTextureReused = false; + this.updateResultTexture(); + } + } + + // Reuses the specified texture as the render texture (in ancestor). + reuseTextureAsRenderTexture(nativeTexture) { + if (this._renderTexture !== nativeTexture) { + this.releaseRenderTexture(); + this._renderTexture = nativeTexture; + this._renderTextureReused = true; + } + } + + hasRenderTexture() { + return !!this._renderTexture; + } + + getRenderTexture() { + if (!this._renderTexture) { + this._renderTexture = this.ctx.allocateRenderTexture(this._core._w, this._core._h); + this._renderTextureReused = false; + } + return this._renderTexture; + } + + getResultTexture() { + return this._renderTexture; + } + +} + +class ElementCore { + + constructor(element) { + this._element = element; + + this.ctx = element.stage.ctx; + + // The memory layout of the internal variables is affected by their position in the constructor. + // It boosts performance to order them by usage of cpu-heavy functions (renderSimple and update). + + this._recalc = 0; + + this._parent = null; + + this._onUpdate = null; + + this._pRecalc = 0; + + this._worldContext = new ElementCoreContext(); + + this._hasUpdates = false; + + this._localAlpha = 1; + + this._onAfterCalcs = null; + + this._onAfterUpdate = null; + + // All local translation/transform updates: directly propagated from x/y/w/h/scale/whatever. + this._localPx = 0; + this._localPy = 0; + + this._localTa = 1; + this._localTb = 0; + this._localTc = 0; + this._localTd = 1; + + this._isComplex = false; + + this._dimsUnknown = false; + + this._clipping = false; + + // Used by both update and render. + this._zSort = false; + + this._outOfBounds = 0; + + /** + * The texture source to be displayed. + * @type {TextureSource} + */ + this._displayedTextureSource = null; + + this._zContextUsage = 0; + + this._children = null; + + this._hasRenderUpdates = 0; + + this._zIndexedChildren = null; + + this._renderContext = this._worldContext; + + this.renderState = this.ctx.renderState; + + this._scissor = null; + + // The ancestor ElementCore that owns the inherited shader. Null if none is active (default shader). + this._shaderOwner = null; + + + this._updateTreeOrder = 0; + + this._colorUl = this._colorUr = this._colorBl = this._colorBr = 0xFFFFFFFF; + + this._x = 0; + this._y = 0; + this._w = 0; + this._h = 0; + + this._optFlags = 0; + this._funcX = null; + this._funcY = null; + this._funcW = null; + this._funcH = null; + + this._scaleX = 1; + this._scaleY = 1; + this._pivotX = 0.5; + this._pivotY = 0.5; + this._mountX = 0; + this._mountY = 0; + this._rotation = 0; + + this._alpha = 1; + this._visible = true; + + this._ulx = 0; + this._uly = 0; + this._brx = 1; + this._bry = 1; + + this._zIndex = 0; + this._forceZIndexContext = false; + this._zParent = null; + + this._isRoot = false; + + /** + * Iff true, during zSort, this element should be 're-sorted' because either: + * - zIndex did chang + * - zParent did change + * - element was moved in the render tree + * @type {boolean} + */ + this._zIndexResort = false; + + this._shader = null; + + // Element is rendered on another texture. + this._renderToTextureEnabled = false; + + this._texturizer = null; + + this._useRenderToTexture = false; + + this._boundsMargin = null; + + this._recBoundsMargin = null; + + this._withinBoundsMargin = false; + + this._viewport = null; + + this._clipbox = true; + + this.render = this._renderSimple; + + this._layout = null; + } + + get offsetX() { + if (this._funcX) { + return this._funcX; + } else { + if (this.hasFlexLayout()) { + return this._layout.originalX; + } else { + return this._x; + } + } + } + + set offsetX(v) { + if (Utils.isFunction(v)) { + this.funcX = v; + } else { + this._disableFuncX(); + if (this.hasFlexLayout()) { + this.x += (v - this._layout.originalX); + this._layout.setOriginalXWithoutUpdatingLayout(v); + } else { + this.x = v; + } + } + } + + get x() { + return this._x; + } + + set x(v) { + if (v !== this._x) { + this._updateLocalTranslateDelta(v - this._x, 0); + this._x = v; + } + } + + get funcX() { + return (this._optFlags & 1 ? this._funcX : null); + } + + set funcX(v) { + if (this._funcX !== v) { + this._optFlags |= 1; + this._funcX = v; + if (this.hasFlexLayout()) { + this._layout.setOriginalXWithoutUpdatingLayout(0); + this.layout.forceLayout(); + } else { + this._x = 0; + this._triggerRecalcTranslate(); + } + } + } + + _disableFuncX() { + this._optFlags = this._optFlags & (0xFFFF - 1); + this._funcX = null; + } + + get offsetY() { + if (this._funcY) { + return this._funcY; + } else { + if (this.hasFlexLayout()) { + return this._layout.originalY; + } else { + return this._y; + } + } + } + + set offsetY(v) { + if (Utils.isFunction(v)) { + this.funcY = v; + } else { + this._disableFuncY(); + if (this.hasFlexLayout()) { + this.y += (v - this._layout.originalY); + this._layout.setOriginalYWithoutUpdatingLayout(v); + } else { + this.y = v; + } + } + } + + get y() { + return this._y; + } + + set y(v) { + if (v !== this._y) { + this._updateLocalTranslateDelta(0, v - this._y); + this._y = v; + } + } + + get funcY() { + return (this._optFlags & 2 ? this._funcY : null); + } + + set funcY(v) { + if (this._funcY !== v) { + this._optFlags |= 2; + this._funcY = v; + if (this.hasFlexLayout()) { + this._layout.setOriginalYWithoutUpdatingLayout(0); + this.layout.forceLayout(); + } else { + this._y = 0; + this._triggerRecalcTranslate(); + } + } + } + + _disableFuncY() { + this._optFlags = this._optFlags & (0xFFFF - 2); + this._funcY = null; + } + + get funcW() { + return (this._optFlags & 4 ? this._funcW : null); + } + + set funcW(v) { + if (this._funcW !== v) { + this._optFlags |= 4; + this._funcW = v; + if (this.hasFlexLayout()) { + this._layout._originalWidth = 0; + this.layout.changedDimensions(true, false); + } else { + this._w = 0; + this._triggerRecalcTranslate(); + } + } + } + + disableFuncW() { + this._optFlags = this._optFlags & (0xFFFF - 4); + this._funcW = null; + } + + get funcH() { + return (this._optFlags & 8 ? this._funcH : null); + } + + set funcH(v) { + if (this._funcH !== v) { + this._optFlags |= 8; + this._funcH = v; + if (this.hasFlexLayout()) { + this._layout._originalHeight = 0; + this.layout.changedDimensions(false, true); + } else { + this._h = 0; + this._triggerRecalcTranslate(); + } + } + } + + disableFuncH() { + this._optFlags = this._optFlags & (0xFFFF - 8); + this._funcH = null; + } + + get w() { + return this._w; + } + + getRenderWidth() { + if (this.hasFlexLayout()) { + return this._layout.originalWidth; + } else { + return this._w; + } + } + + get h() { + return this._h; + } + + getRenderHeight() { + if (this.hasFlexLayout()) { + return this._layout.originalHeight; + } else { + return this._h; + } + } + + get scaleX() { + return this._scaleX; + } + + set scaleX(v) { + if (this._scaleX !== v) { + this._scaleX = v; + this._updateLocalTransform(); + } + } + + get scaleY() { + return this._scaleY; + } + + set scaleY(v) { + if (this._scaleY !== v) { + this._scaleY = v; + this._updateLocalTransform(); + } + } + + get scale() { + return this.scaleX; + } + + set scale(v) { + if (this._scaleX !== v || this._scaleY !== v) { + this._scaleX = v; + this._scaleY = v; + this._updateLocalTransform(); + } + } + + get pivotX() { + return this._pivotX; + } + + set pivotX(v) { + if (this._pivotX !== v) { + this._pivotX = v; + this._updateLocalTranslate(); + } + } + + get pivotY() { + return this._pivotY; + } + + set pivotY(v) { + if (this._pivotY !== v) { + this._pivotY = v; + this._updateLocalTranslate(); + } + } + + get pivot() { + return this._pivotX; + } + + set pivot(v) { + if (this._pivotX !== v || this._pivotY !== v) { + this._pivotX = v; + this._pivotY = v; + this._updateLocalTranslate(); + } + } + + get mountX() { + return this._mountX; + } + + set mountX(v) { + if (this._mountX !== v) { + this._mountX = v; + this._updateLocalTranslate(); + } + } + + get mountY() { + return this._mountY; + } + + set mountY(v) { + if (this._mountY !== v) { + this._mountY = v; + this._updateLocalTranslate(); + } + } + + get mount() { + return this._mountX; + } + + set mount(v) { + if (this._mountX !== v || this._mountY !== v) { + this._mountX = v; + this._mountY = v; + this._updateLocalTranslate(); + } + } + + get rotation() { + return this._rotation; + } + + set rotation(v) { + if (this._rotation !== v) { + this._rotation = v; + this._updateLocalTransform(); + } + } + + get alpha() { + return this._alpha; + } + + set alpha(v) { + // Account for rounding errors. + v = (v > 1 ? 1 : (v < 1e-14 ? 0 : v)); + if (this._alpha !== v) { + let prev = this._alpha; + this._alpha = v; + this._updateLocalAlpha(); + if ((prev === 0) !== (v === 0)) { + this._element._updateEnabledFlag(); + } + } + } + + get visible() { + return this._visible; + } + + set visible(v) { + if (this._visible !== v) { + this._visible = v; + this._updateLocalAlpha(); + this._element._updateEnabledFlag(); + + if (this.hasFlexLayout()) { + this.layout.setVisible(v); + } + } + } + + _updateLocalTransform() { + if (this._rotation !== 0 && this._rotation % (2 * Math.PI)) { + // check to see if the rotation is the same as the previous render. This means we only need to use sin and cos when rotation actually changes + let _sr = Math.sin(this._rotation); + let _cr = Math.cos(this._rotation); + + this._setLocalTransform( + _cr * this._scaleX, + -_sr * this._scaleY, + _sr * this._scaleX, + _cr * this._scaleY + ); + } else { + this._setLocalTransform( + this._scaleX, + 0, + 0, + this._scaleY + ); + } + this._updateLocalTranslate(); + }; + + _updateLocalTranslate() { + this._recalcLocalTranslate(); + this._triggerRecalcTranslate(); + }; + + _recalcLocalTranslate() { + let pivotXMul = this._pivotX * this._w; + let pivotYMul = this._pivotY * this._h; + let px = this._x - (pivotXMul * this._localTa + pivotYMul * this._localTb) + pivotXMul; + let py = this._y - (pivotXMul * this._localTc + pivotYMul * this._localTd) + pivotYMul; + px -= this._mountX * this._w; + py -= this._mountY * this._h; + this._localPx = px; + this._localPy = py; + } + + _updateLocalTranslateDelta(dx, dy) { + this._addLocalTranslate(dx, dy); + }; + + _updateLocalAlpha() { + this._setLocalAlpha(this._visible ? this._alpha : 0); + }; + + /** + * @param {number} type + * 0: no updates + * 1: re-invoke shader + * 3: re-create render texture and re-invoke shader + */ + setHasRenderUpdates(type) { + if (this._worldContext.alpha) { + // Ignore if 'world invisible'. Render updates will be reset to 3 for every element that becomes visible. + let p = this; + p._hasRenderUpdates = Math.max(type, p._hasRenderUpdates); + while ((p = p._parent) && (p._hasRenderUpdates !== 3)) { + p._hasRenderUpdates = 3; + } + } + } + + /** + * @param {Number} type + * 1: alpha + * 2: translate + * 4: transform + * 128: becomes visible + * 256: flex layout updated + */ + _setRecalc(type) { + this._recalc |= type; + + this._setHasUpdates(); + + // Any changes in descendants should trigger texture updates. + if (this._parent) { + this._parent.setHasRenderUpdates(3); + } + } + + _setHasUpdates() { + let p = this; + while(p && !p._hasUpdates) { + p._hasUpdates = true; + p = p._parent; + } + } + + getParent() { + return this._parent; + } + + setParent(parent) { + if (parent !== this._parent) { + let prevIsZContext = this.isZContext(); + let prevParent = this._parent; + this._parent = parent; + + // Notify flex layout engine. + if (this._layout || (parent && parent.isFlexContainer())) { + this.layout.setParent(prevParent, parent); + } + + if (prevParent) { + // When elements are deleted, the render texture must be re-rendered. + prevParent.setHasRenderUpdates(3); + } + + this._setRecalc(1 + 2 + 4); + + if (this._parent) { + // Force parent to propagate hasUpdates flag. + this._parent._setHasUpdates(); + } + + if (this._zIndex === 0) { + this.setZParent(parent); + } else { + this.setZParent(parent ? parent.findZContext() : null); + } + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(prevParent.findZContext()); + } + } + + // Tree order did change: even if zParent stays the same, we must resort. + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + + if (!this._shader) { + let newShaderOwner = parent && !parent._renderToTextureEnabled ? parent._shaderOwner : null; + if (newShaderOwner !== this._shaderOwner) { + this.setHasRenderUpdates(1); + this._setShaderOwnerRecursive(newShaderOwner); + } + } + } + }; + + enableZSort(force = false) { + if (!this._zSort && this._zContextUsage > 0) { + this._zSort = true; + if (force) { + // ZSort must be done, even if this element is invisible. + // This is done to prevent memory leaks when removing element from inactive render branches. + this.ctx.forceZSort(this); + } + } + } + + addChildAt(index, child) { + if (!this._children) this._children = []; + this._children.splice(index, 0, child); + child.setParent(this); + }; + + setChildAt(index, child) { + if (!this._children) this._children = []; + this._children[index].setParent(null); + this._children[index] = child; + child.setParent(this); + } + + removeChildAt(index) { + let child = this._children[index]; + this._children.splice(index, 1); + child.setParent(null); + }; + + removeChildren() { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].setParent(null); + } + + this._children.splice(0); + + if (this._zIndexedChildren) { + this._zIndexedChildren.splice(0); + } + } + }; + + syncChildren(removed, added, order) { + this._children = order; + for (let i = 0, n = removed.length; i < n; i++) { + removed[i].setParent(null); + } + for (let i = 0, n = added.length; i < n; i++) { + added[i].setParent(this); + } + } + + moveChild(fromIndex, toIndex) { + let c = this._children[fromIndex]; + this._children.splice(fromIndex, 1); + this._children.splice(toIndex, 0, c); + + // Tree order changed: must resort!; + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + } + + _setLocalTransform(a, b, c, d) { + this._setRecalc(4); + this._localTa = a; + this._localTb = b; + this._localTc = c; + this._localTd = d; + + // We also regard negative scaling as a complex case, so that we can optimize the non-complex case better. + this._isComplex = (b !== 0) || (c !== 0) || (a < 0) || (d < 0); + }; + + _addLocalTranslate(dx, dy) { + this._localPx += dx; + this._localPy += dy; + this._triggerRecalcTranslate(); + } + + _setLocalAlpha(a) { + if (!this._worldContext.alpha && ((this._parent && this._parent._worldContext.alpha) && a)) { + // Element is becoming visible. We need to force update. + this._setRecalc(1 + 128); + } else { + this._setRecalc(1); + } + + if (a < 1e-14) { + // Tiny rounding errors may cause failing visibility tests. + a = 0; + } + + this._localAlpha = a; + }; + + setDimensions(w, h, isEstimate = this._dimsUnknown) { + // In case of an estimation, the update loop should perform different bound checks. + this._dimsUnknown = isEstimate; + + if (this.hasFlexLayout()) { + this._layout.originalWidth = w; + this._layout.originalHeight = h; + } else { + if (this._w !== w || this._h !== h) { + this._updateDimensions(w, h); + return true; + } + } + return false; + }; + + _updateDimensions(w, h) { + if (this._w !== w || this._h !== h) { + this._w = w; + this._h = h; + + this._triggerRecalcTranslate(); + + if (this._texturizer) { + this._texturizer.releaseRenderTexture(); + this._texturizer.updateResultTexture(); + } + // Due to width/height change: update the translation vector. + this._updateLocalTranslate(); + } + } + + setTextureCoords(ulx, uly, brx, bry) { + this.setHasRenderUpdates(3); + + this._ulx = ulx; + this._uly = uly; + this._brx = brx; + this._bry = bry; + }; + + get displayedTextureSource() { + return this._displayedTextureSource; + } + + setDisplayedTextureSource(textureSource) { + this.setHasRenderUpdates(3); + this._displayedTextureSource = textureSource; + }; + + get isRoot() { + return this._isRoot; + } + + setAsRoot() { + // Use parent dummy. + this._parent = new ElementCore(this._element); + + // After setting root, make sure it's updated. + this._parent._hasRenderUpdates = 3; + this._parent._hasUpdates = true; + + // Root is, and will always be, the primary zContext. + this._isRoot = true; + + this.ctx.root = this; + + // Set scissor area of 'fake parent' to stage's viewport. + this._parent._viewport = [0, 0, this.ctx.stage.coordsWidth, this.ctx.stage.coordsHeight]; + this._parent._scissor = this._parent._viewport; + + // When recBoundsMargin is null, the defaults are used (100 for all sides). + this._parent._recBoundsMargin = null; + + this._setRecalc(1 + 2 + 4); + }; + + isAncestorOf(c) { + let p = c; + while (p = p._parent) { + if (this === p) { + return true; + } + } + return false; + }; + + isZContext() { + return (this._forceZIndexContext || this._renderToTextureEnabled || this._zIndex !== 0 || this._isRoot || !this._parent); + }; + + findZContext() { + if (this.isZContext()) { + return this; + } else { + return this._parent.findZContext(); + } + }; + + setZParent(newZParent) { + if (this._zParent !== newZParent) { + if (this._zParent !== null) { + if (this._zIndex !== 0) { + this._zParent.decZContextUsage(); + } + + // We must filter out this item upon the next resort. + this._zParent.enableZSort(); + } + + if (newZParent !== null) { + let hadZContextUsage = (newZParent._zContextUsage > 0); + + // @pre: new parent's children array has already been modified. + if (this._zIndex !== 0) { + newZParent.incZContextUsage(); + } + + if (newZParent._zContextUsage > 0) { + if (!hadZContextUsage && (this._parent === newZParent)) ; else { + // Add new child to array. + newZParent._zIndexedChildren.push(this); + } + + // Order should be checked. + newZParent.enableZSort(); + } + } + + this._zParent = newZParent; + + // Newly added element must be marked for resort. + this._zIndexResort = true; + } + }; + + incZContextUsage() { + this._zContextUsage++; + if (this._zContextUsage === 1) { + if (!this._zIndexedChildren) { + this._zIndexedChildren = []; + } + if (this._children) { + // Copy. + for (let i = 0, n = this._children.length; i < n; i++) { + this._zIndexedChildren.push(this._children[i]); + } + // Initially, children are already sorted properly (tree order). + this._zSort = false; + } + } + }; + + decZContextUsage() { + this._zContextUsage--; + if (this._zContextUsage === 0) { + this._zSort = false; + this._zIndexedChildren.splice(0); + } + }; + + get zIndex() { + return this._zIndex; + } + + set zIndex(zIndex) { + if (this._zIndex !== zIndex) { + this.setHasRenderUpdates(1); + + let newZParent = this._zParent; + + let prevIsZContext = this.isZContext(); + if (zIndex === 0 && this._zIndex !== 0) { + if (this._parent === this._zParent) { + if (this._zParent) { + this._zParent.decZContextUsage(); + } + } else { + newZParent = this._parent; + } + } else if (zIndex !== 0 && this._zIndex === 0) { + newZParent = this._parent ? this._parent.findZContext() : null; + if (newZParent === this._zParent) { + if (this._zParent) { + this._zParent.incZContextUsage(); + this._zParent.enableZSort(); + } + } + } else if (zIndex !== this._zIndex) { + if (this._zParent && this._zParent._zContextUsage) { + this._zParent.enableZSort(); + } + } + + if (newZParent !== this._zParent) { + this.setZParent(null); + } + + this._zIndex = zIndex; + + if (newZParent !== this._zParent) { + this.setZParent(newZParent); + } + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(this._parent.findZContext()); + } + } + + // Make sure that resort is done. + this._zIndexResort = true; + if (this._zParent) { + this._zParent.enableZSort(); + } + } + }; + + get forceZIndexContext() { + return this._forceZIndexContext; + } + + set forceZIndexContext(v) { + this.setHasRenderUpdates(1); + + let prevIsZContext = this.isZContext(); + this._forceZIndexContext = v; + + if (prevIsZContext !== this.isZContext()) { + if (!this.isZContext()) { + this.disableZContext(); + } else { + this.enableZContext(this._parent.findZContext()); + } + } + }; + + enableZContext(prevZContext) { + if (prevZContext && prevZContext._zContextUsage > 0) { + // Transfer from upper z context to this z context. + const results = this._getZIndexedDescs(); + results.forEach((c) => { + if (this.isAncestorOf(c) && c._zIndex !== 0) { + c.setZParent(this); + } + }); + } + } + + _getZIndexedDescs() { + const results = []; + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i]._getZIndexedDescsRec(results); + } + } + return results; + } + + _getZIndexedDescsRec(results) { + if (this._zIndex) { + results.push(this); + } else if (this._children && !this.isZContext()) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i]._getZIndexedDescsRec(results); + } + } + } + + disableZContext() { + // Transfer from this z context to upper z context. + if (this._zContextUsage > 0) { + let newZParent = this._parent.findZContext(); + + // Make sure that z-indexed children are up to date (old ones removed). + if (this._zSort) { + this.sortZIndexedChildren(); + } + + this._zIndexedChildren.slice().forEach(function (c) { + if (c._zIndex !== 0) { + c.setZParent(newZParent); + } + }); + } + }; + + get colorUl() { + return this._colorUl; + } + + set colorUl(color) { + if (this._colorUl !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorUl = color; + } + } + + get colorUr() { + return this._colorUr; + } + + set colorUr(color) { + if (this._colorUr !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorUr = color; + } + }; + + get colorBl() { + return this._colorBl; + } + + set colorBl(color) { + if (this._colorBl !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorBl = color; + } + }; + + get colorBr() { + return this._colorBr; + } + + set colorBr(color) { + if (this._colorBr !== color) { + this.setHasRenderUpdates(this._displayedTextureSource ? 3 : 1); + this._colorBr = color; + } + }; + + + set onUpdate(f) { + this._onUpdate = f; + this._setRecalc(7); + } + + set onAfterUpdate(f) { + this._onAfterUpdate = f; + this._setRecalc(7); + } + + set onAfterCalcs(f) { + this._onAfterCalcs = f; + this._setRecalc(7); + } + + get shader() { + return this._shader; + } + + set shader(v) { + this.setHasRenderUpdates(1); + + let prevShader = this._shader; + this._shader = v; + if (!v && prevShader) { + // Disabled shader. + let newShaderOwner = (this._parent && !this._parent._renderToTextureEnabled ? this._parent._shaderOwner : null); + this._setShaderOwnerRecursive(newShaderOwner); + } else if (v) { + // Enabled shader. + this._setShaderOwnerRecursive(this); + } + } + + get activeShader() { + return this._shaderOwner ? this._shaderOwner.shader : this.renderState.defaultShader; + } + + get activeShaderOwner() { + return this._shaderOwner; + } + + get clipping() { + return this._clipping; + } + + set clipping(v) { + if (this._clipping !== v) { + this._clipping = v; + + // Force update of scissor by updating translate. + // Alpha must also be updated because the scissor area may have been empty. + this._setRecalc(1 + 2); + } + } + + get clipbox() { + return this._clipbox; + } + + set clipbox(v) { + // In case of out-of-bounds element, all children will also be ignored. + // It will save us from executing the update/render loops for those. + // The optimization will be used immediately during the next frame. + this._clipbox = v; + } + + _setShaderOwnerRecursive(elementCore) { + this._shaderOwner = elementCore; + + if (this._children && !this._renderToTextureEnabled) { + for (let i = 0, n = this._children.length; i < n; i++) { + let c = this._children[i]; + if (!c._shader) { + c._setShaderOwnerRecursive(elementCore); + c._hasRenderUpdates = 3; + } + } + } + }; + + _setShaderOwnerChildrenRecursive(elementCore) { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + let c = this._children[i]; + if (!c._shader) { + c._setShaderOwnerRecursive(elementCore); + c._hasRenderUpdates = 3; + } + } + } + }; + + _hasRenderContext() { + return this._renderContext !== this._worldContext; + } + + get renderContext() { + return this._renderContext; + } + + updateRenderToTextureEnabled() { + // Enforce texturizer initialisation. + let v = this.texturizer._enabled; + + if (v) { + this._enableRenderToTexture(); + } else { + this._disableRenderToTexture(); + this._texturizer.releaseRenderTexture(); + } + } + + _enableRenderToTexture() { + if (!this._renderToTextureEnabled) { + let prevIsZContext = this.isZContext(); + + this._renderToTextureEnabled = true; + + this._renderContext = new ElementCoreContext(); + + // If render to texture is active, a new shader context is started. + this._setShaderOwnerChildrenRecursive(null); + + if (!prevIsZContext) { + // Render context forces z context. + this.enableZContext(this._parent ? this._parent.findZContext() : null); + } + + this.setHasRenderUpdates(3); + + // Make sure that the render coordinates get updated. + this._setRecalc(7); + + this.render = this._renderAdvanced; + } + } + + _disableRenderToTexture() { + if (this._renderToTextureEnabled) { + this._renderToTextureEnabled = false; + + this._setShaderOwnerChildrenRecursive(this._shaderOwner); + + this._renderContext = this._worldContext; + + if (!this.isZContext()) { + this.disableZContext(); + } + + // Make sure that the render coordinates get updated. + this._setRecalc(7); + + this.setHasRenderUpdates(3); + + this.render = this._renderSimple; + } + } + + isWhite() { + return (this._colorUl === 0xFFFFFFFF) && (this._colorUr === 0xFFFFFFFF) && (this._colorBl === 0xFFFFFFFF) && (this._colorBr === 0xFFFFFFFF); + } + + hasSimpleTexCoords() { + return (this._ulx === 0) && (this._uly === 0) && (this._brx === 1) && (this._bry === 1); + } + + _stashTexCoords() { + this._stashedTexCoords = [this._ulx, this._uly, this._brx, this._bry]; + this._ulx = 0; + this._uly = 0; + this._brx = 1; + this._bry = 1; + } + + _unstashTexCoords() { + this._ulx = this._stashedTexCoords[0]; + this._uly = this._stashedTexCoords[1]; + this._brx = this._stashedTexCoords[2]; + this._bry = this._stashedTexCoords[3]; + this._stashedTexCoords = null; + } + + _stashColors() { + this._stashedColors = [this._colorUl, this._colorUr, this._colorBr, this._colorBl]; + this._colorUl = 0xFFFFFFFF; + this._colorUr = 0xFFFFFFFF; + this._colorBr = 0xFFFFFFFF; + this._colorBl = 0xFFFFFFFF; + } + + _unstashColors() { + this._colorUl = this._stashedColors[0]; + this._colorUr = this._stashedColors[1]; + this._colorBr = this._stashedColors[2]; + this._colorBl = this._stashedColors[3]; + this._stashedColors = null; + } + + isVisible() { + return (this._localAlpha > 1e-14); + }; + + get outOfBounds() { + return this._outOfBounds; + } + + set boundsMargin(v) { + + /** + * null: inherit from parent. + * number[4]: specific margins: left, top, right, bottom. + */ + this._boundsMargin = v ? v.slice() : null; + + // We force recalc in order to set all boundsMargin recursively during the next update. + this._triggerRecalcTranslate(); + } + + get boundsMargin() { + return this._boundsMargin; + } + + update() { + this._recalc |= this._parent._pRecalc; + + if (this._layout && this._layout.isEnabled()) { + if (this._recalc & 256) { + this._layout.layoutFlexTree(); + } + } else if ((this._recalc & 2) && this._optFlags) { + this._applyRelativeDimFuncs(); + } + + if (this._onUpdate) { + // Block all 'upwards' updates when changing things in this branch. + this._hasUpdates = true; + this._onUpdate(this.element, this); + } + + const pw = this._parent._worldContext; + let w = this._worldContext; + const visible = (pw.alpha && this._localAlpha); + + /** + * We must update if: + * - branch contains updates (even when invisible because it may contain z-indexed descendants) + * - there are (inherited) updates and this branch is visible + * - this branch becomes invisible (descs may be z-indexed so we must update all alpha values) + */ + if (this._hasUpdates || (this._recalc && visible) || (w.alpha && !visible)) { + let recalc = this._recalc; + + // Update world coords/alpha. + if (recalc & 1) { + if (!w.alpha && visible) { + // Becomes visible. + this._hasRenderUpdates = 3; + } + w.alpha = pw.alpha * this._localAlpha; + + if (w.alpha < 1e-14) { + // Tiny rounding errors may cause failing visibility tests. + w.alpha = 0; + } + } + + if (recalc & 6) { + w.px = pw.px + this._localPx * pw.ta; + w.py = pw.py + this._localPy * pw.td; + if (pw.tb !== 0) w.px += this._localPy * pw.tb; + if (pw.tc !== 0) w.py += this._localPx * pw.tc; + } + + if (recalc & 4) { + w.ta = this._localTa * pw.ta; + w.tb = this._localTd * pw.tb; + w.tc = this._localTa * pw.tc; + w.td = this._localTd * pw.td; + + if (this._isComplex) { + w.ta += this._localTc * pw.tb; + w.tb += this._localTb * pw.ta; + w.tc += this._localTc * pw.td; + w.td += this._localTb * pw.tc; + } + } + + // Update render coords/alpha. + const pr = this._parent._renderContext; + if (this._parent._hasRenderContext()) { + const init = this._renderContext === this._worldContext; + if (init) { + // First render context build: make sure that it is fully initialized correctly. + // Otherwise, if we get into bounds later, the render context would not be initialized correctly. + this._renderContext = new ElementCoreContext(); + } + + const r = this._renderContext; + + // Update world coords/alpha. + if (init || (recalc & 1)) { + r.alpha = pr.alpha * this._localAlpha; + + if (r.alpha < 1e-14) { + r.alpha = 0; + } + } + + if (init || (recalc & 6)) { + r.px = pr.px + this._localPx * pr.ta; + r.py = pr.py + this._localPy * pr.td; + if (pr.tb !== 0) r.px += this._localPy * pr.tb; + if (pr.tc !== 0) r.py += this._localPx * pr.tc; + } + + if (init) { + // We set the recalc toggle, because we must make sure that the scissor is updated. + recalc |= 2; + } + + if (init || (recalc & 4)) { + r.ta = this._localTa * pr.ta; + r.tb = this._localTd * pr.tb; + r.tc = this._localTa * pr.tc; + r.td = this._localTd * pr.td; + + if (this._isComplex) { + r.ta += this._localTc * pr.tb; + r.tb += this._localTb * pr.ta; + r.tc += this._localTc * pr.td; + r.td += this._localTb * pr.tc; + } + } + } else { + this._renderContext = this._worldContext; + } + + if (this.ctx.updateTreeOrder === -1) { + this.ctx.updateTreeOrder = this._updateTreeOrder + 1; + } else { + this._updateTreeOrder = this.ctx.updateTreeOrder++; + } + + // Determine whether we must use a 'renderTexture'. + const useRenderToTexture = this._renderToTextureEnabled && this._texturizer.mustRenderToTexture(); + if (this._useRenderToTexture !== useRenderToTexture) { + // Coords must be changed. + this._recalc |= 2 + 4; + + // Scissor may change: force update. + recalc |= 2; + + if (!this._useRenderToTexture) { + // We must release the texture. + this._texturizer.release(); + } + } + this._useRenderToTexture = useRenderToTexture; + + const r = this._renderContext; + + const bboxW = this._dimsUnknown ? 2048 : this._w; + const bboxH = this._dimsUnknown ? 2048 : this._h; + + // Calculate a bbox for this element. + let sx, sy, ex, ey; + const rComplex = (r.tb !== 0) || (r.tc !== 0) || (r.ta < 0) || (r.td < 0); + if (rComplex) { + sx = Math.min(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + ex = Math.max(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + sy = Math.min(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + ey = Math.max(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + } else { + sx = r.px; + ex = r.px + r.ta * bboxW; + sy = r.py; + ey = r.py + r.td * bboxH; + } + + if (this._dimsUnknown && (rComplex || this._localTa < 1 || this._localTb < 1)) { + // If we are dealing with a non-identity matrix, we must extend the bbox so that withinBounds and + // scissors will include the complete range of (positive) dimensions up to ,lh. + const nx = this._x * pr.ta + this._y * pr.tb + pr.px; + const ny = this._x * pr.tc + this._y * pr.td + pr.py; + if (nx < sx) sx = nx; + if (ny < sy) sy = ny; + if (nx > ex) ex = nx; + if (ny > ey) ey = ny; + } + + if (recalc & 6 || !this._scissor /* initial */) { + // Determine whether we must 'clip'. + if (this._clipping && r.isSquare()) { + // If the parent renders to a texture, it's scissor should be ignored; + const area = this._parent._useRenderToTexture ? this._parent._viewport : this._parent._scissor; + if (area) { + // Merge scissor areas. + const lx = Math.max(area[0], sx); + const ly = Math.max(area[1], sy); + this._scissor = [ + lx, + ly, + Math.min(area[2] + area[0], ex) - lx, + Math.min(area[3] + area[1], ey) - ly + ]; + } else { + this._scissor = [sx, sy, ex - sx, ey - sy]; + } + } else { + // No clipping: reuse parent scissor. + this._scissor = this._parent._useRenderToTexture ? this._parent._viewport : this._parent._scissor; + } + } + + // Calculate the outOfBounds margin. + if (this._boundsMargin) { + this._recBoundsMargin = this._boundsMargin; + } else { + this._recBoundsMargin = this._parent._recBoundsMargin; + } + + if (this._onAfterCalcs) { + // After calcs may change render coords, scissor and/or recBoundsMargin. + if (this._onAfterCalcs(this.element)) { + // Recalculate bbox. + if (rComplex) { + sx = Math.min(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + ex = Math.max(0, bboxW * r.ta, bboxW * r.ta + bboxH * r.tb, bboxH * r.tb) + r.px; + sy = Math.min(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + ey = Math.max(0, bboxW * r.tc, bboxW * r.tc + bboxH * r.td, bboxH * r.td) + r.py; + } else { + sx = r.px; + ex = r.px + r.ta * bboxW; + sy = r.py; + ey = r.py + r.td * bboxH; + } + + if (this._dimsUnknown && (rComplex || this._localTa < 1 || this._localTb < 1)) { + const nx = this._x * pr.ta + this._y * pr.tb + pr.px; + const ny = this._x * pr.tc + this._y * pr.td + pr.py; + if (nx < sx) sx = nx; + if (ny < sy) sy = ny; + if (nx > ex) ex = nx; + if (ny > ey) ey = ny; + } + } + } + + if (this._parent._outOfBounds === 2) { + // Inherit parent out of boundsness. + this._outOfBounds = 2; + + if (this._withinBoundsMargin) { + this._withinBoundsMargin = false; + this.element._disableWithinBoundsMargin(); + } + } else { + if (recalc & 6) { + // Recheck if element is out-of-bounds (all settings that affect this should enable recalc bit 2 or 4). + this._outOfBounds = 0; + let withinMargin = true; + + // Offscreens are always rendered as long as the parent is within bounds. + if (!this._renderToTextureEnabled || !this._texturizer || !this._texturizer.renderOffscreen) { + if (this._scissor && (this._scissor[2] <= 0 || this._scissor[3] <= 0)) { + // Empty scissor area. + this._outOfBounds = 2; + } else { + // Use bbox to check out-of-boundness. + if ((this._scissor[0] > ex) || + (this._scissor[1] > ey) || + (sx > (this._scissor[0] + this._scissor[2])) || + (sy > (this._scissor[1] + this._scissor[3])) + ) { + this._outOfBounds = 1; + } + + if (this._outOfBounds) { + if (this._clipping || this._useRenderToTexture || (this._clipbox && (bboxW && bboxH))) { + this._outOfBounds = 2; + } + } + } + + withinMargin = (this._outOfBounds === 0); + if (!withinMargin) { + // Re-test, now with margins. + if (this._recBoundsMargin) { + withinMargin = !((ex < this._scissor[0] - this._recBoundsMargin[2]) || + (ey < this._scissor[1] - this._recBoundsMargin[3]) || + (sx > this._scissor[0] + this._scissor[2] + this._recBoundsMargin[0]) || + (sy > this._scissor[1] + this._scissor[3] + this._recBoundsMargin[1])); + } else { + withinMargin = !((ex < this._scissor[0] - 100) || + (ey < this._scissor[1] - 100) || + (sx > this._scissor[0] + this._scissor[2] + 100) || + (sy > this._scissor[1] + this._scissor[3] + 100)); + } + if (withinMargin && this._outOfBounds === 2) { + // Children must be visited because they may contain elements that are within margin, so must be visible. + this._outOfBounds = 1; + } + } + } + + if (this._withinBoundsMargin !== withinMargin) { + this._withinBoundsMargin = withinMargin; + + if (this._withinBoundsMargin) { + // This may update things (txLoaded events) in the element itself, but also in descendants and ancestors. + + // Changes in ancestors should be executed during the next call of the stage update. But we must + // take care that the _recalc and _hasUpdates flags are properly registered. That's why we clear + // both before entering the children, and use _pRecalc to transfer inherited updates instead of + // _recalc directly. + + // Changes in descendants are automatically executed within the current update loop, though we must + // take care to not update the hasUpdates flag unnecessarily in ancestors. We achieve this by making + // sure that the hasUpdates flag of this element is turned on, which blocks it for ancestors. + this._hasUpdates = true; + + const recalc = this._recalc; + this._recalc = 0; + this.element._enableWithinBoundsMargin(); + + if (this._recalc) { + // This element needs to be re-updated now, because we want the dimensions (and other changes) to be updated. + return this.update(); + } + + this._recalc = recalc; + } else { + this.element._disableWithinBoundsMargin(); + } + } + } + } + + if (this._useRenderToTexture) { + // Set viewport necessary for children scissor calculation. + if (this._viewport) { + this._viewport[2] = bboxW; + this._viewport[3] = bboxH; + } else { + this._viewport = [0, 0, bboxW, bboxH]; + } + } + + // Filter out bits that should not be copied to the children (currently all are). + this._pRecalc = (this._recalc & 135); + + // Clear flags so that future updates are properly detected. + this._recalc = 0; + this._hasUpdates = false; + + if (this._outOfBounds < 2) { + if (this._useRenderToTexture) { + if (this._worldContext.isIdentity()) { + // Optimization. + // The world context is already identity: use the world context as render context to prevents the + // ancestors from having to update the render context. + this._renderContext = this._worldContext; + } else { + // Temporarily replace the render coord attribs by the identity matrix. + // This allows the children to calculate the render context. + this._renderContext = ElementCoreContext.IDENTITY; + } + } + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].update(); + } + } + + if (this._useRenderToTexture) { + this._renderContext = r; + } + } else { + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._hasUpdates) { + this._children[i].update(); + } else { + // Make sure we don't lose the 'inherited' updates. + this._children[i]._recalc |= this._pRecalc; + this._children[i].updateOutOfBounds(); + } + } + } + } + + if (this._onAfterUpdate) { + this._onAfterUpdate(this.element); + } + } else { + if (this.ctx.updateTreeOrder === -1 || this._updateTreeOrder >= this.ctx.updateTreeOrder) { + // If new tree order does not interfere with the current (gaps allowed) there's no need to traverse the branch. + this.ctx.updateTreeOrder = -1; + } else { + this.updateTreeOrder(); + } + } + } + + _applyRelativeDimFuncs() { + if (this._optFlags & 1) { + const x = this._funcX(this._parent.w); + if (x !== this._x) { + this._localPx += (x - this._x); + this._x = x; + } + } + if (this._optFlags & 2) { + const y = this._funcY(this._parent.h); + if (y !== this._y) { + this._localPy += (y - this._y); + this._y = y; + } + } + + let changedDims = false; + if (this._optFlags & 4) { + const w = this._funcW(this._parent.w); + if (w !== this._w) { + this._w = w; + changedDims = true; + } + } + if (this._optFlags & 8) { + const h = this._funcH(this._parent.h); + if (h !== this._h) { + this._h = h; + changedDims = true; + } + } + + if (changedDims) { + // Recalc mount, scale position. + this._recalcLocalTranslate(); + + this.element.onDimensionsChanged(this._w, this._h); + } + } + + updateOutOfBounds() { + // Propagate outOfBounds flag to descendants (necessary because of z-indexing). + // Invisible elements are not drawn anyway. When alpha is updated, so will _outOfBounds. + if (this._outOfBounds !== 2 && this._renderContext.alpha > 0) { + + // Inherit parent out of boundsness. + this._outOfBounds = 2; + + if (this._withinBoundsMargin) { + this._withinBoundsMargin = false; + this.element._disableWithinBoundsMargin(); + } + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].updateOutOfBounds(); + } + } + } + } + + updateTreeOrder() { + if (this._localAlpha && (this._outOfBounds !== 2)) { + this._updateTreeOrder = this.ctx.updateTreeOrder++; + + if (this._children) { + for (let i = 0, n = this._children.length; i < n; i++) { + this._children[i].updateTreeOrder(); + } + } + } + } + + _renderSimple() { + this._hasRenderUpdates = 0; + + if (this._zSort) { + this.sortZIndexedChildren(); + } + + if (this._outOfBounds < 2 && this._renderContext.alpha) { + let renderState = this.renderState; + + if ((this._outOfBounds === 0) && this._displayedTextureSource) { + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + this.renderState.addQuad(this); + } + + // Also add children to the VBO. + if (this._children) { + if (this._zContextUsage) { + for (let i = 0, n = this._zIndexedChildren.length; i < n; i++) { + this._zIndexedChildren[i].render(); + } + } else { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._zIndex === 0) { + // If zIndex is set, this item already belongs to a zIndexedChildren array in one of the ancestors. + this._children[i].render(); + } + + } + } + } + } + } + + _renderAdvanced() { + const hasRenderUpdates = this._hasRenderUpdates; + + // We must clear the hasRenderUpdates flag before rendering, because updating result textures in combination + // with z-indexing may trigger render updates on a render branch that is 'half done'. + // We need to ensure that the full render branch is marked for render updates, not only half (leading to freeze). + this._hasRenderUpdates = 0; + + if (this._zSort) { + this.sortZIndexedChildren(); + } + + if (this._outOfBounds < 2 && this._renderContext.alpha) { + let renderState = this.renderState; + + let mustRenderChildren = true; + let renderTextureInfo; + let prevRenderTextureInfo; + if (this._useRenderToTexture) { + if (this._w === 0 || this._h === 0) { + // Ignore this branch and don't draw anything. + return; + } else if (!this._texturizer.hasRenderTexture() || (hasRenderUpdates >= 3)) { + // Switch to default shader for building up the render texture. + renderState.setShader(renderState.defaultShader, this); + + prevRenderTextureInfo = renderState.renderTextureInfo; + + renderTextureInfo = { + nativeTexture: null, + offset: 0, // Set by CoreRenderState. + w: this._w, + h: this._h, + empty: true, + cleared: false, + ignore: false, + cache: false + }; + + if (this._texturizer.hasResultTexture() || (!renderState.isCachingTexturizer && (hasRenderUpdates < 3))) { + /** + * We don't always cache render textures. + * + * The rule is, that caching for a specific render texture is only enabled if: + * - There is a result texture to be updated. + * - There were no render updates -within the contents- since last frame (ElementCore.hasRenderUpdates < 3) + * - AND there are no ancestors that are being cached during this frame (CoreRenderState.isCachingTexturizer) + * If an ancestor is cached anyway, it's probably not necessary to keep deeper caches. If the top level is to + * change while a lower one is not, that lower level will be cached instead. + * + * In case of the fast blur element, this prevents having to cache all blur levels and stages, saving a huge amount + * of GPU memory! + * + * Especially when using multiple stacked layers of the same dimensions that are RTT this will have a very + * noticable effect on performance as less render textures need to be allocated. + */ + renderTextureInfo.cache = true; + renderState.isCachingTexturizer = true; + } + + if (!this._texturizer.hasResultTexture()) { + // We can already release the current texture to the pool, as it will be rebuild anyway. + // In case of multiple layers of 'filtering', this may save us from having to create one + // render-to-texture layer. + // Notice that we don't do this when there is a result texture, as any other element may rely on + // that result texture being filled. + this._texturizer.releaseRenderTexture(); + } + + renderState.setRenderTextureInfo(renderTextureInfo); + renderState.setScissor(null); + + if (this._displayedTextureSource) { + let r = this._renderContext; + + // Use an identity context for drawing the displayed texture to the render texture. + this._renderContext = ElementCoreContext.IDENTITY; + + // Add displayed texture source in local coordinates. + this.renderState.addQuad(this); + + this._renderContext = r; + } + } else { + mustRenderChildren = false; + } + } else { + if ((this._outOfBounds === 0) && this._displayedTextureSource) { + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + this.renderState.addQuad(this); + } + } + + // Also add children to the VBO. + if (mustRenderChildren && this._children) { + if (this._zContextUsage) { + for (let i = 0, n = this._zIndexedChildren.length; i < n; i++) { + this._zIndexedChildren[i].render(); + } + } else { + for (let i = 0, n = this._children.length; i < n; i++) { + if (this._children[i]._zIndex === 0) { + // If zIndex is set, this item already belongs to a zIndexedChildren array in one of the ancestors. + this._children[i].render(); + } + } + } + } + + if (this._useRenderToTexture) { + let updateResultTexture = false; + if (mustRenderChildren) { + // Finished refreshing renderTexture. + renderState.finishedRenderTexture(); + + // If nothing was rendered, we store a flag in the texturizer and prevent unnecessary + // render-to-texture and filtering. + this._texturizer.empty = renderTextureInfo.empty; + + if (renderTextureInfo.empty) { + // We ignore empty render textures and do not draw the final quad. + + // The following cleans up memory and enforces that the result texture is also cleared. + this._texturizer.releaseRenderTexture(); + } else if (renderTextureInfo.nativeTexture) { + // If nativeTexture is set, we can reuse that directly instead of creating a new render texture. + this._texturizer.reuseTextureAsRenderTexture(renderTextureInfo.nativeTexture); + + renderTextureInfo.ignore = true; + } else { + if (this._texturizer.renderTextureReused) { + // Quad operations must be written to a render texture actually owned. + this._texturizer.releaseRenderTexture(); + } + // Just create the render texture. + renderTextureInfo.nativeTexture = this._texturizer.getRenderTexture(); + } + + // Restore the parent's render texture. + renderState.setRenderTextureInfo(prevRenderTextureInfo); + + updateResultTexture = true; + } + + if (!this._texturizer.empty) { + let resultTexture = this._texturizer.getResultTexture(); + if (updateResultTexture) { + if (resultTexture) { + // Logging the update frame can be handy for userland. + resultTexture.update = renderState.stage.frameCounter; + } + this._texturizer.updateResultTexture(); + } + + if (!this._texturizer.renderOffscreen) { + // Render result texture to the actual render target. + renderState.setShader(this.activeShader, this._shaderOwner); + renderState.setScissor(this._scissor); + + // If no render texture info is set, the cache can be reused. + const cache = !renderTextureInfo || renderTextureInfo.cache; + + renderState.setTexturizer(this._texturizer, cache); + this._stashTexCoords(); + if (!this._texturizer.colorize) this._stashColors(); + this.renderState.addQuad(this, true); + if (!this._texturizer.colorize) this._unstashColors(); + this._unstashTexCoords(); + renderState.setTexturizer(null); + } + } + } + + if (renderTextureInfo && renderTextureInfo.cache) { + // Allow siblings to cache. + renderState.isCachingTexturizer = false; + } + } + } + + get zSort() { + return this._zSort; + } + + sortZIndexedChildren() { + /** + * We want to avoid resorting everything. Instead, we do a single pass of the full array: + * - filtering out elements with a different zParent than this (were removed) + * - filtering out, but also gathering (in a temporary array) the elements that have zIndexResort flag + * - then, finally, we merge-sort both the new array and the 'old' one + * - element may have been added 'double', so when merge-sorting also check for doubles. + * - if the old one is larger (in size) than it should be, splice off the end of the array. + */ + + const n = this._zIndexedChildren.length; + let ptr = 0; + const a = this._zIndexedChildren; + + // Notice that items may occur multiple times due to z-index changing. + const b = []; + for (let i = 0; i < n; i++) { + if (a[i]._zParent === this) { + if (a[i]._zIndexResort) { + b.push(a[i]); + } else { + if (ptr !== i) { + a[ptr] = a[i]; + } + ptr++; + } + } + } + + const m = b.length; + if (m) { + for (let j = 0; j < m; j++) { + b[j]._zIndexResort = false; + } + + b.sort(ElementCore.sortZIndexedChildren); + const n = ptr; + if (!n) { + ptr = 0; + let j = 0; + do { + a[ptr++] = b[j++]; + } while(j < m); + + if (a.length > ptr) { + // Slice old (unnecessary) part off array. + a.splice(ptr); + } + } else { + // Merge-sort arrays; + ptr = 0; + let i = 0; + let j = 0; + const mergeResult = []; + do { + const v = (a[i]._zIndex === b[j]._zIndex ? a[i]._updateTreeOrder - b[j]._updateTreeOrder : a[i]._zIndex - b[j]._zIndex); + + const add = v > 0 ? b[j++] : a[i++]; + + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + + if (i >= n) { + do { + const add = b[j++]; + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + } while(j < m); + break; + } else if (j >= m) { + do { + const add = a[i++]; + if (ptr === 0 || (mergeResult[ptr - 1] !== add)) { + mergeResult[ptr++] = add; + } + } while(i < n); + break; + } + } while(true); + + this._zIndexedChildren = mergeResult; + } + } else { + if (a.length > ptr) { + // Slice old (unnecessary) part off array. + a.splice(ptr); + } + } + + this._zSort = false; + }; + + get localTa() { + return this._localTa; + }; + + get localTb() { + return this._localTb; + }; + + get localTc() { + return this._localTc; + }; + + get localTd() { + return this._localTd; + }; + + get element() { + return this._element; + } + + get renderUpdates() { + return this._hasRenderUpdates; + } + + get texturizer() { + if (!this._texturizer) { + this._texturizer = new ElementTexturizer(this); + } + return this._texturizer; + } + + getCornerPoints() { + let w = this._worldContext; + + return [ + w.px, + w.py, + w.px + this._w * w.ta, + w.py + this._w * w.tc, + w.px + this._w * w.ta + this._h * w.tb, + w.py + this._w * w.tc + this._h * w.td, + w.px + this._h * w.tb, + w.py + this._h * w.td + ] + }; + + getRenderTextureCoords(relX, relY) { + let r = this._renderContext; + return [ + r.px + r.ta * relX + r.tb * relY, + r.py + r.tc * relX + r.td * relY + ] + } + + getAbsoluteCoords(relX, relY) { + let w = this._renderContext; + return [ + w.px + w.ta * relX + w.tb * relY, + w.py + w.tc * relX + w.td * relY + ] + } + + + get layout() { + this._ensureLayout(); + return this._layout; + } + + get flex() { + return this._layout ? this._layout.flex : null; + } + + set flex(v) { + this.layout.flex = v; + } + + get flexItem() { + return this._layout ? this._layout.flexItem : null; + } + + set flexItem(v) { + this.layout.flexItem = v; + } + + isFlexItem() { + return !!this._layout && this._layout.isFlexItemEnabled(); + } + + isFlexContainer() { + return !!this._layout && this._layout.isFlexEnabled(); + } + + enableFlexLayout() { + this._ensureLayout(); + } + + _ensureLayout() { + if (!this._layout) { + this._layout = new FlexTarget(this); + } + } + + disableFlexLayout() { + this._triggerRecalcTranslate(); + } + + hasFlexLayout() { + return (this._layout && this._layout.isEnabled()); + } + + setLayout(x, y, w, h) { + this.x = x; + this.y = y; + this._updateDimensions(w, h); + } + + triggerLayout() { + this._setRecalc(256); + } + + _triggerRecalcTranslate() { + this._setRecalc(2); + } + +} + +class ElementCoreContext { + + constructor() { + this.alpha = 1; + + this.px = 0; + this.py = 0; + + this.ta = 1; + this.tb = 0; + this.tc = 0; + this.td = 1; + } + + isIdentity() { + return this.alpha === 1 && + this.px === 0 && + this.py === 0 && + this.ta === 1 && + this.tb === 0 && + this.tc === 0 && + this.td === 1; + } + + isSquare() { + return this.tb === 0 && this.tc === 0; + } + +} + +ElementCoreContext.IDENTITY = new ElementCoreContext(); +ElementCore.sortZIndexedChildren = function(a, b) { + return (a._zIndex === b._zIndex ? a._updateTreeOrder - b._updateTreeOrder : a._zIndex - b._zIndex); +}; + +/** + * This is a partial (and more efficient) implementation of the event emitter. + * It attempts to maintain a one-to-one mapping between events and listeners, skipping an array lookup. + * Only if there are multiple listeners, they are combined in an array. + */ +class EventEmitter { + + constructor() { + // This is set (and kept) to true when events are used at all. + this._hasEventListeners = false; + } + + on(name, listener) { + if (!this._hasEventListeners) { + this._eventFunction = {}; + this._eventListeners = {}; + this._hasEventListeners = true; + } + + const current = this._eventFunction[name]; + if (!current) { + this._eventFunction[name] = listener; + } else { + if (this._eventFunction[name] !== EventEmitter.combiner) { + this._eventListeners[name] = [this._eventFunction[name], listener]; + this._eventFunction[name] = EventEmitter.combiner; + } else { + this._eventListeners[name].push(listener); + } + } + } + + has(name, listener) { + if (this._hasEventListeners) { + const current = this._eventFunction[name]; + if (current) { + if (current === EventEmitter.combiner) { + const listeners = this._eventListeners[name]; + let index = listeners.indexOf(listener); + return (index >= 0); + } else if (this._eventFunction[name] === listener) { + return true; + } + } + } + return false; + } + + off(name, listener) { + if (this._hasEventListeners) { + const current = this._eventFunction[name]; + if (current) { + if (current === EventEmitter.combiner) { + const listeners = this._eventListeners[name]; + let index = listeners.indexOf(listener); + if (index >= 0) { + listeners.splice(index, 1); + } + if (listeners.length === 1) { + this._eventFunction[name] = listeners[0]; + this._eventListeners[name] = undefined; + } + } else if (this._eventFunction[name] === listener) { + this._eventFunction[name] = undefined; + } + } + } + } + + removeListener(name, listener) { + this.off(name, listener); + } + + emit(name, arg1, arg2, arg3) { + if (this._hasEventListeners) { + const func = this._eventFunction[name]; + if (func) { + if (func === EventEmitter.combiner) { + func(this, name, arg1, arg2, arg3); + } else { + func(arg1, arg2, arg3); + } + } + } + } + + listenerCount(name) { + if (this._hasEventListeners) { + const func = this._eventFunction[name]; + if (func) { + if (func === EventEmitter.combiner) { + return this._eventListeners[name].length; + } else { + return 1; + } + } + } else { + return 0; + } + } + + removeAllListeners(name) { + if (this._hasEventListeners) { + delete this._eventFunction[name]; + delete this._eventListeners[name]; + } + } + +} + +EventEmitter.combiner = function(object, name, arg1, arg2, arg3) { + const listeners = object._eventListeners[name]; + if (listeners) { + // Because listener may detach itself while being invoked, we use a forEach instead of for loop. + listeners.forEach((listener) => { + listener(arg1, arg2, arg3); + }); + } +}; + +EventEmitter.addAsMixin = function(cls) { + cls.prototype.on = EventEmitter.prototype.on; + cls.prototype.has = EventEmitter.prototype.has; + cls.prototype.off = EventEmitter.prototype.off; + cls.prototype.removeListener = EventEmitter.prototype.removeListener; + cls.prototype.emit = EventEmitter.prototype.emit; + cls.prototype.listenerCount = EventEmitter.prototype.listenerCount; + cls.prototype.removeAllListeners = EventEmitter.prototype.removeAllListeners; +}; + +class Shader { + + constructor(coreContext) { + this._initialized = false; + + this.ctx = coreContext; + + /** + * The (enabled) elements that use this shader. + * @type {Set} + */ + this._elements = new Set(); + } + + static create(stage, v) { + let shader; + if (Utils.isObjectLiteral(v)) { + if (v.type) { + shader = stage.renderer.createShader(stage.ctx, v); + } else { + shader = this.shader; + } + + if (shader) { + Base.patchObject(shader, v); + } + } else if (v === null) { + shader = stage.ctx.renderState.defaultShader; + } else if (v === undefined) { + shader = null; + } else { + if (v.isShader) { + if (!stage.renderer.isValidShaderType(v.constructor)) { + console.error("Invalid shader type"); + v = null; + } + shader = v; + } else { + console.error("Please specify a shader type."); + return; + } + } + + return shader; + } + + static getWebGL() { + return undefined; + } + + static getC2d() { + return undefined; + } + + addElement(elementCore) { + this._elements.add(elementCore); + } + + removeElement(elementCore) { + this._elements.delete(elementCore); + if (!this._elements) { + this.cleanup(); + } + } + + redraw() { + this._elements.forEach(elementCore => { + elementCore.setHasRenderUpdates(2); + }); + } + + patch(settings) { + Base.patchObject(this, settings); + } + + useDefault() { + // Should return true if this shader is configured (using it's properties) to not have any effect. + // This may allow the render engine to avoid unnecessary shader program switches or even texture copies. + return false; + } + + addEmpty() { + // Draws this shader even if there are no quads to be added. + // This is handy for custom shaders. + return false; + } + + cleanup() { + // Called when no more enabled elements have this shader. + } + + get isShader() { + return true; + } +} + +class Texture { + + /** + * @param {Stage} stage + */ + constructor(stage) { + this.stage = stage; + + this.manager = this.stage.textureManager; + + this.id = Texture.id++; + + /** + * All enabled elements that use this texture object (either as texture or displayedTexture). + * @type {Set} + */ + this.elements = new Set(); + + /** + * The number of enabled elements that are active. + * @type {number} + */ + this._activeCount = 0; + + /** + * The associated texture source. + * Should not be changed. + * @type {TextureSource} + */ + this._source = null; + + /** + * A resize mode can be set to cover or contain a certain area. + * It will reset the texture clipping settings. + * When manual texture clipping is performed, the resizeMode is reset. + * @type {{type: string, width: number, height: number}} + * @private + */ + this._resizeMode = null; + + /** + * The texture clipping x-offset. + * @type {number} + */ + this._x = 0; + + /** + * The texture clipping y-offset. + * @type {number} + */ + this._y = 0; + + /** + * The texture clipping width. If 0 then full width. + * @type {number} + */ + this._w = 0; + + /** + * The texture clipping height. If 0 then full height. + * @type {number} + */ + this._h = 0; + + /** + * Render precision (0.5 = fuzzy, 1 = normal, 2 = sharp even when scaled twice, etc.). + * @type {number} + * @private + */ + this._precision = 1; + + /** + * The (maximum) expected texture source width. Used for within bounds determination while texture is not yet loaded. + * If not set, 2048 is used by ElementCore.update. + * @type {number} + */ + this.mw = 0; + + /** + * The (maximum) expected texture source height. Used for within bounds determination while texture is not yet loaded. + * If not set, 2048 is used by ElementCore.update. + * @type {number} + */ + this.mh = 0; + + /** + * Indicates if Texture.prototype.texture uses clipping. + * @type {boolean} + */ + this.clipping = false; + + /** + * Indicates whether this texture must update (when it becomes used again). + * @type {boolean} + * @private + */ + this._mustUpdate = true; + + } + + get source() { + if (this._mustUpdate || this.stage.hasUpdateSourceTexture(this)) { + this._performUpdateSource(true); + this.stage.removeUpdateSourceTexture(this); + } + return this._source; + } + + addElement(v) { + if (!this.elements.has(v)) { + this.elements.add(v); + + if (this.elements.size === 1) { + if (this._source) { + this._source.addTexture(this); + } + } + + if (v.active) { + this.incActiveCount(); + } + } + } + + removeElement(v) { + if (this.elements.delete(v)) { + if (this.elements.size === 0) { + if (this._source) { + this._source.removeTexture(this); + } + } + + if (v.active) { + this.decActiveCount(); + } + } + } + + incActiveCount() { + // Ensure that texture source's activeCount has transferred ownership. + const source = this.source; + + if (source) { + this._checkForNewerReusableTextureSource(); + } + + this._activeCount++; + if (this._activeCount === 1) { + this.becomesUsed(); + } + } + + decActiveCount() { + const source = this.source; // Force updating the source. + this._activeCount--; + if (!this._activeCount) { + this.becomesUnused(); + } + } + + becomesUsed() { + if (this.source) { + this.source.incActiveTextureCount(); + } + } + + onLoad() { + if (this._resizeMode) { + this._applyResizeMode(); + } + + this.elements.forEach(element => { + if (element.active) { + element.onTextureSourceLoaded(); + } + }); + } + + _checkForNewerReusableTextureSource() { + // When this source became unused and cleaned up, it may have disappeared from the reusable texture map. + // In the meantime another texture may have been generated loaded with the same lookup id. + // If this is the case, use that one instead to make sure only one active texture source per lookup id exists. + const source = this.source; + if (!source.isLoaded()) { + const reusable = this._getReusableTextureSource(); + if (reusable && reusable.isLoaded() && (reusable !== source)) { + this._replaceTextureSource(reusable); + } + } else { + if (this._resizeMode) { + this._applyResizeMode(); + } + } + } + + becomesUnused() { + if (this.source) { + this.source.decActiveTextureCount(); + } + } + + isUsed() { + return this._activeCount > 0; + } + + /** + * Returns the lookup id for the current texture settings, to be able to reuse it. + * @returns {string|null} + */ + _getLookupId() { + // Default: do not reuse texture. + return null; + } + + /** + * Generates a loader function that is able to generate the texture for the current settings of this texture. + * It should return a function that receives a single callback argument. + * That callback should be called with the following arguments: + * - err + * - options: object + * - source: ArrayBuffer|WebGlTexture|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap + * - w: Number + * - h: Number + * - permanent: Boolean + * - hasAlpha: boolean + * - permultiplyAlpha: boolean + * - flipBlueRed: boolean + * - renderInfo: object + * The loader itself may return a Function that is called when loading of the texture is cancelled. This can be used + * to stop fetching an image when it is no longer in element, for example. + */ + _getSourceLoader() { + throw new Error("Texture.generate must be implemented."); + } + + get isValid() { + return this._getIsValid(); + } + + /** + * If texture is not 'valid', no source can be created for it. + * @returns {boolean} + */ + _getIsValid() { + return true; + } + + /** + * This must be called when the texture source must be re-generated. + */ + _changed() { + // If no element is actively using this texture, ignore it altogether. + if (this.isUsed()) { + this._updateSource(); + } else { + this._mustUpdate = true; + } + } + + _updateSource() { + // We delay all updateSource calls to the next drawFrame, so that we can bundle them. + // Otherwise we may reload a texture more often than necessary, when, for example, patching multiple text + // properties. + this.stage.addUpdateSourceTexture(this); + } + + _performUpdateSource(force = false) { + // If, in the meantime, the texture was no longer used, just remember that it must update until it becomes used + // again. + if (force || this.isUsed()) { + this._mustUpdate = false; + let source = this._getTextureSource(); + this._replaceTextureSource(source); + } + } + + _getTextureSource() { + let source = null; + if (this._getIsValid()) { + const lookupId = this._getLookupId(); + source = this._getReusableTextureSource(lookupId); + if (!source) { + source = this.manager.getTextureSource(this._getSourceLoader(), lookupId); + } + } + return source; + } + + _getReusableTextureSource(lookupId = this._getLookupId()) { + if (this._getIsValid()) { + if (lookupId) { + return this.manager.getReusableTextureSource(lookupId); + } + } + return null; + } + + _replaceTextureSource(newSource = null) { + let oldSource = this._source; + + this._source = newSource; + + if (this.elements.size) { + if (oldSource) { + if (this._activeCount) { + oldSource.decActiveTextureCount(); + } + + oldSource.removeTexture(this); + } + + if (newSource) { + // Must happen before setDisplayedTexture to ensure sprite map texcoords are used. + newSource.addTexture(this); + if (this._activeCount) { + newSource.incActiveTextureCount(); + } + } + } + + if (this.isUsed()) { + if (newSource) { + if (newSource.isLoaded()) { + this.elements.forEach(element => { + if (element.active) { + element._setDisplayedTexture(this); + } + }); + } else { + const loadError = newSource.loadError; + if (loadError) { + this.elements.forEach(element => { + if (element.active) { + element.onTextureSourceLoadError(loadError); + } + }); + } + } + } else { + this.elements.forEach(element => { + if (element.active) { + element._setDisplayedTexture(null); + } + }); + } + } + } + + load() { + // Make sure that source is up to date. + if (this.source) { + if (!this.isLoaded()) { + this.source.load(true); + } + } + } + + isLoaded() { + return this._source && this._source.isLoaded(); + } + + get loadError() { + return this._source && this._source.loadError; + } + + free() { + if (this._source) { + this._source.free(); + } + } + + set resizeMode({type = "cover", w = 0, h = 0, clipX = 0.5, clipY = 0.5}) { + this._resizeMode = {type, w, h, clipX, clipY}; + if (this.isLoaded()) { + this._applyResizeMode(); + } + } + + get resizeMode() { + return this._resizeMode; + } + + _clearResizeMode() { + this._resizeMode = null; + } + + _applyResizeMode() { + if (this._resizeMode.type === "cover") { + this._applyResizeCover(); + } else if (this._resizeMode.type === "contain") { + this._applyResizeContain(); + } + this._updatePrecision(); + this._updateClipping(); + } + + _applyResizeCover() { + const scaleX = this._resizeMode.w / this._source.w; + const scaleY = this._resizeMode.h / this._source.h; + let scale = Math.max(scaleX, scaleY); + if (!scale) return; + this._precision = 1/scale; + if (scaleX && scaleX < scale) { + const desiredSize = this._precision * this._resizeMode.w; + const choppedOffPixels = this._source.w - desiredSize; + this._x = choppedOffPixels * this._resizeMode.clipX; + this._w = this._source.w - choppedOffPixels; + } + if (scaleY && scaleY < scale) { + const desiredSize = this._precision * this._resizeMode.h; + const choppedOffPixels = this._source.h - desiredSize; + this._y = choppedOffPixels * this._resizeMode.clipY; + this._h = this._source.h - choppedOffPixels; + } + } + + _applyResizeContain() { + const scaleX = this._resizeMode.w / this._source.w; + const scaleY = this._resizeMode.h / this._source.h; + let scale = scaleX; + if (!scale || scaleY < scale) { + scale = scaleY; + } + if (!scale) return; + this._precision = 1/scale; + } + + enableClipping(x, y, w, h) { + this._clearResizeMode(); + + x *= this._precision; + y *= this._precision; + w *= this._precision; + h *= this._precision; + if (this._x !== x || this._y !== y || this._w !== w || this._h !== h) { + this._x = x; + this._y = y; + this._w = w; + this._h = h; + + this._updateClipping(true); + } + } + + disableClipping() { + this._clearResizeMode(); + + if (this._x || this._y || this._w || this._h) { + this._x = 0; + this._y = 0; + this._w = 0; + this._h = 0; + + this._updateClipping(); + } + } + + _updateClipping() { + this.clipping = !!(this._x || this._y || this._w || this._h); + + let self = this; + this.elements.forEach(function(element) { + // Ignore if not the currently displayed texture. + if (element.displayedTexture === self) { + element.onDisplayedTextureClippingChanged(); + } + }); + } + + _updatePrecision() { + let self = this; + this.elements.forEach(function(element) { + // Ignore if not the currently displayed texture. + if (element.displayedTexture === self) { + element.onPrecisionChanged(); + } + }); + } + + getNonDefaults() { + let nonDefaults = {}; + nonDefaults['type'] = this.constructor.name; + if (this.x !== 0) nonDefaults['x'] = this.x; + if (this.y !== 0) nonDefaults['y'] = this.y; + if (this.w !== 0) nonDefaults['w'] = this.w; + if (this.h !== 0) nonDefaults['h'] = this.h; + if (this.precision !== 1) nonDefaults['precision'] = this.precision; + return nonDefaults; + } + + get px() { + return this._x; + } + + get py() { + return this._y; + } + + get pw() { + return this._w; + } + + get ph() { + return this._h; + } + + get x() { + return this._x / this._precision; + } + set x(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._x !== v) { + this._x = v; + this._updateClipping(); + } + } + + get y() { + return this._y / this._precision; + } + set y(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._y !== v) { + this._y = v; + this._updateClipping(); + } + } + + get w() { + return this._w / this._precision; + } + + set w(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._w !== v) { + this._w = v; + this._updateClipping(); + } + } + + get h() { + return this._h / this._precision; + } + + set h(v) { + this._clearResizeMode(); + v = v * this._precision; + if (this._h !== v) { + this._h = v; + this._updateClipping(); + } + } + + get precision() { + return this._precision; + } + + set precision(v) { + this._clearResizeMode(); + if (this._precision !== v) { + this._precision = v; + this._updatePrecision(); + } + } + + isAutosizeTexture() { + return true; + } + + getRenderWidth() { + if (!this.isAutosizeTexture()) { + // In case of the rectangle texture, we'd prefer to not cause a 1x1 w,h as it would interfere with flex layout fit-to-contents. + return 0; + } + + // If dimensions are unknown (texture not yet loaded), use maximum width as a fallback as render width to allow proper bounds checking. + return (this._w || (this._source ? this._source.getRenderWidth() - this._x : 0)) / this._precision; + } + + getRenderHeight() { + if (!this.isAutosizeTexture()) { + // In case of the rectangle texture, we'd prefer to not cause a 1x1 w,h as it would interfere with flex layout fit-to-contents. + return 0; + } + + return (this._h || (this._source ? this._source.getRenderHeight() - this._y : 0)) / this._precision; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +Texture.prototype.isTexture = true; + +Texture.id = 0; + +class ImageTexture extends Texture { + + constructor(stage) { + super(stage); + + this._src = undefined; + this._hasAlpha = false; + } + + get src() { + return this._src; + } + + set src(v) { + if (this._src !== v) { + this._src = v; + this._changed(); + } + } + + get hasAlpha() { + return this._hasAlpha; + } + + set hasAlpha(v) { + if (this._hasAlpha !== v) { + this._hasAlpha = v; + this._changed(); + } + } + + _getIsValid() { + return !!this._src; + } + + _getLookupId() { + return this._src; + } + + _getSourceLoader() { + let src = this._src; + let hasAlpha = this._hasAlpha; + if (this.stage.getOption('srcBasePath')) { + var fc = src.charCodeAt(0); + if ((src.indexOf("//") === -1) && ((fc >= 65 && fc <= 90) || (fc >= 97 && fc <= 122) || fc == 46)) { + // Alphabetical or dot: prepend base path. + src = this.stage.getOption('srcBasePath') + src; + } + } + + return (cb) => { + return this.stage.platform.loadSrcTexture({src: src, hasAlpha: hasAlpha}, cb); + } + } + + getNonDefaults() { + const obj = super.getNonDefaults(); + if (this._src) { + obj.src = this._src; + } + return obj; + } + +} + +class TextTextureRenderer { + + constructor(stage, canvas, settings) { + this._stage = stage; + this._canvas = canvas; + this._context = this._canvas.getContext('2d'); + this._settings = settings; + } + + getPrecision() { + return this._settings.precision; + }; + + setFontProperties() { + this._context.font = Utils.isSpark ? this._stage.platform.getFontSetting(this) : this._getFontSetting(); + this._context.textBaseline = this._settings.textBaseline; + }; + + _getFontSetting() { + let ff = this._settings.fontFace; + + if (!Array.isArray(ff)) { + ff = [ff]; + } + + let ffs = []; + for (let i = 0, n = ff.length; i < n; i++) { + if (ff[i] === "serif" || ff[i] === "sans-serif") { + ffs.push(ff[i]); + } else { + ffs.push(`"${ff[i]}"`); + } + } + + return `${this._settings.fontStyle} ${this._settings.fontSize * this.getPrecision()}px ${ffs.join(",")}` + } + + _load() { + if (Utils.isWeb && document.fonts) { + const fontSetting = this._getFontSetting(); + try { + if (!document.fonts.check(fontSetting, this._settings.text)) { + // Use a promise that waits for loading. + return document.fonts.load(fontSetting, this._settings.text).catch(err => { + // Just load the fallback font. + console.warn('Font load error', err, fontSetting); + }).then(() => { + if (!document.fonts.check(fontSetting, this._settings.text)) { + console.warn('Font not found', fontSetting); + } + }); + } + } catch(e) { + console.warn("Can't check font loading for " + fontSetting); + } + } + } + + draw() { + // We do not use a promise so that loading is performed syncronous when possible. + const loadPromise = this._load(); + if (!loadPromise) { + return Utils.isSpark ? this._stage.platform.drawText(this) : this._draw(); + } else { + return loadPromise.then(() => { + return Utils.isSpark ? this._stage.platform.drawText(this) : this._draw(); + }); + } + } + + _draw() { + let renderInfo = {}; + + const precision = this.getPrecision(); + + let paddingLeft = this._settings.paddingLeft * precision; + let paddingRight = this._settings.paddingRight * precision; + const fontSize = this._settings.fontSize * precision; + let offsetY = this._settings.offsetY === null ? null : (this._settings.offsetY * precision); + let lineHeight = this._settings.lineHeight * precision; + const w = this._settings.w * precision; + const h = this._settings.h * precision; + let wordWrapWidth = this._settings.wordWrapWidth * precision; + const cutSx = this._settings.cutSx * precision; + const cutEx = this._settings.cutEx * precision; + const cutSy = this._settings.cutSy * precision; + const cutEy = this._settings.cutEy * precision; + + // Set font properties. + this.setFontProperties(); + + // Total width. + let width = w || (2048 / this.getPrecision()); + + // Inner width. + let innerWidth = width - (paddingLeft); + if (innerWidth < 10) { + width += (10 - innerWidth); + innerWidth += (10 - innerWidth); + } + + if (!wordWrapWidth) { + wordWrapWidth = innerWidth; + } + + // word wrap + // preserve original text + let linesInfo; + if (this._settings.wordWrap) { + linesInfo = this.wrapText(this._settings.text, wordWrapWidth); + } else { + linesInfo = {l: this._settings.text.split(/(?:\r\n|\r|\n)/), n: []}; + let n = linesInfo.l.length; + for (let i = 0; i < n - 1; i++) { + linesInfo.n.push(i); + } + } + let lines = linesInfo.l; + + if (this._settings.maxLines && lines.length > this._settings.maxLines) { + let usedLines = lines.slice(0, this._settings.maxLines); + + let otherLines = null; + if (this._settings.maxLinesSuffix) { + // Wrap again with max lines suffix enabled. + let w = this._settings.maxLinesSuffix ? this._context.measureText(this._settings.maxLinesSuffix).width : 0; + let al = this.wrapText(usedLines[usedLines.length - 1], wordWrapWidth - w); + usedLines[usedLines.length - 1] = al.l[0] + this._settings.maxLinesSuffix; + otherLines = [al.l.length > 1 ? al.l[1] : '']; + } else { + otherLines = ['']; + } + + // Re-assemble the remaining text. + let i, n = lines.length; + let j = 0; + let m = linesInfo.n.length; + for (i = this._settings.maxLines; i < n; i++) { + otherLines[j] += (otherLines[j] ? " " : "") + lines[i]; + if (i + 1 < m && linesInfo.n[i + 1]) { + j++; + } + } + + renderInfo.remainingText = otherLines.join("\n"); + + renderInfo.moreTextLines = true; + + lines = usedLines; + } else { + renderInfo.moreTextLines = false; + renderInfo.remainingText = ""; + } + + // calculate text width + let maxLineWidth = 0; + let lineWidths = []; + for (let i = 0; i < lines.length; i++) { + let lineWidth = this._context.measureText(lines[i]).width; + lineWidths.push(lineWidth); + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + + renderInfo.lineWidths = lineWidths; + + if (!w) { + // Auto-set width to max text length. + width = maxLineWidth + paddingLeft + paddingRight; + innerWidth = maxLineWidth; + } + + // calculate text height + lineHeight = lineHeight || fontSize; + + let height; + if (h) { + height = h; + } else { + height = lineHeight * (lines.length - 1) + 0.5 * fontSize + Math.max(lineHeight, fontSize) + offsetY; + } + + if (offsetY === null) { + offsetY = fontSize; + } + + renderInfo.w = width; + renderInfo.h = height; + renderInfo.lines = lines; + renderInfo.precision = precision; + + if (!width) { + // To prevent canvas errors. + width = 1; + } + + if (!height) { + // To prevent canvas errors. + height = 1; + } + + if (cutSx || cutEx) { + width = Math.min(width, cutEx - cutSx); + } + + if (cutSy || cutEy) { + height = Math.min(height, cutEy - cutSy); + } + + // Add extra margin to prevent issue with clipped text when scaling. + this._canvas.width = Math.ceil(width + this._stage.getOption('textRenderIssueMargin')); + this._canvas.height = Math.ceil(height); + + // Canvas context has been reset. + this.setFontProperties(); + + if (fontSize >= 128) { + // WpeWebKit bug: must force compositing because cairo-traps-compositor will not work with text first. + this._context.globalAlpha = 0.01; + this._context.fillRect(0, 0, 0.01, 0.01); + this._context.globalAlpha = 1.0; + } + + if (cutSx || cutSy) { + this._context.translate(-cutSx, -cutSy); + } + + let linePositionX; + let linePositionY; + + let drawLines = []; + + // Draw lines line by line. + for (let i = 0, n = lines.length; i < n; i++) { + linePositionX = 0; + linePositionY = (i * lineHeight) + offsetY; + + if (this._settings.textAlign === 'right') { + linePositionX += (innerWidth - lineWidths[i]); + } else if (this._settings.textAlign === 'center') { + linePositionX += ((innerWidth - lineWidths[i]) / 2); + } + linePositionX += paddingLeft; + + drawLines.push({text: lines[i], x: linePositionX, y: linePositionY, w: lineWidths[i]}); + } + + // Highlight. + if (this._settings.highlight) { + let color = this._settings.highlightColor || 0x00000000; + + let hlHeight = (this._settings.highlightHeight * precision || fontSize * 1.5); + let offset = (this._settings.highlightOffset !== null ? this._settings.highlightOffset * precision : -0.5 * fontSize); + const hlPaddingLeft = (this._settings.highlightPaddingLeft !== null ? this._settings.highlightPaddingLeft * precision : paddingLeft); + const hlPaddingRight = (this._settings.highlightPaddingRight !== null ? this._settings.highlightPaddingRight * precision : paddingRight); + + this._context.fillStyle = StageUtils.getRgbaString(color); + for (let i = 0; i < drawLines.length; i++) { + let drawLine = drawLines[i]; + this._context.fillRect((drawLine.x - hlPaddingLeft), (drawLine.y + offset), (drawLine.w + hlPaddingRight + hlPaddingLeft), hlHeight); + } + } + + // Text shadow. + let prevShadowSettings = null; + if (this._settings.shadow) { + prevShadowSettings = [this._context.shadowColor, this._context.shadowOffsetX, this._context.shadowOffsetY, this._context.shadowBlur]; + + this._context.shadowColor = StageUtils.getRgbaString(this._settings.shadowColor); + this._context.shadowOffsetX = this._settings.shadowOffsetX * precision; + this._context.shadowOffsetY = this._settings.shadowOffsetY * precision; + this._context.shadowBlur = this._settings.shadowBlur * precision; + } + + this._context.fillStyle = StageUtils.getRgbaString(this._settings.textColor); + for (let i = 0, n = drawLines.length; i < n; i++) { + let drawLine = drawLines[i]; + this._context.fillText(drawLine.text, drawLine.x, drawLine.y); + } + + if (prevShadowSettings) { + this._context.shadowColor = prevShadowSettings[0]; + this._context.shadowOffsetX = prevShadowSettings[1]; + this._context.shadowOffsetY = prevShadowSettings[2]; + this._context.shadowBlur = prevShadowSettings[3]; + } + + if (cutSx || cutSy) { + this._context.translate(cutSx, cutSy); + } + + this.renderInfo = renderInfo; + }; + + /** + * Applies newlines to a string to have it optimally fit into the horizontal + * bounds set by the Text object's wordWrapWidth property. + */ + wrapText(text, wordWrapWidth) { + // Greedy wrapping algorithm that will wrap words as the line grows longer. + // than its horizontal bounds. + let lines = text.split(/\r?\n/g); + let allLines = []; + let realNewlines = []; + for (let i = 0; i < lines.length; i++) { + let resultLines = []; + let result = ''; + let spaceLeft = wordWrapWidth; + let words = lines[i].split(' '); + for (let j = 0; j < words.length; j++) { + let wordWidth = this._context.measureText(words[j]).width; + let wordWidthWithSpace = wordWidth + this._context.measureText(' ').width; + if (j === 0 || wordWidthWithSpace > spaceLeft) { + // Skip printing the newline if it's the first word of the line that is. + // greater than the word wrap width. + if (j > 0) { + resultLines.push(result); + result = ''; + } + result += words[j]; + spaceLeft = wordWrapWidth - wordWidth; + } + else { + spaceLeft -= wordWidthWithSpace; + result += ' ' + words[j]; + } + } + + if (result) { + resultLines.push(result); + result = ''; + } + + allLines = allLines.concat(resultLines); + + if (i < lines.length - 1) { + realNewlines.push(allLines.length); + } + } + + return {l: allLines, n: realNewlines}; + }; + +} + +class TextTexture extends Texture { + + constructor(stage) { + super(stage); + + // We use the stage precision as the default precision in case of a text texture. + this._precision = this.stage.getOption('precision'); + } + + get text() { + return this._text; + } + + set text(v) { + if (this._text !== v) { + this._text = "" + v; + this._changed(); + } + } + + get w() { + return this._w; + } + + set w(v) { + if (this._w !== v) { + this._w = v; + this._changed(); + } + } + + get h() { + return this._h; + } + + set h(v) { + if (this._h !== v) { + this._h = v; + this._changed(); + } + } + + get fontStyle() { + return this._fontStyle; + } + + set fontStyle(v) { + if (this._fontStyle !== v) { + this._fontStyle = v; + this._changed(); + } + } + + get fontSize() { + return this._fontSize; + } + + set fontSize(v) { + if (this._fontSize !== v) { + this._fontSize = v; + this._changed(); + } + } + + get fontFace() { + return this._fontFace; + } + + set fontFace(v) { + if (this._fontFace !== v) { + this._fontFace = v; + this._changed(); + } + } + + get wordWrap() { + return this._wordWrap; + } + + set wordWrap(v) { + if (this._wordWrap !== v) { + this._wordWrap = v; + this._changed(); + } + } + + get wordWrapWidth() { + return this._wordWrapWidth; + } + + set wordWrapWidth(v) { + if (this._wordWrapWidth !== v) { + this._wordWrapWidth = v; + this._changed(); + } + } + + get lineHeight() { + return this._lineHeight; + } + + set lineHeight(v) { + if (this._lineHeight !== v) { + this._lineHeight = v; + this._changed(); + } + } + + get textBaseline() { + return this._textBaseline; + } + + set textBaseline(v) { + if (this._textBaseline !== v) { + this._textBaseline = v; + this._changed(); + } + } + + get textAlign() { + return this._textAlign; + } + + set textAlign(v) { + if (this._textAlign !== v) { + this._textAlign = v; + this._changed(); + } + } + + get offsetY() { + return this._offsetY; + } + + set offsetY(v) { + if (this._offsetY !== v) { + this._offsetY = v; + this._changed(); + } + } + + get maxLines() { + return this._maxLines; + } + + set maxLines(v) { + if (this._maxLines !== v) { + this._maxLines = v; + this._changed(); + } + } + + get maxLinesSuffix() { + return this._maxLinesSuffix; + } + + set maxLinesSuffix(v) { + if (this._maxLinesSuffix !== v) { + this._maxLinesSuffix = v; + this._changed(); + } + } + + get textColor() { + return this._textColor; + } + + set textColor(v) { + if (this._textColor !== v) { + this._textColor = v; + this._changed(); + } + } + + get paddingLeft() { + return this._paddingLeft; + } + + set paddingLeft(v) { + if (this._paddingLeft !== v) { + this._paddingLeft = v; + this._changed(); + } + } + + get paddingRight() { + return this._paddingRight; + } + + set paddingRight(v) { + if (this._paddingRight !== v) { + this._paddingRight = v; + this._changed(); + } + } + + get shadow() { + return this._shadow; + } + + set shadow(v) { + if (this._shadow !== v) { + this._shadow = v; + this._changed(); + } + } + + get shadowColor() { + return this._shadowColor; + } + + set shadowColor(v) { + if (this._shadowColor !== v) { + this._shadowColor = v; + this._changed(); + } + } + + get shadowOffsetX() { + return this._shadowOffsetX; + } + + set shadowOffsetX(v) { + if (this._shadowOffsetX !== v) { + this._shadowOffsetX = v; + this._changed(); + } + } + + get shadowOffsetY() { + return this._shadowOffsetY; + } + + set shadowOffsetY(v) { + if (this._shadowOffsetY !== v) { + this._shadowOffsetY = v; + this._changed(); + } + } + + get shadowBlur() { + return this._shadowBlur; + } + + set shadowBlur(v) { + if (this._shadowBlur !== v) { + this._shadowBlur = v; + this._changed(); + } + } + + get highlight() { + return this._highlight; + } + + set highlight(v) { + if (this._highlight !== v) { + this._highlight = v; + this._changed(); + } + } + + get highlightHeight() { + return this._highlightHeight; + } + + set highlightHeight(v) { + if (this._highlightHeight !== v) { + this._highlightHeight = v; + this._changed(); + } + } + + get highlightColor() { + return this._highlightColor; + } + + set highlightColor(v) { + if (this._highlightColor !== v) { + this._highlightColor = v; + this._changed(); + } + } + + get highlightOffset() { + return this._highlightOffset; + } + + set highlightOffset(v) { + if (this._highlightOffset !== v) { + this._highlightOffset = v; + this._changed(); + } + } + + get highlightPaddingLeft() { + return this._highlightPaddingLeft; + } + + set highlightPaddingLeft(v) { + if (this._highlightPaddingLeft !== v) { + this._highlightPaddingLeft = v; + this._changed(); + } + } + + get highlightPaddingRight() { + return this._highlightPaddingRight; + } + + set highlightPaddingRight(v) { + if (this._highlightPaddingRight !== v) { + this._highlightPaddingRight = v; + this._changed(); + } + } + + get cutSx() { + return this._cutSx; + } + + set cutSx(v) { + if (this._cutSx !== v) { + this._cutSx = v; + this._changed(); + } + } + + get cutEx() { + return this._cutEx; + } + + set cutEx(v) { + if (this._cutEx !== v) { + this._cutEx = v; + this._changed(); + } + } + + get cutSy() { + return this._cutSy; + } + + set cutSy(v) { + if (this._cutSy !== v) { + this._cutSy = v; + this._changed(); + } + } + + get cutEy() { + return this._cutEy; + } + + set cutEy(v) { + if (this._cutEy !== v) { + this._cutEy = v; + this._changed(); + } + } + + get precision() { + return super.precision; + } + + set precision(v) { + // We actually draw differently when the precision changes. + if (this.precision !== v) { + super.precision = v; + this._changed(); + } + } + + _getIsValid() { + return !!this.text; + } + + _getLookupId() { + let parts = []; + + if (this.w !== 0) parts.push("w " + this.w); + if (this.h !== 0) parts.push("h " + this.h); + if (this.fontStyle !== "normal") parts.push("fS" + this.fontStyle); + if (this.fontSize !== 40) parts.push("fs" + this.fontSize); + if (this.fontFace !== null) parts.push("ff" + (Array.isArray(this.fontFace) ? this.fontFace.join(",") : this.fontFace)); + if (this.wordWrap !== true) parts.push("wr" + (this.wordWrap ? 1 : 0)); + if (this.wordWrapWidth !== 0) parts.push("ww" + this.wordWrapWidth); + if (this.lineHeight !== null) parts.push("lh" + this.lineHeight); + if (this.textBaseline !== "alphabetic") parts.push("tb" + this.textBaseline); + if (this.textAlign !== "left") parts.push("ta" + this.textAlign); + if (this.offsetY !== null) parts.push("oy" + this.offsetY); + if (this.maxLines !== 0) parts.push("ml" + this.maxLines); + if (this.maxLinesSuffix !== "..") parts.push("ms" + this.maxLinesSuffix); + parts.push("pc" + this.precision); + if (this.textColor !== 0xffffffff) parts.push("co" + this.textColor.toString(16)); + if (this.paddingLeft !== 0) parts.push("pl" + this.paddingLeft); + if (this.paddingRight !== 0) parts.push("pr" + this.paddingRight); + if (this.shadow !== false) parts.push("sh" + (this.shadow ? 1 : 0)); + if (this.shadowColor !== 0xff000000) parts.push("sc" + this.shadowColor.toString(16)); + if (this.shadowOffsetX !== 0) parts.push("sx" + this.shadowOffsetX); + if (this.shadowOffsetY !== 0) parts.push("sy" + this.shadowOffsetY); + if (this.shadowBlur !== 5) parts.push("sb" + this.shadowBlur); + if (this.highlight !== false) parts.push("hL" + (this.highlight ? 1 : 0)); + if (this.highlightHeight !== 0) parts.push("hh" + this.highlightHeight); + if (this.highlightColor !== 0xff000000) parts.push("hc" + this.highlightColor.toString(16)); + if (this.highlightOffset !== null) parts.push("ho" + this.highlightOffset); + if (this.highlightPaddingLeft !== null) parts.push("hl" + this.highlightPaddingLeft); + if (this.highlightPaddingRight !== null) parts.push("hr" + this.highlightPaddingRight); + + if (this.cutSx) parts.push("csx" + this.cutSx); + if (this.cutEx) parts.push("cex" + this.cutEx); + if (this.cutSy) parts.push("csy" + this.cutSy); + if (this.cutEy) parts.push("cey" + this.cutEy); + + let id = "TX$" + parts.join("|") + ":" + this.text; + return id; + } + + _getSourceLoader() { + const args = this.cloneArgs(); + + // Inherit font face from stage. + if (args.fontFace === null) { + args.fontFace = this.stage.getOption('defaultFontFace'); + } + + return function(cb) { + const canvas = this.stage.platform.getDrawingCanvas(); + const renderer = new TextTextureRenderer(this.stage, canvas, args); + const p = renderer.draw(); + + if (p) { + p.then(() => { + cb(null, Object.assign({renderInfo: renderer.renderInfo}, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas))); + }).catch((err) => { + cb(err); + }); + } else { + cb(null, Object.assign({renderInfo: renderer.renderInfo}, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas))); + } + } + } + + getNonDefaults() { + const nonDefaults = super.getNonDefaults(); + if (this.text !== "") nonDefaults['text'] = this.text; + if (this.w !== 0) nonDefaults['w'] = this.w; + if (this.h !== 0) nonDefaults['h'] = this.h; + if (this.fontStyle !== "normal") nonDefaults['fontStyle'] = this.fontStyle; + if (this.fontSize !== 40) nonDefaults["fontSize"] = this.fontSize; + if (this.fontFace !== null) nonDefaults["fontFace"] = this.fontFace; + if (this.wordWrap !== true) nonDefaults["wordWrap"] = this.wordWrap; + if (this.wordWrapWidth !== 0) nonDefaults["wordWrapWidth"] = this.wordWrapWidth; + if (this.lineHeight !== null) nonDefaults["lineHeight"] = this.lineHeight; + if (this.textBaseline !== "alphabetic") nonDefaults["textBaseline"] = this.textBaseline; + if (this.textAlign !== "left") nonDefaults["textAlign"] = this.textAlign; + if (this.offsetY !== null) nonDefaults["offsetY"] = this.offsetY; + if (this.maxLines !== 0) nonDefaults["maxLines"] = this.maxLines; + if (this.maxLinesSuffix !== "..") nonDefaults["maxLinesSuffix"] = this.maxLinesSuffix; + if (this.precision !== this.stage.getOption('precision')) nonDefaults["precision"] = this.precision; + if (this.textColor !== 0xffffffff) nonDefaults["textColor"] = this.textColor; + if (this.paddingLeft !== 0) nonDefaults["paddingLeft"] = this.paddingLeft; + if (this.paddingRight !== 0) nonDefaults["paddingRight"] = this.paddingRight; + if (this.shadow !== false) nonDefaults["shadow"] = this.shadow; + if (this.shadowColor !== 0xff000000) nonDefaults["shadowColor"] = this.shadowColor; + if (this.shadowOffsetX !== 0) nonDefaults["shadowOffsetX"] = this.shadowOffsetX; + if (this.shadowOffsetY !== 0) nonDefaults["shadowOffsetY"] = this.shadowOffsetY; + if (this.shadowBlur !== 5) nonDefaults["shadowBlur"] = this.shadowBlur; + if (this.highlight !== false) nonDefaults["highlight"] = this.highlight; + if (this.highlightHeight !== 0) nonDefaults["highlightHeight"] = this.highlightHeight; + if (this.highlightColor !== 0xff000000) nonDefaults["highlightColor"] = this.highlightColor; + if (this.highlightOffset !== 0) nonDefaults["highlightOffset"] = this.highlightOffset; + if (this.highlightPaddingLeft !== 0) nonDefaults["highlightPaddingLeft"] = this.highlightPaddingLeft; + if (this.highlightPaddingRight !== 0) nonDefaults["highlightPaddingRight"] = this.highlightPaddingRight; + + if (this.cutSx) nonDefaults["cutSx"] = this.cutSx; + if (this.cutEx) nonDefaults["cutEx"] = this.cutEx; + if (this.cutSy) nonDefaults["cutSy"] = this.cutSy; + if (this.cutEy) nonDefaults["cutEy"] = this.cutEy; + return nonDefaults; + } + + cloneArgs() { + let obj = {}; + obj.text = this._text; + obj.w = this._w; + obj.h = this._h; + obj.fontStyle = this._fontStyle; + obj.fontSize = this._fontSize; + obj.fontFace = this._fontFace; + obj.wordWrap = this._wordWrap; + obj.wordWrapWidth = this._wordWrapWidth; + obj.lineHeight = this._lineHeight; + obj.textBaseline = this._textBaseline; + obj.textAlign = this._textAlign; + obj.offsetY = this._offsetY; + obj.maxLines = this._maxLines; + obj.maxLinesSuffix = this._maxLinesSuffix; + obj.precision = this._precision; + obj.textColor = this._textColor; + obj.paddingLeft = this._paddingLeft; + obj.paddingRight = this._paddingRight; + obj.shadow = this._shadow; + obj.shadowColor = this._shadowColor; + obj.shadowOffsetX = this._shadowOffsetX; + obj.shadowOffsetY = this._shadowOffsetY; + obj.shadowBlur = this._shadowBlur; + obj.highlight = this._highlight; + obj.highlightHeight = this._highlightHeight; + obj.highlightColor = this._highlightColor; + obj.highlightOffset = this._highlightOffset; + obj.highlightPaddingLeft = this._highlightPaddingLeft; + obj.highlightPaddingRight = this._highlightPaddingRight; + obj.cutSx = this._cutSx; + obj.cutEx = this._cutEx; + obj.cutSy = this._cutSy; + obj.cutEy = this._cutEy; + return obj; + } + + +} + +// Because there are so many properties, we prefer to use the prototype for default values. +// This causes a decrease in performance, but also a decrease in memory usage. +let proto = TextTexture.prototype; +proto._text = ""; +proto._w = 0; +proto._h = 0; +proto._fontStyle = "normal"; +proto._fontSize = 40; +proto._fontFace = null; +proto._wordWrap = true; +proto._wordWrapWidth = 0; +proto._lineHeight = null; +proto._textBaseline = "alphabetic"; +proto._textAlign = "left"; +proto._offsetY = null; +proto._maxLines = 0; +proto._maxLinesSuffix = ".."; +proto._textColor = 0xFFFFFFFF; +proto._paddingLeft = 0; +proto._paddingRight = 0; +proto._shadow = false; +proto._shadowColor = 0xFF000000; +proto._shadowOffsetX = 0; +proto._shadowOffsetY = 0; +proto._shadowBlur = 5; +proto._highlight = false; +proto._highlightHeight = 0; +proto._highlightColor = 0xFF000000; +proto._highlightOffset = 0; +proto._highlightPaddingLeft = 0; +proto._highlightPaddingRight = 0; +proto._cutSx = 0; +proto._cutEx = 0; +proto._cutSy = 0; +proto._cutEy = 0; + +class SourceTexture extends Texture { + + constructor(stage) { + super(stage); + + this._textureSource = undefined; + } + + get textureSource() { + return this._textureSource; + } + + set textureSource(v) { + if (v !== this._textureSource) { + if (v.isResultTexture) { + // In case of a result texture, automatically inherit the precision. + this._precision = this.stage.getRenderPrecision(); + } + this._textureSource = v; + this._changed(); + } + } + + _getTextureSource() { + return this._textureSource; + } + +} + +class Transition extends EventEmitter { + + constructor(manager, settings, element, property) { + super(); + + this.manager = manager; + + this._settings = settings; + + this._element = element; + this._getter = Element.getGetter(property); + this._setter = Element.getSetter(property); + + this._merger = settings.merger; + + if (!this._merger) { + this._merger = Element.getMerger(property); + } + + this._startValue = this._getter(this._element); + this._targetValue = this._startValue; + + this._p = 1; + this._delayLeft = 0; + } + + start(targetValue) { + this._startValue = this._getter(this._element); + + if (!this.isAttached()) { + // We don't support transitions on non-attached elements. Just set value without invoking listeners. + this._targetValue = targetValue; + this._p = 1; + this._updateDrawValue(); + } else { + if (targetValue === this._startValue) { + this.reset(targetValue, 1); + } else { + this._targetValue = targetValue; + this._p = 0; + this._delayLeft = this._settings.delay; + this.emit('start'); + this.add(); + } + } + } + + finish() { + if (this._p < 1) { + // Value setting and will must be invoked (async) upon next transition cycle. + this._p = 1; + } + } + + stop() { + // Just stop where the transition is at. + this.emit('stop'); + this.manager.removeActive(this); + } + + pause() { + this.stop(); + } + + play() { + this.manager.addActive(this); + } + + reset(targetValue, p) { + if (!this.isAttached()) { + // We don't support transitions on non-attached elements. Just set value without invoking listeners. + this._startValue = this._getter(this._element); + this._targetValue = targetValue; + this._p = 1; + this._updateDrawValue(); + } else { + this._startValue = this._getter(this._element); + this._targetValue = targetValue; + this._p = p; + this.add(); + } + } + + _updateDrawValue() { + this._setter(this._element, this.getDrawValue()); + } + + add() { + this.manager.addActive(this); + } + + isAttached() { + return this._element.attached; + } + + isRunning() { + return (this._p < 1.0); + } + + progress(dt) { + if (!this.isAttached()) { + // Skip to end of transition so that it is removed. + this._p = 1; + } + + if (this.p < 1) { + if (this.delayLeft > 0) { + this._delayLeft -= dt; + + if (this.delayLeft < 0) { + dt = -this.delayLeft; + this._delayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + if (this._settings.duration == 0) { + this._p = 1; + } else { + this._p += dt / this._settings.duration; + } + if (this._p >= 1) { + // Finished!; + this._p = 1; + } + } + + this._updateDrawValue(); + + this.invokeListeners(); + } + + invokeListeners() { + this.emit('progress', this.p); + if (this.p === 1) { + this.emit('finish'); + } + } + + updateTargetValue(targetValue) { + let t = this._settings.timingFunctionImpl(this.p); + if (t === 1) { + this._targetValue = targetValue; + } else if (t === 0) { + this._startValue = this._targetValue; + this._targetValue = targetValue; + } else { + this._startValue = targetValue - ((targetValue - this._targetValue) / (1 - t)); + this._targetValue = targetValue; + } + } + + getDrawValue() { + if (this.p >= 1) { + return this.targetValue; + } else { + let v = this._settings._timingFunctionImpl(this.p); + return this._merger(this.targetValue, this.startValue, v); + } + } + + skipDelay() { + this._delayLeft = 0; + } + + get startValue() { + return this._startValue; + } + + get targetValue() { + return this._targetValue; + } + + get p() { + return this._p; + } + + get delayLeft() { + return this._delayLeft; + } + + get element() { + return this._element; + } + + get settings() { + return this._settings; + } + + set settings(v) { + this._settings = v; + } + +} + +Transition.prototype.isTransition = true; + +/** + * Manages a list of objects. + * Objects may be patched. Then, they can be referenced using the 'ref' (string) property. + */ +class ObjectList { + + constructor() { + this._items = []; + this._refs = {}; + } + + get() { + return this._items; + } + + get first() { + return this._items[0]; + } + + get last() { + return this._items.length ? this._items[this._items.length - 1] : undefined; + } + + add(item) { + this.addAt(item, this._items.length); + } + + addAt(item, index) { + if (index >= 0 && index <= this._items.length) { + let currentIndex = this._items.indexOf(item); + if (currentIndex === index) { + return item; + } + + if (currentIndex != -1) { + this.setAt(item, index); + } else { + if (item.ref) { + this._refs[item.ref] = item; + } + this._items.splice(index, 0, item); + this.onAdd(item, index); + } + } else { + throw new Error('addAt: The index ' + index + ' is out of bounds ' + this._items.length); + } + } + + replaceByRef(item) { + if (item.ref) { + const existingItem = this.getByRef(item.ref); + if (!existingItem) { + throw new Error('replaceByRef: no item found with reference: ' + item.ref); + } + this.replace(item, existingItem); + } else { + throw new Error('replaceByRef: no ref specified in item'); + } + this.addAt(item, this._items.length); + + } + + replace(item, prevItem) { + const index = this.getIndex(prevItem); + if (index === -1) { + throw new Error('replace: The previous item does not exist'); + } + this.setAt(item, index); + } + + setAt(item, index) { + if (index >= 0 && index <= this._items.length) { + let currentIndex = this._items.indexOf(item); + if (currentIndex != -1) { + if (currentIndex !== index) { + const fromIndex = currentIndex; + if (fromIndex !== index) { + this._items.splice(fromIndex, 1); + this._items.splice(index, 0, item); + this.onMove(item, fromIndex, index); + } + } + } else { + if (index < this._items.length) { + if (this._items[index].ref) { + this._refs[this._items[index].ref] = undefined; + } + } + + const prevItem = this._items[index]; + + // Doesn't exist yet: overwrite current. + this._items[index] = item; + + if (item.ref) { + this._refs[item.ref] = item; + } + + this.onSet(item, index, prevItem); + } + } else { + throw new Error('setAt: The index ' + index + ' is out of bounds ' + this._items.length); + } + } + + getAt(index) { + return this._items[index]; + } + + getIndex(item) { + return this._items.indexOf(item); + } + + remove(item) { + let index = this._items.indexOf(item); + + if (index !== -1) { + this.removeAt(index); + } + }; + + removeAt(index) { + let item = this._items[index]; + + if (item.ref) { + this._refs[item.ref] = undefined; + } + + this._items.splice(index, 1); + + this.onRemove(item, index); + + return item; + }; + + clear() { + let n = this._items.length; + if (n) { + let prev = this._items; + this._items = []; + this._refs = {}; + this.onSync(prev, [], []); + } + }; + + a(o) { + if (Utils.isObjectLiteral(o)) { + let c = this.createItem(o); + c.patch(o); + this.add(c); + return c; + } else if (Array.isArray(o)) { + for (let i = 0, n = o.length; i < n; i++) { + this.a(o[i]); + } + return null; + } else if (this.isItem(o)) { + this.add(o); + return o; + } + }; + + get length() { + return this._items.length; + } + + _getRefs() { + return this._refs; + } + + getByRef(ref) { + return this._refs[ref]; + } + + clearRef(ref) { + delete this._refs[ref]; + } + + setRef(ref, child) { + this._refs[ref] = child; + } + + patch(settings) { + if (Utils.isObjectLiteral(settings)) { + this._setByObject(settings); + } else if (Array.isArray(settings)) { + this._setByArray(settings); + } + } + + _setByObject(settings) { + // Overrule settings of known referenced items. + let refs = this._getRefs(); + let crefs = Object.keys(settings); + for (let i = 0, n = crefs.length; i < n; i++) { + let cref = crefs[i]; + let s = settings[cref]; + + let c = refs[cref]; + if (!c) { + if (this.isItem(s)) { + // Replace previous item; + s.ref = cref; + this.add(s); + } else { + // Create new item. + c = this.createItem(s); + c.ref = cref; + c.patch(s); + this.add(c); + } + } else { + if (this.isItem(s)) { + if (c !== s) { + // Replace previous item; + let idx = this.getIndex(c); + s.ref = cref; + this.setAt(s, idx); + } + } else { + c.patch(s); + } + } + } + } + + _equalsArray(array) { + let same = true; + if (array.length === this._items.length) { + for (let i = 0, n = this._items.length; (i < n) && same; i++) { + same = same && (this._items[i] === array[i]); + } + } else { + same = false; + } + return same; + } + + _setByArray(array) { + // For performance reasons, first check if the arrays match exactly and bail out if they do. + if (this._equalsArray(array)) { + return; + } + + for (let i = 0, n = this._items.length; i < n; i++) { + this._items[i].marker = true; + } + + let refs; + let newItems = []; + for (let i = 0, n = array.length; i < n; i++) { + let s = array[i]; + if (this.isItem(s)) { + s.marker = false; + newItems.push(s); + } else { + let cref = s.ref; + let c; + if (cref) { + if (!refs) refs = this._getRefs(); + c = refs[cref]; + } + + if (!c) { + // Create new item. + c = this.createItem(s); + } else { + c.marker = false; + } + + if (Utils.isObjectLiteral(s)) { + c.patch(s); + } + + newItems.push(c); + } + } + + this._setItems(newItems); + } + + _setItems(newItems) { + let prevItems = this._items; + this._items = newItems; + + // Remove the items. + let removed = prevItems.filter(item => {let m = item.marker; delete item.marker; return m}); + let added = newItems.filter(item => (prevItems.indexOf(item) === -1)); + + if (removed.length || added.length) { + // Recalculate refs. + this._refs = {}; + for (let i = 0, n = this._items.length; i < n; i++) { + let ref = this._items[i].ref; + if (ref) { + this._refs[ref] = this._items[i]; + } + } + } + + this.onSync(removed, added, newItems); + } + + sort(f) { + const items = this._items.slice(); + items.sort(f); + this._setByArray(items); + } + + onAdd(item, index) { + } + + onRemove(item, index) { + } + + onSync(removed, added, order) { + } + + onSet(item, index, prevItem) { + } + + onMove(item, fromIndex, toIndex) { + } + + createItem(object) { + throw new Error("ObjectList.createItem must create and return a new object"); + } + + isItem(object) { + return false; + } + + forEach(f) { + this.get().forEach(f); + } + +} + +/** + * Manages the list of children for an element. + */ + +class ElementChildList extends ObjectList { + + constructor(element) { + super(); + this._element = element; + } + + _connectParent(item) { + const prevParent = item.parent; + if (prevParent && prevParent !== this._element) { + // Cleanup in previous child list, without + const prevChildList = item.parent.childList; + const index = prevChildList.getIndex(item); + + if (item.ref) { + prevChildList._refs[item.ref] = undefined; + } + prevChildList._items.splice(index, 1); + + // Also clean up element core. + prevParent.core.removeChildAt(index); + + } + + item._setParent(this._element); + + // We are expecting the caller to sync it to the core. + } + + onAdd(item, index) { + this._connectParent(item); + this._element.core.addChildAt(index, item.core); + } + + onRemove(item, index) { + item._setParent(null); + this._element.core.removeChildAt(index); + } + + onSync(removed, added, order) { + for (let i = 0, n = removed.length; i < n; i++) { + removed[i]._setParent(null); + } + for (let i = 0, n = added.length; i < n; i++) { + this._connectParent(added[i]); + } + let gc = i => i.core; + this._element.core.syncChildren(removed.map(gc), added.map(gc), order.map(gc)); + } + + onSet(item, index, prevItem) { + prevItem._setParent(null); + + this._connectParent(item); + this._element.core.setChildAt(index, item.core); + } + + onMove(item, fromIndex, toIndex) { + this._element.core.moveChild(fromIndex, toIndex); + } + + createItem(object) { + if (object.type) { + return new object.type(this._element.stage); + } else { + return this._element.stage.createElement(); + } + } + + isItem(object) { + return object.isElement; + } + +} + +/** + * Render tree node. + * Copyright Metrological, 2017 + */ + +class Element { + + constructor(stage) { + this.stage = stage; + + this.__id = Element.id++; + + this.__start(); + + // EventEmitter constructor. + this._hasEventListeners = false; + + this.__core = new ElementCore(this); + + /** + * A reference that can be used while merging trees. + * @type {string} + */ + this.__ref = null; + + /** + * An element is attached if it is a descendant of the stage root. + * @type {boolean} + */ + this.__attached = false; + + /** + * An element is enabled when it is attached and it is visible (worldAlpha > 0). + * @type {boolean} + */ + this.__enabled = false; + + /** + * An element is active when it is enabled and it is within bounds. + * @type {boolean} + */ + this.__active = false; + + /** + * @type {Element} + */ + this.__parent = null; + + /** + * The texture that is currently set. + * @type {Texture} + */ + this.__texture = null; + + /** + * The currently displayed texture. While this.texture is loading, this one may be different. + * @type {Texture} + */ + this.__displayedTexture = null; + + /** + * Tags that can be used to identify/search for a specific element. + * @type {String[]} + */ + this.__tags = null; + + /** + * The tree's tags mapping. + * This contains all elements for all known tags, at all times. + * @type {Map} + */ + this.__treeTags = null; + + /** + * Creates a tag context: tagged elements in this branch will not be reachable from ancestors of this elements. + * @type {boolean} + */ + this.__tagRoot = false; + + /** + * (Lazy-initialised) list of children owned by this elements. + * @type {ElementChildList} + */ + this.__childList = null; + + this._w = 0; + + this._h = 0; + } + + __start() { + } + + get id() { + return this.__id; + } + + set ref(ref) { + if (this.__ref !== ref) { + const charcode = ref.charCodeAt(0); + if (!Utils.isUcChar(charcode)) { + this._throwError("Ref must start with an upper case character: " + ref); + } + if (this.__ref !== null) { + this.removeTag(this.__ref); + if (this.__parent) { + this.__parent.__childList.clearRef(this.__ref); + } + } + + this.__ref = ref; + + if (this.__ref) { + this._addTag(this.__ref); + if (this.__parent) { + this.__parent.__childList.setRef(this.__ref, this); + } + } + } + } + + get ref() { + return this.__ref; + } + + get core() { + return this.__core; + } + + setAsRoot() { + this.__core.setAsRoot(); + this._updateAttachedFlag(); + this._updateEnabledFlag(); + } + + get isRoot() { + return this.__core.isRoot; + } + + _setParent(parent) { + if (this.__parent === parent) return; + + if (this.__parent) { + this._unsetTagsParent(); + } + + this.__parent = parent; + + if (parent) { + this._setTagsParent(); + } + + this._updateAttachedFlag(); + this._updateEnabledFlag(); + + if (this.isRoot && parent) { + this._throwError("Root should not be added as a child! Results are unspecified!"); + } + }; + + getDepth() { + let depth = 0; + + let p = this.__parent; + while(p) { + depth++; + p = p.__parent; + } + + return depth; + }; + + getAncestor(l) { + let p = this; + while (l > 0 && p.__parent) { + p = p.__parent; + l--; + } + return p; + }; + + getAncestorAtDepth(depth) { + let levels = this.getDepth() - depth; + if (levels < 0) { + return null; + } + return this.getAncestor(levels); + }; + + isAncestorOf(c) { + let p = c; + while(p = p.parent) { + if (this === p) { + return true; + } + } + return false; + }; + + getSharedAncestor(c) { + let o1 = this; + let o2 = c; + let l1 = o1.getDepth(); + let l2 = o2.getDepth(); + if (l1 > l2) { + o1 = o1.getAncestor(l1 - l2); + } else if (l2 > l1) { + o2 = o2.getAncestor(l2 - l1); + } + + do { + if (o1 === o2) { + return o1; + } + + o1 = o1.__parent; + o2 = o2.__parent; + } while (o1 && o2); + + return null; + }; + + get attached() { + return this.__attached; + } + + get enabled() { + return this.__enabled; + } + + get active() { + return this.__active; + } + + _isAttached() { + return (this.__parent ? this.__parent.__attached : (this.stage.root === this)); + }; + + _isEnabled() { + return this.__core.visible && (this.__core.alpha > 0) && (this.__parent ? this.__parent.__enabled : (this.stage.root === this)); + }; + + _isActive() { + return this._isEnabled() && this.withinBoundsMargin; + }; + + /** + * Updates the 'attached' flag for this branch. + */ + _updateAttachedFlag() { + let newAttached = this._isAttached(); + if (this.__attached !== newAttached) { + this.__attached = newAttached; + + if (newAttached) { + this._onSetup(); + } + + let children = this._children.get(); + if (children) { + let m = children.length; + if (m > 0) { + for (let i = 0; i < m; i++) { + children[i]._updateAttachedFlag(); + } + } + } + + if (newAttached) { + this._onAttach(); + } else { + this._onDetach(); + } + } + }; + + /** + * Updates the 'enabled' flag for this branch. + */ + _updateEnabledFlag() { + let newEnabled = this._isEnabled(); + if (this.__enabled !== newEnabled) { + if (newEnabled) { + this._onEnabled(); + this._setEnabledFlag(); + } else { + this._onDisabled(); + this._unsetEnabledFlag(); + } + + let children = this._children.get(); + if (children) { + let m = children.length; + if (m > 0) { + for (let i = 0; i < m; i++) { + children[i]._updateEnabledFlag(); + } + } + } + } + }; + + _setEnabledFlag() { + this.__enabled = true; + + // Force re-check of texture because dimensions might have changed (cutting). + this._updateDimensions(); + this._updateTextureCoords(); + + if (this.__texture) { + this.__texture.addElement(this); + } + + if (this.withinBoundsMargin) { + this._setActiveFlag(); + } + + if (this.__core.shader) { + this.__core.shader.addElement(this.__core); + } + + } + + _unsetEnabledFlag() { + if (this.__active) { + this._unsetActiveFlag(); + } + + if (this.__texture) { + this.__texture.removeElement(this); + } + + if (this.__core.shader) { + this.__core.shader.removeElement(this.__core); + } + + if (this._texturizer) { + this.texturizer.filters.forEach(filter => filter.removeElement(this.__core)); + } + + this.__enabled = false; + } + + _setActiveFlag() { + this.__active = true; + + // This must happen before enabling the texture, because it may already be loaded or load directly. + if (this.__texture) { + this.__texture.incActiveCount(); + } + + if (this.__texture) { + this._enableTexture(); + } + this._onActive(); + } + + _unsetActiveFlag() { + if (this.__texture) { + this.__texture.decActiveCount(); + } + + this.__active = false; + if (this.__texture) { + this._disableTexture(); + } + + if (this._hasTexturizer()) { + this.texturizer.deactivate(); + } + + this._onInactive(); + } + + _onSetup() { + } + + _onAttach() { + } + + _onDetach() { + } + + _onEnabled() { + } + + _onDisabled() { + } + + _onActive() { + } + + _onInactive() { + } + + _onResize() { + } + + _getRenderWidth() { + if (this._w) { + return this._w; + } else if (this.__displayedTexture) { + return this.__displayedTexture.getRenderWidth(); + } else if (this.__texture) { + // Texture already loaded, but not yet updated (probably because this element is not active). + return this.__texture.getRenderWidth(); + } else { + return 0; + } + }; + + _getRenderHeight() { + if (this._h) { + return this._h; + } else if (this.__displayedTexture) { + return this.__displayedTexture.getRenderHeight(); + } else if (this.__texture) { + // Texture already loaded, but not yet updated (probably because this element is not active). + return this.__texture.getRenderHeight(); + } else { + return 0; + } + }; + + get renderWidth() { + if (this.__enabled) { + // Render width is only maintained if this element is enabled. + return this.__core.getRenderWidth(); + } else { + return this._getRenderWidth(); + } + } + + get renderHeight() { + if (this.__enabled) { + return this.__core.getRenderHeight(); + } else { + return this._getRenderHeight(); + } + } + + get finalX() { + return this.__core.x; + } + + get finalY() { + return this.__core.y; + } + + get finalW() { + return this.__core.w; + } + + get finalH() { + return this.__core.h; + } + + textureIsLoaded() { + return this.__texture && this.__texture.isLoaded(); + } + + loadTexture() { + if (this.__texture) { + this.__texture.load(); + + if (!this.__texture.isUsed() || !this._isEnabled()) { + // Loading the texture will have no effect on the dimensions of this element. + // Manually update them, so that calcs can be performed immediately in userland. + this._updateDimensions(); + } + } + } + + _enableTextureError() { + // txError event should automatically be re-triggered when a element becomes active. + const loadError = this.__texture.loadError; + if (loadError) { + this.emit('txError', loadError, this.__texture._source); + } + } + + _enableTexture() { + if (this.__texture.isLoaded()) { + this._setDisplayedTexture(this.__texture); + } else { + // We don't want to retain the old 'ghost' image as it wasn't visible anyway. + this._setDisplayedTexture(null); + + this._enableTextureError(); + } + } + + _disableTexture() { + // We disable the displayed texture because, when the texture changes while invisible, we should use that w, h, + // mw, mh for checking within bounds. + this._setDisplayedTexture(null); + } + + get texture() { + return this.__texture; + } + + set texture(v) { + let texture; + if (Utils.isObjectLiteral(v)) { + if (v.type) { + texture = new v.type(this.stage); + } else { + texture = this.texture; + } + + if (texture) { + Base.patchObject(texture, v); + } + } else if (!v) { + texture = null; + } else { + if (v.isTexture) { + texture = v; + } else if (v.isTextureSource) { + texture = new SourceTexture(this.stage); + texture.textureSource = v; + } else { + console.error("Please specify a texture type."); + return; + } + } + + const prevTexture = this.__texture; + if (texture !== prevTexture) { + this.__texture = texture; + + if (this.__texture) { + if (this.__enabled) { + this.__texture.addElement(this); + + if (this.withinBoundsMargin) { + if (this.__texture.isLoaded()) { + this._setDisplayedTexture(this.__texture); + } else { + this._enableTextureError(); + } + } + } + } else { + // Make sure that current texture is cleared when the texture is explicitly set to null. + this._setDisplayedTexture(null); + } + + if (prevTexture && prevTexture !== this.__displayedTexture) { + prevTexture.removeElement(this); + } + + this._updateDimensions(); + } + } + + get displayedTexture() { + return this.__displayedTexture; + } + + _setDisplayedTexture(v) { + let prevTexture = this.__displayedTexture; + + if (prevTexture && (v !== prevTexture)) { + if (this.__texture !== prevTexture) { + // The old displayed texture is deprecated. + prevTexture.removeElement(this); + } + } + + const prevSource = this.__core.displayedTextureSource ? this.__core.displayedTextureSource._source : null; + const sourceChanged = (v ? v._source : null) !== prevSource; + + this.__displayedTexture = v; + this._updateDimensions(); + + if (this.__displayedTexture) { + if (sourceChanged) { + // We don't need to reference the displayed texture because it was already referenced (this.texture === this.displayedTexture). + this._updateTextureCoords(); + this.__core.setDisplayedTextureSource(this.__displayedTexture._source); + } + } else { + this.__core.setDisplayedTextureSource(null); + } + + if (sourceChanged) { + if (this.__displayedTexture) { + this.emit('txLoaded', this.__displayedTexture); + } else { + this.emit('txUnloaded', this.__displayedTexture); + } + } + } + + onTextureSourceLoaded() { + // This function is called when element is enabled, but we only want to set displayed texture for active elements. + if (this.active) { + // We may be dealing with a texture reloading, so we must force update. + this._setDisplayedTexture(this.__texture); + } + }; + + onTextureSourceLoadError(e) { + this.emit('txError', e, this.__texture._source); + }; + + forceRenderUpdate() { + this.__core.setHasRenderUpdates(3); + } + + onDisplayedTextureClippingChanged() { + this._updateDimensions(); + this._updateTextureCoords(); + }; + + onPrecisionChanged() { + this._updateDimensions(); + }; + + onDimensionsChanged(w, h) { + if (this.texture instanceof TextTexture) { + this.texture.w = w; + this.texture.h = h; + this.w = w; + this.h = h; + } + } + + _updateDimensions() { + let w = this._getRenderWidth(); + let h = this._getRenderHeight(); + + let unknownSize = false; + if (!w || !h) { + if (!this.__displayedTexture && this.__texture) { + // We use a 'max width' replacement instead in the ElementCore calcs. + // This makes sure that it is able to determine withinBounds. + w = w || this.__texture.mw; + h = h || this.__texture.mh; + + if ((!w || !h) && this.__texture.isAutosizeTexture()) { + unknownSize = true; + } + } + } + + if (this.__core.setDimensions(w, h, unknownSize)) { + this._onResize(); + } + } + + _updateTextureCoords() { + if (this.displayedTexture && this.displayedTexture._source) { + let displayedTexture = this.displayedTexture; + let displayedTextureSource = this.displayedTexture._source; + + let tx1 = 0, ty1 = 0, tx2 = 1.0, ty2 = 1.0; + if (displayedTexture.clipping) { + // Apply texture clipping. + let w = displayedTextureSource.getRenderWidth(); + let h = displayedTextureSource.getRenderHeight(); + let iw, ih, rw, rh; + iw = 1 / w; + ih = 1 / h; + + if (displayedTexture.pw) { + rw = (displayedTexture.pw) * iw; + } else { + rw = (w - displayedTexture.px) * iw; + } + + if (displayedTexture.ph) { + rh = displayedTexture.ph * ih; + } else { + rh = (h - displayedTexture.py) * ih; + } + + iw *= (displayedTexture.px); + ih *= (displayedTexture.py); + + tx1 = iw; + ty1 = ih; + tx2 = tx2 * rw + iw; + ty2 = ty2 * rh + ih; + + tx1 = Math.max(0, tx1); + ty1 = Math.max(0, ty1); + tx2 = Math.min(1, tx2); + ty2 = Math.min(1, ty2); + } + + if (displayedTextureSource._flipTextureY) { + let tempty = ty2; + ty2 = ty1; + ty1 = tempty; + } + this.__core.setTextureCoords(tx1, ty1, tx2, ty2); + } + } + + getCornerPoints() { + return this.__core.getCornerPoints(); + } + + _unsetTagsParent() { + if (this.__tags) { + this.__tags.forEach((tag) => { + // Remove from treeTags. + let p = this; + while (p = p.__parent) { + let parentTreeTags = p.__treeTags.get(tag); + parentTreeTags.delete(this); + + if (p.__tagRoot) { + break; + } + } + }); + } + + let tags = null; + let n = 0; + if (this.__treeTags) { + if (!this.__tagRoot) { + tags = Utils.iteratorToArray(this.__treeTags.keys()); + n = tags.length; + + if (n > 0) { + for (let i = 0; i < n; i++) { + let tagSet = this.__treeTags.get(tags[i]); + + // Remove from treeTags. + let p = this; + while ((p = p.__parent)) { + let parentTreeTags = p.__treeTags.get(tags[i]); + + tagSet.forEach(function (comp) { + parentTreeTags.delete(comp); + }); + + if (p.__tagRoot) { + break; + } + } + } + } + } + } + }; + + _setTagsParent() { + // Just copy over the 'local' tags. + if (this.__tags) { + this.__tags.forEach((tag) => { + let p = this; + while (p = p.__parent) { + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + s.add(this); + + if (p.__tagRoot) { + break; + } + } + }); + } + + if (this.__treeTags && this.__treeTags.size) { + if (!this.__tagRoot) { + this.__treeTags.forEach((tagSet, tag) => { + let p = this; + while (!p.__tagRoot && (p = p.__parent)) { + if (p.__tagRoot) ; + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + tagSet.forEach(function (comp) { + s.add(comp); + }); + } + }); + } + } + }; + + + _getByTag(tag) { + if (!this.__treeTags) { + return []; + } + let t = this.__treeTags.get(tag); + return t ? Utils.setToArray(t) : []; + }; + + getTags() { + return this.__tags ? this.__tags : []; + }; + + setTags(tags) { + tags = tags.reduce((acc, tag) => { + return acc.concat(tag.split(' ')); + }, []); + + if (this.__ref) { + tags.push(this.__ref); + } + + let i, n = tags.length; + let removes = []; + let adds = []; + for (i = 0; i < n; i++) { + if (!this.hasTag(tags[i])) { + adds.push(tags[i]); + } + } + + let currentTags = this.tags || []; + n = currentTags.length; + for (i = 0; i < n; i++) { + if (tags.indexOf(currentTags[i]) == -1) { + removes.push(currentTags[i]); + } + } + + for (i = 0; i < removes.length; i++) { + this.removeTag(removes[i]); + } + + for (i = 0; i < adds.length; i++) { + this.addTag(adds[i]); + } + } + + addTag(tag) { + if (tag.indexOf(' ') === -1) { + if (Utils.isUcChar(tag.charCodeAt(0))) { + this._throwError("Tag may not start with an upper case character."); + } + + this._addTag(tag); + } else { + const tags = tag.split(' '); + for (let i = 0, m = tags.length; i < m; i++) { + const tag = tags[i]; + + if (Utils.isUcChar(tag.charCodeAt(0))) { + this._throwError("Tag may not start with an upper case character."); + } + + this._addTag(tag); + } + } + } + + _addTag(tag) { + if (!this.__tags) { + this.__tags = []; + } + if (this.__tags.indexOf(tag) === -1) { + this.__tags.push(tag); + + // Add to treeTags hierarchy. + let p = this.__parent; + if (p) { + do { + if (!p.__treeTags) { + p.__treeTags = new Map(); + } + + let s = p.__treeTags.get(tag); + if (!s) { + s = new Set(); + p.__treeTags.set(tag, s); + } + + s.add(this); + + } while (!p.__tagRoot && (p = p.__parent)); + } + } + } + + removeTag(tag) { + let i = this.__tags.indexOf(tag); + if (i !== -1) { + this.__tags.splice(i, 1); + + // Remove from treeTags hierarchy. + let p = this.__parent; + if (p) { + do { + let list = p.__treeTags.get(tag); + if (list) { + list.delete(this); + } + } while (!p.__tagRoot && (p = p.__parent)); + } + } + } + + hasTag(tag) { + return (this.__tags && (this.__tags.indexOf(tag) !== -1)); + } + + /** + * Returns one of the elements from the subtree that have this tag. + * @param {string} tag + * @returns {Element} + */ + _tag(tag) { + if (tag.indexOf(".") !== -1) { + return this.mtag(tag)[0]; + } else { + if (this.__treeTags) { + let t = this.__treeTags.get(tag); + if (t) { + const item = t.values().next(); + return item ? item.value : undefined; + } + } + } + }; + + get tag() { + return this._tag; + } + + set tag(t) { + this.tags = t; + } + + /** + * Returns all elements from the subtree that have this tag. + * @param {string} tag + * @returns {Element[]} + */ + mtag(tag) { + let idx = tag.indexOf("."); + if (idx >= 0) { + let parts = tag.split('.'); + let res = this._getByTag(parts[0]); + let level = 1; + let c = parts.length; + while (res.length && level < c) { + let resn = []; + for (let j = 0, n = res.length; j < n; j++) { + resn = resn.concat(res[j]._getByTag(parts[level])); + } + + res = resn; + level++; + } + return res; + } else { + return this._getByTag(tag); + } + }; + + stag(tag, settings) { + let t = this.mtag(tag); + let n = t.length; + for (let i = 0; i < n; i++) { + Base.patchObject(t[i], settings); + } + } + + get tagRoot() { + return this.__tagRoot; + } + + set tagRoot(v) { + if (this.__tagRoot !== v) { + if (!v) { + this._setTagsParent(); + } else { + this._unsetTagsParent(); + } + + this.__tagRoot = v; + } + } + + sel(path) { + const results = this.select(path); + if (results.length) { + return results[0]; + } else { + return undefined; + } + } + + select(path) { + if (path.indexOf(",") !== -1) { + let selectors = path.split(','); + let res = []; + for (let i = 0; i < selectors.length; i++) { + res = res.concat(this._select(selectors[i])); + } + return res; + } else { + return this._select(path); + } + } + + _select(path) { + if (path === "") return [this]; + + + let pointIdx = path.indexOf("."); + let arrowIdx = path.indexOf(">"); + if (pointIdx === -1 && arrowIdx === -1) { + // Quick case. + return this.mtag(path); + } + + // Detect by first char. + let isRef; + if (arrowIdx === 0) { + isRef = true; + path = path.substr(1); + } else if (pointIdx === 0) { + isRef = false; + path = path.substr(1); + } else { + isRef = false; + } + + return this._selectChilds(path, isRef); + } + + _selectChilds(path, isRef) { + const pointIdx = path.indexOf("."); + const arrowIdx = path.indexOf(">"); + + if (pointIdx === -1 && arrowIdx === -1) { + if (isRef) { + const ref = this.getByRef(path); + return ref ? [ref] : []; + } else { + return this.mtag(path); + } + } + + if ((arrowIdx === -1) || (pointIdx !== -1 && pointIdx < arrowIdx)) { + let next; + const str = path.substr(0, pointIdx); + if (isRef) { + const ref = this.getByRef(str); + next = ref ? [ref] : []; + } else { + next = this.mtag(str); + } + let total = []; + const subPath = path.substr(pointIdx + 1); + for (let i = 0, n = next.length; i < n; i++) { + total = total.concat(next[i]._selectChilds(subPath, false)); + } + return total; + } else { + let next; + const str = path.substr(0, arrowIdx); + if (isRef) { + const ref = this.getByRef(str); + next = ref ? [ref] : []; + } else { + next = this.mtag(str); + } + let total = []; + const subPath = path.substr(arrowIdx + 1); + for (let i = 0, n = next.length; i < n; i++) { + total = total.concat(next[i]._selectChilds(subPath, true)); + } + return total; + } + } + + getByRef(ref) { + return this.childList.getByRef(ref); + } + + getLocationString() { + let i; + i = this.__parent ? this.__parent._children.getIndex(this) : "R"; + let localTags = this.getTags(); + let str = this.__parent ? this.__parent.getLocationString(): ""; + if (this.ref) { + str += ":[" + i + "]" + this.ref; + } else if (localTags.length) { + str += ":[" + i + "]" + localTags.join(","); + } else { + str += ":[" + i + "]#" + this.id; + } + return str; + } + + toString() { + let obj = this.getSettings(); + return Element.getPrettyString(obj, ""); + }; + + static getPrettyString(obj, indent) { + let children = obj.children; + delete obj.children; + + + // Convert singular json settings object. + let colorKeys = ["color", "colorUl", "colorUr", "colorBl", "colorBr"]; + let str = JSON.stringify(obj, function (k, v) { + if (colorKeys.indexOf(k) !== -1) { + return "COLOR[" + v.toString(16) + "]"; + } + return v; + }); + str = str.replace(/"COLOR\[([a-f0-9]{1,8})\]"/g, "0x$1"); + + if (children) { + let childStr = ""; + if (Utils.isObjectLiteral(children)) { + let refs = Object.keys(children); + childStr = ""; + for (let i = 0, n = refs.length; i < n; i++) { + childStr += `\n${indent} "${refs[i]}":`; + delete children[refs[i]].ref; + childStr += Element.getPrettyString(children[refs[i]], indent + " ") + (i < n - 1 ? "," : ""); + } + let isEmpty = (str === "{}"); + str = str.substr(0, str.length - 1) + (isEmpty ? "" : ",") + childStr + "\n" + indent + "}"; + } else { + let n = children.length; + childStr = "["; + for (let i = 0; i < n; i++) { + childStr += Element.getPrettyString(children[i], indent + " ") + (i < n - 1 ? "," : "") + "\n"; + } + childStr += indent + "]}"; + let isEmpty = (str === "{}"); + str = str.substr(0, str.length - 1) + (isEmpty ? "" : ",") + "\"children\":\n" + indent + childStr + "}"; + } + + } + + return str; + } + + getSettings() { + let settings = this.getNonDefaults(); + + let children = this._children.get(); + if (children) { + let n = children.length; + if (n) { + const childArray = []; + let missing = false; + for (let i = 0; i < n; i++) { + childArray.push(children[i].getSettings()); + missing = missing || !children[i].ref; + } + + if (!missing) { + settings.children = {}; + childArray.forEach(child => { + settings.children[child.ref] = child; + }); + } else { + settings.children = childArray; + } + } + } + + settings.id = this.id; + + return settings; + } + + getNonDefaults() { + let settings = {}; + + if (this.constructor !== Element) { + settings.type = this.constructor.name; + } + + if (this.__ref) { + settings.ref = this.__ref; + } + + if (this.__tags && this.__tags.length) { + settings.tags = this.__tags; + } + + if (this.x !== 0) settings.x = this.x; + if (this.y !== 0) settings.y = this.y; + if (this.w !== 0) settings.w = this.w; + if (this.h !== 0) settings.h = this.h; + + if (this.scaleX === this.scaleY) { + if (this.scaleX !== 1) settings.scale = this.scaleX; + } else { + if (this.scaleX !== 1) settings.scaleX = this.scaleX; + if (this.scaleY !== 1) settings.scaleY = this.scaleY; + } + + if (this.pivotX === this.pivotY) { + if (this.pivotX !== 0.5) settings.pivot = this.pivotX; + } else { + if (this.pivotX !== 0.5) settings.pivotX = this.pivotX; + if (this.pivotY !== 0.5) settings.pivotY = this.pivotY; + } + + if (this.mountX === this.mountY) { + if (this.mountX !== 0) settings.mount = this.mountX; + } else { + if (this.mountX !== 0) settings.mountX = this.mountX; + if (this.mountY !== 0) settings.mountY = this.mountY; + } + + if (this.alpha !== 1) settings.alpha = this.alpha; + + if (!this.visible) settings.visible = false; + + if (this.rotation !== 0) settings.rotation = this.rotation; + + if (this.colorUl === this.colorUr && this.colorBl === this.colorBr && this.colorUl === this.colorBl) { + if (this.colorUl !== 0xFFFFFFFF) settings.color = this.colorUl.toString(16); + } else { + if (this.colorUl !== 0xFFFFFFFF) settings.colorUl = this.colorUl.toString(16); + if (this.colorUr !== 0xFFFFFFFF) settings.colorUr = this.colorUr.toString(16); + if (this.colorBl !== 0xFFFFFFFF) settings.colorBl = this.colorBl.toString(16); + if (this.colorBr !== 0xFFFFFFFF) settings.colorBr = this.colorBr.toString(16); + } + + if (this.zIndex) settings.zIndex = this.zIndex; + + if (this.forceZIndexContext) settings.forceZIndexContext = true; + + if (this.clipping) settings.clipping = this.clipping; + + if (!this.clipbox) settings.clipbox = this.clipbox; + + if (this.__texture) { + let tnd = this.__texture.getNonDefaults(); + if (Object.keys(tnd).length) { + settings.texture = tnd; + } + } + + if (this.shader) { + let tnd = this.shader.getNonDefaults(); + if (Object.keys(tnd).length) { + settings.shader = tnd; + } + } + + if (this._hasTexturizer()) { + if (this.texturizer.enabled) { + settings.renderToTexture = this.texturizer.enabled; + } + if (this.texturizer.lazy) { + settings.renderToTextureLazy = this.texturizer.lazy; + } + if (this.texturizer.colorize) { + settings.colorizeResultTexture = this.texturizer.colorize; + } + if (this.texturizer.renderOffscreen) { + settings.renderOffscreen = this.texturizer.renderOffscreen; + } + } + + return settings; + }; + + static getGetter(propertyPath) { + let getter = Element.PROP_GETTERS.get(propertyPath); + if (!getter) { + getter = new Function('obj', 'return obj.' + propertyPath); + Element.PROP_GETTERS.set(propertyPath, getter); + } + return getter; + } + + static getSetter(propertyPath) { + let setter = Element.PROP_SETTERS.get(propertyPath); + if (!setter) { + setter = new Function('obj', 'v', 'obj.' + propertyPath + ' = v'); + Element.PROP_SETTERS.set(propertyPath, setter); + } + return setter; + } + + get withinBoundsMargin() { + return this.__core._withinBoundsMargin; + } + + _enableWithinBoundsMargin() { + // Iff enabled, this toggles the active flag. + if (this.__enabled) { + this._setActiveFlag(); + } + } + + _disableWithinBoundsMargin() { + // Iff active, this toggles the active flag. + if (this.__active) { + this._unsetActiveFlag(); + } + } + + set boundsMargin(v) { + if (!Array.isArray(v) && v !== null) { + throw new Error("boundsMargin should be an array of left-top-right-bottom values or null (inherit margin)"); + } + this.__core.boundsMargin = v; + } + + get boundsMargin() { + return this.__core.boundsMargin; + } + + get x() { + return this.__core.offsetX; + } + + set x(v) { + this.__core.offsetX = v; + } + + get y() { + return this.__core.offsetY; + } + + set y(v) { + this.__core.offsetY = v; + } + + get w() { + return this._w; + } + + set w(v) { + if (Utils.isFunction(v)) { + this._w = 0; + this.__core.funcW = v; + } else { + v = Math.max(v, 0); + if (this._w !== v) { + this.__core.disableFuncW(); + this._w = v; + this._updateDimensions(); + } + } + } + + get h() { + return this._h; + } + + set h(v) { + if (Utils.isFunction(v)) { + this._h = 0; + this.__core.funcH = v; + } else { + v = Math.max(v, 0); + if (this._h !== v) { + this.__core.disableFuncH(); + this._h = v; + this._updateDimensions(); + } + } + } + + get scaleX() { + return this.__core.scaleX; + } + + set scaleX(v) { + this.__core.scaleX = v; + } + + get scaleY() { + return this.__core.scaleY; + } + + set scaleY(v) { + this.__core.scaleY = v; + } + + get scale() { + return this.__core.scale; + } + + set scale(v) { + this.__core.scale = v; + } + + get pivotX() { + return this.__core.pivotX; + } + + set pivotX(v) { + this.__core.pivotX = v; + } + + get pivotY() { + return this.__core.pivotY; + } + + set pivotY(v) { + this.__core.pivotY = v; + } + + get pivot() { + return this.__core.pivot; + } + + set pivot(v) { + this.__core.pivot = v; + } + + get mountX() { + return this.__core.mountX; + } + + set mountX(v) { + this.__core.mountX = v; + } + + get mountY() { + return this.__core.mountY; + } + + set mountY(v) { + this.__core.mountY = v; + } + + get mount() { + return this.__core.mount; + } + + set mount(v) { + this.__core.mount = v; + } + + get rotation() { + return this.__core.rotation; + } + + set rotation(v) { + this.__core.rotation = v; + } + + get alpha() { + return this.__core.alpha; + } + + set alpha(v) { + this.__core.alpha = v; + } + + get visible() { + return this.__core.visible; + } + + set visible(v) { + this.__core.visible = v; + } + + get colorUl() { + return this.__core.colorUl; + } + + set colorUl(v) { + this.__core.colorUl = v; + } + + get colorUr() { + return this.__core.colorUr; + } + + set colorUr(v) { + this.__core.colorUr = v; + } + + get colorBl() { + return this.__core.colorBl; + } + + set colorBl(v) { + this.__core.colorBl = v; + } + + get colorBr() { + return this.__core.colorBr; + } + + set colorBr(v) { + this.__core.colorBr = v; + } + + get color() { + return this.__core.colorUl; + } + + set color(v) { + if (this.colorUl !== v || this.colorUr !== v || this.colorBl !== v || this.colorBr !== v) { + this.colorUl = v; + this.colorUr = v; + this.colorBl = v; + this.colorBr = v; + } + } + + get colorTop() { + return this.colorUl; + } + + set colorTop(v) { + if (this.colorUl !== v || this.colorUr !== v) { + this.colorUl = v; + this.colorUr = v; + } + } + + get colorBottom() { + return this.colorBl; + } + + set colorBottom(v) { + if (this.colorBl !== v || this.colorBr !== v) { + this.colorBl = v; + this.colorBr = v; + } + } + + get colorLeft() { + return this.colorUl; + } + + set colorLeft(v) { + if (this.colorUl !== v || this.colorBl !== v) { + this.colorUl = v; + this.colorBl = v; + } + } + + get colorRight() { + return this.colorUr; + } + + set colorRight(v) { + if (this.colorUr !== v || this.colorBr !== v) { + this.colorUr = v; + this.colorBr = v; + } + } + + get zIndex() {return this.__core.zIndex} + set zIndex(v) { + this.__core.zIndex = v; + } + + get forceZIndexContext() {return this.__core.forceZIndexContext} + set forceZIndexContext(v) { + this.__core.forceZIndexContext = v; + } + + get clipping() {return this.__core.clipping} + set clipping(v) { + this.__core.clipping = v; + } + + get clipbox() {return this.__core.clipbox} + set clipbox(v) { + this.__core.clipbox = v; + } + + get tags() { + return this.getTags(); + } + + set tags(v) { + if (!Array.isArray(v)) v = [v]; + this.setTags(v); + } + + set t(v) { + this.tags = v; + } + + get _children() { + if (!this.__childList) { + this.__childList = new ElementChildList(this, false); + } + return this.__childList; + } + + get childList() { + if (!this._allowChildrenAccess()) { + this._throwError("Direct access to children is not allowed in " + this.getLocationString()); + } + return this._children; + } + + hasChildren() { + return this._allowChildrenAccess() && this.__childList && (this.__childList.length > 0); + } + + _allowChildrenAccess() { + return true; + } + + get children() { + return this.childList.get(); + } + + set children(children) { + this.childList.patch(children); + } + + add(o) { + return this.childList.a(o); + } + + get p() { + return this.__parent; + } + + get parent() { + return this.__parent; + } + + get src() { + if (this.texture && this.texture instanceof ImageTexture) { + return this.texture._src; + } else { + return undefined; + } + } + + set src(v) { + const texture = new ImageTexture(this.stage); + texture.src = v; + this.texture = texture; + } + + set mw(v) { + if (this.texture) { + this.texture.mw = v; + this._updateDimensions(); + } else { + this._throwError('Please set mw after setting a texture.'); + } + } + + set mh(v) { + if (this.texture) { + this.texture.mh = v; + this._updateDimensions(); + } else { + this._throwError('Please set mh after setting a texture.'); + } + } + + get rect() { + return (this.texture === this.stage.rectangleTexture); + } + + set rect(v) { + if (v) { + this.texture = this.stage.rectangleTexture; + } else { + this.texture = null; + } + } + + enableTextTexture() { + if (!this.texture || !(this.texture instanceof TextTexture)) { + this.texture = new TextTexture(this.stage); + + if (!this.texture.w && !this.texture.h) { + // Inherit dimensions from element. + // This allows userland to set dimensions of the Element and then later specify the text. + this.texture.w = this.w; + this.texture.h = this.h; + } + } + return this.texture; + } + + get text() { + if (this.texture && (this.texture instanceof TextTexture)) { + return this.texture; + } else { + return null; + } + } + + set text(v) { + if (!this.texture || !(this.texture instanceof TextTexture)) { + this.enableTextTexture(); + } + if (Utils.isString(v)) { + this.texture.text = v; + } else { + this.texture.patch(v); + } + } + + set onUpdate(f) { + this.__core.onUpdate = f; + } + + set onAfterCalcs(f) { + this.__core.onAfterCalcs = f; + } + + set onAfterUpdate(f) { + this.__core.onAfterUpdate = f; + } + + forceUpdate() { + // Make sure that the update loop is run. + this.__core._setHasUpdates(); + } + + get shader() { + return this.__core.shader; + } + + set shader(v) { + if (Utils.isObjectLiteral(v) && !v.type) { + // Setting properties on an existing shader. + if (this.shader) { + this.shader.patch(v); + } + } else { + const shader = Shader.create(this.stage, v); + + if (this.__enabled && this.__core.shader) { + this.__core.shader.removeElement(this.__core); + } + + this.__core.shader = shader; + + if (this.__enabled && this.__core.shader) { + this.__core.shader.addElement(this.__core); + } + } + } + + _hasTexturizer() { + return !!this.__core._texturizer; + } + + get renderToTexture() { + return this.rtt + } + + set renderToTexture(v) { + this.rtt = v; + } + + get rtt() { + return this._hasTexturizer() && this.texturizer.enabled; + } + + set rtt(v) { + this.texturizer.enabled = v; + } + + get rttLazy() { + return this._hasTexturizer() && this.texturizer.lazy; + } + + set rttLazy(v) { + this.texturizer.lazy = v; + } + + get renderOffscreen() { + return this._hasTexturizer() && this.texturizer.renderOffscreen; + } + + set renderOffscreen(v) { + this.texturizer.renderOffscreen = v; + } + + get colorizeResultTexture() { + return this._hasTexturizer() && this.texturizer.colorize; + } + + set colorizeResultTexture(v) { + this.texturizer.colorize = v; + } + + getTexture() { + return this.texturizer._getTextureSource(); + } + + get texturizer() { + return this.__core.texturizer; + } + + patch(settings) { + let paths = Object.keys(settings); + + for (let i = 0, n = paths.length; i < n; i++) { + let path = paths[i]; + const v = settings[path]; + + const firstCharCode = path.charCodeAt(0); + if (Utils.isUcChar(firstCharCode)) { + // Ref. + const child = this.getByRef(path); + if (!child) { + if (v !== undefined) { + // Add to list immediately. + let c; + if (Utils.isObjectLiteral(v)) { + // Catch this case to capture createMode flag. + c = this.childList.createItem(v); + c.patch(v); + } else if (Utils.isObject(v)) { + c = v; + } + if (c.isElement) { + c.ref = path; + } + + this.childList.a(c); + } + } else { + if (v === undefined) { + if (child.parent) { + child.parent.childList.remove(child); + } + } else if (Utils.isObjectLiteral(v)) { + child.patch(v); + } else if (v.isElement) { + // Replace element by new element. + v.ref = path; + this.childList.replace(v, child); + } else { + this._throwError("Unexpected value for path: " + path); + } + } + } else { + // Property. + Base.patchObjectProperty(this, path, v); + } + } + } + + _throwError(message) { + throw new Error(this.constructor.name + " (" + this.getLocationString() + "): " + message); + } + + animation(settings) { + return this.stage.animations.createAnimation(this, settings); + } + + transition(property, settings = null) { + if (settings === null) { + return this._getTransition(property); + } else { + this._setTransition(property, settings); + // We do not create/return the transition, because it would undo the 'lazy transition creation' optimization. + return null; + } + } + + set transitions(object) { + let keys = Object.keys(object); + keys.forEach(property => { + this.transition(property, object[property]); + }); + } + + set smooth(object) { + let keys = Object.keys(object); + keys.forEach(property => { + let value = object[property]; + if (Array.isArray(value)) { + this.setSmooth(property, value[0], value[1]); + } else { + this.setSmooth(property, value); + } + }); + } + + fastForward(property) { + if (this._transitions) { + let t = this._transitions[property]; + if (t && t.isTransition) { + t.finish(); + } + } + } + + _getTransition(property) { + if (!this._transitions) { + this._transitions = {}; + } + let t = this._transitions[property]; + if (!t) { + // Create default transition. + t = new Transition(this.stage.transitions, this.stage.transitions.defaultTransitionSettings, this, property); + } else if (t.isTransitionSettings) { + // Upgrade to 'real' transition. + t = new Transition( + this.stage.transitions, + t, + this, + property + ); + } + this._transitions[property] = t; + return t; + } + + _setTransition(property, settings) { + if (!settings) { + this._removeTransition(property); + } else { + if (Utils.isObjectLiteral(settings)) { + // Convert plain object to proper settings object. + settings = this.stage.transitions.createSettings(settings); + } + + if (!this._transitions) { + this._transitions = {}; + } + + let current = this._transitions[property]; + if (current && current.isTransition) { + // Runtime settings change. + current.settings = settings; + return current; + } else { + // Initially, only set the settings and upgrade to a 'real' transition when it is used. + this._transitions[property] = settings; + } + } + } + + _removeTransition(property) { + if (this._transitions) { + delete this._transitions[property]; + } + } + + getSmooth(property, v) { + let t = this._getTransition(property); + if (t && t.isAttached()) { + return t.targetValue; + } else { + return v; + } + } + + setSmooth(property, v, settings) { + if (settings) { + this._setTransition(property, settings); + } + let t = this._getTransition(property); + t.start(v); + return t; + } + + get flex() { + return this.__core.flex; + } + + set flex(v) { + this.__core.flex = v; + } + + get flexItem() { + return this.__core.flexItem; + } + + set flexItem(v) { + this.__core.flexItem = v; + } + + static isColorProperty(property) { + return property.toLowerCase().indexOf("color") >= 0; + } + + static getMerger(property) { + if (Element.isColorProperty(property)) { + return StageUtils.mergeColors; + } else { + return StageUtils.mergeNumbers; + } + } +} + +// This gives a slight performance benefit compared to extending EventEmitter. +EventEmitter.addAsMixin(Element); + +Element.prototype.isElement = 1; + +Element.id = 1; + +// Getters reused when referencing element (subobject) properties by a property path, as used in a transition or animation ('x', 'texture.x', etc). +Element.PROP_GETTERS = new Map(); + +// Setters reused when referencing element (subobject) properties by a property path, as used in a transition or animation ('x', 'texture.x', etc). +Element.PROP_SETTERS = new Map(); + +class StateMachine { + + constructor() { + StateMachine.setupStateMachine(this); + } + + static setupStateMachine(target) { + const targetConstructor = target.constructor; + const router = StateMachine.create(targetConstructor); + Object.setPrototypeOf(target, router.prototype); + target.constructor = targetConstructor; + target._initStateMachine(); + } + + /** + * Creates a state machine implementation. + * It extends the original type and should be used when creating new instances. + * The original type is available as static property 'original', and it must be used when subclassing as follows: + * const type = StateMachine.create(class YourNewStateMachineClass extends YourBaseStateMachineClass.original { }) + * @param {Class} type + * @returns {StateMachine} + */ + static create(type) { + if (!type.hasOwnProperty('_sm')) { + // Only need to run once. + const stateMachineType = new StateMachineType(type); + type._sm = stateMachineType; + } + + return type._sm.router; + } + + /** + * Calls the specified method if it exists. + * @param {string} event + * @param {*...} args + */ + fire(event, ...args) { + if (this._hasMethod(event)) { + return this[event](...args); + } + } + + /** + * Returns the current state path (for example "Initialized.Loading"). + * @returns {string} + * @protected + */ + _getState() { + return this._state.__path; + } + + /** + * Returns true iff statePath is (an ancestor of) currentStatePath. + * @param {string} statePath + * @param {string} currentStatePath + * @returns {Boolean} + * @protected + */ + _inState(statePath, currentStatePath = this._state.__path) { + const state = this._sm.getStateByPath(statePath); + const currentState = this._sm.getStateByPath(currentStatePath); + const level = state.__level; + const stateAtLevel = StateMachine._getStateAtLevel(currentState, level); + return (stateAtLevel === state); + } + + /** + * Returns true if the specified class member is defined for the currently set state. + * @param {string} name + * @returns {boolean} + * @protected + */ + _hasMember(name) { + return !!this.constructor.prototype[name]; + } + + /** + * Returns true if the specified class member is a method for the currently set state. + * @param {string} name + * @returns {boolean} + * @protected + */ + _hasMethod(name) { + const member = this.constructor.prototype[name]; + return !!member && (typeof member === "function") + } + + /** + * Switches to the specified state. + * @param {string} statePath + * Substates are seperated by a underscores (for example "Initialized.Loading"). + * @param {*[]} [args] + * Args that are supplied in $enter and $exit events. + * @protected + */ + _setState(statePath, args) { + const setStateId = ++this._setStateCounter; + this._setStateId = setStateId; + + if (this._state.__path !== statePath) { + // Performance optimization. + let newState = this._sm._stateMap[statePath]; + if (!newState) { + // Check for super state. + newState = this._sm.getStateByPath(statePath); + } + + const prevState = this._state; + + const hasDifferentEnterMethod = (newState.prototype.$enter !== this._state.prototype.$enter); + const hasDifferentExitMethod = (newState.prototype.$exit !== this._state.prototype.$exit); + if (hasDifferentEnterMethod || hasDifferentExitMethod) { + const sharedState = StateMachine._getSharedState(this._state, newState); + const context = { + newState: newState.__path, + prevState: prevState.__path, + sharedState: sharedState.__path + }; + const sharedLevel = sharedState.__level; + + if (hasDifferentExitMethod) { + const exitStates = StateMachine._getStatesUntilLevel(this._state, sharedLevel); + for (let i = 0, n = exitStates.length; i < n; i++) { + this.__setState(exitStates[i]); + this._callExit(this._state, args, context); + const stateChangeOverridden = (this._setStateId !== setStateId); + if (stateChangeOverridden) { + return; + } + } + } + + if (hasDifferentEnterMethod) { + const enterStates = StateMachine._getStatesUntilLevel(newState, sharedLevel).reverse(); + for (let i = 0, n = enterStates.length; i < n; i++) { + this.__setState(enterStates[i]); + this._callEnter(this._state, args, context); + const stateChangeOverridden = (this._setStateId !== setStateId); + if (stateChangeOverridden) { + return; + } + } + } + + } + + this.__setState(newState); + + if (this._changedState) { + const context = { + newState: newState.__path, + prevState: prevState.__path + }; + + if (args) { + this._changedState(context, ...args); + } else { + this._changedState(context); + } + } + + if (this._onStateChange) { + const context = { + newState: newState.__path, + prevState: prevState.__path + }; + this._onStateChange(context); + } + + } + } + + _callEnter(state, args = [], context) { + const hasParent = !!state.__parent; + if (state.prototype.$enter) { + if (!hasParent || (state.__parent.prototype.$enter !== state.prototype.$enter)) { + state.prototype.$enter.apply(this, [context, ...args]); + } + } + } + + _callExit(state, args = [], context) { + const hasParent = !!state.__parent; + if (state.prototype.$exit) { + if (!hasParent || (state.__parent.prototype.$exit !== state.prototype.$exit)) { + state.prototype.$exit.apply(this, [context, ...args]); + } + } + } + + __setState(state) { + this._state = state; + this._stateIndex = state.__index; + this.constructor = state; + } + + _initStateMachine() { + this._state = null; + this._stateIndex = 0; + this._setStateCounter = 0; + this._sm = this._routedType._sm; + this.__setState(this._sm.getStateByPath("")); + const context = {newState: "", prevState: undefined, sharedState: undefined}; + this._callEnter(this._state, [], context); + this._onStateChange = undefined; + } + + /** + * Between multiple member names, select the one specified in the deepest state. + * If multiple member names are specified in the same deepest state, the first one in the array is returned. + * @param {string[]} memberNames + * @returns {string|undefined} + * @protected + */ + _getMostSpecificHandledMember(memberNames) { + let cur = this._state; + do { + for (let i = 0, n = memberNames.length; i < n; i++) { + const memberName = memberNames[i]; + if (!cur.__parent) { + if (cur.prototype[memberName]) { + return memberName; + } + } else { + const alias = StateMachineType.getStateMemberAlias(cur.__path, memberName); + if (this[alias]) { + return memberName; + } + } + } + cur = cur.__parent; + } while (cur); + } + + static _getStatesUntilLevel(state, level) { + const states = []; + while (state.__level > level) { + states.push(state); + state = state.__parent; + } + return states; + } + + static _getSharedState(state1, state2) { + const state1Array = StateMachine._getAncestorStates(state1); + const state2Array = StateMachine._getAncestorStates(state2); + const n = Math.min(state1Array.length, state2Array.length); + for (let i = 0; i < n; i++) { + if (state1Array[i] !== state2Array[i]) { + return state1Array[i - 1]; + } + } + return state1Array[n - 1]; + } + + static _getAncestorStates(state) { + const result = []; + do { + result.push(state); + } while(state = state.__parent); + return result.reverse(); + } + + static _getStateAtLevel(state, level) { + if (level > state.__level) { + return undefined; + } + + while(level < state.__level) { + state = state.__parent; + } + return state; + } +} + +class StateMachineType { + + constructor(type) { + this._type = type; + this._router = null; + + this.init(); + } + + get router() { + return this._router; + } + + init() { + this._router = this._createRouter(); + + this._stateMap = this._getStateMap(); + + this._addStateMemberDelegatorsToRouter(); + + } + + _createRouter() { + const type = this._type; + + const router = class StateMachineRouter extends type { + constructor() { + super(...arguments); + if (!this.constructor.hasOwnProperty('_isRouter')) { + throw new Error(`You need to extend ${type.name}.original instead of ${type.name}.`); + } + } + }; + router._isRouter = true; + router.prototype._routedType = type; + router.original = type; + + this._mixinStateMachineMethods(router); + + return router; + } + + _mixinStateMachineMethods(router) { + // Mixin the state machine methods, so that we reuse the methods instead of re-creating them. + const names = Object.getOwnPropertyNames(StateMachine.prototype); + for (let i = 0, n = names.length; i < n; i++) { + const name = names[i]; + if (name !== "constructor") { + const descriptor = Object.getOwnPropertyDescriptor(StateMachine.prototype, name); + Object.defineProperty(router.prototype, name, descriptor); + } + } + } + + _addStateMemberDelegatorsToRouter() { + const members = this._getAllMemberNames(); + + members.forEach(member => { + this._addMemberRouter(member); + }); + } + + /** + * @note We are generating code because it yields much better performance. + */ + _addMemberRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + let type = undefined; + descriptors.forEach(descriptor => { + if (descriptor) { + const descType = this._getDescriptorType(descriptor); + if (type && (type !== descType)) { + console.warn(`Member ${member} in ${this._type.name} has inconsistent types.`); + return; + } + type = descType; + } + }); + + switch(type) { + case "method": + this._addMethodRouter(member, descriptors, aliases); + break; + case "getter": + this._addGetterSetterRouters(member); + break; + case "property": + console.warn("Fixed properties are not supported; please use a getter instead!"); + break; + } + } + + _getDescriptor(state, member, isValid = () => true) { + let type = state; + let curState = state; + + do { + const descriptor = Object.getOwnPropertyDescriptor(type.prototype, member); + if (descriptor) { + if (isValid(descriptor)) { + descriptor._source = curState; + return descriptor; + } + } + type = Object.getPrototypeOf(type); + if (type && type.hasOwnProperty('__state')) { + curState = type; + } + } while(type && type.prototype); + return undefined; + } + + _getDescriptorType(descriptor) { + if (descriptor.get || descriptor.set) { + return 'getter'; + } else { + if (typeof descriptor.value === "function") { + return 'method'; + } else { + return 'property'; + } + } + } + + static _supportsSpread() { + if (this.__supportsSpread === undefined) { + this.__supportsSpread = false; + try { + const func = new Function("return [].concat(...arguments);"); + func(); + this.__supportsSpread = true; + } catch(e) {} + } + return this.__supportsSpread; + } + + _addMethodRouter(member, descriptors, aliases) { + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + const supportsSpread = StateMachineType._supportsSpread(); + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + if (supportsSpread) { + code.push(`if (i < ${i}) return this["${cur}"](...arguments); else`); + } else { + code.push(`if (i < ${i}) return this["${cur}"].apply(this, arguments); else`); + } + } else { + code.push(`if (i < ${i}) return ; else`); + } + } + cur = alias; + } + if (cur) { + if (supportsSpread) { + code.push(`return this["${cur}"](...arguments);`); + } else { + code.push(`return this["${cur}"].apply(this, arguments);`); + } + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function([], functionBody); + + const descriptor = {value: router}; + Object.defineProperty(this._router.prototype, member, descriptor); + } + + _addGetterSetterRouters(member) { + const getter = this._getGetterRouter(member); + const setter = this._getSetterRouter(member); + const descriptor = { + get: getter, + set: setter + }; + Object.defineProperty(this._router.prototype, member, descriptor); + } + + _getGetterRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member, (descriptor => descriptor.get)); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + code.push(`if (i < ${i}) return this["${cur}"]; else`); + } else { + code.push(`if (i < ${i}) return ; else`); + } + } + cur = alias; + } + if (cur) { + code.push(`return this["${cur}"];`); + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function([], functionBody); + return router; + } + + _getSetterRouter(member) { + const statePaths = Object.keys(this._stateMap); + const descriptors = []; + const aliases = []; + statePaths.forEach((statePath, index) => { + const state = this._stateMap[statePath]; + const descriptor = this._getDescriptor(state, member, (descriptor => descriptor.set)); + if (descriptor) { + descriptors[index] = descriptor; + + // Add to prototype. + const alias = StateMachineType.getStateMemberAlias(descriptor._source.__path, member); + aliases[index] = alias; + + if (!this._router.prototype.hasOwnProperty(alias)) { + Object.defineProperty(this._router.prototype, alias, descriptor); + } + } else { + descriptors[index] = null; + aliases[index] = null; + } + }); + + const code = [ + // The line ensures that, while debugging, your IDE won't open many tabs. + "//@ sourceURL=StateMachineRouter.js", + "const i = this._stateIndex;" + ]; + let cur = aliases[0]; + for (let i = 1, n = aliases.length; i < n; i++) { + const alias = aliases[i]; + if (alias !== cur) { + if (cur) { + code.push(`if (i < ${i}) this["${cur}"] = arg; else`); + } else { + code.push(`if (i < ${i}) ; else`); + } + } + cur = alias; + } + if (cur) { + code.push(`this["${cur}"] = arg;`); + } else { + code.push(`;`); + } + const functionBody = code.join("\n"); + const router = new Function(["arg"], functionBody); + return router; + } + + static getStateMemberAlias(path, member) { + return "$" + (path ? path + "." : "") + member; + } + + _getAllMemberNames() { + const stateMap = this._stateMap; + const map = Object.keys(stateMap); + let members = new Set(); + map.forEach(statePath => { + if (statePath === "") { + // Root state can be skipped: if the method only occurs in the root state, we don't need to re-delegate it based on state. + return; + } + const state = stateMap[statePath]; + const names = this._getStateMemberNames(state); + names.forEach(name => { + members.add(name); + }); + }); + return [...members]; + } + + _getStateMemberNames(state) { + let type = state; + let members = new Set(); + const isRoot = this._type === state; + do { + const names = this._getStateMemberNamesForType(type); + names.forEach(name => { + members.add(name); + }); + + type = Object.getPrototypeOf(type); + } while(type && type.prototype && (!type.hasOwnProperty("__state") || isRoot)); + + return members; + } + + _getStateMemberNamesForType(type) { + const memberNames = Object.getOwnPropertyNames(type.prototype); + return memberNames.filter(memberName => { + return (memberName !== "constructor") && !StateMachineType._isStateLocalMember(memberName); + }); + } + + static _isStateLocalMember(memberName) { + return (memberName === "$enter") || (memberName === "$exit"); + } + + getStateByPath(statePath) { + if (this._stateMap[statePath]) { + return this._stateMap[statePath]; + } + + // Search for closest match. + const parts = statePath.split("."); + while(parts.pop()) { + const statePath = parts.join("."); + if (this._stateMap[statePath]) { + return this._stateMap[statePath]; + } + } + } + + _getStateMap() { + if (!this._stateMap) { + this._stateMap = this._createStateMap(); + } + return this._stateMap; + } + + _createStateMap() { + const stateMap = {}; + this._addState(this._type, null, "", stateMap); + return stateMap; + } + + _addState(state, parentState, name, stateMap) { + state.__state = true; + state.__name = name; + + this._addStaticStateProperty(state, parentState); + + const parentPath = (parentState ? parentState.__path : ""); + let path = (parentPath ? parentPath + "." : "") + name; + state.__path = path; + state.__level = parentState ? parentState.__level + 1 : 0; + state.__parent = parentState; + state.__index = Object.keys(stateMap).length; + stateMap[path] = state; + + const states = state._states; + if (states) { + const isInheritedFromParent = (parentState && parentState._states === states); + if (!isInheritedFromParent) { + const subStates = state._states(); + subStates.forEach(subState => { + const stateName = StateMachineType._getStateName(subState); + this._addState(subState, state, stateName, stateMap); + }); + } + } + } + + static _getStateName(state) { + const name = state.name; + + const index = name.indexOf('$'); + if (index > 0) { + // Strip off rollup name suffix. + return name.substr(0, index); + } + + return name; + } + + _addStaticStateProperty(state, parentState) { + if (parentState) { + const isClassStateLevel = parentState && !parentState.__parent; + if (isClassStateLevel) { + this._router[state.__name] = state; + } else { + parentState[state.__name] = state; + } + } + } + +} + +/** + * @extends StateMachine + */ +class Component extends Element { + + constructor(stage, properties) { + super(stage); + + // Encapsulate tags to prevent leaking. + this.tagRoot = true; + + if (Utils.isObjectLiteral(properties)) { + Object.assign(this, properties); + } + + this.__initialized = false; + this.__firstActive = false; + this.__firstEnable = false; + + this.__signals = undefined; + + this.__passSignals = undefined; + + this.__construct(); + + // Quick-apply template. + const func = this.constructor.getTemplateFunc(); + func.f(this, func.a); + + this._build(); + } + + __start() { + StateMachine.setupStateMachine(this); + this._onStateChange = Component.prototype.__onStateChange; + } + + get state() { + return this._getState(); + } + + __onStateChange() { + /* FIXME: Workaround for case, where application was shut but component still lives */ + if (this.application) { + this.application.updateFocusPath(); + } + } + + _refocus() { + /* FIXME: Workaround for case, where application was shut but component still lives */ + if (this.application) { + this.application.updateFocusPath(); + } + } + + /** + * Returns a high-performance template patcher. + */ + static getTemplateFunc() { + // We need a different template function per patch id. + const name = "_templateFunc"; + + // Be careful with class-based static inheritance. + const hasName = '__has' + name; + if (this[hasName] !== this) { + this[hasName] = this; + this[name] = this.parseTemplate(this._template()); + } + return this[name]; + } + + static parseTemplate(obj) { + const context = { + loc: [], + store: [], + rid: 0 + }; + + this.parseTemplateRec(obj, context, "element"); + + const code = context.loc.join(";\n"); + const f = new Function("element", "store", code); + return {f:f, a:context.store} + } + + static parseTemplateRec(obj, context, cursor) { + const store = context.store; + const loc = context.loc; + const keys = Object.keys(obj); + keys.forEach(key => { + let value = obj[key]; + if (Utils.isUcChar(key.charCodeAt(0))) { + // Value must be expanded as well. + if (Utils.isObjectLiteral(value)) { + // Ref. + const childCursor = `r${key.replace(/[^a-z0-9]/gi, "") + context.rid}`; + let type = value.type ? value.type : Element; + if (type === Element) { + loc.push(`const ${childCursor} = element.stage.createElement()`); + } else { + store.push(type); + loc.push(`const ${childCursor} = new store[${store.length - 1}](${cursor}.stage)`); + } + loc.push(`${childCursor}.ref = "${key}"`); + context.rid++; + + // Enter sub. + this.parseTemplateRec(value, context, childCursor); + + loc.push(`${cursor}.childList.add(${childCursor})`); + } else if (Utils.isObject(value)) { + // Dynamic assignment. + store.push(value); + loc.push(`${cursor}.childList.add(store[${store.length - 1}])`); + } + } else { + if (key === "text") { + const propKey = cursor + "__text"; + loc.push(`const ${propKey} = ${cursor}.enableTextTexture()`); + this.parseTemplatePropRec(value, context, propKey); + } else if (key === "texture" && Utils.isObjectLiteral(value)) { + const propKey = cursor + "__texture"; + const type = value.type; + if (type) { + store.push(type); + loc.push(`const ${propKey} = new store[${store.length - 1}](${cursor}.stage)`); + this.parseTemplatePropRec(value, context, propKey); + loc.push(`${cursor}["${key}"] = ${propKey}`); + } else { + loc.push(`${propKey} = ${cursor}.texture`); + this.parseTemplatePropRec(value, context, propKey); + } + } else { + // Property; + if (Utils.isNumber(value)) { + loc.push(`${cursor}["${key}"] = ${value}`); + } else if (Utils.isBoolean(value)) { + loc.push(`${cursor}["${key}"] = ${value ? "true" : "false"}`); + } else if (Utils.isObject(value) || Array.isArray(value)) { + // Dynamic assignment. + // Because literal objects may contain dynamics, we store the full object. + store.push(value); + loc.push(`${cursor}["${key}"] = store[${store.length - 1}]`); + } else { + // String etc. + loc.push(`${cursor}["${key}"] = ${JSON.stringify(value)}`); + } + } + } + }); + } + + static parseTemplatePropRec(obj, context, cursor) { + const store = context.store; + const loc = context.loc; + const keys = Object.keys(obj); + keys.forEach(key => { + if (key !== "type") { + const value = obj[key]; + if (Utils.isNumber(value)) { + loc.push(`${cursor}["${key}"] = ${value}`); + } else if (Utils.isBoolean(value)) { + loc.push(`${cursor}["${key}"] = ${value ? "true" : "false"}`); + } else if (Utils.isObject(value) || Array.isArray(value)) { + // Dynamic assignment. + // Because literal objects may contain dynamics, we store the full object. + store.push(value); + loc.push(`${cursor}["${key}"] = store[${store.length - 1}]`); + } else { + // String etc. + loc.push(`${cursor}["${key}"] = ${JSON.stringify(value)}`); + } + } + }); + } + + _onSetup() { + if (!this.__initialized) { + this._setup(); + } + } + + _setup() { + } + + _onAttach() { + if (!this.__initialized) { + this.__init(); + this.__initialized = true; + } + + this._attach(); + } + + _attach() { + } + + _onDetach() { + this._detach(); + } + + _detach() { + } + + _onEnabled() { + if (!this.__firstEnable) { + this._firstEnable(); + this.__firstEnable = true; + } + + this._enable(); + } + + _firstEnable() { + } + + _enable() { + } + + _onDisabled() { + this._disable(); + } + + _disable() { + } + + _onActive() { + if (!this.__firstActive) { + this._firstActive(); + this.__firstActive = true; + } + + this._active(); + } + + _firstActive() { + } + + _active() { + } + + _onInactive() { + this._inactive(); + } + + _inactive() { + } + + get application() { + return this.stage.application; + } + + __construct() { + this._construct(); + } + + _construct() { + } + + _build() { + } + + __init() { + this._init(); + } + + _init() { + } + + _focus(newTarget, prevTarget) { + } + + _unfocus(newTarget) { + } + + _focusChange(target, newTarget) { + } + + _getFocused() { + // Override to delegate focus to child components. + return this; + } + + _setFocusSettings(settings) { + // Override to add custom settings. See Application._handleFocusSettings(). + } + + _handleFocusSettings(settings) { + // Override to react on custom settings. See Application._handleFocusSettings(). + } + + static _template() { + return {} + } + + hasFinalFocus() { + let path = this.application._focusPath; + return path && path.length && path[path.length - 1] === this; + } + + hasFocus() { + let path = this.application._focusPath; + return path && (path.indexOf(this) >= 0); + } + + get cparent() { + return Component.getParent(this); + } + + seekAncestorByType(type) { + let c = this.cparent; + while(c) { + if (c.constructor === type) { + return c; + } + c = c.cparent; + } + } + + getSharedAncestorComponent(element) { + let ancestor = this.getSharedAncestor(element); + while(ancestor && !ancestor.isComponent) { + ancestor = ancestor.parent; + } + return ancestor; + } + + get signals() { + return this.__signals; + } + + set signals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + this.__signals = v; + } + + set alterSignals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + if (!this.__signals) { + this.__signals = {}; + } + for (let key in v) { + const d = v[key]; + if (d === undefined) { + delete this.__signals[key]; + } else { + this.__signals[key] = v; + } + } + } + + get passSignals() { + return this.__passSignals || {}; + } + + set passSignals(v) { + this.__passSignals = Object.assign(this.__passSignals || {}, v); + } + + set alterPassSignals(v) { + if (!Utils.isObjectLiteral(v)) { + this._throwError("Signals: specify an object with signal-to-fire mappings"); + } + if (!this.__passSignals) { + this.__passSignals = {}; + } + for (let key in v) { + const d = v[key]; + if (d === undefined) { + delete this.__passSignals[key]; + } else { + this.__passSignals[key] = v; + } + } + } + + /** + * Signals the parent of the specified event. + * A parent/ancestor that wishes to handle the signal should set the 'signals' property on this component. + * @param {string} event + * @param {...*} args + */ + signal(event, ...args) { + return this._signal(event, args); + } + + _signal(event, args) { + const signalParent = this._getParentSignalHandler(); + if (signalParent) { + if (this.__signals) { + let fireEvent = this.__signals[event]; + if (fireEvent === false) { + // Ignore event. + return; + } + if (fireEvent) { + if (fireEvent === true) { + fireEvent = event; + } + + if (signalParent._hasMethod(fireEvent)) { + return signalParent[fireEvent](...args); + } + } + } + + let passSignal = (this.__passSignals && this.__passSignals[event]); + if (passSignal) { + // Bubble up. + if (passSignal && passSignal !== true) { + // Replace signal name. + event = passSignal; + } + + return signalParent._signal(event, args); + } + } + } + + _getParentSignalHandler() { + return this.cparent ? this.cparent._getSignalHandler() : null; + } + + _getSignalHandler() { + if (this._signalProxy) { + return this.cparent ? this.cparent._getSignalHandler() : null; + } + return this; + } + + get _signalProxy() { + return false; + } + + fireAncestors(name, ...args) { + if (!name.startsWith('$')) { + throw new Error("Ancestor event name must be prefixed by dollar sign."); + } + + return this._doFireAncestors(name, args); + } + + _doFireAncestors(name, args) { + if (this._hasMethod(name)) { + return this.fire(name, ...args); + } else { + const signalParent = this._getParentSignalHandler(); + if (signalParent) { + return signalParent._doFireAncestors(name, args); + } + } + } + + static collectSubComponents(subs, element) { + if (element.hasChildren()) { + const childList = element.__childList; + for (let i = 0, n = childList.length; i < n; i++) { + const child = childList.getAt(i); + if (child.isComponent) { + subs.push(child); + } else { + Component.collectSubComponents(subs, child); + } + } + } + } + + static getComponent(element) { + let parent = element; + while (parent && !parent.isComponent) { + parent = parent.parent; + } + return parent; + } + + static getParent(element) { + return Component.getComponent(element.parent); + } + +} + +Component.prototype.isComponent = true; + +class CoreQuadList { + + constructor(ctx) { + + this.ctx = ctx; + + this.quadTextures = []; + + this.quadElements = []; + } + + get length() { + return this.quadTextures.length; + } + + reset() { + this.quadTextures = []; + this.quadElements = []; + this.dataLength = 0; + } + + getElement(index) { + return this.quadElements[index]._element; + } + + getElementCore(index) { + return this.quadElements[index]; + } + + getTexture(index) { + return this.quadTextures[index]; + } + + getTextureWidth(index) { + let nativeTexture = this.quadTextures[index]; + if (nativeTexture.w) { + // Render texture; + return nativeTexture.w; + } else { + return this.quadElements[index]._displayedTextureSource.w; + } + } + + getTextureHeight(index) { + let nativeTexture = this.quadTextures[index]; + if (nativeTexture.h) { + // Render texture; + return nativeTexture.h; + } else { + return this.quadElements[index]._displayedTextureSource.h; + } + } + +} + +class WebGLCoreQuadList extends CoreQuadList { + + constructor(ctx) { + super(ctx); + + // Allocate a fairly big chunk of memory that should be enough to support ~100000 (default) quads. + // We do not (want to) handle memory overflow. + const byteSize = ctx.stage.getOption('bufferMemory'); + + this.dataLength = 0; + + this.data = new ArrayBuffer(byteSize); + this.floats = new Float32Array(this.data); + this.uints = new Uint32Array(this.data); + } + + getAttribsDataByteOffset(index) { + // Where this quad can be found in the attribs buffer. + return index * 80; + } + + getQuadContents() { + // Debug: log contents of quad buffer. + let floats = this.floats; + let uints = this.uints; + let lines = []; + for (let i = 1; i <= this.length; i++) { + let str = 'entry ' + i + ': '; + for (let j = 0; j < 4; j++) { + let b = i * 20 + j * 4; + str += floats[b] + ',' + floats[b+1] + ':' + floats[b+2] + ',' + floats[b+3] + '[' + uints[b+4].toString(16) + '] '; + } + lines.push(str); + } + + return lines; + } + + +} + +class CoreQuadOperation { + + constructor(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + + this.ctx = ctx; + this.shader = shader; + this.shaderOwner = shaderOwner; + this.renderTextureInfo = renderTextureInfo; + this.scissor = scissor; + this.index = index; + this.length = 0; + + } + + get quads() { + return this.ctx.renderState.quads; + } + + getTexture(index) { + return this.quads.getTexture(this.index + index); + } + + getElementCore(index) { + return this.quads.getElementCore(this.index + index); + } + + getElement(index) { + return this.quads.getElement(this.index + index); + } + + getElementWidth(index) { + return this.getElement(index).renderWidth; + } + + getElementHeight(index) { + return this.getElement(index).renderHeight; + } + + getTextureWidth(index) { + return this.quads.getTextureWidth(this.index + index); + } + + getTextureHeight(index) { + return this.quads.getTextureHeight(this.index + index); + } + + getRenderWidth() { + if (this.renderTextureInfo) { + return this.renderTextureInfo.w; + } else { + return this.ctx.stage.w; + } + } + + getRenderHeight() { + if (this.renderTextureInfo) { + return this.renderTextureInfo.h; + } else { + return this.ctx.stage.h; + } + } + +} + +class WebGLCoreQuadOperation extends CoreQuadOperation { + + constructor(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + super(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + + this.extraAttribsDataByteOffset = 0; + } + + getAttribsDataByteOffset(index) { + // Where this quad can be found in the attribs buffer. + return this.quads.getAttribsDataByteOffset(this.index + index); + } + + /** + * Returns the relative pixel coordinates in the shader owner to gl position coordinates in the render texture. + * @param x + * @param y + * @return {number[]} + */ + getNormalRenderTextureCoords(x, y) { + let coords = this.shaderOwner.getRenderTextureCoords(x, y); + coords[0] /= this.getRenderWidth(); + coords[1] /= this.getRenderHeight(); + coords[0] = coords[0] * 2 - 1; + coords[1] = 1 - coords[1] * 2; + return coords; + } + + getProjection() { + if (this.renderTextureInfo === null) { + return this.ctx.renderExec._projection; + } else { + return this.renderTextureInfo.nativeTexture.projection; + } + } + +} + +class CoreRenderExecutor { + + constructor(ctx) { + this.ctx = ctx; + + this.renderState = ctx.renderState; + + this.gl = this.ctx.stage.gl; + } + + destroy() { + } + + _reset() { + this._bindRenderTexture(null); + this._setScissor(null); + this._clearRenderTexture(); + } + + execute() { + this._reset(); + + let qops = this.renderState.quadOperations; + + let i = 0, n = qops.length; + while (i < n) { + this._processQuadOperation(qops[i]); + i++; + } + } + + _processQuadOperation(quadOperation) { + if (quadOperation.renderTextureInfo && quadOperation.renderTextureInfo.ignore) { + // Ignore quad operations when we are 're-using' another texture as the render texture result. + return; + } + + this._setupQuadOperation(quadOperation); + this._execQuadOperation(quadOperation); + + } + + _setupQuadOperation(quadOperation) { + } + + _execQuadOperation(op) { + // Set render texture. + let nativeTexture = op.renderTextureInfo ? op.renderTextureInfo.nativeTexture : null; + + if (this._renderTexture !== nativeTexture) { + this._bindRenderTexture(nativeTexture); + } + + if (op.renderTextureInfo && !op.renderTextureInfo.cleared) { + this._setScissor(null); + this._clearRenderTexture(); + op.renderTextureInfo.cleared = true; + this._setScissor(op.scissor); + } else { + this._setScissor(op.scissor); + } + + this._renderQuadOperation(op); + } + + _renderQuadOperation(op) { + } + + _bindRenderTexture(renderTexture) { + this._renderTexture = renderTexture; + } + + _clearRenderTexture(renderTexture) { + } + + _setScissor(area) { + } + +} + +class WebGLCoreRenderExecutor extends CoreRenderExecutor { + + constructor(ctx) { + super(ctx); + + this.gl = this.ctx.stage.gl; + + this.init(); + } + + init() { + let gl = this.gl; + + // Create new sharable buffer for params. + this._attribsBuffer = gl.createBuffer(); + + let maxQuads = Math.floor(this.renderState.quads.data.byteLength / 80); + + // Init webgl arrays. + let allIndices = new Uint16Array(maxQuads * 6); + + // fill the indices with the quads to draw. + for (let i = 0, j = 0; i < maxQuads; i += 6, j += 4) { + allIndices[i] = j; + allIndices[i + 1] = j + 1; + allIndices[i + 2] = j + 2; + allIndices[i + 3] = j; + allIndices[i + 4] = j + 2; + allIndices[i + 5] = j + 3; + } + + // The quads buffer can be (re)used to draw a range of quads. + this._quadsBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._quadsBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, allIndices, gl.STATIC_DRAW); + + // The matrix that causes the [0,0 - W,H] box to map to [-1,-1 - 1,1] in the end results. + this._projection = new Float32Array([2/this.ctx.stage.coordsWidth, -2/this.ctx.stage.coordsHeight]); + + } + + destroy() { + super.destroy(); + this.gl.deleteBuffer(this._attribsBuffer); + this.gl.deleteBuffer(this._quadsBuffer); + } + + _reset() { + super._reset(); + + let gl = this.gl; + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.enable(gl.BLEND); + gl.disable(gl.DEPTH_TEST); + + this._stopShaderProgram(); + this._setupBuffers(); + } + + _setupBuffers() { + let gl = this.gl; + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._quadsBuffer); + let element = new Float32Array(this.renderState.quads.data, 0, this.renderState.quads.dataLength); + gl.bindBuffer(gl.ARRAY_BUFFER, this._attribsBuffer); + gl.bufferData(gl.ARRAY_BUFFER, element, gl.DYNAMIC_DRAW); + } + + _setupQuadOperation(quadOperation) { + super._setupQuadOperation(quadOperation); + this._useShaderProgram(quadOperation.shader, quadOperation); + } + + _renderQuadOperation(op) { + let shader = op.shader; + + if (op.length || op.shader.addEmpty()) { + shader.beforeDraw(op); + shader.draw(op); + shader.afterDraw(op); + } + } + + /** + * @param {WebGLShader} shader; + * @param {CoreQuadOperation} operation; + */ + _useShaderProgram(shader, operation) { + if (!shader.hasSameProgram(this._currentShaderProgram)) { + if (this._currentShaderProgram) { + this._currentShaderProgram.stopProgram(); + } + shader.useProgram(); + this._currentShaderProgram = shader; + } + shader.setupUniforms(operation); + } + + _stopShaderProgram() { + if (this._currentShaderProgram) { + // The currently used shader program should be stopped gracefully. + this._currentShaderProgram.stopProgram(); + this._currentShaderProgram = null; + } + } + + _bindRenderTexture(renderTexture) { + super._bindRenderTexture(renderTexture); + + let gl = this.gl; + if (!this._renderTexture) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.viewport(0,0,this.ctx.stage.w,this.ctx.stage.h); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, this._renderTexture.framebuffer); + gl.viewport(0,0,this._renderTexture.w, this._renderTexture.h); + } + } + + _clearRenderTexture() { + super._clearRenderTexture(); + let gl = this.gl; + if (!this._renderTexture) { + let glClearColor = this.ctx.stage.getClearColor(); + if (glClearColor) { + gl.clearColor(glClearColor[0] * glClearColor[3], glClearColor[1] * glClearColor[3], glClearColor[2] * glClearColor[3], glClearColor[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } else { + // Clear texture. + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } + + _setScissor(area) { + super._setScissor(area); + + if (this._scissor === area) { + return; + } + this._scissor = area; + + let gl = this.gl; + if (!area) { + gl.disable(gl.SCISSOR_TEST); + } else { + gl.enable(gl.SCISSOR_TEST); + let precision = this.ctx.stage.getRenderPrecision(); + let y = area[1]; + if (this._renderTexture === null) { + // Flip. + y = (this.ctx.stage.h / precision - (area[1] + area[3])); + } + gl.scissor(Math.round(area[0] * precision), Math.round(y * precision), Math.round(area[2] * precision), Math.round(area[3] * precision)); + } + } + +} + +class CoreRenderState { + + constructor(ctx) { + this.ctx = ctx; + + this.stage = ctx.stage; + + this.defaultShader = this.stage.renderer.getDefaultShader(ctx); + + this.renderer = ctx.stage.renderer; + + this.quads = this.renderer.createCoreQuadList(ctx); + + } + + reset() { + this._renderTextureInfo = null; + + this._scissor = null; + + this._shader = null; + + this._shaderOwner = null; + + this._realShader = null; + + this._check = false; + + this.quadOperations = []; + + this._texturizer = null; + + this._texturizerTemporary = false; + + this._quadOperation = null; + + this.quads.reset(); + + this._temporaryTexturizers = []; + + this._isCachingTexturizer = false; + + } + + get length() { + return this.quads.quadTextures.length; + } + + setShader(shader, owner) { + if (this._shaderOwner !== owner || this._realShader !== shader) { + // Same shader owner: active shader is also the same. + // Prevent any shader usage to save performance. + + this._realShader = shader; + + if (shader.useDefault()) { + // Use the default shader when possible to prevent unnecessary program changes. + shader = this.defaultShader; + } + if (this._shader !== shader || this._shaderOwner !== owner) { + this._shader = shader; + this._shaderOwner = owner; + this._check = true; + } + } + } + + get renderTextureInfo() { + return this._renderTextureInfo; + } + + setScissor(area) { + if (this._scissor !== area) { + if (area) { + this._scissor = area; + } else { + this._scissor = null; + } + this._check = true; + } + } + + getScissor() { + return this._scissor; + } + + setRenderTextureInfo(renderTextureInfo) { + if (this._renderTextureInfo !== renderTextureInfo) { + this._renderTextureInfo = renderTextureInfo; + this._scissor = null; + this._check = true; + } + } + + /** + * Sets the texturizer to be drawn during subsequent addQuads. + * @param {ElementTexturizer} texturizer + */ + setTexturizer(texturizer, cache = false) { + this._texturizer = texturizer; + this._cacheTexturizer = cache; + } + + set isCachingTexturizer(v) { + this._isCachingTexturizer = v; + } + + get isCachingTexturizer() { + return this._isCachingTexturizer; + } + + addQuad(elementCore) { + if (!this._quadOperation) { + this._createQuadOperation(); + } else if (this._check && this._hasChanges()) { + this._finishQuadOperation(); + this._check = false; + } + + let nativeTexture = null; + if (this._texturizer) { + nativeTexture = this._texturizer.getResultTexture(); + + if (!this._cacheTexturizer) { + // We can release the temporary texture immediately after finalizing this quad operation. + this._temporaryTexturizers.push(this._texturizer); + } + } + + if (!nativeTexture) { + nativeTexture = elementCore._displayedTextureSource.nativeTexture; + } + + if (this._renderTextureInfo) { + if (this._shader === this.defaultShader && this._renderTextureInfo.empty) { + // The texture might be reusable under some conditions. We will check them in ElementCore.renderer. + this._renderTextureInfo.nativeTexture = nativeTexture; + this._renderTextureInfo.offset = this.length; + } else { + // It is not possible to reuse another texture when there is more than one quad. + this._renderTextureInfo.nativeTexture = null; + } + this._renderTextureInfo.empty = false; + } + + this.quads.quadTextures.push(nativeTexture); + this.quads.quadElements.push(elementCore); + + this._quadOperation.length++; + + this.renderer.addQuad(this, this.quads, this.length - 1); + } + + finishedRenderTexture() { + if (this._renderTextureInfo.nativeTexture) { + // There was only one texture drawn in this render texture. + // Check if we can reuse it so that we can optimize out an unnecessary render texture operation. + // (it should exactly span this render texture). + if (!this._isRenderTextureReusable()) { + this._renderTextureInfo.nativeTexture = null; + } + } + } + + _isRenderTextureReusable() { + const offset = this._renderTextureInfo.offset; + return (this.quads.quadTextures[offset].w === this._renderTextureInfo.w) && + (this.quads.quadTextures[offset].h === this._renderTextureInfo.h) && + this.renderer.isRenderTextureReusable(this, this._renderTextureInfo) + } + + _hasChanges() { + let q = this._quadOperation; + if (this._shader !== q.shader) return true; + if (this._shaderOwner !== q.shaderOwner) return true; + if (this._renderTextureInfo !== q.renderTextureInfo) return true; + if (this._scissor !== q.scissor) { + if ((this._scissor[0] !== q.scissor[0]) || (this._scissor[1] !== q.scissor[1]) || (this._scissor[2] !== q.scissor[2]) || (this._scissor[3] !== q.scissor[3])) { + return true; + } + } + + return false; + } + + _finishQuadOperation(create = true) { + if (this._quadOperation) { + if (this._quadOperation.length || this._shader.addEmpty()) { + if (!this._quadOperation.scissor || ((this._quadOperation.scissor[2] > 0) && (this._quadOperation.scissor[3] > 0))) { + // Ignore empty clipping regions. + this.quadOperations.push(this._quadOperation); + } + } + + if (this._temporaryTexturizers.length) { + for (let i = 0, n = this._temporaryTexturizers.length; i < n; i++) { + // We can now reuse these render-to-textures in subsequent stages. + // Huge performance benefit when filtering (fast blur). + this._temporaryTexturizers[i].releaseRenderTexture(); + } + this._temporaryTexturizers = []; + } + + this._quadOperation = null; + } + + if (create) { + this._createQuadOperation(); + } + } + + _createQuadOperation() { + this._quadOperation = this.renderer.createCoreQuadOperation( + this.ctx, + this._shader, + this._shaderOwner, + this._renderTextureInfo, + this._scissor, + this.length + ); + this._check = false; + } + + finish() { + if (this._quadOperation) { + // Add remaining. + this._finishQuadOperation(false); + } + + this.renderer.finishRenderState(this); + } + +} + +/** + * Base functionality for shader setup/destroy. + */ +class WebGLShaderProgram { + + constructor(vertexShaderSource, fragmentShaderSource) { + + this.vertexShaderSource = vertexShaderSource; + this.fragmentShaderSource = fragmentShaderSource; + + this._program = null; + + this._uniformLocations = new Map(); + this._attributeLocations = new Map(); + + this._currentUniformValues = {}; + } + + compile(gl) { + if (this._program) return; + + this.gl = gl; + + this._program = gl.createProgram(); + + let glVertShader = this._glCompile(gl.VERTEX_SHADER, this.vertexShaderSource); + let glFragShader = this._glCompile(gl.FRAGMENT_SHADER, this.fragmentShaderSource); + + gl.attachShader(this._program, glVertShader); + gl.attachShader(this._program, glFragShader); + gl.linkProgram(this._program); + + // if linking fails, then log and cleanup + if (!gl.getProgramParameter(this._program, gl.LINK_STATUS)) { + console.error('Error: Could not initialize shader.'); + console.error('gl.VALIDATE_STATUS', gl.getProgramParameter(this._program, gl.VALIDATE_STATUS)); + console.error('gl.getError()', gl.getError()); + + // if there is a program info log, log it + if (gl.getProgramInfoLog(this._program) !== '') { + console.warn('Warning: gl.getProgramInfoLog()', gl.getProgramInfoLog(this._program)); + } + + gl.deleteProgram(this._program); + this._program = null; + } + + // clean up some shaders + gl.deleteShader(glVertShader); + gl.deleteShader(glFragShader); + } + + _glCompile(type, src) { + let shader = this.gl.createShader(type); + + this.gl.shaderSource(shader, src); + this.gl.compileShader(shader); + + if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { + console.log(this.constructor.name, 'Type: ' + (type === this.gl.VERTEX_SHADER ? 'vertex shader' : 'fragment shader') ); + console.log(this.gl.getShaderInfoLog(shader)); + let idx = 0; + console.log("========== source ==========\n" + src.split("\n").map(line => "" + (++idx) + ": " + line).join("\n")); + return null; + } + + return shader; + } + + getUniformLocation(name) { + let location = this._uniformLocations.get(name); + if (location === undefined) { + location = this.gl.getUniformLocation(this._program, name); + this._uniformLocations.set(name, location); + } + + return location; + } + + getAttribLocation(name) { + let location = this._attributeLocations.get(name); + if (location === undefined) { + location = this.gl.getAttribLocation(this._program, name); + this._attributeLocations.set(name, location); + } + + return location; + } + + destroy() { + if (this._program) { + this.gl.deleteProgram(this._program); + this._program = null; + } + } + + get glProgram() { + return this._program; + } + + get compiled() { + return !!this._program; + } + + _valueEquals(v1, v2) { + // Uniform value is either a typed array or a numeric value. + if (v1.length && v2.length) { + for (let i = 0, n = v1.length; i < n; i++) { + if (v1[i] !== v2[i]) return false; + } + return true; + } else { + return (v1 === v2); + } + } + + _valueClone(v) { + if (v.length) { + return v.slice(0); + } else { + return v; + } + } + + setUniformValue(name, value, glFunction) { + let v = this._currentUniformValues[name]; + if (v === undefined || !this._valueEquals(v, value)) { + let clonedValue = this._valueClone(value); + this._currentUniformValues[name] = clonedValue; + + let loc = this.getUniformLocation(name); + if (loc) { + let isMatrix = (glFunction === this.gl.uniformMatrix2fv || glFunction === this.gl.uniformMatrix3fv || glFunction === this.gl.uniformMatrix4fv); + if (isMatrix) { + glFunction.call(this.gl, loc, false, clonedValue); + } else { + glFunction.call(this.gl, loc, clonedValue); + } + } + } + } + +} + +class WebGLShader extends Shader { + + constructor(ctx) { + super(ctx); + + const stage = ctx.stage; + + this._program = stage.renderer.shaderPrograms.get(this.constructor); + if (!this._program) { + this._program = new WebGLShaderProgram(this.constructor.vertexShaderSource, this.constructor.fragmentShaderSource); + + // Let the vbo context perform garbage collection. + stage.renderer.shaderPrograms.set(this.constructor, this._program); + } + + this.gl = stage.gl; + } + + get glProgram() { + return this._program.glProgram; + } + + _init() { + if (!this._initialized) { + this.initialize(); + this._initialized = true; + } + } + + initialize() { + this._program.compile(this.gl); + } + + get initialized() { + return this._initialized; + } + + _uniform(name) { + return this._program.getUniformLocation(name); + } + + _attrib(name) { + return this._program.getAttribLocation(name); + } + + _setUniform(name, value, glFunction) { + this._program.setUniformValue(name, value, glFunction); + } + + useProgram() { + this._init(); + this.gl.useProgram(this.glProgram); + this.beforeUsage(); + this.enableAttribs(); + } + + stopProgram() { + this.afterUsage(); + this.disableAttribs(); + } + + hasSameProgram(other) { + // For performance reasons, we first check for identical references. + return (other && ((other === this) || (other._program === this._program))); + } + + beforeUsage() { + // Override to set settings other than the default settings (blend mode etc). + } + + afterUsage() { + // All settings changed in beforeUsage should be reset here. + } + + enableAttribs() { + + } + + disableAttribs() { + + } + + getExtraAttribBytesPerVertex() { + return 0; + } + + getVertexAttribPointerOffset(operation) { + return operation.extraAttribsDataByteOffset - (operation.index + 1) * 4 * this.getExtraAttribBytesPerVertex(); + } + + setExtraAttribsInBuffer(operation) { + // Set extra attrib data in in operation.quads.data/floats/uints, starting from + // operation.extraAttribsBufferByteOffset. + } + + setupUniforms(operation) { + // Set all shader-specific uniforms. + // Notice that all uniforms should be set, even if they have not been changed within this shader instance. + // The uniforms are shared by all shaders that have the same type (and shader program). + } + + _getProjection(operation) { + return operation.getProjection(); + } + + getFlipY(operation) { + return this._getProjection(operation)[1] < 0; + } + + beforeDraw(operation) { + } + + draw(operation) { + } + + afterDraw(operation) { + } + + cleanup() { + this._initialized = false; + // Program takes little resources, so it is only destroyed when the full stage is destroyed. + } + +} + +class DefaultShader extends WebGLShader { + + enableAttribs() { + // Enables the attribs in the shader program. + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aVertexPosition"), 2, gl.FLOAT, false, 20, 0); + gl.enableVertexAttribArray(this._attrib("aVertexPosition")); + + if (this._attrib("aTextureCoord") !== -1) { + gl.vertexAttribPointer(this._attrib("aTextureCoord"), 2, gl.FLOAT, false, 20, 2 * 4); + gl.enableVertexAttribArray(this._attrib("aTextureCoord")); + } + + if (this._attrib("aColor") !== -1) { + // Some shaders may ignore the color. + gl.vertexAttribPointer(this._attrib("aColor"), 4, gl.UNSIGNED_BYTE, true, 20, 4 * 4); + gl.enableVertexAttribArray(this._attrib("aColor")); + } + } + + disableAttribs() { + // Disables the attribs in the shader program. + let gl = this.gl; + gl.disableVertexAttribArray(this._attrib("aVertexPosition")); + + if (this._attrib("aTextureCoord") !== -1) { + gl.disableVertexAttribArray(this._attrib("aTextureCoord")); + } + + if (this._attrib("aColor") !== -1) { + gl.disableVertexAttribArray(this._attrib("aColor")); + } + } + + setupUniforms(operation) { + this._setUniform("projection", this._getProjection(operation), this.gl.uniform2fv, false); + } + + draw(operation) { + let gl = this.gl; + + let length = operation.length; + + if (length) { + let glTexture = operation.getTexture(0); + let pos = 0; + for (let i = 0; i < length; i++) { + let tx = operation.getTexture(i); + if (glTexture !== tx) { + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.drawElements(gl.TRIANGLES, 6 * (i - pos), gl.UNSIGNED_SHORT, (pos + operation.index) * 6 * 2); + glTexture = tx; + pos = i; + } + } + if (pos < length) { + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.drawElements(gl.TRIANGLES, 6 * (length - pos), gl.UNSIGNED_SHORT, (pos + operation.index) * 6 * 2); + } + } + } + +} + +DefaultShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +DefaultShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor; + } +`; + +class Renderer { + + constructor(stage) { + this.stage = stage; + this._defaultShader = undefined; + } + + gc(aggressive) { + } + + destroy() { + } + + getDefaultShader(ctx = this.stage.ctx) { + if (!this._defaultShader) { + this._defaultShader = this._createDefaultShader(ctx); + } + return this._defaultShader; + } + + _createDefaultShader(ctx) { + } + + isValidShaderType(shaderType) { + return (shaderType.prototype instanceof this._getShaderBaseType()); + } + + createShader(ctx, settings) { + const shaderType = settings.type; + // If shader type is not correct, use a different platform. + if (!this.isValidShaderType(shaderType)) { + const convertedShaderType = this._getShaderAlternative(shaderType); + if (!convertedShaderType) { + console.warn("Shader has no implementation for render target: " + shaderType.name); + return this._createDefaultShader(ctx); + } + return new convertedShaderType(ctx); + } else { + const shader = new shaderType(ctx); + Base.patchObject(this, settings); + return shader; + } + } + + _getShaderBaseType() { + } + + _getShaderAlternative(shaderType) { + return this.getDefaultShader(); + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + console.warn('copyRenderTexture not supported by renderer'); + } +} + +class WebGLRenderer extends Renderer { + + constructor(stage) { + super(stage); + this.shaderPrograms = new Map(); + } + + destroy() { + this.shaderPrograms.forEach(shaderProgram => shaderProgram.destroy()); + } + + _createDefaultShader(ctx) { + return new DefaultShader(ctx); + } + + _getShaderBaseType() { + return WebGLShader + } + + _getShaderAlternative(shaderType) { + return shaderType.getWebGL && shaderType.getWebGL(); + } + + createCoreQuadList(ctx) { + return new WebGLCoreQuadList(ctx); + } + + createCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + return new WebGLCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + } + + createCoreRenderExecutor(ctx) { + return new WebGLCoreRenderExecutor(ctx); + } + + createCoreRenderState(ctx) { + return new CoreRenderState(ctx); + } + + createRenderTexture(w, h, pw, ph) { + const gl = this.stage.gl; + const glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, pw, ph, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + glTexture.params = {}; + glTexture.params[gl.TEXTURE_MAG_FILTER] = gl.LINEAR; + glTexture.params[gl.TEXTURE_MIN_FILTER] = gl.LINEAR; + glTexture.params[gl.TEXTURE_WRAP_S] = gl.CLAMP_TO_EDGE; + glTexture.params[gl.TEXTURE_WRAP_T] = gl.CLAMP_TO_EDGE; + glTexture.options = {format: gl.RGBA, internalFormat: gl.RGBA, type: gl.UNSIGNED_BYTE}; + + // We need a specific framebuffer for every render texture. + glTexture.framebuffer = gl.createFramebuffer(); + glTexture.projection = new Float32Array([2/w, 2/h]); + + gl.bindFramebuffer(gl.FRAMEBUFFER, glTexture.framebuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, glTexture, 0); + + return glTexture; + } + + freeRenderTexture(glTexture) { + let gl = this.stage.gl; + gl.deleteFramebuffer(glTexture.framebuffer); + gl.deleteTexture(glTexture); + } + + uploadTextureSource(textureSource, options) { + const gl = this.stage.gl; + + const source = options.source; + + const format = { + premultiplyAlpha: true, + hasAlpha: true + }; + + if (options && options.hasOwnProperty('premultiplyAlpha')) { + format.premultiplyAlpha = options.premultiplyAlpha; + } + + if (options && options.hasOwnProperty('flipBlueRed')) { + format.flipBlueRed = options.flipBlueRed; + } + + if (options && options.hasOwnProperty('hasAlpha')) { + format.hasAlpha = options.hasAlpha; + } + + if (!format.hasAlpha) { + format.premultiplyAlpha = false; + } + + format.texParams = options.texParams || {}; + format.texOptions = options.texOptions || {}; + + let glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, format.premultiplyAlpha); + + if (Utils.isNode) { + gl.pixelStorei(gl.UNPACK_FLIP_BLUE_RED, !!format.flipBlueRed); + } + + const texParams = format.texParams; + if (!texParams[gl.TEXTURE_MAG_FILTER]) texParams[gl.TEXTURE_MAG_FILTER] = gl.LINEAR; + if (!texParams[gl.TEXTURE_MIN_FILTER]) texParams[gl.TEXTURE_MIN_FILTER] = gl.LINEAR; + if (!texParams[gl.TEXTURE_WRAP_S]) texParams[gl.TEXTURE_WRAP_S] = gl.CLAMP_TO_EDGE; + if (!texParams[gl.TEXTURE_WRAP_T]) texParams[gl.TEXTURE_WRAP_T] = gl.CLAMP_TO_EDGE; + + Object.keys(texParams).forEach(key => { + const value = texParams[key]; + gl.texParameteri(gl.TEXTURE_2D, parseInt(key), value); + }); + + const texOptions = format.texOptions; + texOptions.format = texOptions.format || (format.hasAlpha ? gl.RGBA : gl.RGB); + texOptions.type = texOptions.type || gl.UNSIGNED_BYTE; + texOptions.internalFormat = texOptions.internalFormat || texOptions.format; + + this.stage.platform.uploadGlTexture(gl, textureSource, source, texOptions); + + glTexture.params = Utils.cloneObjShallow(texParams); + glTexture.options = Utils.cloneObjShallow(texOptions); + + return glTexture; + } + + freeTextureSource(textureSource) { + this.stage.gl.deleteTexture(textureSource.nativeTexture); + } + + addQuad(renderState, quads, index) { + let offset = (index * 20); + const elementCore = quads.quadElements[index]; + + let r = elementCore._renderContext; + + let floats = renderState.quads.floats; + let uints = renderState.quads.uints; + const mca = StageUtils.mergeColorAlpha; + + if (r.tb !== 0 || r.tc !== 0) { + floats[offset++] = r.px; + floats[offset++] = r.py; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUl, r.alpha); + floats[offset++] = r.px + elementCore._w * r.ta; + floats[offset++] = r.py + elementCore._w * r.tc; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUr, r.alpha); + floats[offset++] = r.px + elementCore._w * r.ta + elementCore._h * r.tb; + floats[offset++] = r.py + elementCore._w * r.tc + elementCore._h * r.td; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._bry; + uints[offset++] = mca(elementCore._colorBr, r.alpha); + floats[offset++] = r.px + elementCore._h * r.tb; + floats[offset++] = r.py + elementCore._h * r.td; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._bry; + uints[offset] = mca(elementCore._colorBl, r.alpha); + } else { + // Simple. + let cx = r.px + elementCore._w * r.ta; + let cy = r.py + elementCore._h * r.td; + + floats[offset++] = r.px; + floats[offset++] = r.py; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUl, r.alpha); + floats[offset++] = cx; + floats[offset++] = r.py; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._uly; + uints[offset++] = mca(elementCore._colorUr, r.alpha); + floats[offset++] = cx; + floats[offset++] = cy; + floats[offset++] = elementCore._brx; + floats[offset++] = elementCore._bry; + uints[offset++] = mca(elementCore._colorBr, r.alpha); + floats[offset++] = r.px; + floats[offset++] = cy; + floats[offset++] = elementCore._ulx; + floats[offset++] = elementCore._bry; + uints[offset] = mca(elementCore._colorBl, r.alpha); + } + } + + isRenderTextureReusable(renderState, renderTextureInfo) { + let offset = (renderState._renderTextureInfo.offset * 80) / 4; + let floats = renderState.quads.floats; + let uints = renderState.quads.uints; + return ((floats[offset] === 0) && + (floats[offset + 1] === 0) && + (floats[offset + 2] === 0) && + (floats[offset + 3] === 0) && + (uints[offset + 4] === 0xFFFFFFFF) && + (floats[offset + 5] === renderTextureInfo.w) && + (floats[offset + 6] === 0) && + (floats[offset + 7] === 1) && + (floats[offset + 8] === 0) && + (uints[offset + 9] === 0xFFFFFFFF) && + (floats[offset + 10] === renderTextureInfo.w) && + (floats[offset + 11] === renderTextureInfo.h) && + (floats[offset + 12] === 1) && + (floats[offset + 13] === 1) && + (uints[offset + 14] === 0xFFFFFFFF) && + (floats[offset + 15] === 0) && + (floats[offset + 16] === renderTextureInfo.h) && + (floats[offset + 17] === 0) && + (floats[offset + 18] === 1) && + (uints[offset + 19] === 0xFFFFFFFF)); + } + + finishRenderState(renderState) { + // Set extra shader attribute data. + let offset = renderState.length * 80; + for (let i = 0, n = renderState.quadOperations.length; i < n; i++) { + renderState.quadOperations[i].extraAttribsDataByteOffset = offset; + let extra = renderState.quadOperations[i].shader.getExtraAttribBytesPerVertex() * 4 * renderState.quadOperations[i].length; + offset += extra; + if (extra) { + renderState.quadOperations[i].shader.setExtraAttribsInBuffer(renderState.quadOperations[i], renderState.quads); + } + } + renderState.quads.dataLength = offset; + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + const gl = this.stage.gl; + gl.bindTexture(gl.TEXTURE_2D, nativeTexture); + gl.bindFramebuffer(gl.FRAMEBUFFER, renderTexture.framebuffer); + const precision = renderTexture.precision; + gl.copyTexSubImage2D( + gl.TEXTURE_2D, + 0, + precision * (options.sx || 0), + precision * (options.sy || 0), + precision * (options.x || 0), + precision * (options.y || 0), + precision * (options.w || renderTexture.ow), + precision * (options.h || renderTexture.oh)); + } + +} + +class C2dCoreQuadList extends CoreQuadList { + + constructor(ctx) { + super(ctx); + + this.renderContexts = []; + this.modes = []; + } + + setRenderContext(index, v) { + this.renderContexts[index] = v; + } + + setSimpleTc(index, v) { + if (v) { + this.modes[index] |= 1; + } else { + this.modes[index] -= (this.modes[index] & 1); + } + } + + setWhite(index, v) { + if (v) { + this.modes[index] |= 2; + } else { + this.modes[index] -= (this.modes[index] & 2); + } + } + + getRenderContext(index) { + return this.renderContexts[index]; + } + + getSimpleTc(index) { + return (this.modes[index] & 1); + } + + getWhite(index) { + return (this.modes[index] & 2); + } + +} + +class C2dCoreQuadOperation extends CoreQuadOperation { + + getRenderContext(index) { + return this.quads.getRenderContext(this.index + index); + } + + getSimpleTc(index) { + return this.quads.getSimpleTc(this.index + index); + } + + getWhite(index) { + return this.quads.getWhite(this.index + index); + } + +} + +class C2dCoreRenderExecutor extends CoreRenderExecutor { + + init() { + this._mainRenderTexture = this.ctx.stage.getCanvas(); + } + + _renderQuadOperation(op) { + let shader = op.shader; + + if (op.length || op.shader.addEmpty()) { + const target = this._renderTexture || this._mainRenderTexture; + shader.beforeDraw(op, target); + shader.draw(op, target); + shader.afterDraw(op, target); + } + } + + _clearRenderTexture() { + const ctx = this._getContext(); + + let clearColor = [0, 0, 0, 0]; + if (this._mainRenderTexture.ctx === ctx) { + clearColor = this.ctx.stage.getClearColor(); + } + + const renderTexture = ctx.canvas; + ctx.setTransform(1, 0, 0, 1, 0, 0); + if (!clearColor[0] && !clearColor[1] && !clearColor[2] && !clearColor[3]) { + ctx.clearRect(0, 0, renderTexture.width, renderTexture.height); + } else { + ctx.fillStyle = StageUtils.getRgbaStringFromArray(clearColor); + // Do not use fillRect because it produces artifacts. + ctx.globalCompositeOperation = 'copy'; + ctx.beginPath(); + ctx.rect(0, 0, renderTexture.width, renderTexture.height); + ctx.closePath(); + ctx.fill(); + ctx.globalCompositeOperation = 'source-over'; + } + } + + _getContext() { + if (this._renderTexture) { + return this._renderTexture.ctx; + } else { + return this._mainRenderTexture.ctx; + } + } + + _restoreContext() { + const ctx = this._getContext(); + ctx.restore(); + ctx.save(); + ctx._scissor = null; + } + + _setScissor(area) { + const ctx = this._getContext(); + + if (!C2dCoreRenderExecutor._equalScissorAreas(ctx.canvas, ctx._scissor, area)) { + // Clipping is stored in the canvas context state. + // We can't reset clipping alone so we need to restore the full context. + this._restoreContext(); + + let precision = this.ctx.stage.getRenderPrecision(); + if (area) { + ctx.beginPath(); + ctx.rect(Math.round(area[0] * precision), Math.round(area[1] * precision), Math.round(area[2] * precision), Math.round(area[3] * precision)); + ctx.closePath(); + ctx.clip(); + } + ctx._scissor = area; + } + } + + static _equalScissorAreas(canvas, area, current) { + if (!area) { + area = [0, 0, canvas.width, canvas.height]; + } + if (!current) { + current = [0, 0, canvas.width, canvas.height]; + } + return Utils.equalValues(area, current) + } + +} + +class C2dShader extends Shader { + + beforeDraw(operation) { + } + + draw(operation) { + } + + afterDraw(operation) { + } + +} + +class DefaultShader$1 extends C2dShader { + + constructor(ctx) { + super(ctx); + this._rectangleTexture = ctx.stage.rectangleTexture.source.nativeTexture; + this._tintManager = this.ctx.stage.renderer.tintManager; + } + + draw(operation, target) { + const ctx = target.ctx; + let length = operation.length; + for (let i = 0; i < length; i++) { + const tx = operation.getTexture(i); + const vc = operation.getElementCore(i); + const rc = operation.getRenderContext(i); + const white = operation.getWhite(i); + const stc = operation.getSimpleTc(i); + + //@todo: try to optimize out per-draw transform setting. split translate, transform. + const precision = this.ctx.stage.getRenderPrecision(); + ctx.setTransform(rc.ta * precision, rc.tc * precision, rc.tb * precision, rc.td * precision, rc.px * precision, rc.py * precision); + + const rect = (tx === this._rectangleTexture); + const info = {operation, target, index: i, rect}; + + if (rect) { + // Check for gradient. + if (white) { + ctx.fillStyle = 'white'; + } else { + this._setColorGradient(ctx, vc); + } + + ctx.globalAlpha = rc.alpha; + this._beforeDrawEl(info); + ctx.fillRect(0, 0, vc.w, vc.h); + this._afterDrawEl(info); + ctx.globalAlpha = 1.0; + } else { + // @todo: set image smoothing based on the texture. + + // @todo: optimize by registering whether identity texcoords are used. + ctx.globalAlpha = rc.alpha; + this._beforeDrawEl(info); + + // @todo: test if rounding yields better performance. + + // Notice that simple texture coords can be turned on even though vc._ulx etc are not simple, because + // we are rendering a render-to-texture (texcoords were stashed). Same is true for 'white' color btw. + const sourceX = stc ? 0 : (vc._ulx * tx.w); + const sourceY = stc ? 0 : (vc._uly * tx.h); + const sourceW = (stc ? 1 : (vc._brx - vc._ulx)) * tx.w; + const sourceH = (stc ? 1 : (vc._bry - vc._uly)) * tx.h; + + let colorize = !white; + if (colorize) { + // @todo: cache the tint texture for better performance. + + // Draw to intermediate texture with background color/gradient. + // This prevents us from having to create a lot of render texture canvases. + + // Notice that we don't support (non-rect) gradients, only color tinting for c2d. We'll just take the average color. + let color = vc._colorUl; + if (vc._colorUl !== vc._colorUr || vc._colorUr !== vc._colorBl || vc._colorBr !== vc._colorBl) { + color = StageUtils.mergeMultiColorsEqual([vc._colorUl, vc._colorUr, vc._colorBl, vc._colorBr]); + } + + const alpha = ((color / 16777216) | 0) / 255.0; + ctx.globalAlpha *= alpha; + + const rgb = color & 0x00FFFFFF; + const tintTexture = this._tintManager.getTintTexture(tx, rgb); + + // Actually draw result. + ctx.fillStyle = 'white'; + ctx.drawImage(tintTexture, sourceX, sourceY, sourceW, sourceH, 0, 0, vc.w, vc.h); + } else { + ctx.fillStyle = 'white'; + ctx.drawImage(tx, sourceX, sourceY, sourceW, sourceH, 0, 0, vc.w, vc.h); + } + this._afterDrawEl(info); + ctx.globalAlpha = 1.0; + } + } + } + + _setColorGradient(ctx, vc, w = vc.w, h = vc.h, transparency = true) { + let color = vc._colorUl; + let gradient; + //@todo: quick single color check. + //@todo: cache gradient/fill style (if possible, probably context-specific). + + if (vc._colorUl === vc._colorUr) { + if (vc._colorBl === vc._colorBr) { + if (vc._colorUl === vc.colorBl) ; else { + // Vertical gradient. + gradient = ctx.createLinearGradient(0, 0, 0, h); + if (transparency) { + gradient.addColorStop(0, StageUtils.getRgbaString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbaString(vc._colorBl)); + } else { + gradient.addColorStop(0, StageUtils.getRgbString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbString(vc._colorBl)); + + } + } + } + } else { + if (vc._colorUl === vc._colorBl && vc._colorUr === vc._colorBr) { + // Horizontal gradient. + gradient = ctx.createLinearGradient(0, 0, w, 0); + if (transparency) { + gradient.addColorStop(0, StageUtils.getRgbaString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbaString(vc._colorBr)); + } else { + gradient.addColorStop(0, StageUtils.getRgbString(vc._colorUl)); + gradient.addColorStop(1, StageUtils.getRgbString(vc._colorBr)); + } + } + } + + if (gradient) { + ctx.fillStyle = gradient; + } else { + ctx.fillStyle = transparency ? StageUtils.getRgbaString(color) : StageUtils.getRgbString(color); + } + } + + _beforeDrawEl(info) { + } + + _afterDrawEl(info) { + } + +} + +class C2dTextureTintManager { + + constructor(stage) { + this.stage = stage; + this._usedMemory = 0; + this._cachedNativeTextures = new Set(); + } + + destroy() { + this.gc(true); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + + this.stage.addMemoryUsage(delta); + } + + delete(nativeTexture) { + // Should be called when native texture is cleaned up. + if (this._hasCache(nativeTexture)) { + const cache = this._getCache(nativeTexture); + const prevMemUsage = cache.memoryUsage; + cache.clear(); + this._cachedNativeTextures.delete(nativeTexture); + this._addMemoryUsage(cache.memoryUsage - prevMemUsage); + } + } + + getTintTexture(nativeTexture, color) { + const frame = this.stage.frameCounter; + + this._cachedNativeTextures.add(nativeTexture); + + const cache = this._getCache(nativeTexture); + + const item = cache.get(color); + item.lf = frame; + + if (item.tx) { + if (nativeTexture.update > item.u) { + // Native texture was updated in the mean time: renew. + this._tintTexture(item.tx, nativeTexture, color); + } + + return item.tx; + } else { + const before = cache.memoryUsage; + + // Find blanco tint texture. + let target = cache.reuseTexture(frame); + if (target) { + target.ctx.clearRect(0, 0, target.width, target.height); + } else { + // Allocate new. + target = document.createElement('canvas'); + target.width = nativeTexture.w; + target.height = nativeTexture.h; + target.ctx = target.getContext('2d'); + } + + this._tintTexture(target, nativeTexture, color); + cache.set(color, target, frame); + + const after = cache.memoryUsage; + + if (after !== before) { + this._addMemoryUsage(after - before); + } + + return target; + } + } + + _tintTexture(target, source, color) { + let col = color.toString(16); + while (col.length < 6) { + col = "0" + col; + } + target.ctx.fillStyle = '#' + col; + target.ctx.globalCompositeOperation = 'copy'; + target.ctx.fillRect(0, 0, source.w, source.h); + target.ctx.globalCompositeOperation = 'multiply'; + target.ctx.drawImage(source, 0, 0, source.w, source.h, 0, 0, target.width, target.height); + + // Alpha-mix the texture. + target.ctx.globalCompositeOperation = 'destination-in'; + target.ctx.drawImage(source, 0, 0, source.w, source.h, 0, 0, target.width, target.height); + } + + _hasCache(nativeTexture) { + return !!nativeTexture._tintCache; + } + + _getCache(nativeTexture) { + if (!nativeTexture._tintCache) { + nativeTexture._tintCache = new C2dTintCache(nativeTexture); + } + return nativeTexture._tintCache; + } + + gc(aggressive = false) { + const frame = this.stage.frameCounter; + let delta = 0; + this._cachedNativeTextures.forEach(texture => { + const cache = this._getCache(texture); + if (aggressive) { + delta += cache.memoryUsage; + texture.clear(); + } else { + const before = cache.memoryUsage; + cache.cleanup(frame); + cache.releaseBlancoTextures(); + delta += (cache.memoryUsage - before); + } + }); + + if (aggressive) { + this._cachedNativeTextures.clear(); + } + + if (delta) { + this._addMemoryUsage(delta); + } + } + +} + +class C2dTintCache { + + constructor(nativeTexture) { + this._tx = nativeTexture; + this._colors = new Map(); + this._blancoTextures = null; + this._lastCleanupFrame = 0; + this._memTextures = 0; + } + + get memoryUsage() { + return this._memTextures * this._tx.w * this._tx.h; + } + + releaseBlancoTextures() { + this._memTextures -= this._blancoTextures.length; + this._blancoTextures = []; + } + + clear() { + // Dereference the textures. + this._blancoTextures = null; + this._colors.clear(); + this._memTextures = 0; + } + + get(color) { + let item = this._colors.get(color); + if (!item) { + item = {lf: -1, tx: undefined, u: -1}; + this._colors.set(color, item); + } + return item; + } + + set(color, texture, frame) { + const item = this.get(color); + item.lf = frame; + item.tx = texture; + item.u = frame; + this._memTextures++; + } + + cleanup(frame) { + // We only need to clean up once per frame. + if (this._lastCleanupFrame !== frame) { + + // We limit blanco textures reuse to one frame only to prevent memory usage growth. + this._blancoTextures = []; + + this._colors.forEach((item, color) => { + // Clean up entries that were not used last frame. + if (item.lf < frame - 1) { + if (item.tx) { + // Keep as reusable blanco texture. + this._blancoTextures.push(item.tx); + } + this._colors.delete(color); + } + }); + + this._lastCleanupFrame = frame; + } + } + + reuseTexture(frame) { + // Try to reuse textures, because creating them every frame is expensive. + this.cleanup(frame); + if (this._blancoTextures && this._blancoTextures.length) { + this._memTextures--; + return this._blancoTextures.pop(); + } + } + +} + +class C2dRenderer extends Renderer { + + constructor(stage) { + super(stage); + + this.tintManager = new C2dTextureTintManager(stage); + + this.setupC2d(this.stage.c2d.canvas); + } + + destroy() { + this.tintManager.destroy(); + } + + _createDefaultShader(ctx) { + return new DefaultShader$1(ctx); + } + + _getShaderBaseType() { + return C2dShader + } + + _getShaderAlternative(shaderType) { + return shaderType.getC2d && shaderType.getC2d(); + } + + createCoreQuadList(ctx) { + return new C2dCoreQuadList(ctx); + } + + createCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index) { + return new C2dCoreQuadOperation(ctx, shader, shaderOwner, renderTextureInfo, scissor, index); + } + + createCoreRenderExecutor(ctx) { + return new C2dCoreRenderExecutor(ctx); + } + + createCoreRenderState(ctx) { + return new CoreRenderState(ctx); + } + + createRenderTexture(w, h, pw, ph) { + const canvas = document.createElement('canvas'); + canvas.width = pw; + canvas.height = ph; + this.setupC2d(canvas); + return canvas; + } + + freeRenderTexture(nativeTexture) { + this.tintManager.delete(nativeTexture); + } + + gc(aggressive) { + this.tintManager.gc(aggressive); + } + + uploadTextureSource(textureSource, options) { + // For canvas, we do not need to upload. + if (options.source.buffer) { + // Convert RGBA buffer to canvas. + const canvas = document.createElement('canvas'); + canvas.width = options.w; + canvas.height = options.h; + + const imageData = new ImageData(new Uint8ClampedArray(options.source.buffer), options.w, options.h); + canvas.getContext('2d').putImageData(imageData, 0, 0); + return canvas; + } + + return options.source; + } + + freeTextureSource(textureSource) { + this.tintManager.delete(textureSource.nativeTexture); + } + + addQuad(renderState, quads, index) { + // Render context changes while traversing so we save it by ref. + const elementCore = quads.quadElements[index]; + quads.setRenderContext(index, elementCore._renderContext); + quads.setWhite(index, elementCore.isWhite()); + quads.setSimpleTc(index, elementCore.hasSimpleTexCoords()); + } + + isRenderTextureReusable(renderState, renderTextureInfo) { + // @todo: check render coords/matrix, maybe move this to core? + return false; + } + + finishRenderState(renderState) { + } + + setupC2d(canvas) { + const ctx = canvas.getContext('2d'); + canvas.ctx = ctx; + + ctx._scissor = null; + + // Save base state so we can restore the defaults later. + canvas.ctx.save(); + } + +} + +class ImageWorker { + + constructor(options = {}) { + this._items = new Map(); + this._id = 0; + + this._initWorker(); + } + + destroy() { + if (this._worker) { + this._worker.terminate(); + } + } + + _initWorker() { + const code = `(${createWorker.toString()})()`; + const blob = new Blob([code.replace('"use strict";', '')]); // firefox adds "use strict"; to any function which might block worker execution so knock it off + const blobURL = (window.URL ? URL : webkitURL).createObjectURL(blob, { + type: 'application/javascript; charset=utf-8' + }); + this._worker = new Worker(blobURL); + + this._worker.postMessage({type: 'config', config: {path: window.location.href}}); + + this._worker.onmessage = (e) => { + if (e.data && e.data.id) { + const id = e.data.id; + const item = this._items.get(id); + if (item) { + if (e.data.type == 'data') { + this.finish(item, e.data.info); + } else { + this.error(item, e.data.info); + } + } + } + }; + } + + create(src) { + const id = ++this._id; + const item = new ImageWorkerImage(this, id, src); + this._items.set(id, item); + this._worker.postMessage({type: "add", id: id, src: src}); + return item; + } + + cancel(image) { + this._worker.postMessage({type: "cancel", id: image.id}); + this._items.delete(image.id); + } + + error(image, info) { + image.error(info); + this._items.delete(image.id); + } + + finish(image, info) { + image.load(info); + this._items.delete(image.id); + } + +} + +class ImageWorkerImage { + + constructor(manager, id, src) { + this._manager = manager; + this._id = id; + this._src = src; + this._onError = null; + this._onLoad = null; + } + + get id() { + return this._id; + } + + get src() { + return this._src; + } + + set onError(f) { + this._onError = f; + } + + set onLoad(f) { + this._onLoad = f; + } + + cancel() { + this._manager.cancel(this); + } + + load(info) { + if (this._onLoad) { + this._onLoad(info); + } + } + + error(info) { + if (this._onError) { + this._onError(info); + } + } + +} + +/** + * Notice that, within the createWorker function, we must only use ES5 code to keep it ES5-valid after babelifying, as + * the converted code of this section is converted to a blob and used as the js of the web worker thread. + */ +const createWorker = function() { + + function ImageWorkerServer() { + + this.items = new Map(); + + var t = this; + onmessage = function(e) { + t._receiveMessage(e); + }; + + } + + ImageWorkerServer.isPathAbsolute = function(path) { + return /^(?:\/|[a-z]+:\/\/)/.test(path); + }; + + ImageWorkerServer.prototype._receiveMessage = function(e) { + if (e.data.type === 'config') { + this.config = e.data.config; + + var base = this.config.path; + var parts = base.split("/"); + parts.pop(); + this._relativeBase = parts.join("/") + "/"; + + } else if (e.data.type === 'add') { + this.add(e.data.id, e.data.src); + } else if (e.data.type === 'cancel') { + this.cancel(e.data.id); + } + }; + + ImageWorkerServer.prototype.add = function(id, src) { + // Convert relative URLs. + if (!ImageWorkerServer.isPathAbsolute(src)) { + src = this._relativeBase + src; + } + + if (src.substr(0,2) === "//") { + // This doesn't work for image workers. + src = "http:" + src; + } + + var item = new ImageWorkerServerItem(id, src); + const t = this; + item.onFinish = function(result) { + t.finish(item, result); + }; + item.onError = function(info) { + t.error(item, info); + }; + this.items.set(id, item); + item.start(); + }; + + ImageWorkerServer.prototype.cancel = function(id) { + var item = this.items.get(id); + if (item) { + item.cancel(); + this.items.delete(id); + } + }; + + ImageWorkerServer.prototype.finish = function(item, {imageBitmap, hasAlphaChannel}) { + postMessage({ + type: "data", + id: item.id, + info: { + imageBitmap, + hasAlphaChannel + } + }, [imageBitmap]); + this.items.delete(item.id); + }; + + ImageWorkerServer.prototype.error = function(item, {type, message}) { + postMessage({ + type: "error", + id: item.id, + info: { + type, + message + } + }); + this.items.delete(item.id); + }; + + ImageWorkerServer.isWPEBrowser = function() { + return (navigator.userAgent.indexOf("WPE") !== -1); + }; + + function ImageWorkerServerItem(id, src) { + + this._onError = undefined; + this._onFinish = undefined; + this._id = id; + this._src = src; + this._xhr = undefined; + this._mimeType = undefined; + this._canceled = false; + + } + + Object.defineProperty(ImageWorkerServerItem.prototype, 'id', { + get: function() { + return this._id; + } + }); + + Object.defineProperty(ImageWorkerServerItem.prototype, 'onFinish', { + get: function() { + return this._onFinish; + }, + set: function(f) { + this._onFinish = f; + } + }); + + Object.defineProperty(ImageWorkerServerItem.prototype, 'onError', { + get: function() { + return this._onError; + }, + set: function(f) { + this._onError = f; + } + }); + + ImageWorkerServerItem.prototype.start = function() { + this._xhr = new XMLHttpRequest(); + this._xhr.open("GET", this._src, true); + this._xhr.responseType = "blob"; + + var t = this; + this._xhr.onerror = function(oEvent) { + t.error({type: "connection", message: "Connection error"}); + }; + + this._xhr.onload = function(oEvent) { + var blob = t._xhr.response; + t._mimeType = blob.type; + + t._createImageBitmap(blob); + }; + + this._xhr.send(); + }; + + ImageWorkerServerItem.prototype._createImageBitmap = function(blob) { + var t = this; + createImageBitmap(blob, {premultiplyAlpha: 'premultiply', colorSpaceConversion: 'none', imageOrientation: 'none'}).then(function(imageBitmap) { + t.finish({ + imageBitmap, + hasAlphaChannel: t._hasAlphaChannel() + }); + }).catch(function(e) { + t.error({type: "parse", message: "Error parsing image data"}); + }); + }; + + ImageWorkerServerItem.prototype._hasAlphaChannel = function() { + if (ImageWorkerServer.isWPEBrowser()) { + // When using unaccelerated rendering image (https://github.com/WebPlatformForEmbedded/WPEWebKit/blob/wpe-20170728/Source/WebCore/html/ImageBitmap.cpp#L52), + // everything including JPG images are in RGBA format. Upload is way faster when using an alpha channel. + // @todo: after hardware acceleration is fixed and re-enabled, JPG should be uploaded in RGB to get the best possible performance and memory usage. + return true; + } else { + return (this._mimeType.indexOf("image/png") !== -1); + } + }; + + ImageWorkerServerItem.prototype.cancel = function() { + if (this._canceled) return; + if (this._xhr) { + this._xhr.abort(); + } + this._canceled = true; + }; + + ImageWorkerServerItem.prototype.error = function(type, message) { + if (!this._canceled && this._onError) { + this._onError({type, message}); + } + }; + + ImageWorkerServerItem.prototype.finish = function(info) { + if (!this._canceled && this._onFinish) { + this._onFinish(info); + } + }; + + var worker = new ImageWorkerServer(); +}; + +/** + * Platform-specific functionality. + * Copyright Metrological, 2017; + */ +class WebPlatform { + + init(stage) { + this.stage = stage; + this._looping = false; + this._awaitingLoop = false; + + if (this.stage.getOption("useImageWorker")) { + if (!window.createImageBitmap || !window.Worker) { + console.warn("Can't use image worker because browser does not have createImageBitmap and Web Worker support"); + } else { + console.log('Using image worker!'); + this._imageWorker = new ImageWorker(); + } + } + } + + destroy() { + if (this._imageWorker) { + this._imageWorker.destroy(); + } + this._removeKeyHandler(); + } + + startLoop() { + this._looping = true; + if (!this._awaitingLoop) { + this.loop(); + } + } + + stopLoop() { + this._looping = false; + } + + loop() { + let self = this; + let lp = function() { + self._awaitingLoop = false; + if (self._looping) { + self.stage.drawFrame(); + requestAnimationFrame(lp); + self._awaitingLoop = true; + } + }; + requestAnimationFrame(lp); + } + + uploadGlTexture(gl, textureSource, source, options) { + if (source instanceof ImageData || source instanceof HTMLImageElement || source instanceof HTMLCanvasElement || source instanceof HTMLVideoElement || (window.ImageBitmap && source instanceof ImageBitmap)) { + // Web-specific data types. + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, options.format, options.type, source); + } else { + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, textureSource.w, textureSource.h, 0, options.format, options.type, source); + } + } + + loadSrcTexture({src, hasAlpha}, cb) { + let cancelCb = undefined; + let isPng = (src.indexOf(".png") >= 0); + if (this._imageWorker) { + // WPE-specific image parser. + const image = this._imageWorker.create(src); + image.onError = function(err) { + return cb("Image load error"); + }; + image.onLoad = function({imageBitmap, hasAlphaChannel}) { + cb(null, { + source: imageBitmap, + renderInfo: {src: src}, + hasAlpha: hasAlphaChannel, + premultiplyAlpha: true + }); + }; + cancelCb = function() { + image.cancel(); + }; + } else { + let image = new Image(); + if (!(src.substr(0,5) == "data:")) { + // Base64. + image.crossOrigin = "Anonymous"; + } + image.onerror = function(err) { + // Ignore error message when cancelled. + if (image.src) { + return cb("Image load error"); + } + }; + image.onload = function() { + cb(null, { + source: image, + renderInfo: {src: src}, + hasAlpha: isPng || hasAlpha + }); + }; + image.src = src; + + cancelCb = function() { + image.onerror = null; + image.onload = null; + image.removeAttribute('src'); + }; + } + + return cancelCb; + } + + createWebGLContext(w, h) { + let canvas = this.stage.getOption('canvas') || document.createElement('canvas'); + + if (w && h) { + canvas.width = w; + canvas.height = h; + } + + let opts = { + alpha: true, + antialias: false, + premultipliedAlpha: true, + stencil: true, + preserveDrawingBuffer: false + }; + + let gl = canvas.getContext('webgl', opts) || canvas.getContext('experimental-webgl', opts); + if (!gl) { + throw new Error('This browser does not support webGL.'); + } + + return gl; + } + + createCanvasContext(w, h) { + let canvas = this.stage.getOption('canvas') || document.createElement('canvas'); + + if (w && h) { + canvas.width = w; + canvas.height = h; + } + + let c2d = canvas.getContext('2d'); + if (!c2d) { + throw new Error('This browser does not support 2d canvas.'); + } + + return c2d; + } + + getHrTime() { + return window.performance ? window.performance.now() : (new Date()).getTime(); + } + + getDrawingCanvas() { + // We can't reuse this canvas because textures may load async. + return document.createElement('canvas'); + } + + getTextureOptionsForDrawingCanvas(canvas) { + let options = {}; + options.source = canvas; + return options; + } + + nextFrame(changes) { + /* WebGL blits automatically */ + } + + registerKeyHandler(keyhandler) { + this._keyListener = e => { + keyhandler(e); + }; + window.addEventListener('keydown', this._keyListener); + } + + _removeKeyHandler() { + if (this._keyListener) { + window.removeEventListener('keydown', this._keyListener); + } + } + +} + +class PlatformLoader { + static load(options) { + if (options.platform) { + return options.platform; + } else { + if (Utils.isWeb) { + return WebPlatform; + } else { + throw new Error("You must specify the platform class to be used."); + } + } + } +} + +class Utils$1 { + + static isFunction(value) { + return typeof value === 'function'; + } + + static isNumber(value) { + return typeof value === 'number'; + } + + static isInteger(value) { + return (typeof value === 'number' && (value % 1) === 0); + } + + static isBoolean(value) { + return value === true || value === false; + } + + static isString(value) { + return typeof value == 'string'; + } + + static isObject(value) { + let type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + static isPlainObject(value) { + let type = typeof value; + return !!value && (type == 'object'); + } + + static isObjectLiteral(value){ + return typeof value === 'object' && value && value.constructor === Object + } + + static getArrayIndex(index, arr) { + return Utils$1.getModuloIndex(index, arr.length); + } + + static equalValues(v1, v2) { + if ((typeof v1) !== (typeof v2)) return false + if (Utils$1.isObjectLiteral(v1)) { + return Utils$1.isObjectLiteral(v2) && Utils$1.equalObjectLiterals(v1, v2) + } else if (Array.isArray(v1)) { + return Array.isArray(v2) && Utils$1.equalArrays(v1, v2) + } else { + return v1 === v2 + } + } + + static equalObjectLiterals(obj1, obj2) { + let keys1 = Object.keys(obj1); + let keys2 = Object.keys(obj2); + if (keys1.length !== keys2.length) { + return false + } + + for (let i = 0, n = keys1.length; i < n; i++) { + const k1 = keys1[i]; + const k2 = keys2[i]; + if (k1 !== k2) { + return false + } + + const v1 = obj1[k1]; + const v2 = obj2[k2]; + + if (!Utils$1.equalValues(v1, v2)) { + return false + } + } + + return true; + } + + static equalArrays(v1, v2) { + if (v1.length !== v2.length) { + return false + } + for (let i = 0, n = v1.length; i < n; i++) { + if (!this.equalValues(v1[i], v2[i])) { + return false + } + } + + return true + } + +} + +/** + * Maintains the state of a WebGLRenderingContext. + */ +class WebGLState { + + constructor(id, gl) { + this._id = id; + this._gl = gl; + this._program = undefined; + this._buffers = new Map(); + this._framebuffers = new Map(); + this._renderbuffers = new Map(); + + // Contains vertex attribute definition arrays (enabled, size, type, normalized, stride, offset). + this._vertexAttribs = new Array(16); + this._nonDefaultFlags = new Set(); + this._settings = new Map(); + this._textures = new Array(8); + this._maxTexture = 0; + this._activeTexture = gl.TEXTURE0; + this._pixelStorei = new Array(5); + } + + _getDefaultFlag(cap) { + return (cap === this._gl.DITHER); + } + + setFlag(cap, v) { + const def = this._getDefaultFlag(cap); + if (v === def) { + return this._nonDefaultFlags.delete(cap); + } else { + if (!this._nonDefaultFlags.has(cap)) { + this._nonDefaultFlags.add(cap); + return true; + } else { + return false; + } + } + } + + setBuffer(target, buffer) { + const change = this._buffers.get(target) !== buffer; + this._buffers.set(target, buffer); + + if (change && (target === this._gl.ARRAY_BUFFER)) { + // When the array buffer is changed all attributes are cleared. + this._vertexAttribs = []; + } + + return change; + } + + setFramebuffer(target, buffer) { + const change = this._framebuffers.get(target) !== buffer; + this._framebuffers.set(target, buffer); + return change; + } + + setRenderbuffer(target, buffer) { + const change = this._renderbuffers.get(target) !== buffer; + this._renderbuffers.set(target, buffer); + return change; + } + + setProgram(program) { + const change = this._program !== program; + this._program = program; + return change + } + + setSetting(func, v) { + const s = this._settings.get(func); + const change = !s || !Utils$1.equalValues(s, v); + this._settings.set(func, v); + return change + } + + disableVertexAttribArray(index) { + const va = this._vertexAttribs[index]; + if (va && va[5]) { + va[5] = false; + return true; + } + return false; + } + + enableVertexAttribArray(index) { + const va = this._vertexAttribs[index]; + if (va) { + if (!va[0]) { + va[0] = true; + return true; + } + } else { + this._vertexAttribs[index] = [0, 0, 0, 0, 0, true]; + return true; + } + return false; + } + + vertexAttribPointer(index, props) { + let va = this._vertexAttribs[index]; + let equal = false; + if (va) { + equal = va[0] === props[0] && + va[1] === props[1] && + va[2] === props[2] && + va[3] === props[3] && + va[4] === props[4]; + } + + if (equal) { + return false; + } else { + props[5] = va ? va[5] : false; + return true; + } + } + + setActiveTexture(texture) { + const changed = this._activeTexture !== texture; + this._activeTexture = texture; + return changed; + } + + bindTexture(target, texture) { + const activeIndex = WebGLState._getTextureIndex(this._activeTexture); + this._maxTexture = Math.max(this._maxTexture, activeIndex + 1); + const current = this._textures[activeIndex]; + const targetIndex = WebGLState._getTextureTargetIndex(target); + if (current) { + if (current[targetIndex] === texture) { + return false; + } + current[targetIndex] = texture; + return true; + } else { + if (texture) { + this._textures[activeIndex] = []; + this._textures[activeIndex][targetIndex] = texture; + return true + } else { + return false + } + } + } + + setPixelStorei(pname, param) { + const i = WebGLState._getPixelStoreiIndex(pname); + const change = !Utils$1.equalValues(this._pixelStorei[i], param); + this._pixelStorei[i] = param; + return change; + } + + migrate(s) { + const t = this; + + // Warning: migrate should call the original prototype methods directly. + + this._migrateFlags(t, s); + + // useProgram + if (s._program !== t._program) { + this._gl._useProgram(s._program); + } + + this._migrateFramebuffers(t, s); + this._migrateRenderbuffers(t, s); + + const buffersChanged = this._migrateBuffers(t, s); + this._migrateAttributes(t, s, buffersChanged); + + this._migrateFlags(t, s); + + this._migrateSettings(t, s); + + this._migratePixelStorei(t, s); + + this._migrateTextures(t, s); + + } + + _migratePixelStorei(t, s) { + for (let i = 0, n = t._pixelStorei.length; i < n; i++) { + if (t._pixelStorei[i] !== s._pixelStorei[i]) { + const value = s._pixelStorei[i] !== undefined ? s._pixelStorei[i] : WebGLState._getDefaultPixelStoreiByIndex(i); + this._gl._pixelStorei(WebGLState._getPixelStoreiByIndex(i), value); + } + } + } + + _migrateTextures(t, s) { + const max = Math.max(t._maxTexture, s._maxTexture); + + let activeTexture = t._activeTexture; + + for (let i = 0; i < max; i++) { + const sTargets = s._textures[i]; + const tTargets = t._textures[i]; + const textureNumb = WebGLState._getTextureByIndex(i); + + const targetMax = Math.max(tTargets ? tTargets.length : 0, sTargets ? sTargets.length : 0); + for (let j = 0, n = targetMax; j < n; j++) { + const target = WebGLState._getTextureTargetByIndex(j); + if (activeTexture !== textureNumb) { + this._gl._activeTexture(textureNumb); + activeTexture = textureNumb; + } + + const texture = (sTargets && sTargets[j]) || null; + this._gl._bindTexture(target, texture); + } + } + + if (s._activeTexture !== activeTexture) { + this._gl._activeTexture(s._activeTexture); + } + } + + _migrateBuffers(t, s) { + s._buffers.forEach((framebuffer, target) => { + if (t._buffers.get(target) !== framebuffer) { + this._gl._bindBuffer(target, framebuffer); + } + }); + + t._buffers.forEach((buffer, target) => { + const b = s._buffers.get(target); + if (b === undefined) { + this._gl._bindBuffer(target, null); + } + }); + return (s._buffers.get(this._gl.ARRAY_BUFFER) !== t._buffers.get(this._gl.ARRAY_BUFFER)) + } + + _migrateFramebuffers(t, s) { + s._framebuffers.forEach((framebuffer, target) => { + if (t._framebuffers.get(target) !== framebuffer) { + this._gl._bindFramebuffer(target, framebuffer); + } + }); + + t._framebuffers.forEach((framebuffer, target) => { + const fb = s._framebuffers.get(target); + if (fb === undefined) { + this._gl._bindFramebuffer(target, null); + } + }); + } + + _migrateRenderbuffers(t, s) { + s._renderbuffers.forEach((renderbuffer, target) => { + if (t._renderbuffers.get(target) !== renderbuffer) { + this._gl._bindRenderbuffer(target, renderbuffer); + } + }); + + t._renderbuffers.forEach((renderbuffer, target) => { + const fb = s._renderbuffers.get(target); + if (fb === undefined) { + this._gl._bindRenderbuffer(target, null); + } + }); + } + + _migrateAttributes(t, s, buffersChanged) { + + if (!buffersChanged) { + t._vertexAttribs.forEach((attrib, index) => { + if (!s._vertexAttribs[index]) { + // We can't 'delete' a vertex attrib so we'll disable it. + this._gl._disableVertexAttribArray(index); + } + }); + + s._vertexAttribs.forEach((attrib, index) => { + this._gl._vertexAttribPointer(index, attrib[0], attrib[1], attrib[2], attrib[4]); + if (attrib[5]) { + this._gl._enableVertexAttribArray(index); + } else { + this._gl._disableVertexAttribArray(index); + } + }); + } else { + // When buffers are changed, previous attributes were reset automatically. + s._vertexAttribs.forEach((attrib, index) => { + if (attrib[0]) { + // Do not set vertex attrib pointer when it was just the default value. + this._gl._vertexAttribPointer(index, attrib[0], attrib[1], attrib[2], attrib[3], attrib[4]); + } + if (attrib[5]) { + this._gl._enableVertexAttribArray(index); + } + }); + } + } + + _migrateSettings(t, s) { + const defaults = this.constructor.getDefaultSettings(); + t._settings.forEach((value, func) => { + const name = func.name || func.xname; + if (!s._settings.has(func)) { + let args = defaults.get(name); + if (Utils$1.isFunction(args)) { + args = args(this._gl); + } + // We are actually setting the setting for optimization purposes. + s._settings.set(func, args); + func.apply(this._gl, args); + } + }); + s._settings.forEach((value, func) => { + const tValue = t._settings.get(func); + if (!tValue || !Utils$1.equalValues(tValue, value)) { + func.apply(this._gl, value); + } + }); + } + + _migrateFlags(t, s) { + t._nonDefaultFlags.forEach(setting => { + if (!s._nonDefaultFlags.has(setting)) { + if (this._getDefaultFlag(setting)) { + this._gl._enable(setting); + } else { + this._gl._disable(setting); + } + } + }); + s._nonDefaultFlags.forEach(setting => { + if (!t._nonDefaultFlags.has(setting)) { + if (this._getDefaultFlag(setting)) { + this._gl._disable(setting); + } else { + this._gl._enable(setting); + } + } + }); + } + + static getDefaultSettings() { + if (!this._defaultSettings) { + this._defaultSettings = new Map(); + const d = this._defaultSettings; + const g = WebGLRenderingContext.prototype; + d.set("viewport", function(gl) {return [0,0,gl.canvas.width, gl.canvas.height]}); + d.set("scissor", function(gl) {return [0,0,gl.canvas.width, gl.canvas.height]}); + d.set("blendColor", [0, 0, 0, 0]); + d.set("blendEquation", [g.FUNC_ADD]); + d.set("blendEquationSeparate", [g.FUNC_ADD, g.FUNC_ADD]); + d.set("blendFunc", [g.ONE, g.ZERO]); + d.set("blendFuncSeparate", [g.ONE, g.ZERO, g.ONE, g.ZERO]); + d.set("clearColor", [0, 0, 0, 0]); + d.set("clearDepth", [1]); + d.set("clearStencil", [0]); + d.set("colorMask", [true, true, true, true]); + d.set("cullFace", [g.BACK]); + d.set("depthFunc", [g.LESS]); + d.set("depthMask", [true]); + d.set("depthRange", [0, 1]); + d.set("frontFace", [g.CCW]); + d.set("lineWidth", [1]); + d.set("polygonOffset", [0, 0]); + d.set("sampleCoverage", [1, false]); + d.set("stencilFunc", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateFront", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateBack", [g.ALWAYS, 0, 1]); + d.set("_stencilFuncSeparateFrontAndBack", [g.ALWAYS, 0, 1]); + d.set("stencilMask", [1]); + d.set("_stencilMaskSeparateFront", [1]); + d.set("_stencilMaskSeparateBack", [1]); + d.set("_stencilMaskSeparateFrontAndBack", [1]); + d.set("stencilOp", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateFront", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateBack", [g.KEEP, g.KEEP, g.KEEP]); + d.set("_stencilOpSeparateFrontAndBack", [g.KEEP, g.KEEP, g.KEEP]); + d.set("vertexAttrib1f", []); + d.set("vertexAttrib1fv", []); + d.set("vertexAttrib2f", []); + d.set("vertexAttrib2fv", []); + d.set("vertexAttrib3f", []); + d.set("vertexAttrib3fv", []); + d.set("vertexAttrib4f", []); + d.set("vertexAttrib4fv", []); + } + return this._defaultSettings + } + + static _getTextureTargetIndex(target) { + switch(target) { + case 0x0DE1: + /* TEXTURE_2D */ + return 0; + case 0x8513: + /* TEXTURE_CUBE_MAP */ + return 1; + default: + // Shouldn't happen. + throw new Error('Unknown texture target: ' + target); + } + } + + static _getTextureTargetByIndex(index) { + if (!this._textureTargetIndices) { + this._textureTargetIndices = [0x0DE1, 0x8513]; + } + return this._textureTargetIndices[index] + } + + static _getTextureIndex(index) { + return index - 0x84C0 /* GL_TEXTURE0 */; + } + + static _getTextureByIndex(index) { + return index + 0x84C0; + } + + static _getPixelStoreiIndex(pname) { + switch(pname) { + case 0x0D05: + /* PACK_ALIGNMENT */ + return 0; + case 0x0CF5: + /* UNPACK_ALIGNMENT */ + return 1; + case 0x9240: + /* UNPACK_FLIP_Y_WEBGL */ + return 2; + case 0x9241: + /* UNPACK_PREMULTIPLY_ALPHA_WEBGL */ + return 3; + case 0x9243: + /* UNPACK_COLORSPACE_CONVERSION_WEBGL */ + return 4; + //@todo: support WebGL2 properties, see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/pixelStorei + case 0x9245: + /* UNPACK_FLIP_BLUE_RED */ + return 5; + default: + // Shouldn't happen. + throw new Error('Unknown pixelstorei: ' + pname); + } + } + + static _getPixelStoreiByIndex(index) { + if (!this._pixelStoreiIndices) { + this._pixelStoreiIndices = [0x0D05, 0x0CF5, 0x9240, 0x9241, 0x9243]; + } + return this._pixelStoreiIndices[index] + } + + static _getDefaultPixelStoreiByIndex(index) { + if (!this._pixelStoreiDefaults) { + this._pixelStoreiDefaults = [4, 4, false, false, WebGLRenderingContext.prototype.BROWSER_DEFAULT_WEBGL]; + } + return this._pixelStoreiDefaults[index] + } +} + +class WebGLStateManager { + + _initStateManager(id = "default") { + this._states = {}; + this._state = this._getState(id); + } + + _getState(id) { + if (!this._states[id]) { + this._states[id] = new WebGLState(id, this); + } + return this._states[id]; + } + + switchState(id = "default") { + if (this._state._id !== id) { + const newState = this._getState(id); + this._state.migrate(newState); + this._state = newState; + } + } + + $useProgram(program) { + if (this._state.setProgram(program)) + this._useProgram(program); + } + + $bindBuffer(target, fb) { + if (this._state.setBuffer(target, fb)) + this._bindBuffer(target, fb); + } + + $bindFramebuffer(target, fb) { + if (this._state.setFramebuffer(target, fb)) + this._bindFramebuffer(target, fb); + } + + $bindRenderbuffer(target, fb) { + if (this._state.setRenderbuffer(target, fb)) + this._bindRenderbuffer(target, fb); + } + + $enable(cap) { + if (this._state.setFlag(cap, true)) + this._enable(cap); + } + + $disable(cap) { + if (this._state.setFlag(cap, false)) + this._disable(cap); + } + + $viewport(x, y, w, h) { + if (this._state.setSetting(this._viewport, [x, y, w, h])) + this._viewport(x, y, w, h); + } + + $scissor(x, y, w, h) { + if (this._state.setSetting(this._scissor, [x, y, w, h])) + this._scissor(x, y, w, h); + } + + $disableVertexAttribArray(index) { + if (this._state.disableVertexAttribArray(index)) + this._disableVertexAttribArray(index); + } + + $enableVertexAttribArray(index) { + if (this._state.enableVertexAttribArray(index)) + this._enableVertexAttribArray(index); + } + + $vertexAttribPointer(index, size, type, normalized, stride, offset) { + if (this._state.vertexAttribPointer(index, [size, type, normalized, stride, offset])) + this._vertexAttribPointer(index, size, type, normalized, stride, offset); + } + + $activeTexture(texture) { + if (this._state.setActiveTexture(texture)) + this._activeTexture(texture); + } + + $bindTexture(target, texture) { + if (this._state.bindTexture(target, texture)) + this._bindTexture(target, texture); + } + + $pixelStorei(pname, param) { + if (this._state.setPixelStorei(pname, param)) { + this._pixelStorei(pname, param); + } + } + + $stencilFuncSeparate(face, func, ref, mask) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilFuncSeparateFront; + break; + case this.BACK: + f = this._stencilFuncSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilFuncSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [func, ref, mask])) + f.apply(this, [func, ref, mask]); + } + + _stencilFuncSeparateFront(func, ref, mask) { + this._stencilFuncSeparate(this.FRONT, func, ref, mask); + } + + _stencilFuncSeparateBack(func, ref, mask) { + this._stencilFuncSeparate(this.BACK, func, ref, mask); + } + + _stencilFuncSeparateFrontAndBack(func, ref, mask) { + this._stencilFuncSeparate(this.FRONT_AND_BACK, func, ref, mask); + } + + $stencilMaskSeparate(face, mask) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilMaskSeparateFront; + break; + case this.BACK: + f = this._stencilMaskSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilMaskSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [mask])) + f.apply(this, [mask]); + } + + _stencilMaskSeparateFront(mask) { + this._stencilMaskSeparate(this.FRONT, mask); + } + + _stencilMaskSeparateBack(mask) { + this._stencilMaskSeparate(this.BACK, mask); + } + + _stencilMaskSeparateFrontAndBack(mask) { + this._stencilMaskSeparate(this.FRONT_AND_BACK, mask); + } + + $stencilOpSeparate(face, fail, zfail, zpass) { + let f; + switch(face) { + case this.FRONT: + f = this._stencilOpSeparateFront; + break; + case this.BACK: + f = this._stencilOpSeparateBack; + break; + case this.FRONT_AND_BACK: + f = this._stencilOpSeparateFrontAndBack; + break; + } + + if (this._state.setSetting(f, [fail, zfail, zpass])) + f.apply(this, [fail, zfail, zpass]); + } + + _stencilOpSeparateFront(fail, zfail, zpass) { + this._stencilOpSeparate(this.FRONT, fail, zfail, zpass); + } + + _stencilOpSeparateBack(fail, zfail, zpass) { + this._stencilOpSeparate(this.BACK, fail, zfail, zpass); + } + + _stencilOpSeparateFrontAndBack(fail, zfail, zpass) { + this._stencilOpSeparate(this.FRONT_AND_BACK, fail, zfail, zpass); + } + + $blendColor(red, green, blue, alpha) { + if (this._state.setSetting(this._blendColor, [red, green, blue, alpha])) + this._blendColor(red, green, blue, alpha); + } + + $blendEquation(mode) { + if (this._state.setSetting(this._blendEquation, [mode])) + this._blendEquation(mode); + } + + $blendEquationSeparate(modeRGB, modeAlpha) { + if (this._state.setSetting(this._blendEquationSeparate, [modeRGB, modeAlpha])) + this._blendEquationSeparate(modeRGB, modeAlpha); + } + + $blendFunc(sfactor, dfactor) { + if (this._state.setSetting(this._blendFunc, [sfactor, dfactor])) + this._blendFunc(sfactor, dfactor); + } + + $blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha) { + if (this._state.setSetting(this._blendFuncSeparate, [srcRGB, dstRGB, srcAlpha, dstAlpha])) + this._blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + } + + $clearColor(red, green, blue, alpha) { + if (this._state.setSetting(this._clearColor, [red, green, blue, alpha])) + this._clearColor(red, green, blue, alpha); + } + + $clearDepth(depth) { + if (this._state.setSetting(this._clearDepth, [depth])) + this._clearDepth(depth); + } + + $clearStencil(s) { + if (this._state.setSetting(this._clearStencil, [s])) + this._clearStencil(s); + } + + $colorMask(red, green, blue, alpha) { + if (this._state.setSetting(this._colorMask, [red, green, blue, alpha])) + this._colorMask(red, green, blue, alpha); + } + + $cullFace(mode) { + if (this._state.setSetting(this._cullFace, [mode])) + this._cullFace(mode); + } + + $depthFunc(func) { + if (this._state.setSetting(this._depthFunc, [func])) + this._depthFunc(func); + } + + $depthMask(flag) { + if (this._state.setSetting(this._depthMask, [flag])) + this._depthMask(flag); + } + + $depthRange(zNear, zFar) { + if (this._state.setSetting(this._depthRange, [zNear, zFar])) + this._depthRange(zNear, zFar); + } + + $frontFace(mode) { + if (this._state.setSetting(this._frontFace, [mode])) + this._frontFace(mode); + } + + $lineWidth(width) { + if (this._state.setSetting(this._lineWidth, [width])) + this._lineWidth(width); + } + + $polygonOffset(factor, units) { + if (this._state.setSetting(this._polygonOffset, [factor, units])) + this._polygonOffset(factor, units); + } + + $sampleCoverage(value, invert) { + if (this._state.setSetting(this._sampleCoverage, [value, invert])) + this._sampleCoverage(value, invert); + } + + $stencilFunc(func, ref, mask) { + if (this._state.setSetting(this._stencilFunc, [func, ref, mask])) + this._stencilFunc(func, ref, mask); + } + + $stencilMask(mask) { + if (this._state.setSetting(this._stencilMask, [mask])) + this._stencilMask(mask); + } + + $stencilOp(fail, zfail, zpass) { + if (this._state.setSetting(this._stencilOp, [fail, zfail, zpass])) + this._stencilOp(fail, zfail, zpass); + } + + $vertexAttrib1f(indx, x) { + if (this._state.setSetting(this._vertexAttrib1f, [indx, x])) + this._vertexAttrib1f(indx, x); + } + + $vertexAttrib1fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib1fv, [indx, values])) + this._vertexAttrib1fv(indx, values); + } + + $vertexAttrib2f(indx, x, y) { + if (this._state.setSetting(this._vertexAttrib2f, [indx, x, y])) + this._vertexAttrib2f(indx, x, y); + } + + $vertexAttrib2fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib2fv, [indx, values])) + this._vertexAttrib2fv(indx, values); + } + + $vertexAttrib3f(indx, x, y, z) { + if (this._state.setSetting(this._vertexAttrib3f, [indx, x, y, z])) + this._vertexAttrib3f(indx, x, y, z); + } + + $vertexAttrib3fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib3fv, [indx, values])) + this._vertexAttrib3fv(indx, values); + } + + $vertexAttrib4f(indx, x, y, z, w) { + if (this._state.setSetting(this._vertexAttrib4f, [indx, x, y, z, w])) + this._vertexAttrib4f(indx, x, y, z, w); + } + + $vertexAttrib4fv(indx, values) { + if (this._state.setSetting(this._vertexAttrib4fv, [indx, values])) + this._vertexAttrib4fv(indx, values); + } + + /** + * Sets up the rendering context for context sharing. + * @param {WebGLRenderingContext} gl + * @param {string} id + */ + static enable(gl, id = "default") { + const names = Object.getOwnPropertyNames(WebGLStateManager.prototype); + const WebGLRenderingContextProto = gl.__proto__; + names.forEach(name => { + if (name !== "constructor") { + const method = WebGLStateManager.prototype[name]; + if (name.charAt(0) === "$") { + name = name.substr(1); + } + if (gl[name]) { + if (!gl[name].name) { + // We do this for compatibility with the Chrome WebGL Inspector plugin. + gl[name].xname = name; + } + gl['_' + name] = gl[name]; + } + gl[name] = method; + } + }); + + WebGLStateManager.prototype._initStateManager.call(gl, id); + + return gl; + } + +} + +class TextureManager { + + constructor(stage) { + this.stage = stage; + + /** + * The currently used amount of texture memory. + * @type {number} + */ + this._usedMemory = 0; + + /** + * All uploaded texture sources. + * @type {TextureSource[]} + */ + this._uploadedTextureSources = []; + + /** + * The texture source lookup id to texture source hashmap. + * @type {Map} + */ + this.textureSourceHashmap = new Map(); + + } + + get usedMemory() { + return this._usedMemory; + } + + destroy() { + for (let i = 0, n = this._uploadedTextureSources.length; i < n; i++) { + this._nativeFreeTextureSource(this._uploadedTextureSources[i]); + } + + this.textureSourceHashmap.clear(); + this._usedMemory = 0; + } + + getReusableTextureSource(id) { + return this.textureSourceHashmap.get(id); + } + + getTextureSource(func, id) { + // Check if texture source is already known. + let textureSource = id ? this.textureSourceHashmap.get(id) : null; + if (!textureSource) { + // Create new texture source. + textureSource = new TextureSource(this, func); + + if (id) { + textureSource.lookupId = id; + this.textureSourceHashmap.set(id, textureSource); + } + } + + return textureSource; + } + + uploadTextureSource(textureSource, options) { + if (textureSource.isLoaded()) return; + + this._addMemoryUsage(textureSource.w * textureSource.h); + + // Load texture. + const nativeTexture = this._nativeUploadTextureSource(textureSource, options); + + textureSource._nativeTexture = nativeTexture; + + // We attach w and h to native texture (we need it in CoreRenderState._isRenderTextureReusable). + nativeTexture.w = textureSource.w; + nativeTexture.h = textureSource.h; + + nativeTexture.update = this.stage.frameCounter; + + this._uploadedTextureSources.push(textureSource); + + this.addToLookupMap(textureSource); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + this.stage.addMemoryUsage(delta); + } + + addToLookupMap(textureSource) { + const lookupId = textureSource.lookupId; + if (lookupId) { + if (!this.textureSourceHashmap.has(lookupId)) { + this.textureSourceHashmap.set(lookupId, textureSource); + } + } + } + + gc() { + this.freeUnusedTextureSources(); + this._cleanupLookupMap(); + } + + freeUnusedTextureSources() { + let remainingTextureSources = []; + for (let i = 0, n = this._uploadedTextureSources.length; i < n; i++) { + let ts = this._uploadedTextureSources[i]; + if (ts.allowCleanup()) { + this._freeManagedTextureSource(ts); + } else { + remainingTextureSources.push(ts); + } + } + + this._uploadedTextureSources = remainingTextureSources; + + this._cleanupLookupMap(); + } + + _freeManagedTextureSource(textureSource) { + if (textureSource.isLoaded()) { + this._nativeFreeTextureSource(textureSource); + this._addMemoryUsage(-textureSource.w * textureSource.h); + } + + // Should be reloaded. + textureSource.loadingSince = null; + } + + _cleanupLookupMap() { + // We keep those that still have value (are being loaded or already loaded, or are likely to be reused). + this.textureSourceHashmap.forEach((textureSource, lookupId) => { + if (!(textureSource.isLoaded() || textureSource.isLoading()) && !textureSource.isUsed()) { + this.textureSourceHashmap.delete(lookupId); + } + }); + } + + /** + * Externally free texture source. + * @param textureSource + */ + freeTextureSource(textureSource) { + const index = this._uploadedTextureSources.indexOf(textureSource); + const managed = (index !== -1); + + if (textureSource.isLoaded()) { + if (managed) { + this._addMemoryUsage(-textureSource.w * textureSource.h); + this._uploadedTextureSources.splice(index, 1); + } + this._nativeFreeTextureSource(textureSource); + } + + // Should be reloaded. + textureSource.loadingSince = null; + } + + _nativeUploadTextureSource(textureSource, options) { + return this.stage.renderer.uploadTextureSource(textureSource, options); + } + + _nativeFreeTextureSource(textureSource) { + this.stage.renderer.freeTextureSource(textureSource); + textureSource.clearNativeTexture(); + } + +} + +/** + * Allows throttling of loading texture sources, keeping the app responsive. + */ +class TextureThrottler { + + constructor(stage) { + this.stage = stage; + + this.genericCancelCb = (textureSource) => { + this._remove(textureSource); + }; + + this._sources = []; + this._data = []; + } + + destroy() { + this._sources = []; + this._data = []; + } + + processSome() { + if (this._sources.length) { + const start = Date.now(); + do { + this._processItem(); + } while(this._sources.length && (Date.now() - start < TextureThrottler.MAX_UPLOAD_TIME_PER_FRAME)); + } + } + + _processItem() { + const source = this._sources.pop(); + const data = this._data.pop(); + if (source.isLoading()) { + source.processLoadedSource(data); + } + } + + add(textureSource, data) { + this._sources.push(textureSource); + this._data.push(data); + } + + _remove(textureSource) { + const index = this._sources.indexOf(textureSource); + if (index >= 0) { + this._sources.splice(index, 1); + this._data.splice(index, 1); + } + } + +} + +TextureThrottler.MAX_UPLOAD_TIME_PER_FRAME = 10; + +class CoreContext { + + constructor(stage) { + this.stage = stage; + + this.root = null; + + this.updateTreeOrder = 0; + + this.renderState = this.stage.renderer.createCoreRenderState(this); + + this.renderExec = this.stage.renderer.createCoreRenderExecutor(this); + this.renderExec.init(); + + this._usedMemory = 0; + this._renderTexturePool = []; + + this._renderTextureId = 1; + + this._zSorts = []; + } + + get usedMemory() { + return this._usedMemory; + } + + destroy() { + this._renderTexturePool.forEach(texture => this._freeRenderTexture(texture)); + this._usedMemory = 0; + } + + hasRenderUpdates() { + return !!this.root._parent._hasRenderUpdates; + } + + render() { + // Clear flag to identify if anything changes before the next frame. + this.root._parent._hasRenderUpdates = 0; + + this._render(); + } + + update() { + this._update(); + + // Due to the boundsVisibility flag feature (and onAfterUpdate hook), it is possible that other elements were + // changed during the update loop (for example due to the txLoaded event). We process these changes immediately + // (but not recursively to prevent infinite loops). + if (this.root._hasUpdates) { + this._update(); + } + + this._performForcedZSorts(); + } + + /** + * Certain ElementCore items may be forced to zSort to strip out references to prevent memleaks.. + */ + _performForcedZSorts() { + const n = this._zSorts.length; + if (n) { + // Forced z-sorts (ElementCore may force a z-sort in order to free memory/prevent memory leaks). + for (let i = 0, n = this._zSorts.length; i < n; i++) { + if (this._zSorts[i].zSort) { + this._zSorts[i].sortZIndexedChildren(); + } + } + this._zSorts = []; + } + } + + _update() { + this.updateTreeOrder = 0; + + this.root.update(); + } + + _render() { + // Obtain a sequence of the quad operations. + this._fillRenderState(); + + if (this.stage.getOption('readPixelsBeforeDraw')) { + const pixels = new Uint8Array(4); + const gl = this.stage.gl; + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + } + + // Now run them with the render executor. + this._performRender(); + } + + _fillRenderState() { + this.renderState.reset(); + this.root.render(); + this.renderState.finish(); + } + + _performRender() { + this.renderExec.execute(); + } + + _addMemoryUsage(delta) { + this._usedMemory += delta; + this.stage.addMemoryUsage(delta); + } + + allocateRenderTexture(w, h) { + let prec = this.stage.getRenderPrecision(); + let pw = Math.max(1, Math.round(w * prec)); + let ph = Math.max(1, Math.round(h * prec)); + + // Search last item first, so that last released render texture is preferred (may cause memory cache benefits). + const n = this._renderTexturePool.length; + for (let i = n - 1; i >= 0; i--) { + const texture = this._renderTexturePool[i]; + // We don't want to reuse the same render textures within the same frame because that will create gpu stalls. + if (texture.w === pw && texture.h === ph && (texture.update !== this.stage.frameCounter)) { + texture.f = this.stage.frameCounter; + this._renderTexturePool.splice(i, 1); + return texture; + } + } + + const texture = this._createRenderTexture(w, h, pw, ph); + texture.precision = prec; + return texture; + } + + releaseRenderTexture(texture) { + this._renderTexturePool.push(texture); + } + + freeUnusedRenderTextures(maxAge = 60) { + // Clean up all textures that are no longer used. + // This cache is short-lived because it is really just meant to supply running shaders that are + // updated during a number of frames. + let limit = this.stage.frameCounter - maxAge; + + this._renderTexturePool = this._renderTexturePool.filter(texture => { + if (texture.f <= limit) { + this._freeRenderTexture(texture); + return false; + } + return true; + }); + } + + _createRenderTexture(w, h, pw, ph) { + this._addMemoryUsage(pw * ph); + + const texture = this.stage.renderer.createRenderTexture(w, h, pw, ph); + texture.id = this._renderTextureId++; + texture.f = this.stage.frameCounter; + texture.ow = w; + texture.oh = h; + texture.w = pw; + texture.h = ph; + + return texture; + } + + _freeRenderTexture(nativeTexture) { + this.stage.renderer.freeRenderTexture(nativeTexture); + this._addMemoryUsage(-nativeTexture.w * nativeTexture.h); + } + + copyRenderTexture(renderTexture, nativeTexture, options) { + this.stage.renderer.copyRenderTexture(renderTexture, nativeTexture, options); + } + + forceZSort(elementCore) { + this._zSorts.push(elementCore); + } + +} + +class TransitionSettings { + constructor(stage) { + this.stage = stage; + this._timingFunction = 'ease'; + this._timingFunctionImpl = StageUtils.getTimingFunction(this._timingFunction); + this.delay = 0; + this.duration = 0.2; + this.merger = null; + } + + get timingFunction() { + return this._timingFunction; + } + + set timingFunction(v) { + this._timingFunction = v; + this._timingFunctionImpl = StageUtils.getTimingFunction(v); + } + + get timingFunctionImpl() { + return this._timingFunctionImpl; + } + + patch(settings) { + Base.patchObject(this, settings); + } +} + +TransitionSettings.prototype.isTransitionSettings = true; + +class TransitionManager { + + constructor(stage) { + this.stage = stage; + + this.stage.on('frameStart', () => this.progress()); + + /** + * All transitions that are running and attached. + * (we don't support transitions on un-attached elements to prevent memory leaks) + * @type {Set} + */ + this.active = new Set(); + + this.defaultTransitionSettings = new TransitionSettings(this.stage); + } + + progress() { + if (this.active.size) { + let dt = this.stage.dt; + + let filter = false; + this.active.forEach(function(a) { + a.progress(dt); + if (!a.isRunning()) { + filter = true; + } + }); + + if (filter) { + this.active = new Set([...this.active].filter(t => (t.isRunning()))); + } + } + } + + createSettings(settings) { + const transitionSettings = new TransitionSettings(); + Base.patchObject(transitionSettings, settings); + return transitionSettings; + } + + addActive(transition) { + this.active.add(transition); + } + + removeActive(transition) { + this.active.delete(transition); + } +} + +class MultiSpline { + + constructor() { + this._clear(); + } + + _clear() { + this._p = []; + this._pe = []; + this._idp = []; + this._f = []; + this._v = []; + this._lv = []; + this._sm = []; + this._s = []; + this._ve = []; + this._sme = []; + this._se = []; + + this._length = 0; + } + + parse(rgba, def) { + let i, n; + if (!Utils.isObjectLiteral(def)) { + def = {0: def}; + } + + let defaultSmoothness = 0.5; + + let items = []; + for (let key in def) { + if (def.hasOwnProperty(key)) { + let obj = def[key]; + if (!Utils.isObjectLiteral(obj)) { + obj = {v: obj}; + } + + let p = parseFloat(key); + + if (key === "sm") { + defaultSmoothness = obj.v; + } else if (!isNaN(p) && p >= 0 && p <= 2) { + obj.p = p; + + obj.f = Utils.isFunction(obj.v); + obj.lv = obj.f ? obj.v(0, 0) : obj.v; + + items.push(obj); + } + } + } + + // Sort by progress value. + items = items.sort(function(a, b) {return a.p - b.p}); + + n = items.length; + + for (i = 0; i < n; i++) { + let last = (i === n - 1); + if (!items[i].hasOwnProperty('pe')) { + // Progress. + items[i].pe = last ? (items[i].p <= 1 ? 1 : 2 /* support onetotwo stop */) : items[i + 1].p; + } else { + // Prevent multiple items at the same time. + const max = i < n - 1 ? items[i + 1].p : 1; + if (items[i].pe > max) { + items[i].pe = max; + } + } + if (items[i].pe === items[i].p) { + items[i].idp = 0; + } else { + items[i].idp = 1 / (items[i].pe - items[i].p); + } + } + + // Color merger: we need to split/combine RGBA components. + + // Calculate bezier helper values.; + for (i = 0; i < n; i++) { + if (!items[i].hasOwnProperty('sm')) { + // Smoothness.; + items[i].sm = defaultSmoothness; + } + if (!items[i].hasOwnProperty('s')) { + // Slope.; + if (i === 0 || i === n - 1 || (items[i].p === 1 /* for onetotwo */)) { + // Horizontal slope at start and end.; + items[i].s = rgba ? [0, 0, 0, 0] : 0; + } else { + const pi = items[i - 1]; + const ni = items[i + 1]; + if (pi.p === ni.p) { + items[i].s = rgba ? [0, 0, 0, 0] : 0; + } else { + if (rgba) { + const nc = MultiSpline.getRgbaComponents(ni.lv); + const pc = MultiSpline.getRgbaComponents(pi.lv); + const d = 1 / (ni.p - pi.p); + items[i].s = [ + d * (nc[0] - pc[0]), + d * (nc[1] - pc[1]), + d * (nc[2] - pc[2]), + d * (nc[3] - pc[3]) + ]; + } else { + items[i].s = (ni.lv - pi.lv) / (ni.p - pi.p); + } + } + } + } + } + + for (i = 0; i < n - 1; i++) { + // Calculate value function.; + if (!items[i].f) { + + let last = (i === n - 1); + if (!items[i].hasOwnProperty('ve')) { + items[i].ve = last ? items[i].lv : items[i + 1].lv; + } + + // We can only interpolate on numeric values. Non-numeric values are set literally when reached time. + if (Utils.isNumber(items[i].v) && Utils.isNumber(items[i].lv)) { + if (!items[i].hasOwnProperty('sme')) { + items[i].sme = last ? defaultSmoothness : items[i + 1].sm; + } + if (!items[i].hasOwnProperty('se')) { + items[i].se = last ? (rgba ? [0, 0, 0, 0] : 0) : items[i + 1].s; + } + + // Generate spline.; + if (rgba) { + items[i].v = MultiSpline.getSplineRgbaValueFunction(items[i].v, items[i].ve, items[i].p, items[i].pe, items[i].sm, items[i].sme, items[i].s, items[i].se); + } else { + items[i].v = MultiSpline.getSplineValueFunction(items[i].v, items[i].ve, items[i].p, items[i].pe, items[i].sm, items[i].sme, items[i].s, items[i].se); + } + + items[i].f = true; + } + } + } + + if (this.length) { + this._clear(); + } + + for (i = 0, n = items.length; i < n; i++) { + this._add(items[i]); + } + } + + _add(item) { + this._p.push(item.p || 0); + this._pe.push(item.pe || 0); + this._idp.push(item.idp || 0); + this._f.push(item.f || false); + this._v.push(item.hasOwnProperty('v') ? item.v : 0 /* v might be false or null */ ); + this._lv.push(item.lv || 0); + this._sm.push(item.sm || 0); + this._s.push(item.s || 0); + this._ve.push(item.ve || 0); + this._sme.push(item.sme || 0); + this._se.push(item.se || 0); + this._length++; + } + + _getItem(p) { + const n = this._length; + if (!n) { + return -1; + } + + if (p < this._p[0]) { + return 0; + } + + for (let i = 0; i < n; i++) { + if (this._p[i] <= p && p < this._pe[i]) { + return i; + } + } + + return n - 1; + } + + getValue(p) { + const i = this._getItem(p); + if (i === -1) { + return undefined; + } else { + if (this._f[i]) { + const o = Math.min(1, Math.max(0, (p - this._p[i]) * this._idp[i])); + return this._v[i](o); + } else { + return this._v[i]; + } + } + } + + get length() { + return this._length; + } + + static getRgbaComponents(argb) { + let r = ((argb / 65536) | 0) % 256; + let g = ((argb / 256) | 0) % 256; + let b = argb % 256; + let a = ((argb / 16777216) | 0); + return [r, g, b, a]; + }; + + static getSplineValueFunction(v1, v2, p1, p2, o1, i2, s1, s2) { + // Normalize slopes because we use a spline that goes from 0 to 1. + let dp = p2 - p1; + s1 *= dp; + s2 *= dp; + + let helpers = MultiSpline.getSplineHelpers(v1, v2, o1, i2, s1, s2); + if (!helpers) { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + + return v2 * p + v1 * (1 - p); + }; + } else { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + return MultiSpline.calculateSpline(helpers, p); + }; + } + }; + + static getSplineRgbaValueFunction(v1, v2, p1, p2, o1, i2, s1, s2) { + // Normalize slopes because we use a spline that goes from 0 to 1. + let dp = p2 - p1; + s1[0] *= dp; + s1[1] *= dp; + s1[2] *= dp; + s1[3] *= dp; + s2[0] *= dp; + s2[1] *= dp; + s2[2] *= dp; + s2[3] *= dp; + + let cv1 = MultiSpline.getRgbaComponents(v1); + let cv2 = MultiSpline.getRgbaComponents(v2); + + let helpers = [ + MultiSpline.getSplineHelpers(cv1[0], cv2[0], o1, i2, s1[0], s2[0]), + MultiSpline.getSplineHelpers(cv1[1], cv2[1], o1, i2, s1[1], s2[1]), + MultiSpline.getSplineHelpers(cv1[2], cv2[2], o1, i2, s1[2], s2[2]), + MultiSpline.getSplineHelpers(cv1[3], cv2[3], o1, i2, s1[3], s2[3]) + ]; + + if (!helpers[0]) { + return function (p) { + // Linear. + if (p === 0) return v1; + if (p === 1) return v2; + + return MultiSpline.mergeColors(v2, v1, p); + }; + } else { + return function (p) { + if (p === 0) return v1; + if (p === 1) return v2; + + return MultiSpline.getArgbNumber([ + Math.min(255, MultiSpline.calculateSpline(helpers[0], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[1], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[2], p)), + Math.min(255, MultiSpline.calculateSpline(helpers[3], p)) + ]); + }; + } + + }; + + /** + * Creates helpers to be used in the spline function. + * @param {number} v1 + * From value. + * @param {number} v2 + * To value. + * @param {number} o1 + * From smoothness (0 = linear, 1 = smooth). + * @param {number} s1 + * From slope (0 = horizontal, infinite = vertical). + * @param {number} i2 + * To smoothness. + * @param {number} s2 + * To slope. + * @returns {Number[]} + * The helper values to be supplied to the spline function. + * If the configuration is actually linear, null is returned. + */ + static getSplineHelpers(v1, v2, o1, i2, s1, s2) { + if (!o1 && !i2) { + // Linear. + return null; + } + + // Cubic bezier points. + // http://cubic-bezier.com/ + let csx = o1; + let csy = v1 + s1 * o1; + let cex = 1 - i2; + let cey = v2 - s2 * i2; + + let xa = 3 * csx - 3 * cex + 1; + let xb = -6 * csx + 3 * cex; + let xc = 3 * csx; + + let ya = 3 * csy - 3 * cey + v2 - v1; + let yb = 3 * (cey + v1) - 6 * csy; + let yc = 3 * (csy - v1); + let yd = v1; + + return [xa, xb, xc, ya, yb, yc, yd]; + }; + + /** + * Calculates the intermediate spline value based on the specified helpers. + * @param {number[]} helpers + * Obtained from getSplineHelpers. + * @param {number} p + * @return {number} + */ + static calculateSpline(helpers, p) { + let xa = helpers[0]; + let xb = helpers[1]; + let xc = helpers[2]; + let ya = helpers[3]; + let yb = helpers[4]; + let yc = helpers[5]; + let yd = helpers[6]; + + if (xa === -2 && ya === -2 && xc === 0 && yc === 0) { + // Linear. + return p; + } + + // Find t for p. + let t = 0.5, cbx, dx; + + for (let it = 0; it < 20; it++) { + // Cubic bezier function: f(t)=t*(t*(t*a+b)+c). + cbx = t * (t * (t * xa + xb) + xc); + + dx = p - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc) + yd; + } + + // Cubic bezier derivative function: f'(t)=t*(t*(3*a)+2*b)+c + let cbxd = t * (t * (3 * xa) + 2 * xb) + xc; + + if (cbxd > 1e-10 && cbxd < 1e-10) { + // Problematic. Fall back to binary search method. + break; + } + + t += dx / cbxd; + } + + // Fallback: binary search method. This is more reliable when there are near-0 slopes. + let minT = 0; + let maxT = 1; + for (let it = 0; it < 20; it++) { + t = 0.5 * (minT + maxT); + + // Cubic bezier function: f(t)=t*(t*(t*a+b)+c)+d. + cbx = t * (t * (t * xa + xb) + xc); + + dx = p - cbx; + if (dx > -1e-8 && dx < 1e-8) { + // Solution found! + return t * (t * (t * ya + yb) + yc) + yd; + } + + if (dx < 0) { + maxT = t; + } else { + minT = t; + } + } + + return t; + }; + + static mergeColors(c1, c2, p) { + let r1 = ((c1 / 65536) | 0) % 256; + let g1 = ((c1 / 256) | 0) % 256; + let b1 = c1 % 256; + let a1 = ((c1 / 16777216) | 0); + + let r2 = ((c2 / 65536) | 0) % 256; + let g2 = ((c2 / 256) | 0) % 256; + let b2 = c2 % 256; + let a2 = ((c2 / 16777216) | 0); + + let r = r1 * p + r2 * (1 - p); + let g = g1 * p + g2 * (1 - p); + let b = b1 * p + b2 * (1 - p); + let a = a1 * p + a2 * (1 - p); + + return Math.round(a) * 16777216 + Math.round(r) * 65536 + Math.round(g) * 256 + Math.round(b); + }; + + static getArgbNumber(rgba) { + rgba[0] = Math.max(0, Math.min(255, rgba[0])); + rgba[1] = Math.max(0, Math.min(255, rgba[1])); + rgba[2] = Math.max(0, Math.min(255, rgba[2])); + rgba[3] = Math.max(0, Math.min(255, rgba[3])); + let v = ((rgba[3] | 0) << 24) + ((rgba[0] | 0) << 16) + ((rgba[1] | 0) << 8) + (rgba[2] | 0); + if (v < 0) { + v = 0xFFFFFFFF + v + 1; + } + return v; + }; +} + +class AnimationActionSettings { + + constructor(animationSettings) { + + this.animationSettings = animationSettings; + + /** + * The selector that selects the elements. + * @type {string} + */ + this._selector = ""; + + /** + * The value items, ordered by progress offset. + * @type {MultiSpline} + * @private; + */ + this._items = new MultiSpline(); + + /** + * The affected properties (paths). + * @private; + */ + this._props = []; + + /** + * Property setters, indexed according to props. + * @private; + */ + this._propSetters = []; + + this._resetValue = undefined; + this._hasResetValue = false; + + this._hasColorProperty = undefined; + } + + getResetValue() { + if (this._hasResetValue) { + return this._resetValue; + } else { + return this._items.getValue(0); + } + } + + apply(element, p, factor) { + const elements = this.getAnimatedElements(element); + + let v = this._items.getValue(p); + + if (v === undefined || !elements.length) { + return; + } + + if (factor !== 1) { + // Stop factor.; + let sv = this.getResetValue(); + + if (Utils.isNumber(v) && Utils.isNumber(sv)) { + if (this.hasColorProperty()) { + v = StageUtils.mergeColors(v, sv, factor); + } else { + v = StageUtils.mergeNumbers(v, sv, factor); + } + } + } + + // Apply transformation to all components.; + const n = this._propSetters.length; + + const m = elements.length; + for (let j = 0; j < m; j++) { + for (let i = 0; i < n; i++) { + this._propSetters[i](elements[j], v); + } + } + } + + getAnimatedElements(element) { + return element.select(this._selector); + } + + reset(element) { + const elements = this.getAnimatedElements(element); + + let v = this.getResetValue(); + + if (v === undefined || !elements.length) { + return; + } + + // Apply transformation to all components. + const n = this._propSetters.length; + + const m = elements.length; + for (let j = 0; j < m; j++) { + for (let i = 0; i < n; i++) { + this._propSetters[i](elements[j], v); + } + } + } + + set selector(v) { + this._selector = v; + } + + set t(v) { + this.selector = v; + } + + get resetValue() { + return this._resetValue; + } + + set resetValue(v) { + this._resetValue = v; + this._hasResetValue = (v !== undefined); + } + + set rv(v) { + this.resetValue = v; + } + + set value(v) { + this._items.parse(this.hasColorProperty(), v); + } + + set v(v) { + this.value = v; + } + + set properties(v) { + if (!Array.isArray(v)) { + v = [v]; + } + + this._props = []; + + v.forEach((prop) => { + this._props.push(prop); + this._propSetters.push(Element.getSetter(prop)); + }); + } + + set property(v) { + this._hasColorProperty = undefined; + this.properties = v; + } + + set p(v) { + this.properties = v; + } + + patch(settings) { + Base.patchObject(this, settings); + } + + hasColorProperty() { + if (this._hasColorProperty === undefined) { + this._hasColorProperty = this._props.length ? Element.isColorProperty(this._props[0]) : false; + } + return this._hasColorProperty; + } +} + +AnimationActionSettings.prototype.isAnimationActionSettings = true; + +class AnimationSettings { + constructor() { + /** + * @type {AnimationActionSettings[]} + */ + this._actions = []; + + this.delay = 0; + this.duration = 1; + + this.repeat = 0; + this.repeatOffset = 0; + this.repeatDelay = 0; + + this.autostop = false; + + this.stopMethod = AnimationSettings.STOP_METHODS.FADE; + this._stopTimingFunction = 'ease'; + this._stopTimingFunctionImpl = StageUtils.getTimingFunction(this._stopTimingFunction); + this.stopDuration = 0; + this.stopDelay = 0; + } + + get actions() { + return this._actions; + } + + set actions(v) { + this._actions = []; + for (let i = 0, n = v.length; i < n; i++) { + const e = v[i]; + if (!e.isAnimationActionSettings) { + const aas = new AnimationActionSettings(this); + aas.patch(e); + this._actions.push(aas); + } else { + this._actions.push(e); + } + } + } + + /** + * Applies the animation to the specified element, for the specified progress between 0 and 1. + * @param {Element} element; + * @param {number} p; + * @param {number} factor; + */ + apply(element, p, factor = 1) { + this._actions.forEach(function(action) { + action.apply(element, p, factor); + }); + } + + /** + * Resets the animation to the reset values. + * @param {Element} element; + */ + reset(element) { + this._actions.forEach(function(action) { + action.reset(element); + }); + } + + get stopTimingFunction() { + return this._stopTimingFunction; + } + + set stopTimingFunction(v) { + this._stopTimingFunction = v; + this._stopTimingFunctionImpl = StageUtils.getTimingFunction(v); + } + + get stopTimingFunctionImpl() { + return this._stopTimingFunctionImpl; + } + + patch(settings) { + Base.patchObject(this, settings); + } + +} + +AnimationSettings.STOP_METHODS = { + FADE: 'fade', + REVERSE: 'reverse', + FORWARD: 'forward', + IMMEDIATE: 'immediate', + ONETOTWO: 'onetotwo' +}; + +class Animation extends EventEmitter { + + constructor(manager, settings, element) { + super(); + + this.manager = manager; + + this._settings = settings; + + this._element = element; + + this._state = Animation.STATES.IDLE; + + this._p = 0; + this._delayLeft = 0; + this._repeatsLeft = 0; + + this._stopDelayLeft = 0; + this._stopP = 0; + } + + start() { + if (this._element && this._element.attached) { + this._p = 0; + this._delayLeft = this.settings.delay; + this._repeatsLeft = this.settings.repeat; + this._state = Animation.STATES.PLAYING; + this.emit('start'); + this.checkActive(); + } else { + console.warn("Element must be attached before starting animation"); + } + } + + play() { + if (this._state === Animation.STATES.PAUSED) { + // Continue.; + this._state = Animation.STATES.PLAYING; + this.checkActive(); + this.emit('resume'); + } else if (this._state == Animation.STATES.STOPPING && this.settings.stopMethod == AnimationSettings.STOP_METHODS.REVERSE) { + // Continue.; + this._state = Animation.STATES.PLAYING; + this.emit('stopContinue'); + } else if (this._state != Animation.STATES.PLAYING && this._state != Animation.STATES.FINISHED) { + // Restart.; + this.start(); + } + } + + pause() { + if (this._state === Animation.STATES.PLAYING) { + this._state = Animation.STATES.PAUSED; + this.emit('pause'); + } + } + + replay() { + if (this._state == Animation.STATES.FINISHED) { + this.start(); + } else { + this.play(); + } + } + + skipDelay() { + this._delayLeft = 0; + this._stopDelayLeft = 0; + } + + finish() { + if (this._state === Animation.STATES.PLAYING) { + this._delayLeft = 0; + this._p = 1; + } else if (this._state === Animation.STATES.STOPPING) { + this._stopDelayLeft = 0; + this._p = 0; + } + } + + stop() { + if (this._state === Animation.STATES.STOPPED || this._state === Animation.STATES.IDLE) return; + + this._stopDelayLeft = this.settings.stopDelay || 0; + + if (((this.settings.stopMethod === AnimationSettings.STOP_METHODS.IMMEDIATE) && !this._stopDelayLeft) || this._delayLeft > 0) { + // Stop upon next progress.; + this._state = Animation.STATES.STOPPING; + this.emit('stop'); + } else { + if (this.settings.stopMethod === AnimationSettings.STOP_METHODS.FADE) { + this._stopP = 0; + } + + this._state = Animation.STATES.STOPPING; + this.emit('stop'); + } + + this.checkActive(); + } + + stopNow() { + if (this._state !== Animation.STATES.STOPPED || this._state !== Animation.STATES.IDLE) { + this._state = Animation.STATES.STOPPING; + this._p = 0; + this.emit('stop'); + this.reset(); + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } + + isPaused() { + return this._state === Animation.STATES.PAUSED; + } + + isPlaying() { + return this._state === Animation.STATES.PLAYING; + } + + isStopping() { + return this._state === Animation.STATES.STOPPING; + } + + isFinished() { + return this._state === Animation.STATES.FINISHED; + } + + checkActive() { + if (this.isActive()) { + this.manager.addActive(this); + } + } + + isActive() { + return (this._state == Animation.STATES.PLAYING || this._state == Animation.STATES.STOPPING) && this._element && this._element.attached; + } + + progress(dt) { + if (!this._element) return; + this._progress(dt); + this.apply(); + } + + _progress(dt) { + if (this._state == Animation.STATES.STOPPING) { + this._stopProgress(dt); + return; + } + + if (this._state != Animation.STATES.PLAYING) { + return; + } + + if (this._delayLeft > 0) { + this._delayLeft -= dt; + + if (this._delayLeft < 0) { + dt = -this._delayLeft; + this._delayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + if (this.settings.duration === 0) { + this._p = 1; + } else if (this.settings.duration > 0) { + this._p += dt / this.settings.duration; + } + if (this._p >= 1) { + // Finished!; + if (this.settings.repeat == -1 || this._repeatsLeft > 0) { + if (this._repeatsLeft > 0) { + this._repeatsLeft--; + } + this._p = this.settings.repeatOffset; + + if (this.settings.repeatDelay) { + this._delayLeft = this.settings.repeatDelay; + } + + this.emit('repeat', this._repeatsLeft); + } else { + this._p = 1; + this._state = Animation.STATES.FINISHED; + this.emit('finish'); + if (this.settings.autostop) { + this.stop(); + } + } + } else { + this.emit('progress', this._p); + } + } + + _stopProgress(dt) { + let duration = this._getStopDuration(); + + if (this._stopDelayLeft > 0) { + this._stopDelayLeft -= dt; + + if (this._stopDelayLeft < 0) { + dt = -this._stopDelayLeft; + this._stopDelayLeft = 0; + + this.emit('stopDelayEnd'); + } else { + return; + } + } + if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.IMMEDIATE) { + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.REVERSE) { + if (duration === 0) { + this._p = 0; + } else if (duration > 0) { + this._p -= dt / duration; + } + + if (this._p <= 0) { + this._p = 0; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FADE) { + this._progressStopTransition(dt); + if (this._stopP >= 1) { + this._p = 0; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.ONETOTWO) { + if (this._p < 2) { + if (duration === 0) { + this._p = 2; + } else if (duration > 0) { + if (this._p < 1) { + this._p += dt / this.settings.duration; + } else { + this._p += dt / duration; + } + } + if (this._p >= 2) { + this._p = 2; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else { + this.emit('progress', this._p); + } + } + } else if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FORWARD) { + if (this._p < 1) { + if (this.settings.duration == 0) { + this._p = 1; + } else { + this._p += dt / this.settings.duration; + } + if (this._p >= 1) { + if (this.settings.stopMethod == AnimationSettings.STOP_METHODS.FORWARD) { + this._p = 1; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } else { + if (this._repeatsLeft > 0) { + this._repeatsLeft--; + this._p = 0; + this.emit('repeat', this._repeatsLeft); + } else { + this._p = 1; + this._state = Animation.STATES.STOPPED; + this.emit('stopFinish'); + } + } + } else { + this.emit('progress', this._p); + } + } + } + + } + + _progressStopTransition(dt) { + if (this._stopP < 1) { + if (this._stopDelayLeft > 0) { + this._stopDelayLeft -= dt; + + if (this._stopDelayLeft < 0) { + dt = -this._stopDelayLeft; + this._stopDelayLeft = 0; + + this.emit('delayEnd'); + } else { + return; + } + } + + const duration = this._getStopDuration(); + + if (duration == 0) { + this._stopP = 1; + } else { + this._stopP += dt / duration; + } + if (this._stopP >= 1) { + // Finished!; + this._stopP = 1; + } + } + } + + _getStopDuration() { + return this.settings.stopDuration || this.settings.duration; + } + + apply() { + if (this._state === Animation.STATES.STOPPED) { + this.reset(); + } else { + let factor = 1; + if (this._state === Animation.STATES.STOPPING && this.settings.stopMethod === AnimationSettings.STOP_METHODS.FADE) { + factor = (1 - this.settings.stopTimingFunctionImpl(this._stopP)); + } + this._settings.apply(this._element, this._p, factor); + } + } + + reset() { + this._settings.reset(this._element); + } + + get state() { + return this._state; + } + + get p() { + return this._p; + } + + get delayLeft() { + return this._delayLeft; + } + + get element() { + return this._element; + } + + get frame() { + return Math.round(this._p * this._settings.duration * 60); + } + + get settings() { + return this._settings; + } + +} + +Animation.STATES = { + IDLE: 0, + PLAYING: 1, + STOPPING: 2, + STOPPED: 3, + FINISHED: 4, + PAUSED: 5 +}; + +class AnimationManager { + + constructor(stage) { + this.stage = stage; + + this.stage.on('frameStart', () => this.progress()); + + /** + * All running animations on attached subjects. + * @type {Set} + */ + this.active = new Set(); + } + + progress() { + if (this.active.size) { + let dt = this.stage.dt; + + let filter = false; + this.active.forEach(function(a) { + if (a.isActive()) { + a.progress(dt); + } else { + filter = true; + } + }); + + if (filter) { + this.active = new Set([...this.active].filter(t => t.isActive())); + } + } + } + + createAnimation(element, settings) { + if (Utils.isObjectLiteral(settings)) { + // Convert plain object to proper settings object. + settings = this.createSettings(settings); + } + + return new Animation( + this, + settings, + element + ); + } + + createSettings(settings) { + const animationSettings = new AnimationSettings(); + Base.patchObject(animationSettings, settings); + return animationSettings; + } + + addActive(transition) { + this.active.add(transition); + } +} + +class RectangleTexture extends Texture { + + _getLookupId() { + return '__whitepix'; + } + + _getSourceLoader() { + return function(cb) { + var whitePixel = new Uint8Array([255, 255, 255, 255]); + cb(null, {source: whitePixel, w: 1, h: 1, permanent: true}); + } + } + + isAutosizeTexture() { + return false; + } +} + +/** + * Application render tree. + * Copyright Metrological, 2017; + */ + +class Stage extends EventEmitter { + + constructor(options = {}) { + super(); + this._setOptions(options); + + this._usedMemory = 0; + this._lastGcFrame = 0; + + const platformType = Stage.platform ? Stage.platform : PlatformLoader.load(options); + this.platform = new platformType(); + + if (this.platform.init) { + this.platform.init(this); + } + + this.gl = null; + this.c2d = null; + + const context = this.getOption('context'); + if (context) { + if (context.useProgram) { + this.gl = context; + } else { + this.c2d = context; + } + } else { + if (Utils.isWeb && (!Stage.isWebglSupported() || this.getOption('canvas2d'))) { + console.log('Using canvas2d renderer'); + this.c2d = this.platform.createCanvasContext(this.getOption('w'), this.getOption('h')); + } else { + this.gl = this.platform.createWebGLContext(this.getOption('w'), this.getOption('h')); + } + } + + if (this.gl) { + // Wrap in WebGLStateManager. + // This prevents unnecessary double WebGL commands from being executed, and allows context switching. + // Context switching is necessary when reusing the same context for Three.js. + // Note that the user must make sure that the WebGL context is untouched before creating the application, + // when manually passing over a canvas or context in the options. + WebGLStateManager.enable(this.gl, "lightning"); + } + + this._mode = this.gl ? 0 : 1; + + // Override width and height. + if (this.getCanvas()) { + this._options.w = this.getCanvas().width; + this._options.h = this.getCanvas().height; + } + + if (this._mode === 0) { + this._renderer = new WebGLRenderer(this); + } else { + this._renderer = new C2dRenderer(this); + } + + this.setClearColor(this.getOption('clearColor')); + + this.frameCounter = 0; + + this.transitions = new TransitionManager(this); + this.animations = new AnimationManager(this); + + this.textureManager = new TextureManager(this); + this.textureThrottler = new TextureThrottler(this); + + this.startTime = 0; + this.currentTime = 0; + this.dt = 0; + + // Preload rectangle texture, so that we can skip some border checks for loading textures. + this.rectangleTexture = new RectangleTexture(this); + this.rectangleTexture.load(); + + // Never clean up because we use it all the time. + this.rectangleTexture.source.permanent = true; + + this.ctx = new CoreContext(this); + + this._updateSourceTextures = new Set(); + } + + get renderer() { + return this._renderer; + } + + static isWebglSupported() { + if (Utils.isNode) { + return true; + } + + try { + return !!window.WebGLRenderingContext; + } catch(e) { + return false; + } + } + + /** + * Returns the rendering mode. + * @returns {number} + * 0: WebGL + * 1: Canvas2d + */ + get mode() { + return this._mode; + } + + isWebgl() { + return this.mode === 0; + } + + isC2d() { + return this.mode === 1; + } + + getOption(name) { + return this._options[name]; + } + + _setOptions(o) { + this._options = {}; + + let opt = (name, def) => { + let value = o[name]; + + if (value === undefined) { + this._options[name] = def; + } else { + this._options[name] = value; + } + }; + + opt('canvas', null); + opt('context', null); + opt('w', 1920); + opt('h', 1080); + opt('srcBasePath', null); + opt('memoryPressure', 24e6); + opt('bufferMemory', 2e6); + opt('textRenderIssueMargin', 0); + opt('clearColor', [0, 0, 0, 0]); + opt('defaultFontFace', 'sans-serif'); + opt('fixedDt', 0); + opt('useImageWorker', true); + opt('autostart', true); + opt('precision', 1); + opt('canvas2d', false); + opt('platform', null); + opt('readPixelsBeforeDraw', false); + } + + setApplication(app) { + this.application = app; + } + + init() { + this.application.setAsRoot(); + if (this.getOption('autostart')) { + this.platform.startLoop(); + } + } + + destroy() { + this.platform.stopLoop(); + this.platform.destroy(); + this.ctx.destroy(); + this.textureManager.destroy(); + this._renderer.destroy(); + } + + stop() { + this.platform.stopLoop(); + } + + resume() { + this.platform.startLoop(); + } + + get root() { + return this.application; + } + + getCanvas() { + return this._mode ? this.c2d.canvas : this.gl.canvas; + } + + getRenderPrecision() { + return this._options.precision; + } + + /** + * Marks a texture for updating it's source upon the next drawFrame. + * @param texture + */ + addUpdateSourceTexture(texture) { + if (this._updatingFrame) { + // When called from the upload loop, we must immediately load the texture in order to avoid a 'flash'. + texture._performUpdateSource(); + } else { + this._updateSourceTextures.add(texture); + } + } + + removeUpdateSourceTexture(texture) { + if (this._updateSourceTextures) { + this._updateSourceTextures.delete(texture); + } + } + + hasUpdateSourceTexture(texture) { + return (this._updateSourceTextures && this._updateSourceTextures.has(texture)); + } + + drawFrame() { + this.startTime = this.currentTime; + this.currentTime = this.platform.getHrTime(); + + if (this._options.fixedDt) { + this.dt = this._options.fixedDt; + } else { + this.dt = (!this.startTime) ? .02 : .001 * (this.currentTime - this.startTime); + } + + this.emit('frameStart'); + + if (this._updateSourceTextures.size) { + this._updateSourceTextures.forEach(texture => { + texture._performUpdateSource(); + }); + this._updateSourceTextures = new Set(); + } + + this.emit('update'); + + const changes = this.ctx.hasRenderUpdates(); + + // Update may cause textures to be loaded in sync, so by processing them here we may be able to show them + // during the current frame already. + this.textureThrottler.processSome(); + + if (changes) { + this._updatingFrame = true; + this.ctx.update(); + this.ctx.render(); + this._updatingFrame = false; + } + + this.platform.nextFrame(changes); + + this.emit('frameEnd'); + + this.frameCounter++; + } + + isUpdatingFrame() { + return this._updatingFrame; + } + + renderFrame() { + this.ctx.frame(); + } + + forceRenderUpdate() { + // Enforce re-rendering. + if (this.root) { + this.root.core._parent.setHasRenderUpdates(1); + } + } + + setClearColor(clearColor) { + this.forceRenderUpdate(); + if (clearColor === null) { + // Do not clear. + this._clearColor = null; + } else if (Array.isArray(clearColor)) { + this._clearColor = clearColor; + } else { + this._clearColor = StageUtils.getRgbaComponentsNormalized(clearColor); + } + } + + getClearColor() { + return this._clearColor; + } + + createElement(settings) { + if (settings) { + return this.element(settings); + } else { + return new Element(this); + } + } + + createShader(settings) { + return Shader.create(this, settings); + } + + element(settings) { + if (settings.isElement) return settings; + + let element; + if (settings.type) { + element = new settings.type(this); + } else { + element = new Element(this); + } + + element.patch(settings); + + return element; + } + + c(settings) { + return this.element(settings); + } + + get w() { + return this._options.w; + } + + get h() { + return this._options.h; + } + + get coordsWidth() { + return this.w / this._options.precision; + } + + get coordsHeight() { + return this.h / this._options.precision; + } + + addMemoryUsage(delta) { + this._usedMemory += delta; + if (this._lastGcFrame !== this.frameCounter) { + if (this._usedMemory > this.getOption('memoryPressure')) { + this.gc(false); + if (this._usedMemory > this.getOption('memoryPressure') - 2e6) { + // Too few released. Aggressive cleanup. + this.gc(true); + } + } + } + } + + get usedMemory() { + return this._usedMemory; + } + + gc(aggressive) { + if (this._lastGcFrame !== this.frameCounter) { + this._lastGcFrame = this.frameCounter; + const memoryUsageBefore = this._usedMemory; + this.gcTextureMemory(aggressive); + this.gcRenderTextureMemory(aggressive); + this.renderer.gc(aggressive); + + console.log(`GC${aggressive ? "[aggressive]" : ""}! Frame ${this._lastGcFrame} Freed ${((memoryUsageBefore - this._usedMemory) / 1e6).toFixed(2)}MP from GPU memory. Remaining: ${(this._usedMemory / 1e6).toFixed(2)}MP`); + const other = this._usedMemory - this.textureManager.usedMemory - this.ctx.usedMemory; + console.log(` Textures: ${(this.textureManager.usedMemory / 1e6).toFixed(2)}MP, Render Textures: ${(this.ctx.usedMemory / 1e6).toFixed(2)}MP, Renderer caches: ${(other / 1e6).toFixed(2)}MP`); + } + } + + gcTextureMemory(aggressive = false) { + if (aggressive && this.ctx.root.visible) { + // Make sure that ALL textures are cleaned; + this.ctx.root.visible = false; + this.textureManager.gc(); + this.ctx.root.visible = true; + } else { + this.textureManager.gc(); + } + } + + gcRenderTextureMemory(aggressive = false) { + if (aggressive && this.root.visible) { + // Make sure that ALL render textures are cleaned; + this.root.visible = false; + this.ctx.freeUnusedRenderTextures(0); + this.root.visible = true; + } else { + this.ctx.freeUnusedRenderTextures(0); + } + } + + getDrawingCanvas() { + return this.platform.getDrawingCanvas(); + } + + update() { + this.ctx.update(); + } + +} + +class Application extends Component { + + constructor(options = {}, properties) { + // Save options temporarily to avoid having to pass it through the constructor. + Application._temp_options = options; + + // Booting flag is used to postpone updateFocusSettings; + Application.booting = true; + const stage = new Stage(options.stage); + super(stage, properties); + Application.booting = false; + + this.__updateFocusCounter = 0; + + // We must construct while the application is not yet attached. + // That's why we 'init' the stage later (which actually emits the attach event). + this.stage.init(); + + // Initially, the focus settings are updated after both the stage and application are constructed. + this.updateFocusSettings(); + + this.__keymap = this.getOption('keys'); + if (this.__keymap) { + this.stage.platform.registerKeyHandler((e) => { + this._receiveKeydown(e); + }); + } + } + + getOption(name) { + return this.__options[name]; + } + + _setOptions(o) { + this.__options = {}; + + let opt = (name, def) => { + let value = o[name]; + + if (value === undefined) { + this.__options[name] = def; + } else { + this.__options[name] = value; + } + }; + + opt('debug', false); + opt('keys', { + 38: "Up", + 40: "Down", + 37: "Left", + 39: "Right", + 13: "Enter", + 8: "Back", + 27: "Exit" + }); + } + + __construct() { + this.stage.setApplication(this); + + this._setOptions(Application._temp_options); + delete Application._temp_options; + + super.__construct(); + } + + __init() { + super.__init(); + this.__updateFocus(); + } + + updateFocusPath() { + this.__updateFocus(); + } + + __updateFocus() { + const notOverridden = this.__updateFocusRec(); + + if (!Application.booting && notOverridden) { + this.updateFocusSettings(); + } + } + + __updateFocusRec() { + const updateFocusId = ++this.__updateFocusCounter; + this.__updateFocusId = updateFocusId; + + const newFocusPath = this.__getFocusPath(); + const newFocusedComponent = newFocusPath[newFocusPath.length - 1]; + const prevFocusedComponent = this._focusPath ? this._focusPath[this._focusPath.length - 1] : undefined; + + if (!prevFocusedComponent) { + // Focus events. + this._focusPath = []; + for (let i = 0, n = newFocusPath.length; i < n; i++) { + this._focusPath.push(newFocusPath[i]); + this._focusPath[i]._focus(newFocusedComponent, undefined); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + return true; + } else { + let m = Math.min(this._focusPath.length, newFocusPath.length); + let index; + for (index = 0; index < m; index++) { + if (this._focusPath[index] !== newFocusPath[index]) { + break; + } + } + + if (this._focusPath.length !== newFocusPath.length || index !== newFocusPath.length) { + if (this.__options.debug) { + console.log('FOCUS ' + newFocusedComponent.getLocationString()); + } + // Unfocus events. + for (let i = this._focusPath.length - 1; i >= index; i--) { + const unfocusedElement = this._focusPath.pop(); + unfocusedElement._unfocus(newFocusedComponent, prevFocusedComponent); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + + // Focus events. + for (let i = index, n = newFocusPath.length; i < n; i++) { + this._focusPath.push(newFocusPath[i]); + this._focusPath[i]._focus(newFocusedComponent, prevFocusedComponent); + const focusOverridden = (this.__updateFocusId !== updateFocusId); + if (focusOverridden) { + return false; + } + } + + // Focus changed events. + for (let i = 0; i < index; i++) { + this._focusPath[i]._focusChange(newFocusedComponent, prevFocusedComponent); + } + } + } + + return true; + } + + updateFocusSettings() { + const focusedComponent = this._focusPath[this._focusPath.length - 1]; + + // Get focus settings. These can be used for dynamic application-wide settings that depend on the + // focus directly (such as the application background). + const focusSettings = {}; + const defaultSetFocusSettings = Component.prototype._setFocusSettings; + for (let i = 0, n = this._focusPath.length; i < n; i++) { + if (this._focusPath[i]._setFocusSettings !== defaultSetFocusSettings) { + this._focusPath[i]._setFocusSettings(focusSettings); + } + } + + const defaultHandleFocusSettings = Component.prototype._handleFocusSettings; + for (let i = 0, n = this._focusPath.length; i < n; i++) { + if (this._focusPath[i]._handleFocusSettings !== defaultHandleFocusSettings) { + this._focusPath[i]._handleFocusSettings(focusSettings, this.__prevFocusSettings, focusedComponent); + } + } + + this.__prevFocusSettings = focusSettings; + } + + _handleFocusSettings(settings, prevSettings, focused, prevFocused) { + // Override to handle focus-based settings. + } + + __getFocusPath() { + const path = [this]; + let current = this; + do { + const nextFocus = current._getFocused(); + if (!nextFocus || (nextFocus === current)) { + // Found!; + break; + } + + + let ptr = nextFocus.cparent; + if (ptr === current) { + path.push(nextFocus); + } else { + // Not an immediate child: include full path to descendant. + const newParts = [nextFocus]; + do { + if (!ptr) { + current._throwError("Return value for _getFocused must be an attached descendant component but its '" + nextFocus.getLocationString() + "'"); + } + newParts.push(ptr); + ptr = ptr.cparent; + } while (ptr !== current); + + // Add them reversed. + for (let i = 0, n = newParts.length; i < n; i++) { + path.push(newParts[n - i - 1]); + } + } + + current = nextFocus; + } while(true); + + return path; + } + + get focusPath() { + return this._focusPath; + } + + /** + * Injects an event in the state machines, top-down from application to focused component. + */ + focusTopDownEvent(events, ...args) { + const path = this.focusPath; + const n = path.length; + + // Multiple events. + for (let i = 0; i < n; i++) { + const event = path[i]._getMostSpecificHandledMember(events); + if (event !== undefined) { + const returnValue = path[i][event](...args); + if (returnValue !== false) { + return true; + } + } + } + + return false; + } + + /** + * Injects an event in the state machines, bottom-up from focused component to application. + */ + focusBottomUpEvent(events, ...args) { + const path = this.focusPath; + const n = path.length; + + // Multiple events. + for (let i = n - 1; i >= 0; i--) { + const event = path[i]._getMostSpecificHandledMember(events); + if (event !== undefined) { + const returnValue = path[i][event](...args); + if (returnValue !== false) { + return true; + } + } + } + + return false; + } + + _receiveKeydown(e) { + const obj = e; + if (this.__keymap[e.keyCode]) { + if (!this.stage.application.focusTopDownEvent(["_capture" + this.__keymap[e.keyCode], "_captureKey"], obj)) { + this.stage.application.focusBottomUpEvent(["_handle" + this.__keymap[e.keyCode], "_handleKey"], obj); + } + } else { + if (!this.stage.application.focusTopDownEvent(["_captureKey"], obj)) { + this.stage.application.focusBottomUpEvent(["_handleKey"], obj); + } + } + this.updateFocusPath(); + } + + destroy() { + if (!this._destroyed) { + this._destroy(); + this.stage.destroy(); + this._destroyed = true; + } + } + + _destroy() { + // This forces the _detach, _disabled and _active events to be called. + this.stage.setApplication(undefined); + this._updateAttachedFlag(); + this._updateEnabledFlag(); + } + + getCanvas() { + return this.stage.getCanvas(); + } + +} + +class StaticCanvasTexture extends Texture { + + constructor(stage) { + super(stage); + this._factory = undefined; + this._lookupId = undefined; + } + + set content({factory, lookupId = undefined}) { + this._factory = factory; + this._lookupId = lookupId; + this._changed(); + } + + _getIsValid() { + return !!this._factory; + } + + _getLookupId() { + return this._lookupId; + } + + _getSourceLoader() { + const f = this._factory; + return (cb) => { + return f((err, canvas) => { + if (err) { + return cb(err); + } + cb(null, this.stage.platform.getTextureOptionsForDrawingCanvas(canvas)); + }, this.stage); + } + } + +} + +class Tools { + + static getCanvasTexture(canvasFactory, lookupId) { + return {type: StaticCanvasTexture, content: {factory: canvasFactory, lookupId: lookupId}} + } + + static getRoundRect(w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (!Array.isArray(radius)){ + // upper-left, upper-right, bottom-right, bottom-left. + radius = [radius, radius, radius, radius]; + } + + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createRoundRect(cb, stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor); + } else { + cb(null, this.createRoundRect(stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor)); + } + }; + let id = 'rect' + [w, h, strokeWidth, strokeColor, fill ? 1 : 0, fillColor].concat(radius).join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createRoundRect(stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (fill === undefined) fill = true; + if (strokeWidth === undefined) strokeWidth = 0; + + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + canvas.width = w + strokeWidth + 2; + canvas.height = h + strokeWidth + 2; + + ctx.beginPath(); + let x = 0.5 * strokeWidth + 1, y = 0.5 * strokeWidth + 1; + + ctx.moveTo(x + radius[0], y); + ctx.lineTo(x + w - radius[1], y); + ctx.arcTo(x + w, y, x + w, y + radius[1], radius[1]); + ctx.lineTo(x + w, y + h - radius[2]); + ctx.arcTo(x + w, y + h, x + w - radius[2], y + h, radius[2]); + ctx.lineTo(x + radius[3], y + h); + ctx.arcTo(x, y + h, x, y + h - radius[3], radius[3]); + ctx.lineTo(x, y + radius[0]); + ctx.arcTo(x, y, x + radius[0], y, radius[0]); + ctx.closePath(); + + if (fill) { + if (Utils.isNumber(fillColor)) { + ctx.fillStyle = StageUtils.getRgbaString(fillColor); + } else { + ctx.fillStyle = "white"; + } + ctx.fill(); + } + + if (strokeWidth) { + if (Utils.isNumber(strokeColor)) { + ctx.strokeStyle = StageUtils.getRgbaString(strokeColor); + } else { + ctx.strokeStyle = "white"; + } + ctx.lineWidth = strokeWidth; + ctx.stroke(); + } + + return canvas; + } + + static getShadowRect(w, h, radius = 0, blur = 5, margin = blur * 2) { + if (!Array.isArray(radius)){ + // upper-left, upper-right, bottom-right, bottom-left. + radius = [radius, radius, radius, radius]; + } + + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createShadowRect(cb, stage, w, h, radius, blur, margin); + } else { + cb(null, this.createShadowRect(stage, w, h, radius, blur, margin)); + } + }; + let id = 'shadow' + [w, h, blur, margin].concat(radius).join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createShadowRect(stage, w, h, radius, blur, margin) { + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + canvas.width = w + margin * 2; + canvas.height = h + margin * 2; + + // WpeWebKit bug: we experienced problems without this with shadows in noncompositedwebgl mode. + ctx.globalAlpha = 0.01; + ctx.fillRect(0, 0, 0.01, 0.01); + ctx.globalAlpha = 1.0; + + ctx.shadowColor = StageUtils.getRgbaString(0xFFFFFFFF); + ctx.fillStyle = StageUtils.getRgbaString(0xFFFFFFFF); + ctx.shadowBlur = blur; + ctx.shadowOffsetX = (w + 10) + margin; + ctx.shadowOffsetY = margin; + + ctx.beginPath(); + const x = -(w + 10); + const y = 0; + + ctx.moveTo(x + radius[0], y); + ctx.lineTo(x + w - radius[1], y); + ctx.arcTo(x + w, y, x + w, y + radius[1], radius[1]); + ctx.lineTo(x + w, y + h - radius[2]); + ctx.arcTo(x + w, y + h, x + w - radius[2], y + h, radius[2]); + ctx.lineTo(x + radius[3], y + h); + ctx.arcTo(x, y + h, x, y + h - radius[3], radius[3]); + ctx.lineTo(x, y + radius[0]); + ctx.arcTo(x, y, x + radius[0], y, radius[0]); + ctx.closePath(); + ctx.fill(); + + return canvas; + } + + static getSvgTexture(url, w, h) { + let factory = (cb, stage) => { + if (Utils.isSpark) { + stage.platform.createSvg(cb, stage, url, w, h); + } else { + this.createSvg(cb, stage, url, w, h); + } + }; + let id = 'svg' + [w, h, url].join(","); + return Tools.getCanvasTexture(factory, id); + } + + static createSvg(cb, stage, url, w, h) { + let canvas = stage.platform.getDrawingCanvas(); + let ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = true; + + let img = new Image(); + img.onload = () => { + canvas.width = w; + canvas.height = h; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + cb(null, canvas); + }; + img.onError = (err) => { + cb(err); + }; + img.src = url; + } + +} + +class ObjMerger { + + static isMf(f) { + return Utils.isFunction(f) && f.__mf; + } + + static mf(f) { + // Set as merge function. + f.__mf = true; + return f; + } + + static merge(a, b) { + const aks = Object.keys(a); + const bks = Object.keys(b); + + if (!bks.length) { + return a; + } + + // Create index array for all elements. + const ai = {}; + const bi = {}; + for (let i = 0, n = bks.length; i < n; i++) { + const key = bks[i]; + ai[key] = -1; + bi[key] = i; + } + for (let i = 0, n = aks.length; i < n; i++) { + const key = aks[i]; + ai[key] = i; + if (bi[key] === undefined) { + bi[key] = -1; + } + } + + const aksl = aks.length; + + const result = {}; + for (let i = 0, n = bks.length; i < n; i++) { + const key = bks[i]; + + // Prepend all items in a that are not in b - before the now added b attribute. + const aIndex = ai[key]; + let curIndex = aIndex; + while(--curIndex >= 0) { + const akey = aks[curIndex]; + if (bi[akey] !== -1) { + // Already found? Stop processing. + // Not yet found but exists in b? Also stop processing: wait until we find it in b. + break; + } + } + while(++curIndex < aIndex) { + const akey = aks[curIndex]; + result[akey] = a[akey]; + } + + const bv = b[key]; + const av = a[key]; + let r; + if (this.isMf(bv)) { + r = bv(av); + } else { + if (!Utils.isObjectLiteral(av) || !Utils.isObjectLiteral(bv)) { + r = bv; + } else { + r = ObjMerger.merge(av, bv); + } + } + + // When marked as undefined, property is deleted. + if (r !== undefined) { + result[key] = r; + } + } + + // Append remaining final items in a. + let curIndex = aksl; + while(--curIndex >= 0) { + const akey = aks[curIndex]; + if (bi[akey] !== -1) { + break; + } + } + while(++curIndex < aksl) { + const akey = aks[curIndex]; + result[akey] = a[akey]; + } + + return result; + } + +} + +/** + * Manages the list of children for an element. + */ + +class ObjectListProxy extends ObjectList { + + constructor(target) { + super(); + this._target = target; + } + + onAdd(item, index) { + this._target.addAt(item, index); + } + + onRemove(item, index) { + this._target.removeAt(index); + } + + onSync(removed, added, order) { + this._target._setByArray(order); + } + + onSet(item, index) { + this._target.setAt(item, index); + } + + onMove(item, fromIndex, toIndex) { + this._target.setAt(item, toIndex); + } + + createItem(object) { + return this._target.createItem(object); + } + + isItem(object) { + return this._target.isItem(object); + } + +} + +/** + * Manages the list of children for an element. + */ + +class ObjectListWrapper extends ObjectListProxy { + + constructor(target, wrap) { + super(target); + this._wrap = wrap; + } + + wrap(item) { + let wrapper = this._wrap(item); + item._wrapper = wrapper; + return wrapper; + } + + onAdd(item, index) { + item = this.wrap(item); + super.onAdd(item, index); + } + + onRemove(item, index) { + super.onRemove(item, index); + } + + onSync(removed, added, order) { + added.forEach(a => this.wrap(a)); + order = order.map(a => a._wrapper); + super.onSync(removed, added, order); + } + + onSet(item, index) { + item = this.wrap(item); + super.onSet(item, index); + } + + onMove(item, fromIndex, toIndex) { + super.onMove(item, fromIndex, toIndex); + } + +} + +class NoiseTexture extends Texture { + + _getLookupId() { + return '__noise'; + } + + _getSourceLoader() { + const gl = this.stage.gl; + return function(cb) { + const noise = new Uint8Array(128 * 128 * 4); + for (let i = 0; i < 128 * 128 * 4; i+=4) { + const v = Math.floor(Math.random() * 256); + noise[i] = v; + noise[i+1] = v; + noise[i+2] = v; + noise[i+3] = 255; + } + const texParams = {}; + + if (gl) { + texParams[gl.TEXTURE_WRAP_S] = gl.REPEAT; + texParams[gl.TEXTURE_WRAP_T] = gl.REPEAT; + texParams[gl.TEXTURE_MIN_FILTER] = gl.NEAREST; + texParams[gl.TEXTURE_MAG_FILTER] = gl.NEAREST; + } + + cb(null, {source: noise, w: 128, h: 128, texParams: texParams}); + } + } + +} + +class HtmlTexture extends Texture { + + constructor(stage) { + super(stage); + this._htmlElement = undefined; + this._scale = 1; + } + + set htmlElement(v) { + this._htmlElement = v; + this._changed(); + } + + get htmlElement() { + return this._htmlElement; + } + + set scale(v) { + this._scale = v; + this._changed(); + } + + get scale() { + return this._scale; + } + + set html(v) { + if (!v) { + this.htmlElement = undefined; + } else { + const d = document.createElement('div'); + d.innerHTML = "
" + v + "
"; + this.htmlElement = d.firstElementChild; + } + } + + get html() { + return this._htmlElement.innerHTML; + } + + _getIsValid() { + return this.htmlElement; + } + + _getLookupId() { + return this._scale + ":" + this._htmlElement.innerHTML; + } + + _getSourceLoader() { + const htmlElement = this._htmlElement; + const scale = this._scale; + return function(cb) { + if (!window.html2canvas) { + return cb(new Error("Please include html2canvas (https://html2canvas.hertzen.com/)")); + } + + const area = HtmlTexture.getPreloadArea(); + area.appendChild(htmlElement); + + html2canvas(htmlElement, {backgroundColor: null, scale: scale}).then(function(canvas) { + area.removeChild(htmlElement); + if (canvas.height === 0) { + return cb(new Error("Canvas height is 0")); + } + cb(null, {source: canvas, width: canvas.width, height: canvas.height}); + }).catch(e => { + console.error(e); + }); + } + } + + static getPreloadArea() { + if (!this._preloadArea) { + // Preload area must be included in document body and must be visible to trigger html element rendering. + this._preloadArea = document.createElement('div'); + if (this._preloadArea.attachShadow) { + // Use a shadow DOM if possible to prevent styling from interfering. + this._preloadArea.attachShadow({mode: 'closed'}); + } + this._preloadArea.style.opacity = 0; + this._preloadArea.style.pointerEvents = 'none'; + this._preloadArea.style.position = 'fixed'; + this._preloadArea.style.display = 'block'; + this._preloadArea.style.top = '100vh'; + this._preloadArea.style.overflow = 'hidden'; + document.body.appendChild(this._preloadArea); + } + return this._preloadArea; + } +} + +class StaticTexture extends Texture { + + constructor(stage, options) { + super(stage); + + this._options = options; + } + + set options(v) { + if (this._options !== v) { + this._options = v; + this._changed(); + } + } + + get options() { + return this._options; + } + + _getIsValid() { + return !!this._options; + } + + _getSourceLoader() { + return (cb) => { + cb(null, this._options); + } + } +} + +class ListComponent extends Component { + + constructor(stage) { + super(stage); + + this._wrapper = super._children.a({}); + + this._reloadVisibleElements = false; + + this._visibleItems = new Set(); + + this._index = 0; + + this._started = false; + + /** + * The transition definition that is being used when scrolling the items. + * @type TransitionSettings + */ + this._scrollTransitionSettings = this.stage.transitions.createSettings({}); + + /** + * The scroll area size in pixels per item. + */ + this._itemSize = 100; + + this._viewportScrollOffset = 0; + + this._itemScrollOffset = 0; + + /** + * Should the list jump when scrolling between end to start, or should it be continuous, like a carrousel? + */ + this._roll = false; + + /** + * Allows restricting the start scroll position. + */ + this._rollMin = 0; + + /** + * Allows restricting the end scroll position. + */ + this._rollMax = 0; + + /** + * Definition for a custom animation that is applied when an item is (partially) selected. + * @type AnimationSettings + */ + this._progressAnimation = null; + + /** + * Inverts the scrolling direction. + * @type {boolean} + * @private + */ + this._invertDirection = false; + + /** + * Layout the items horizontally or vertically? + * @type {boolean} + * @private + */ + this._horizontal = true; + + this.itemList = new ListItems(this); + } + + _allowChildrenAccess() { + return false; + } + + get items() { + return this.itemList.get(); + } + + set items(children) { + this.itemList.patch(children); + } + + start() { + this._wrapper.transition(this.property, this._scrollTransitionSettings); + this._scrollTransition = this._wrapper.transition(this.property); + this._scrollTransition.on('progress', p => this.update()); + + this.setIndex(0, true, true); + + this._started = true; + + this.update(); + } + + setIndex(index, immediate = false, closest = false) { + let nElements = this.length; + if (!nElements) return; + + this.emit('unfocus', this.getElement(this.realIndex), this._index, this.realIndex); + + if (closest) { + // Scroll to same offset closest to the index. + let offset = Utils.getModuloIndex(index, nElements); + let o = Utils.getModuloIndex(this.index, nElements); + let diff = offset - o; + if (diff > 0.5 * nElements) { + diff -= nElements; + } else if (diff < -0.5 * nElements) { + diff += nElements; + } + this._index += diff; + } else { + this._index = index; + } + + if (this._roll || (this.viewportSize > this._itemSize * nElements)) { + this._index = Utils.getModuloIndex(this._index, nElements); + } + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + let value = direction * this._index * this._itemSize; + + if (this._roll) { + let min, max, scrollDelta; + if (direction == 1) { + max = (nElements - 1) * this._itemSize; + scrollDelta = this._viewportScrollOffset * this.viewportSize - this._itemScrollOffset * this._itemSize; + + max -= scrollDelta; + + min = this.viewportSize - (this._itemSize + scrollDelta); + + if (this._rollMin) min -= this._rollMin; + if (this._rollMax) max += this._rollMax; + + value = Math.max(Math.min(value, max), min); + } else { + max = (nElements * this._itemSize - this.viewportSize); + scrollDelta = this._viewportScrollOffset * this.viewportSize - this._itemScrollOffset * this._itemSize; + + max += scrollDelta; + + let min = scrollDelta; + + if (this._rollMin) min -= this._rollMin; + if (this._rollMax) max += this._rollMax; + + value = Math.min(Math.max(-max, value), -min); + } + } + + this._scrollTransition.start(value); + + if (immediate) { + this._scrollTransition.finish(); + } + + this.emit('focus', this.getElement(this.realIndex), this._index, this.realIndex); + } + + getAxisPosition() { + let target = -this._scrollTransition._targetValue; + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + let value = -direction * this._index * this._itemSize; + + return this._viewportScrollOffset * this.viewportSize + (value - target); + } + + update() { + if (!this._started) return; + + let nElements = this.length; + if (!nElements) return; + + let direction = (this._horizontal ^ this._invertDirection ? -1 : 1); + + // Map position to index value. + let v = (this._horizontal ? this._wrapper.x : this._wrapper.y); + + let viewportSize = this.viewportSize; + let scrollDelta = this._viewportScrollOffset * viewportSize - this._itemScrollOffset * this._itemSize; + v += scrollDelta; + + let s, e, ps, pe; + if (direction == -1) { + s = Math.floor(-v / this._itemSize); + ps = 1 - ((-v / this._itemSize) - s); + e = Math.floor((viewportSize - v) / this._itemSize); + pe = (((viewportSize - v) / this._itemSize) - e); + } else { + s = Math.ceil(v / this._itemSize); + ps = 1 + (v / this._itemSize) - s; + e = Math.ceil((v - viewportSize) / this._itemSize); + pe = e - ((v - viewportSize) / this._itemSize); + } + if (this._roll || (viewportSize > this._itemSize * nElements)) { + // Don't show additional items. + if (e >= nElements) { + e = nElements - 1; + pe = 1; + } + if (s >= nElements) { + s = nElements - 1; + ps = 1; + } + if (e <= -1) { + e = 0; + pe = 1; + } + if (s <= -1) { + s = 0; + ps = 1; + } + } + + let offset = -direction * s * this._itemSize; + + let item; + for (let index = s; (direction == -1 ? index <= e : index >= e); (direction == -1 ? index++ : index--)) { + let realIndex = Utils.getModuloIndex(index, nElements); + + let element = this.getElement(realIndex); + item = element.parent; + this._visibleItems.delete(item); + if (this._horizontal) { + item.x = offset + scrollDelta; + } else { + item.y = offset + scrollDelta; + } + + let wasVisible = item.visible; + item.visible = true; + + if (!wasVisible || this._reloadVisibleElements) { + // Turned visible. + this.emit('visible', index, realIndex); + } + + + + if (this._progressAnimation) { + let p = 1; + if (index == s) { + p = ps; + } else if (index == e) { + p = pe; + } + + // Use animation to progress. + this._progressAnimation.apply(element, p); + } + + offset += this._itemSize; + } + + // Handle item visibility. + let self = this; + this._visibleItems.forEach(function(invisibleItem) { + invisibleItem.visible = false; + self._visibleItems.delete(invisibleItem); + }); + + for (let index = s; (direction == -1 ? index <= e : index >= e); (direction == -1 ? index++ : index--)) { + let realIndex = Utils.getModuloIndex(index, nElements); + this._visibleItems.add(this.getWrapper(realIndex)); + } + + this._reloadVisibleElements = false; + } + + setPrevious() { + this.setIndex(this._index - 1); + } + + setNext() { + this.setIndex(this._index + 1); + } + + getWrapper(index) { + return this._wrapper.children[index]; + } + + getElement(index) { + let e = this._wrapper.children[index]; + return e ? e.children[0] : null; + } + + reload() { + this._reloadVisibleElements = true; + this.update(); + } + + get element() { + let e = this._wrapper.children[this.realIndex]; + return e ? e.children[0] : null; + } + + get length() { + return this._wrapper.children.length; + } + + get property() { + return this._horizontal ? 'x' : 'y'; + } + + get viewportSize() { + return this._horizontal ? this.w : this.h; + } + + get index() { + return this._index; + } + + get realIndex() { + return Utils.getModuloIndex(this._index, this.length); + } + + get itemSize() { + return this._itemSize; + } + + set itemSize(v) { + this._itemSize = v; + this.update(); + } + + get viewportScrollOffset() { + return this._viewportScrollOffset; + } + + set viewportScrollOffset(v) { + this._viewportScrollOffset = v; + this.update(); + } + + get itemScrollOffset() { + return this._itemScrollOffset; + } + + set itemScrollOffset(v) { + this._itemScrollOffset = v; + this.update(); + } + + get scrollTransitionSettings() { + return this._scrollTransitionSettings; + } + + set scrollTransitionSettings(v) { + this._scrollTransitionSettings.patch(v); + } + + set scrollTransition(v) { + this._scrollTransitionSettings.patch(v); + } + + get scrollTransition() { + return this._scrollTransition; + } + + get progressAnimation() { + return this._progressAnimation; + } + + set progressAnimation(v) { + if (Utils.isObjectLiteral(v)) { + this._progressAnimation = this.stage.animations.createSettings(v); + } else { + this._progressAnimation = v; + } + this.update(); + } + + get roll() { + return this._roll; + } + + set roll(v) { + this._roll = v; + this.update(); + } + + get rollMin() { + return this._rollMin; + } + + set rollMin(v) { + this._rollMin = v; + this.update(); + } + + get rollMax() { + return this._rollMax; + } + + set rollMax(v) { + this._rollMax = v; + this.update(); + } + + get invertDirection() { + return this._invertDirection; + } + + set invertDirection(v) { + if (!this._started) { + this._invertDirection = v; + } + } + + get horizontal() { + return this._horizontal; + } + + set horizontal(v) { + if (v !== this._horizontal) { + if (!this._started) { + this._horizontal = v; + } + } + } + +} +class ListItems extends ObjectListWrapper { + constructor(list) { + let wrap = (item => { + let parent = item.stage.createElement(); + parent.add(item); + parent.visible = false; + return parent; + }); + + super(list._wrapper._children, wrap); + this.list = list; + } + + onAdd(item, index) { + super.onAdd(item, index); + this.checkStarted(index); + } + + checkStarted(index) { + this.list._reloadVisibleElements = true; + if (!this.list._started) { + this.list.start(); + } else { + if (this.list.length === 1) { + this.list.setIndex(0, true, true); + } else { + if (this.list._index >= this.list.length) { + this.list.setIndex(0); + } + } + this.list.update(); + } + } + + onRemove(item, index) { + super.onRemove(item, index); + let ri = this.list.realIndex; + if (ri === index) { + if (ri === this.list.length) { + ri--; + } + if (ri >= 0) { + this.list.setIndex(ri); + } + } else if (ri > index) { + this.list.setIndex(ri - 1); + } + + this.list._reloadVisibleElements = true; + } + + onSet(item, index) { + super.onSet(item, index); + this.checkStarted(index); + } + + onSync(removed, added, order) { + super.onSync(removed, added, order); + this.checkStarted(0); + } + + get _signalProxy() { + return true; + } + +} + +class LinearBlurShader extends DefaultShader { + + constructor(context) { + super(context); + + this._direction = new Float32Array([1, 0]); + this._kernelRadius = 1; + } + + get x() { + return this._direction[0]; + } + + set x(v) { + this._direction[0] = v; + this.redraw(); + } + + get y() { + return this._direction[1]; + } + + set y(v) { + this._direction[1] = v; + this.redraw(); + } + + get kernelRadius() { + return this._kernelRadius; + } + + set kernelRadius(v) { + this._kernelRadius = v; + this.redraw(); + } + + + useDefault() { + return (this._kernelRadius === 0); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("direction", this._direction, this.gl.uniform2fv); + this._setUniform("kernelRadius", this._kernelRadius, this.gl.uniform1i); + + const w = operation.getRenderWidth(); + const h = operation.getRenderHeight(); + this._setUniform("resolution", new Float32Array([w, h]), this.gl.uniform2fv); + } +} + +LinearBlurShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + uniform vec2 resolution; + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform vec2 direction; + uniform int kernelRadius; + + vec4 blur1(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.3333333333333333) * direction; + color += texture2D(image, uv) * 0.29411764705882354; + color += texture2D(image, uv + (off1 / resolution)) * 0.35294117647058826; + color += texture2D(image, uv - (off1 / resolution)) * 0.35294117647058826; + return color; + } + + vec4 blur2(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.3846153846) * direction; + vec2 off2 = vec2(3.2307692308) * direction; + color += texture2D(image, uv) * 0.2270270270; + color += texture2D(image, uv + (off1 / resolution)) * 0.3162162162; + color += texture2D(image, uv - (off1 / resolution)) * 0.3162162162; + color += texture2D(image, uv + (off2 / resolution)) * 0.0702702703; + color += texture2D(image, uv - (off2 / resolution)) * 0.0702702703; + return color; + } + + vec4 blur3(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) { + vec4 color = vec4(0.0); + vec2 off1 = vec2(1.411764705882353) * direction; + vec2 off2 = vec2(3.2941176470588234) * direction; + vec2 off3 = vec2(5.176470588235294) * direction; + color += texture2D(image, uv) * 0.1964825501511404; + color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344; + color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344; + color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732; + color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732; + color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057; + color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057; + return color; + } + + void main(void){ + if (kernelRadius == 1) { + gl_FragColor = blur1(uSampler, vTextureCoord, resolution, direction) * vColor; + } else if (kernelRadius == 2) { + gl_FragColor = blur2(uSampler, vTextureCoord, resolution, direction) * vColor; + } else { + gl_FragColor = blur3(uSampler, vTextureCoord, resolution, direction) * vColor; + } + } +`; + +/** + * 4x4 box blur shader which works in conjunction with a 50% rescale. + */ +class BoxBlurShader extends DefaultShader { + + setupUniforms(operation) { + super.setupUniforms(operation); + const dx = 1.0 / operation.getTextureWidth(0); + const dy = 1.0 / operation.getTextureHeight(0); + this._setUniform("stepTextureCoord", new Float32Array([dx, dy]), this.gl.uniform2fv); + } + +} + +BoxBlurShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + uniform vec2 stepTextureCoord; + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec4 vColor; + varying vec2 vTextureCoordUl; + varying vec2 vTextureCoordUr; + varying vec2 vTextureCoordBl; + varying vec2 vTextureCoordBr; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoordUl = aTextureCoord - stepTextureCoord; + vTextureCoordBr = aTextureCoord + stepTextureCoord; + vTextureCoordUr = vec2(vTextureCoordBr.x, vTextureCoordUl.y); + vTextureCoordBl = vec2(vTextureCoordUl.x, vTextureCoordBr.y); + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +BoxBlurShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoordUl; + varying vec2 vTextureCoordUr; + varying vec2 vTextureCoordBl; + varying vec2 vTextureCoordBr; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + vec4 color = 0.25 * (texture2D(uSampler, vTextureCoordUl) + texture2D(uSampler, vTextureCoordUr) + texture2D(uSampler, vTextureCoordBl) + texture2D(uSampler, vTextureCoordBr)); + gl_FragColor = color * vColor; + } +`; + +class BlurShader extends DefaultShader$1 { + + constructor(context) { + super(context); + this._kernelRadius = 1; + } + + get kernelRadius() { + return this._kernelRadius; + } + + set kernelRadius(v) { + this._kernelRadius = v; + this.redraw(); + } + + useDefault() { + return this._amount === 0; + } + + _beforeDrawEl({target}) { + target.ctx.filter = "blur(" + this._kernelRadius + "px)"; + } + + _afterDrawEl({target}) { + target.ctx.filter = "none"; + } + +} + +class FastBlurComponent extends Component { + static _template() { + return {} + } + + get wrap() { + return this.tag("Wrap"); + } + + set content(v) { + return this.wrap.content = v; + } + + get content() { + return this.wrap.content; + } + + set padding(v) { + this.wrap._paddingX = v; + this.wrap._paddingY = v; + this.wrap._updateBlurSize(); + } + + set paddingX(v) { + this.wrap._paddingX = v; + this.wrap._updateBlurSize(); + } + + set paddingY(v) { + this.wrap._paddingY = v; + this.wrap._updateBlurSize(); + } + + set amount(v) { + return this.wrap.amount = v; + } + + get amount() { + return this.wrap.amount; + } + + _onResize() { + this.wrap.w = this.renderWidth; + this.wrap.h = this.renderHeight; + } + + get _signalProxy() { + return true; + } + + _build() { + this.patch({ + Wrap: {type: this.stage.gl ? WebGLFastBlurComponent : C2dFastBlurComponent} + }); + } + +} + + +class C2dFastBlurComponent extends Component { + + static _template() { + return { + forceZIndexContext: true, + rtt: true, + Textwrap: {shader: {type: BlurShader}, Content: {}} + } + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap>Content"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + + } + + static getSpline() { + if (!this._multiSpline) { + this._multiSpline = new MultiSpline(); + this._multiSpline.parse(false, {0: 0, 0.25: 1.5, 0.5: 5.5, 0.75: 18, 1: 39}); + } + return this._multiSpline; + } + + get content() { + return this.sel('Textwrap>Content'); + } + + set content(v) { + this.sel('Textwrap>Content').patch(v, true); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + this._wrapper.x = paddingX; + this._textwrap.x = -paddingX; + + this._wrapper.y = paddingY; + this._textwrap.y = -paddingY; + + this._textwrap.w = w + paddingX * 2; + this._textwrap.h = h + paddingY * 2; + } + + get amount() { + return this._amount; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._textwrap.shader.kernelRadius = C2dFastBlurComponent._amountToKernelRadius(v); + } + + static _amountToKernelRadius(v) { + return C2dFastBlurComponent.getSpline().getValue(Math.min(1, v * 0.25)); + } + + get _signalProxy() { + return true; + } + +} + +class WebGLFastBlurComponent extends Component { + + static _template() { + const onUpdate = function(element, elementCore) { + if ((elementCore._recalc & (2 + 128))) { + const w = elementCore.w; + const h = elementCore.h; + let cur = elementCore; + do { + cur = cur._children[0]; + cur._element.w = w; + cur._element.h = h; + } while(cur._children); + } + }; + + return { + Textwrap: {rtt: true, forceZIndexContext: true, renderOffscreen: true, Content: {}}, + Layers: { + L0: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L1: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L2: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L3: {rtt: true, onUpdate: onUpdate, renderOffscreen: true, visible: false, Content: {shader: {type: BoxBlurShader}}} + }, + Result: {shader: {type: FastBlurOutputShader}, visible: false} + } + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap>Content"); + this._layers = this.sel("Layers"); + this._output = this.sel("Result"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + } + + _buildLayers() { + const filterShaderSettings = [{x:1,y:0,kernelRadius:1},{x:0,y:1,kernelRadius:1},{x:1.5,y:0,kernelRadius:1},{x:0,y:1.5,kernelRadius:1}]; + const filterShaders = filterShaderSettings.map(s => { + const shader = Shader.create(this.stage, Object.assign({type: LinearBlurShader}, s)); + return shader; + }); + + this._setLayerTexture(this.getLayerContents(0), this._textwrap.getTexture(), []); + this._setLayerTexture(this.getLayerContents(1), this.getLayer(0).getTexture(), [filterShaders[0], filterShaders[1]]); + + // Notice that 1.5 filters should be applied before 1.0 filters. + this._setLayerTexture(this.getLayerContents(2), this.getLayer(1).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + this._setLayerTexture(this.getLayerContents(3), this.getLayer(2).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + } + + _setLayerTexture(element, texture, steps) { + if (!steps.length) { + element.texture = texture; + } else { + const step = steps.pop(); + const child = element.stage.c({rtt: true, shader: step}); + + // Recurse. + this._setLayerTexture(child, texture, steps); + + element.childList.add(child); + } + return element; + } + + get content() { + return this.sel('Textwrap>Content'); + } + + set content(v) { + this.sel('Textwrap>Content').patch(v, true); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + getLayer(i) { + return this._layers.sel("L" + i); + } + + getLayerContents(i) { + return this.getLayer(i).sel("Content"); + } + + _onResize() { + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + let fw = w + paddingX * 2; + let fh = h + paddingY * 2; + this._textwrap.w = fw; + this._wrapper.x = paddingX; + this.getLayer(0).w = this.getLayerContents(0).w = fw / 2; + this.getLayer(1).w = this.getLayerContents(1).w = fw / 4; + this.getLayer(2).w = this.getLayerContents(2).w = fw / 8; + this.getLayer(3).w = this.getLayerContents(3).w = fw / 16; + this._output.x = -paddingX; + this._textwrap.x = -paddingX; + this._output.w = fw; + + this._textwrap.h = fh; + this._wrapper.y = paddingY; + this.getLayer(0).h = this.getLayerContents(0).h = fh / 2; + this.getLayer(1).h = this.getLayerContents(1).h = fh / 4; + this.getLayer(2).h = this.getLayerContents(2).h = fh / 8; + this.getLayer(3).h = this.getLayerContents(3).h = fh / 16; + this._output.y = -paddingY; + this._textwrap.y = -paddingY; + this._output.h = fh; + + this.w = w; + this.h = h; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._update(); + } + + get amount() { + return this._amount; + } + + _update() { + let v = Math.min(4, Math.max(0, this._amount)); + if (v === 0) { + this._textwrap.renderToTexture = false; + this._output.shader.otherTextureSource = null; + this._output.visible = false; + } else { + this._textwrap.renderToTexture = true; + this._output.visible = true; + + this.getLayer(0).visible = (v > 0); + this.getLayer(1).visible = (v > 1); + this.getLayer(2).visible = (v > 2); + this.getLayer(3).visible = (v > 3); + + if (v <= 1) { + this._output.texture = this._textwrap.getTexture(); + this._output.shader.otherTextureSource = this.getLayer(0).getTexture(); + this._output.shader.a = v; + } else if (v <= 2) { + this._output.texture = this.getLayer(0).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(1).getTexture(); + this._output.shader.a = v - 1; + } else if (v <= 3) { + this._output.texture = this.getLayer(1).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(2).getTexture(); + this._output.shader.a = v - 2; + } else if (v <= 4) { + this._output.texture = this.getLayer(2).getTexture(); + this._output.shader.otherTextureSource = this.getLayer(3).getTexture(); + this._output.shader.a = v - 3; + } + } + } + + set shader(s) { + super.shader = s; + if (!this.renderToTexture) { + console.warn("Please enable renderToTexture to use with a shader."); + } + } + + _firstActive() { + this._buildLayers(); + } + +} + +/** + * Shader that combines two textures into one output. + */ +class FastBlurOutputShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._a = 0; + this._otherTextureSource = null; + } + + get a() { + return this._a; + } + + set a(v) { + this._a = v; + this.redraw(); + } + + set otherTextureSource(v) { + this._otherTextureSource = v; + this.redraw(); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("a", this._a, this.gl.uniform1f); + this._setUniform("uSampler2", 1, this.gl.uniform1i); + } + + beforeDraw(operation) { + let glTexture = this._otherTextureSource ? this._otherTextureSource.nativeTexture : null; + + let gl = this.gl; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.activeTexture(gl.TEXTURE0); + } +} + +FastBlurOutputShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform sampler2D uSampler2; + uniform float a; + void main(void){ + if (a == 1.0) { + gl_FragColor = texture2D(uSampler2, vTextureCoord) * vColor; + } else { + gl_FragColor = ((1.0 - a) * texture2D(uSampler, vTextureCoord) + (a * texture2D(uSampler2, vTextureCoord))) * vColor; + } + } +`; + +class BloomComponent extends Component { + + static _template() { + const onUpdate = function(element, elementCore) { + if ((elementCore._recalc & (2 + 128))) { + const w = elementCore.w; + const h = elementCore.h; + let cur = elementCore; + do { + cur = cur._children[0]; + cur._element.w = w; + cur._element.h = h; + } while(cur._children); + } + }; + + return { + Textwrap: {rtt: true, forceZIndexContext: true, renderOffscreen: true, + BloomBase: {shader: {type: BloomBaseShader}, + Content: {} + } + }, + Layers: { + L0: {rtt: true, onUpdate: onUpdate, scale: 2, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L1: {rtt: true, onUpdate: onUpdate, scale: 4, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L2: {rtt: true, onUpdate: onUpdate, scale: 8, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}}, + L3: {rtt: true, onUpdate: onUpdate, scale: 16, pivot: 0, visible: false, Content: {shader: {type: BoxBlurShader}}} + } + } + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + this._textwrap = this.sel("Textwrap"); + this._wrapper = this.sel("Textwrap.Content"); + this._layers = this.sel("Layers"); + + this._amount = 0; + this._paddingX = 0; + this._paddingY = 0; + } + + _build() { + const filterShaderSettings = [{x:1,y:0,kernelRadius:3},{x:0,y:1,kernelRadius:3},{x:1.5,y:0,kernelRadius:3},{x:0,y:1.5,kernelRadius:3}]; + const filterShaders = filterShaderSettings.map(s => { + const shader = this.stage.createShader(Object.assign({type: LinearBlurShader}, s)); + return shader; + }); + + this._setLayerTexture(this.getLayerContents(0), this._textwrap.getTexture(), []); + this._setLayerTexture(this.getLayerContents(1), this.getLayer(0).getTexture(), [filterShaders[0], filterShaders[1]]); + + // Notice that 1.5 filters should be applied before 1.0 filters. + this._setLayerTexture(this.getLayerContents(2), this.getLayer(1).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + this._setLayerTexture(this.getLayerContents(3), this.getLayer(2).getTexture(), [filterShaders[0], filterShaders[1], filterShaders[2], filterShaders[3]]); + } + + _setLayerTexture(element, texture, steps) { + if (!steps.length) { + element.texture = texture; + } else { + const step = steps.pop(); + const child = element.stage.c({rtt: true, shader: step}); + + // Recurse. + this._setLayerTexture(child, texture, steps); + + element.childList.add(child); + } + return element; + } + + get content() { + return this.sel('Textwrap.Content'); + } + + set content(v) { + this.sel('Textwrap.Content').patch(v); + } + + set padding(v) { + this._paddingX = v; + this._paddingY = v; + this._updateBlurSize(); + } + + set paddingX(v) { + this._paddingX = v; + this._updateBlurSize(); + } + + set paddingY(v) { + this._paddingY = v; + this._updateBlurSize(); + } + + getLayer(i) { + return this._layers.sel("L" + i); + } + + getLayerContents(i) { + return this.getLayer(i).sel("Content"); + } + + _onResize() { + this._updateBlurSize(); + } + + _updateBlurSize() { + let w = this.renderWidth; + let h = this.renderHeight; + + let paddingX = this._paddingX; + let paddingY = this._paddingY; + + let fw = w + paddingX * 2; + let fh = h + paddingY * 2; + this._textwrap.w = fw; + this._wrapper.x = paddingX; + this.getLayer(0).w = this.getLayerContents(0).w = fw / 2; + this.getLayer(1).w = this.getLayerContents(1).w = fw / 4; + this.getLayer(2).w = this.getLayerContents(2).w = fw / 8; + this.getLayer(3).w = this.getLayerContents(3).w = fw / 16; + this._textwrap.x = -paddingX; + + this._textwrap.h = fh; + this._wrapper.y = paddingY; + this.getLayer(0).h = this.getLayerContents(0).h = fh / 2; + this.getLayer(1).h = this.getLayerContents(1).h = fh / 4; + this.getLayer(2).h = this.getLayerContents(2).h = fh / 8; + this.getLayer(3).h = this.getLayerContents(3).h = fh / 16; + this._textwrap.y = -paddingY; + + this.w = w; + this.h = h; + } + + /** + * Sets the amount of blur. A value between 0 and 4. Goes up exponentially for blur. + * Best results for non-fractional values. + * @param v; + */ + set amount(v) { + this._amount = v; + this._update(); + } + + get amount() { + return this._amount; + } + + _update() { + let v = Math.min(4, Math.max(0, this._amount)); + if (v > 0) { + this.getLayer(0).visible = (v > 0); + this.getLayer(1).visible = (v > 1); + this.getLayer(2).visible = (v > 2); + this.getLayer(3).visible = (v > 3); + } + } + + set shader(s) { + super.shader = s; + if (!this.renderToTexture) { + console.warn("Please enable renderToTexture to use with a shader."); + } + } + + _firstActive() { + this._build(); + } + +} + +class BloomBaseShader extends DefaultShader { +} + +BloomBaseShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + float m = max(max(color.r, color.g), color.b); + float c = max(0.0, (m - 0.80)) * 5.0; + color = color * c; + gl_FragColor = color; + } +`; + +class SmoothScaleComponent extends Component { + + static _template() { + return { + ContentWrap: {renderOffscreen: true, forceZIndexContext: true, onAfterUpdate: SmoothScaleComponent._updateDimensions, + Content: {} + }, + Scale: {visible: false} + } + } + + constructor(stage) { + super(stage); + + this._smoothScale = 1; + this._iterations = 0; + } + + get content() { + return this.tag('Content'); + } + + set content(v) { + this.tag('Content').patch(v, true); + } + + get smoothScale() { + return this._smoothScale; + } + + set smoothScale(v) { + if (this._smoothScale !== v) { + let its = 0; + while(v < 0.5 && its < 12) { + its++; + v = v * 2; + } + + this.scale = v; + this._setIterations(its); + + this._smoothScale = v; + } + } + + _setIterations(its) { + if (this._iterations !== its) { + const scalers = this.sel("Scale").childList; + const content = this.sel("ContentWrap"); + while (scalers.length < its) { + const first = scalers.length === 0; + const texture = (first ? content.getTexture() : scalers.last.getTexture()); + scalers.a({rtt: true, renderOffscreen: true, texture: texture}); + } + + SmoothScaleComponent._updateDimensions(this.tag("ContentWrap"), true); + + const useScalers = (its > 0); + this.patch({ + ContentWrap: {renderToTexture: useScalers}, + Scale: {visible: useScalers} + }); + + for (let i = 0, n = scalers.length; i < n; i++) { + scalers.getAt(i).patch({ + visible: i < its, + renderOffscreen: i !== its - 1 + }); + } + this._iterations = its; + } + } + + static _updateDimensions(contentWrap, force) { + const content = contentWrap.children[0]; + let w = content.renderWidth; + let h = content.renderHeight; + if (w !== contentWrap.w || h !== contentWrap.h || force) { + contentWrap.w = w; + contentWrap.h = h; + + const scalers = contentWrap.parent.tag("Scale").children; + for (let i = 0, n = scalers.length; i < n; i++) { + w = w * 0.5; + h = h * 0.5; + scalers[i].w = w; + scalers[i].h = h; + } + } + } + + get _signalProxy() { + return true; + } + +} + +class BorderComponent extends Component { + + static _template() { + return { + Content: {}, + Borders: { + Top: {rect: true, visible: false, mountY: 1}, + Right: {rect: true, visible: false}, + Bottom: {rect: true, visible: false}, + Left: {rect: true, visible: false, mountX: 1} + } + }; + } + + get _signalProxy() { + return true; + } + + constructor(stage) { + super(stage); + + this._borderTop = this.tag("Top"); + this._borderRight = this.tag("Right"); + this._borderBottom = this.tag("Bottom"); + this._borderLeft = this.tag("Left"); + + this.onAfterUpdate = function (element) { + const content = element.childList.first; + let w = element.core.w || content.renderWidth; + let h = element.core.h || content.renderHeight; + element._borderTop.w = w; + element._borderBottom.y = h; + element._borderBottom.w = w; + element._borderLeft.h = h + element._borderTop.h + element._borderBottom.h; + element._borderLeft.y = -element._borderTop.h; + element._borderRight.x = w; + element._borderRight.h = h + element._borderTop.h + element._borderBottom.h; + element._borderRight.y = -element._borderTop.h; + }; + + this.borderWidth = 1; + } + + get content() { + return this.sel('Content'); + } + + set content(v) { + this.sel('Content').patch(v, true); + } + + get borderWidth() { + return this.borderWidthTop; + } + + get borderWidthTop() { + return this._borderTop.h; + } + + get borderWidthRight() { + return this._borderRight.w; + } + + get borderWidthBottom() { + return this._borderBottom.h; + } + + get borderWidthLeft() { + return this._borderLeft.w; + } + + set borderWidth(v) { + this.borderWidthTop = v; + this.borderWidthRight = v; + this.borderWidthBottom = v; + this.borderWidthLeft = v; + } + + set borderWidthTop(v) { + this._borderTop.h = v; + this._borderTop.visible = (v > 0); + } + + set borderWidthRight(v) { + this._borderRight.w = v; + this._borderRight.visible = (v > 0); + } + + set borderWidthBottom(v) { + this._borderBottom.h = v; + this._borderBottom.visible = (v > 0); + } + + set borderWidthLeft(v) { + this._borderLeft.w = v; + this._borderLeft.visible = (v > 0); + } + + get colorBorder() { + return this.colorBorderTop; + } + + get colorBorderTop() { + return this._borderTop.color; + } + + get colorBorderRight() { + return this._borderRight.color; + } + + get colorBorderBottom() { + return this._borderBottom.color; + } + + get colorBorderLeft() { + return this._borderLeft.color; + } + + set colorBorder(v) { + this.colorBorderTop = v; + this.colorBorderRight = v; + this.colorBorderBottom = v; + this.colorBorderLeft = v; + } + + set colorBorderTop(v) { + this._borderTop.color = v; + } + + set colorBorderRight(v) { + this._borderRight.color = v; + } + + set colorBorderBottom(v) { + this._borderBottom.color = v; + } + + set colorBorderLeft(v) { + this._borderLeft.color = v; + } + + get borderTop() { + return this._borderTop; + } + + set borderTop(settings) { + this.borderTop.patch(settings); + } + + get borderRight() { + return this._borderRight; + } + + set borderRight(settings) { + this.borderRight.patch(settings); + } + + get borderBottom() { + return this._borderBottom; + } + + set borderBottom(settings) { + this.borderBottom.patch(settings); + } + + get borderLeft() { + return this._borderLeft; + } + + set borderLeft(settings) { + this.borderLeft.patch(settings); + } + + set borders(settings) { + this.borderTop = settings; + this.borderLeft = settings; + this.borderBottom = settings; + this.borderRight = settings; + } + +} + +class GrayscaleShader extends DefaultShader$1 { + + constructor(context) { + super(context); + this._amount = 1; + } + + static getWebGL() { + return GrayscaleShader$1; + } + + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + _beforeDrawEl({target}) { + target.ctx.filter = "grayscale(" + this._amount + ")"; + } + + _afterDrawEl({target}) { + target.ctx.filter = "none"; + } + +} + +class GrayscaleShader$1 extends DefaultShader { + + constructor(context) { + super(context); + this._amount = 1; + } + + static getC2d() { + return GrayscaleShader; + } + + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("amount", this._amount, this.gl.uniform1f); + } + +} + +GrayscaleShader$1.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float amount; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord) * vColor; + float grayness = 0.2 * color.r + 0.6 * color.g + 0.2 * color.b; + gl_FragColor = vec4(amount * vec3(grayness, grayness, grayness) + (1.0 - amount) * color.rgb, color.a); + } +`; + +/** + * This shader can be used to fix a problem that is known as 'gradient banding'. + */ +class DitheringShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._noiseTexture = new NoiseTexture(ctx.stage); + + this._graining = 1/256; + + this._random = false; + } + + set graining(v) { + this._graining = v; + this.redraw(); + } + + set random(v) { + this._random = v; + this.redraw(); + } + + setExtraAttribsInBuffer(operation) { + // Make sure that the noise texture is uploaded to the GPU. + this._noiseTexture.load(); + + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + + for (let i = 0; i < length; i++) { + + // Calculate noise texture coordinates so that it spans the full element. + let brx = operation.getElementWidth(i) / this._noiseTexture.getRenderWidth(); + let bry = operation.getElementHeight(i) / this._noiseTexture.getRenderHeight(); + + let ulx = 0; + let uly = 0; + if (this._random) { + ulx = Math.random(); + uly = Math.random(); + + brx += ulx; + bry += uly; + + if (Math.random() < 0.5) { + // Flip for more randomness. + const t = ulx; + ulx = brx; + brx = t; + } + + if (Math.random() < 0.5) { + // Flip for more randomness. + const t = uly; + uly = bry; + bry = t; + } + } + + // Specify all corner points. + floats[offset] = ulx; + floats[offset + 1] = uly; + + floats[offset + 2] = brx; + floats[offset + 3] = uly; + + floats[offset + 4] = brx; + floats[offset + 5] = bry; + + floats[offset + 6] = ulx; + floats[offset + 7] = bry; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aNoiseTextureCoord"), 2, gl.FLOAT, false, 8, this.getVertexAttribPointerOffset(operation)); + + let glTexture = this._noiseTexture.source.nativeTexture; + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.activeTexture(gl.TEXTURE0); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("uNoiseSampler", 1, this.gl.uniform1i); + this._setUniform("graining", 2 * this._graining, this.gl.uniform1f); + } + + enableAttribs() { + super.enableAttribs(); + let gl = this.gl; + gl.enableVertexAttribArray(this._attrib("aNoiseTextureCoord")); + } + + disableAttribs() { + super.disableAttribs(); + let gl = this.gl; + gl.disableVertexAttribArray(this._attrib("aNoiseTextureCoord")); + } + + useDefault() { + return this._graining === 0; + } + + afterDraw(operation) { + if (this._random) { + this.redraw(); + } + } + +} + +DitheringShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec2 aNoiseTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec2 vNoiseTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vNoiseTextureCoord = aNoiseTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +DitheringShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec2 vNoiseTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform sampler2D uNoiseSampler; + uniform float graining; + void main(void){ + vec4 noise = texture2D(uNoiseSampler, vNoiseTextureCoord); + vec4 color = texture2D(uSampler, vTextureCoord); + gl_FragColor = (color * vColor) + graining * (noise.r - 0.5); + } +`; + +class CircularPushShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._inputValue = 0; + + this._maxDerivative = 0.01; + + this._normalizedValue = 0; + + // The offset between buckets. A value between 0 and 1. + this._offset = 0; + + this._amount = 0.1; + + this._aspectRatio = 1; + + this._offsetX = 0; + + this._offsetY = 0; + + this.buckets = 100; + } + + get aspectRatio() { + return this._aspectRatio; + } + + set aspectRatio(v) { + this._aspectRatio = v; + this.redraw(); + } + + get offsetX() { + return this._offsetX; + } + + set offsetX(v) { + this._offsetX = v; + this.redraw(); + } + + get offsetY() { + return this._offsetY; + } + + set offsetY(v) { + this._offsetY = v; + this.redraw(); + } + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + set inputValue(v) { + this._inputValue = v; + } + + get inputValue() { + return this._inputValue; + } + + set maxDerivative(v) { + this._maxDerivative = v; + } + + get maxDerivative() { + return this._maxDerivative; + } + + set buckets(v) { + if (v > 100) { + console.warn("CircularPushShader: supports max 100 buckets"); + v = 100; + } + + // This should be set before starting. + this._buckets = v; + + // Init values array in the correct length. + this._values = new Uint8Array(this._getValues(v)); + + this.redraw(); + } + + get buckets() { + return this._buckets; + } + + _getValues(n) { + const v = []; + for (let i = 0; i < n; i++) { + v.push(this._inputValue); + } + return v; + } + + /** + * Progresses the shader with the specified (fractional) number of buckets. + * @param {number} o; + * A number from 0 to 1 (1 = all buckets). + */ + progress(o) { + this._offset += o * this._buckets; + const full = Math.floor(this._offset); + this._offset -= full; + this._shiftBuckets(full); + this.redraw(); + } + + _shiftBuckets(n) { + for (let i = this._buckets - 1; i >= 0; i--) { + const targetIndex = i - n; + if (targetIndex < 0) { + this._normalizedValue = Math.min(this._normalizedValue + this._maxDerivative, Math.max(this._normalizedValue - this._maxDerivative, this._inputValue)); + this._values[i] = 255 * this._normalizedValue; + } else { + this._values[i] = this._values[targetIndex]; + } + } + } + + set offset(v) { + this._offset = v; + this.redraw(); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("aspectRatio", this._aspectRatio, this.gl.uniform1f); + this._setUniform("offsetX", this._offsetX, this.gl.uniform1f); + this._setUniform("offsetY", this._offsetY, this.gl.uniform1f); + this._setUniform("amount", this._amount, this.gl.uniform1f); + this._setUniform("offset", this._offset, this.gl.uniform1f); + this._setUniform("buckets", this._buckets, this.gl.uniform1f); + this._setUniform("uValueSampler", 1, this.gl.uniform1i); + } + + useDefault() { + return this._amount === 0; + } + + beforeDraw(operation) { + const gl = this.gl; + gl.activeTexture(gl.TEXTURE1); + if (!this._valuesTexture) { + this._valuesTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._valuesTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + if (Utils.isNode) { + gl.pixelStorei(gl.UNPACK_FLIP_BLUE_RED, false); + } + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + } else { + gl.bindTexture(gl.TEXTURE_2D, this._valuesTexture); + } + + // Upload new values. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this._buckets, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this._values); + gl.activeTexture(gl.TEXTURE0); + } + + cleanup() { + if (this._valuesTexture) { + this.gl.deleteTexture(this._valuesTexture); + } + } + + +} + +CircularPushShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + uniform float offsetX; + uniform float offsetY; + uniform float aspectRatio; + varying vec2 vTextureCoord; + varying vec2 vPos; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vPos = vTextureCoord * 2.0 - 1.0; + vPos.y = vPos.y * aspectRatio; + vPos.y = vPos.y + offsetY; + vPos.x = vPos.x + offsetX; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +CircularPushShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vPos; + uniform float amount; + uniform float offset; + uniform float values[100]; + uniform float buckets; + uniform sampler2D uSampler; + uniform sampler2D uValueSampler; + void main(void){ + float l = length(vPos); + float m = (l * buckets * 0.678 - offset) / buckets; + float f = texture2D(uValueSampler, vec2(m, 0.0)).a * amount; + vec2 unit = vPos / l; + gl_FragColor = texture2D(uSampler, vTextureCoord - f * unit) * vColor; + } +`; + +class InversionShader extends DefaultShader { + + constructor(context) { + super(context); + this._amount = 1; + } + + set amount(v) { + this._amount = v; + this.redraw(); + } + + get amount() { + return this._amount; + } + + useDefault() { + return this._amount === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + this._setUniform("amount", this._amount, this.gl.uniform1f); + } + +} + +InversionShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float amount; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord); + color.rgb = color.rgb * (1.0 - amount) + amount * (1.0 * color.a - color.rgb); + gl_FragColor = color * vColor; + } +`; + +class OutlineShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + this._width = 5; + this._col = 0xFFFFFFFF; + this._color = [1,1,1,1]; + } + + set width(v) { + this._width = v; + this.redraw(); + } + + get color() { + return this._col; + } + + set color(v) { + if (this._col !== v) { + const col = StageUtils.getRgbaComponentsNormalized(v); + col[0] = col[0] * col[3]; + col[1] = col[1] * col[3]; + col[2] = col[2] * col[3]; + + this._color = col; + + this.redraw(); + + this._col = v; + } + } + + useDefault() { + return (this._width === 0 || this._col[3] === 0); + } + + setupUniforms(operation) { + super.setupUniforms(operation); + let gl = this.gl; + this._setUniform("color", new Float32Array(this._color), gl.uniform4fv); + } + + enableAttribs() { + super.enableAttribs(); + this.gl.enableVertexAttribArray(this._attrib("aCorner")); + } + + disableAttribs() { + super.disableAttribs(); + this.gl.disableVertexAttribArray(this._attrib("aCorner")); + } + + setExtraAttribsInBuffer(operation) { + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + + for (let i = 0; i < length; i++) { + + const elementCore = operation.getElementCore(i); + + // We are setting attributes such that if the value is < 0 or > 1, a border should be drawn. + const ddw = this._width / elementCore.w; + const dw = ddw / (1 - 2 * ddw); + const ddh = this._width / elementCore.h; + const dh = ddh / (1 - 2 * ddh); + + // Specify all corner points. + floats[offset] = -dw; + floats[offset + 1] = -dh; + + floats[offset + 2] = 1 + dw; + floats[offset + 3] = -dh; + + floats[offset + 4] = 1 + dw; + floats[offset + 5] = 1 + dh; + + floats[offset + 6] = -dw; + floats[offset + 7] = 1 + dh; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aCorner"), 2, gl.FLOAT, false, 8, this.getVertexAttribPointerOffset(operation)); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + +} + +OutlineShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + attribute vec2 aCorner; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec2 vCorner; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vCorner = aCorner; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +OutlineShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vCorner; + uniform vec4 color; + uniform sampler2D uSampler; + void main(void){ + vec2 m = min(vCorner, 1.0 - vCorner); + float value = step(0.0, min(m.x, m.y)); + gl_FragColor = mix(color, texture2D(uSampler, vTextureCoord) * vColor, value); + } +`; + +/** + * @see https://github.com/pixijs/pixi-filters/tree/master/filters/pixelate/src + */ +class PixelateShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._size = new Float32Array([4, 4]); + } + + get x() { + return this._size[0]; + } + + set x(v) { + this._size[0] = v; + this.redraw(); + } + + get y() { + return this._size[1]; + } + + set y(v) { + this._size[1] = v; + this.redraw(); + } + + get size() { + return this._size[0]; + } + + set size(v) { + this._size[0] = v; + this._size[1] = v; + this.redraw(); + } + + useDefault() { + return ((this._size[0] === 0) && (this._size[1] === 0)); + } + + static getWebGLImpl() { + return WebGLPixelateShaderImpl; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + let gl = this.gl; + this._setUniform("size", new Float32Array(this._size), gl.uniform2fv); + } + + getExtraAttribBytesPerVertex() { + return 8; + } + + enableAttribs() { + super.enableAttribs(); + this.gl.enableVertexAttribArray(this._attrib("aTextureRes")); + } + + disableAttribs() { + super.disableAttribs(); + this.gl.disableVertexAttribArray(this._attrib("aTextureRes")); + } + + setExtraAttribsInBuffer(operation) { + let offset = operation.extraAttribsDataByteOffset / 4; + let floats = operation.quads.floats; + + let length = operation.length; + for (let i = 0; i < length; i++) { + let w = operation.quads.getTextureWidth(operation.index + i); + let h = operation.quads.getTextureHeight(operation.index + i); + + floats[offset] = w; + floats[offset + 1] = h; + floats[offset + 2] = w; + floats[offset + 3] = h; + floats[offset + 4] = w; + floats[offset + 5] = h; + floats[offset + 6] = w; + floats[offset + 7] = h; + + offset += 8; + } + } + + beforeDraw(operation) { + let gl = this.gl; + gl.vertexAttribPointer(this._attrib("aTextureRes"), 2, gl.FLOAT, false, this.getExtraAttribBytesPerVertex(), this.getVertexAttribPointerOffset(operation)); + } +} + +PixelateShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + attribute vec2 aTextureRes; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vTextureRes; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + vTextureRes = aTextureRes; + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +PixelateShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 vTextureRes; + + uniform vec2 size; + uniform sampler2D uSampler; + + vec2 mapCoord( vec2 coord ) + { + coord *= vTextureRes.xy; + return coord; + } + + vec2 unmapCoord( vec2 coord ) + { + coord /= vTextureRes.xy; + return coord; + } + + vec2 pixelate(vec2 coord, vec2 size) + { + return floor( coord / size ) * size; + } + + void main(void) + { + vec2 coord = mapCoord(vTextureCoord); + coord = pixelate(coord, size); + coord = unmapCoord(coord); + gl_FragColor = texture2D(uSampler, coord) * vColor; + } +`; + +class RadialFilterShader extends DefaultShader { + constructor(context) { + super(context); + this._radius = 0; + this._cutoff = 1; + } + + set radius(v) { + this._radius = v; + this.redraw(); + } + + get radius() { + return this._radius; + } + + set cutoff(v) { + this._cutoff = v; + this.redraw(); + } + + get cutoff() { + return this._cutoff; + } + + useDefault() { + return this._radius === 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + // We substract half a pixel to get a better cutoff effect. + this._setUniform("radius", 2 * (this._radius - 0.5) / operation.getRenderWidth(), this.gl.uniform1f); + this._setUniform("cutoff", 0.5 * operation.getRenderWidth() / this._cutoff, this.gl.uniform1f); + } + +} + +RadialFilterShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 pos; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + pos = gl_Position.xy; + } +`; + +RadialFilterShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec2 pos; + varying vec4 vColor; + uniform sampler2D uSampler; + uniform float radius; + uniform float cutoff; + void main(void){ + vec4 color = texture2D(uSampler, vTextureCoord); + float f = max(0.0, min(1.0, 1.0 - (length(pos) - radius) * cutoff)); + gl_FragColor = texture2D(uSampler, vTextureCoord) * vColor * f; + } +`; + +class RadialGradientShader extends DefaultShader { + + constructor(context) { + super(context); + + this._x = 0; + this._y = 0; + + this.color = 0xFFFF0000; + + this._radiusX = 100; + this._radiusY = 100; + } + + set x(v) { + this._x = v; + this.redraw(); + } + + set y(v) { + this._y = v; + this.redraw(); + } + + set radiusX(v) { + this._radiusX = v; + this.redraw(); + } + + get radiusX() { + return this._radiusX; + } + + set radiusY(v) { + this._radiusY = v; + this.redraw(); + } + + get radiusY() { + return this._radiusY; + } + + set radius(v) { + this.radiusX = v; + this.radiusY = v; + } + + get color() { + return this._color; + } + + set color(v) { + if (this._color !== v) { + const col = StageUtils.getRgbaComponentsNormalized(v); + col[0] = col[0] * col[3]; + col[1] = col[1] * col[3]; + col[2] = col[2] * col[3]; + + this._rawColor = new Float32Array(col); + + this.redraw(); + + this._color = v; + } + } + + setupUniforms(operation) { + super.setupUniforms(operation); + // We substract half a pixel to get a better cutoff effect. + const rtc = operation.getNormalRenderTextureCoords(this._x, this._y); + this._setUniform("center", new Float32Array(rtc), this.gl.uniform2fv); + + this._setUniform("radius", 2 * this._radiusX / operation.getRenderWidth(), this.gl.uniform1f); + + + // Radial gradient shader is expected to be used on a single element. That element's alpha is used. + this._setUniform("alpha", operation.getElementCore(0).renderContext.alpha, this.gl.uniform1f); + + this._setUniform("color", this._rawColor, this.gl.uniform4fv); + this._setUniform("aspectRatio", (this._radiusX/this._radiusY) * operation.getRenderHeight()/operation.getRenderWidth(), this.gl.uniform1f); + } + +} + +RadialGradientShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + uniform vec2 center; + uniform float aspectRatio; + varying vec2 pos; + varying vec2 vTextureCoord; + varying vec4 vColor; + void main(void){ + gl_Position = vec4(aVertexPosition.x * projection.x - 1.0, aVertexPosition.y * -abs(projection.y) + 1.0, 0.0, 1.0); + vTextureCoord = aTextureCoord; + vColor = aColor; + gl_Position.y = -sign(projection.y) * gl_Position.y; + pos = gl_Position.xy - center; + pos.y = pos.y * aspectRatio; + } +`; + +RadialGradientShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec2 pos; + uniform sampler2D uSampler; + uniform float radius; + uniform vec4 color; + uniform float alpha; + void main(void){ + float dist = length(pos); + gl_FragColor = mix(color * alpha, texture2D(uSampler, vTextureCoord) * vColor, min(1.0, dist / radius)); + } +`; + +class Light3dShader extends DefaultShader { + + constructor(ctx) { + super(ctx); + + this._strength = 0.5; + this._ambient = 0.5; + this._fudge = 0.4; + + this._rx = 0; + this._ry = 0; + + this._z = 0; + this._pivotX = NaN; + this._pivotY = NaN; + this._pivotZ = 0; + + this._lightY = 0; + this._lightZ = 0; + } + + setupUniforms(operation) { + super.setupUniforms(operation); + + let vr = operation.shaderOwner; + let element = vr.element; + + let pivotX = isNaN(this._pivotX) ? element.pivotX * vr.w : this._pivotX; + let pivotY = isNaN(this._pivotY) ? element.pivotY * vr.h : this._pivotY; + let coords = vr.getRenderTextureCoords(pivotX, pivotY); + + // Counter normal rotation. + + let rz = -Math.atan2(vr._renderContext.tc, vr._renderContext.ta); + + let gl = this.gl; + this._setUniform("pivot", new Float32Array([coords[0], coords[1], this._pivotZ]), gl.uniform3fv); + this._setUniform("rot", new Float32Array([this._rx, this._ry, rz]), gl.uniform3fv); + + this._setUniform("z", this._z, gl.uniform1f); + this._setUniform("lightY", this.lightY, gl.uniform1f); + this._setUniform("lightZ", this.lightZ, gl.uniform1f); + this._setUniform("strength", this._strength, gl.uniform1f); + this._setUniform("ambient", this._ambient, gl.uniform1f); + this._setUniform("fudge", this._fudge, gl.uniform1f); + } + + set strength(v) { + this._strength = v; + this.redraw(); + } + + get strength() { + return this._strength; + } + + set ambient(v) { + this._ambient = v; + this.redraw(); + } + + get ambient() { + return this._ambient; + } + + set fudge(v) { + this._fudge = v; + this.redraw(); + } + + get fudge() { + return this._fudge; + } + + get rx() { + return this._rx; + } + + set rx(v) { + this._rx = v; + this.redraw(); + } + + get ry() { + return this._ry; + } + + set ry(v) { + this._ry = v; + this.redraw(); + } + + get z() { + return this._z; + } + + set z(v) { + this._z = v; + this.redraw(); + } + + get pivotX() { + return this._pivotX; + } + + set pivotX(v) { + this._pivotX = v + 1; + this.redraw(); + } + + get pivotY() { + return this._pivotY; + } + + set pivotY(v) { + this._pivotY = v + 1; + this.redraw(); + } + + get lightY() { + return this._lightY; + } + + set lightY(v) { + this._lightY = v; + this.redraw(); + } + + get pivotZ() { + return this._pivotZ; + } + + set pivotZ(v) { + this._pivotZ = v; + this.redraw(); + } + + get lightZ() { + return this._lightZ; + } + + set lightZ(v) { + this._lightZ = v; + this.redraw(); + } + + useDefault() { + return (this._rx === 0 && this._ry === 0 && this._z === 0 && this._strength === 0 && this._ambient === 1); + } + +} + +Light3dShader.vertexShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + attribute vec2 aVertexPosition; + attribute vec2 aTextureCoord; + attribute vec4 aColor; + uniform vec2 projection; + varying vec2 vTextureCoord; + varying vec4 vColor; + + uniform float fudge; + uniform float strength; + uniform float ambient; + uniform float z; + uniform float lightY; + uniform float lightZ; + uniform vec3 pivot; + uniform vec3 rot; + varying vec3 pos; + + void main(void) { + pos = vec3(aVertexPosition.xy, z); + + pos -= pivot; + + // Undo XY rotation + mat2 iRotXy = mat2( cos(rot.z), sin(rot.z), + -sin(rot.z), cos(rot.z)); + pos.xy = iRotXy * pos.xy; + + // Perform 3d rotations + gl_Position.x = cos(rot.x) * pos.x - sin(rot.x) * pos.z; + gl_Position.y = pos.y; + gl_Position.z = sin(rot.x) * pos.x + cos(rot.x) * pos.z; + + pos.x = gl_Position.x; + pos.y = cos(rot.y) * gl_Position.y - sin(rot.y) * gl_Position.z; + pos.z = sin(rot.y) * gl_Position.y + cos(rot.y) * gl_Position.z; + + // Redo XY rotation + iRotXy[0][1] = -iRotXy[0][1]; + iRotXy[1][0] = -iRotXy[1][0]; + pos.xy = iRotXy * pos.xy; + + // Undo translate to pivot position + pos.xyz += pivot; + + pos = vec3(pos.x * projection.x - 1.0, pos.y * -abs(projection.y) + 1.0, pos.z * projection.x); + + // Set depth perspective + float perspective = 1.0 + fudge * pos.z; + + pos.z += lightZ * projection.x; + + // Map coords to gl coordinate space. + // Set z to 0 because we don't want to perform z-clipping + gl_Position = vec4(pos.xy, 0.0, perspective); + + // Correct light source position. + pos.y += lightY * abs(projection.y); + + vTextureCoord = aTextureCoord; + vColor = aColor; + + gl_Position.y = -sign(projection.y) * gl_Position.y; + } +`; + +Light3dShader.fragmentShaderSource = ` + #ifdef GL_ES + precision lowp float; + #endif + varying vec2 vTextureCoord; + varying vec4 vColor; + varying vec3 pos; + uniform sampler2D uSampler; + uniform float ambient; + uniform float strength; + void main(void){ + vec4 rgba = texture2D(uSampler, vTextureCoord); + float d = length(pos); + float n = 1.0 / max(0.1, d); + rgba.rgb = rgba.rgb * (strength * n + ambient); + gl_FragColor = rgba * vColor; + } +`; + +const lightning = { + Application, + Component, + Base, + Utils, + StageUtils, + Element, + Tools, + Stage, + ElementCore, + ElementTexturizer, + Texture, + EventEmitter, + shaders: { + Grayscale: GrayscaleShader$1, + BoxBlur: BoxBlurShader, + Dithering: DitheringShader, + CircularPush: CircularPushShader, + Inversion: InversionShader, + LinearBlur: LinearBlurShader, + Outline: OutlineShader, + Pixelate: PixelateShader, + RadialFilter: RadialFilterShader, + RadialGradient: RadialGradientShader, + Light3d: Light3dShader, + WebGLShader, + WebGLDefaultShader: DefaultShader, + C2dShader, + C2dDefaultShader: DefaultShader$1, + c2d: { + Grayscale: GrayscaleShader, + Blur: BlurShader + } + }, + textures: { + RectangleTexture, + NoiseTexture, + TextTexture, + ImageTexture, + HtmlTexture, + StaticTexture, + StaticCanvasTexture, + SourceTexture + }, + components: { + FastBlurComponent, + BloomComponent, + SmoothScaleComponent, + BorderComponent, + ListComponent + }, + tools: { + ObjMerger, + ObjectListProxy, + ObjectListWrapper + } +}; + +if (Utils.isWeb) { + window.lng = lightning; +} + +class SparkMediaplayer extends lightning.Component { + + _construct(){ + this._skipRenderToTexture = false; + } + + static _supportedEvents() + { + return ['onProgressUpdate', 'onEndOfStream']; + } + + static _template() { + return { + Video: { + VideoWrap: { + VideoTexture: { + visible: false, + pivot: 0.5, + texture: {type: lightning.textures.StaticTexture, options: {}} + } + } + } + }; + } + + set skipRenderToTexture (v) { + this._skipRenderToTexture = v; + } + + set textureMode(v) { + return this._textureMode = v; + } + + get textureMode() { + return this._textureMode; + } + + get videoView() { + return this.tag("Video"); + } + + _init() { + + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + + this.videoEl = sparkscene.create({ + t: "video", + id: "video-player", + autoPlay: "false", + proxy:proxyServer + }); + + var _this = this; + sparkscene.on('onClose' , function(e) { + _this.close(); + }); + + this.eventHandlers = []; + } + + _registerListeners() { + SparkMediaplayer._supportedEvents().forEach(event => { + const handler = (e) => { + this.fire(event, {videoElement: this.videoEl, event: e}); + }; + this.eventHandlers.push(handler); + this.videoEl.on(event, handler); + }); + } + + _deregisterListeners() { + SparkMediaplayer._supportedEvents().forEach((event, index) => { + this.videoEl.delListener(event, this.eventHandlers[index]); + }); + this.eventHandlers = []; + } + + _attach() { + this._registerListeners(); + } + + _detach() { + this._deregisterListeners(); + } + + updateSettings(settings = {}) { + // The Component that 'consumes' the media player. + this._consumer = settings.consumer; + + if (this._consumer && this._consumer.getMediaplayerSettings) { + // Allow consumer to add settings. + settings = Object.assign(settings, this._consumer.getMediaplayerSettings()); + } + + if (!lightning.Utils.equalValues(this._stream, settings.stream)) { + if (settings.stream && settings.stream.keySystem) { + navigator.requestMediaKeySystemAccess(settings.stream.keySystem.id, settings.stream.keySystem.config).then((keySystemAccess) => { + return keySystemAccess.createMediaKeys(); + }).then((createdMediaKeys) => { + return this.videoEl.setMediaKeys(createdMediaKeys); + }).then(() => { + if (settings.stream && settings.stream.src) + this.open(settings.stream.src); + }).catch(() => { + console.error('Failed to set up MediaKeys'); + }); + } else if (settings.stream && settings.stream.src) { + this.open(settings.stream.src); + this._setHide(settings.hide); + this._setVideoArea(settings.videoPos); + this.doPlay(); + } else { + this.close(); + } + this._stream = settings.stream; + } + } + + _setHide(hide) { + this.videoEl.a = hide ? 0 : 1; + } + + open(url) { + console.log('Playing stream', url); + if (this.application.noVideo) { + console.log('noVideo option set, so ignoring: ' + url); + return; + } + if (this.videoEl.url === url) return this.reload(); + this.videoEl.url = url; + } + + close() { + this.videoEl.stop(); + this._clearSrc(); + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + this.videoEl.play(); + } + + doPause() { + this.videoEl.pause(); + } + + reload() { + var url = this.videoEl.url; + this.close(); + this.videoEl.url = url; + } + + getPosition() { + return Promise.resolve(this.videoEl.position); + } + + setPosition(pos) { + this.videoEl.position = pos; + } + + getDuration() { + return Promise.resolve(this.videoEl.duration); + } + + seek(time, absolute = false) { + if(absolute) { + this.videoEl.position = time; + } + else { + this.videoEl.setPositionRelative(time); + } + } + + _setVideoArea(videoPos) { + if (lightning.Utils.equalValues(this._videoPos, videoPos)) { + return; + } + + this._videoPos = videoPos; + + if (this.textureMode) { + this.videoTextureView.patch({ + smooth: { + x: videoPos[0], + y: videoPos[1], + w: videoPos[2] - videoPos[0], + h: videoPos[3] - videoPos[1] + } + }); + } else { + const precision = this.stage.getRenderPrecision(); + this.videoEl.x = Math.round(videoPos[0] * precision) + 'px'; + this.videoEl.y = Math.round(videoPos[1] * precision) + 'px'; + this.videoEl.w = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px'; + this.videoEl.h = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px'; + } + } + + _fireConsumer(event, args) { + if (this._consumer) { + this._consumer.fire(event, args); + } + } + + _equalInitData(buf1, buf2) { + if (!buf1 || !buf2) return false; + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Int8Array(buf1); + const dv2 = new Int8Array(buf2); + for (let i = 0 ; i != buf1.byteLength ; i++) + if (dv1[i] != dv2[i]) return false; + return true; + } + + error(args) { + this._fireConsumer('$mediaplayerError', args); + this._setState(""); + return ""; + } + + loadeddata(args) { + this._fireConsumer('$mediaplayerLoadedData', args); + } + + play(args) { + this._fireConsumer('$mediaplayerPlay', args); + } + + playing(args) { + this._fireConsumer('$mediaplayerPlaying', args); + this._setState("Playing"); + } + + canplay(args) { + this.videoEl.play(); + this._fireConsumer('$mediaplayerStart', args); + } + + loadstart(args) { + this._fireConsumer('$mediaplayerLoad', args); + } + + seeked(args) { + this._fireConsumer('$mediaplayerSeeked', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + seeking(args) { + this._fireConsumer('$mediaplayerSeeking', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + onEndOfStream(args) { + this._fireConsumer('$mediaplayerEnded', args); + this._setState(""); + } + + onProgressUpdate(args) { + this._fireConsumer('$mediaplayerProgress', { + currentTime: this.videoEl.position, + duration: this.videoEl.duration || 1 + }); + } + + durationchange(args) { + this._fireConsumer('$mediaplayerDurationChange', args); + } + + encrypted(args) { + const video = args.videoElement; + const event = args.event; + // FIXME: Double encrypted events need to be properly filtered by Gstreamer + if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { + this._previousInitData = event.initData; + this._fireConsumer('$mediaplayerEncrypted', args); + } + } + + static _states() { + return [ + class Playing extends this { + $enter() { + this._startUpdatingVideoTexture(); + } + $exit() { + this._stopUpdatingVideoTexture(); + } + pause(args) { + this._fireConsumer('$mediaplayerPause', args); + this._setState("Playing.Paused"); + } + _clearSrc() { + this._fireConsumer('$mediaplayerStop', {}); + this._setState(""); + } + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } +} + +class SparkPlatform { + + init(stage) { + this.stage = stage; + this._looping = false; + this._awaitingLoop = false; + this._sparkCanvas = null; + } + + destroy() { + } + + startLoop() { + this._looping = true; + if (!this._awaitingLoop) { + this.loop(); + } + } + + stopLoop() { + this._looping = false; + } + + loop() { + let self = this; + let lp = function() { + self._awaitingLoop = false; + if (self._looping) { + self.stage.drawFrame(); + if (self.changes) { + // We depend on blit to limit to 60fps. + setImmediate(lp); + } else { + setTimeout(lp, 32); + } + self._awaitingLoop = true; + } + }; + setTimeout(lp, 32); + } + + uploadGlTexture(gl, textureSource, source, options) { + gl.texImage2D(gl.TEXTURE_2D, 0, options.internalFormat, textureSource.w, textureSource.h, 0, options.format, options.type, source); + } + + loadSrcTexture({src}, cb) { + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + let imageResource = sparkscene.create({t:"imageResource", url:src, proxy:proxyServer}); + imageResource.ready.then(function(res) { + let sparkImage = sparkscene.create({t:"image", resource:res}); + const sparkGl = this.stage.gl; + sparkImage.ready.then( function(obj) { + let texture = sparkImage.texture(); + cb(null, {source: sparkGl.createWebGLTexture(texture), w: sparkImage.resource.w, h: sparkImage.resource.h, premultiplyAlpha: false, flipBlueRed: false, imageRef: sparkImage, flipTextureY:true}); + }); + }.bind(this)); + } + + createRoundRect(cb, stage, w, h, radius, strokeWidth, strokeColor, fill, fillColor) { + if (fill === undefined) fill = true; + if (strokeWidth === undefined) strokeWidth = 0; + if (fillColor === undefined) fillColor = 0; + + fillColor = fill ? fillColor : 0; + fillColor = fillColor.toString(16); + let opacity = 1; + if (fillColor.length >= 8) + { + let alpha = fillColor.substring(0,2); + let red = fillColor.substring(2,4); + let green = fillColor.substring(4,6); + let blue = fillColor.substring(6); + fillColor = "#" + red + green + blue; + opacity = "0x"+alpha; + opacity = parseInt(opacity, 16) / 255; + } + let boundW = w+strokeWidth; + let boundH = h+strokeWidth; + let data = "data:image/svg,"+ + `` + + `` + + ''; + + let imageObj = sparkscene.create({ t: "image", url:data}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }); + } + + createShadowRect(cb, stage, w, h, radius, blur, margin) { + let boundW = w + margin * 2; + let boundH = h + margin * 2; + let data = "data:image/svg,"+ + ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + let imageObj = sparkscene.create({ t: "image", url:data}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }); + } + + createSvg(cb, stage, url, w, h) { + let proxyServer = ""; + if (sparkQueryParams && sparkQueryParams.sparkProxyServer) { + proxyServer = sparkQueryParams.sparkProxyServer; + } + let imageResource = sparkscene.create({t:"imageResource", url:src, proxy:proxyServer}); + let imageObj = sparkscene.create({ t: "image", resource:imageResource}); + imageObj.ready.then( function(obj) { + let canvas = {}; + canvas.flipTextureY = true; + canvas.internal = imageObj; + canvas.width = w; + canvas.height = h; + imageObj.w = w; + imageObj.h = h; + cb(null, canvas); + }, function(obj) { + let canvas = {}; + canvas.internal = imageObj; + cb(null, canvas); }); + } + + createWebGLContext(w, h) { + let options = {width: w, height: h, title: "WebGL"}; + const windowOptions = this.stage.getOption('window'); + if (windowOptions) { + options = Object.assign(options, windowOptions); + } + let gl = sparkgles2.init(options); + return gl; + } + + getWebGLCanvas() { + return; + } + + getTextureOptionsForDrawingCanvas(canvas) { + let options = {}; + + if (canvas && canvas.internal) + { + options.source = this.stage.gl.createWebGLTexture(canvas.internal.texture()); + options.w = canvas.width; + options.h = canvas.height; + options.imageRef = canvas.internal; + if (canvas.flipTextureY) { + options.flipTextureY = true; + } + } + options.premultiplyAlpha = false; + options.flipBlueRed = false; + return options; + } + + getHrTime() { + let hrTime = process.hrtime(); + return 1e3 * hrTime[0] + (hrTime[1] / 1e6); + } + + getDrawingCanvas() { + let sparkCanvas; + { + this._sparkCanvas = null; + } + if (this._sparkCanvas === null) { + sparkCanvas = {}; + sparkCanvas.internal = sparkscene.create({t: "textCanvas"}); + sparkCanvas.internal.colorMode = "ARGB"; + this._sparkCanvas = sparkCanvas; + this._sparkCanvas.getContext = function() { + return sparkCanvas.internal; + }; + } + return this._sparkCanvas; + } + + nextFrame(changes) { + this.changes = changes; + if (this.stage && this.stage.gl) { + this.stage.gl.scissor(0,0,0,0); + } + //gles2.nextFrame(changes); + } + + registerKeyHandler(keyhandler) { + console.warn("No support for key handling"); + } + + drawText(textTextureRenderer) { + let canvasInternal = textTextureRenderer._canvas.internal; // _canvas.internal is a pxTextCanvas object created in getDrawingCanvas() + let drawPromise = new Promise((resolve, reject) => { + canvasInternal.ready.then( () => { // waiting for the empty scene + canvasInternal.parent = sparkscene.root; + textTextureRenderer.setFontProperties(); + canvasInternal.font.ready.then(() => { // the font might have been coerced + canvasInternal.pixelSize = textTextureRenderer._settings.fontSize * textTextureRenderer.getPrecision(); + // Original Lightining code with some changes begins here + // Changes to the original code are: + // Replaced: `this.` => `textTextureRenderer.` + // Replaced `StageUtils.getRgbaString(color)` => `color` + // Replaced `this._canvas.width` => `canvasInternal.width` and `this._canvas.height` => `canvasInternal.height` after the line: // Add extra margin to prevent issue with clipped text when scaling. + // setFontProperties() calls are commented out as redundant + // Setting canvas label to faciliatate debugging (this is optional and can be removed): + // canvasInternal.label = textTextureRenderer._settings.text.slice(0, 10) + '..'; + let renderInfo = {}; + const precision = textTextureRenderer.getPrecision(); + let paddingLeft = textTextureRenderer._settings.paddingLeft * precision; + let paddingRight = textTextureRenderer._settings.paddingRight * precision; + const fontSize = textTextureRenderer._settings.fontSize * precision; + let offsetY = textTextureRenderer._settings.offsetY === null ? null : (textTextureRenderer._settings.offsetY * precision); + let lineHeight = textTextureRenderer._settings.lineHeight * precision; + const w = textTextureRenderer._settings.w * precision; + const h = textTextureRenderer._settings.h * precision; + let wordWrapWidth = textTextureRenderer._settings.wordWrapWidth * precision; + const cutSx = textTextureRenderer._settings.cutSx * precision; + const cutEx = textTextureRenderer._settings.cutEx * precision; + const cutSy = textTextureRenderer._settings.cutSy * precision; + const cutEy = textTextureRenderer._settings.cutEy * precision; + + canvasInternal.label = textTextureRenderer._settings.text.slice(0, 10) + '..'; // allows to distinguish different canvases by label, useful for debugging + // Set font properties. + // textTextureRenderer.setFontProperties(); + // Total width. + let width = w || (2048 / textTextureRenderer.getPrecision()); + // Inner width. + let innerWidth = width - (paddingLeft); + if (innerWidth < 10) { + width += (10 - innerWidth); + innerWidth += (10 - innerWidth); + } + if (!wordWrapWidth) { + wordWrapWidth = innerWidth; + } + // word wrap + // preserve original text + let linesInfo; + if (textTextureRenderer._settings.wordWrap) { + linesInfo = textTextureRenderer.wrapText(textTextureRenderer._settings.text, wordWrapWidth); + } else { + linesInfo = {l: textTextureRenderer._settings.text.split(/(?:\r\n|\r|\n)/), n: []}; + let n = linesInfo.l.length; + for (let i = 0; i < n - 1; i++) { + linesInfo.n.push(i); + } + } + let lines = linesInfo.l; + if (textTextureRenderer._settings.maxLines && lines.length > textTextureRenderer._settings.maxLines) { + let usedLines = lines.slice(0, textTextureRenderer._settings.maxLines); + let otherLines = null; + if (textTextureRenderer._settings.maxLinesSuffix) { + // Wrap again with max lines suffix enabled. + let w = textTextureRenderer._settings.maxLinesSuffix ? textTextureRenderer._context.measureText(textTextureRenderer._settings.maxLinesSuffix).width : 0; + let al = textTextureRenderer.wrapText(usedLines[usedLines.length - 1], wordWrapWidth - w); + usedLines[usedLines.length - 1] = al.l[0] + textTextureRenderer._settings.maxLinesSuffix; + otherLines = [al.l.length > 1 ? al.l[1] : '']; + } else { + otherLines = ['']; + } + // Re-assemble the remaining text. + let i, n = lines.length; + let j = 0; + let m = linesInfo.n.length; + for (i = textTextureRenderer._settings.maxLines; i < n; i++) { + otherLines[j] += (otherLines[j] ? " " : "") + lines[i]; + if (i + 1 < m && linesInfo.n[i + 1]) { + j++; + } + } + renderInfo.remainingText = otherLines.join("\n"); + renderInfo.moreTextLines = true; + lines = usedLines; + } else { + renderInfo.moreTextLines = false; + renderInfo.remainingText = ""; + } + // calculate text width + let maxLineWidth = 0; + let lineWidths = []; + for (let i = 0; i < lines.length; i++) { + let lineWidth = textTextureRenderer._context.measureText(lines[i]).width; + lineWidths.push(lineWidth); + maxLineWidth = Math.max(maxLineWidth, lineWidth); + } + renderInfo.lineWidths = lineWidths; + if (!w) { + // Auto-set width to max text length. + width = maxLineWidth + paddingLeft + paddingRight; + innerWidth = maxLineWidth; + } + // calculate text height + lineHeight = lineHeight || fontSize; + let height; + if (h) { + height = h; + } else { + height = lineHeight * (lines.length - 1) + 0.5 * fontSize + Math.max(lineHeight, fontSize) + offsetY; + } + if (offsetY === null) { + offsetY = fontSize; + } + renderInfo.w = width; + renderInfo.h = height; + renderInfo.lines = lines; + renderInfo.precision = precision; + if (!width) { + // To prevent canvas errors. + width = 1; + } + if (!height) { + // To prevent canvas errors. + height = 1; + } + if (cutSx || cutEx) { + width = Math.min(width, cutEx - cutSx); + } + if (cutSy || cutEy) { + height = Math.min(height, cutEy - cutSy); + } + // Add extra margin to prevent issue with clipped text when scaling. + canvasInternal.width = Math.ceil(width + textTextureRenderer._stage.getOption('textRenderIssueMargin')); + canvasInternal.height = Math.ceil(height); + // Canvas context has been reset. + // textTextureRenderer.setFontProperties(); + if (fontSize >= 128) { + // WpeWebKit bug: must force compositing because cairo-traps-compositor will not work with text first. + textTextureRenderer._context.globalAlpha = 0.01; + textTextureRenderer._context.fillRect(0, 0, 0.01, 0.01); + textTextureRenderer._context.globalAlpha = 1.0; + } + if (cutSx || cutSy) { + textTextureRenderer._context.translate(-cutSx, -cutSy); + } + let linePositionX; + let linePositionY; + let drawLines = []; + // Draw lines line by line. + for (let i = 0, n = lines.length; i < n; i++) { + linePositionX = 0; + linePositionY = (i * lineHeight) + offsetY; + if (textTextureRenderer._settings.textAlign === 'right') { + linePositionX += (innerWidth - lineWidths[i]); + } else if (textTextureRenderer._settings.textAlign === 'center') { + linePositionX += ((innerWidth - lineWidths[i]) / 2); + } + linePositionX += paddingLeft; + drawLines.push({text: lines[i], x: linePositionX, y: linePositionY, w: lineWidths[i]}); + } + // Highlight. + if (textTextureRenderer._settings.highlight) { + let color = textTextureRenderer._settings.highlightColor || 0x00000000; + let hlHeight = (textTextureRenderer._settings.highlightHeight * precision || fontSize * 1.5); + let offset = (textTextureRenderer._settings.highlightOffset !== null ? textTextureRenderer._settings.highlightOffset * precision : -0.5 * fontSize); + const hlPaddingLeft = (textTextureRenderer._settings.highlightPaddingLeft !== null ? textTextureRenderer._settings.highlightPaddingLeft * precision : paddingLeft); + const hlPaddingRight = (textTextureRenderer._settings.highlightPaddingRight !== null ? textTextureRenderer._settings.highlightPaddingRight * precision : paddingRight); + + textTextureRenderer._context.fillStyle = color; + for (let i = 0; i < drawLines.length; i++) { + let drawLine = drawLines[i]; + textTextureRenderer._context.fillRect((drawLine.x - hlPaddingLeft), (drawLine.y + offset), (drawLine.w + hlPaddingRight + hlPaddingLeft), hlHeight); + } + } + // Text shadow. + let prevShadowSettings = null; + if (textTextureRenderer._settings.shadow) { + prevShadowSettings = [textTextureRenderer._context.shadowColor, textTextureRenderer._context.shadowOffsetX, textTextureRenderer._context.shadowOffsetY, textTextureRenderer._context.shadowBlur]; + textTextureRenderer._context.shadowColor = textTextureRenderer._settings.shadowColor; + textTextureRenderer._context.shadowOffsetX = textTextureRenderer._settings.shadowOffsetX * precision; + textTextureRenderer._context.shadowOffsetY = textTextureRenderer._settings.shadowOffsetY * precision; + textTextureRenderer._context.shadowBlur = textTextureRenderer._settings.shadowBlur * precision; + } + textTextureRenderer._context.fillStyle = textTextureRenderer._settings.textColor; + for (let i = 0, n = drawLines.length; i < n; i++) { + let drawLine = drawLines[i]; + textTextureRenderer._context.fillText(drawLine.text, drawLine.x, drawLine.y); + } + + if (prevShadowSettings) { + textTextureRenderer._context.shadowColor = prevShadowSettings[0]; + textTextureRenderer._context.shadowOffsetX = prevShadowSettings[1]; + textTextureRenderer._context.shadowOffsetY = prevShadowSettings[2]; + textTextureRenderer._context.shadowBlur = prevShadowSettings[3]; + } + + if (cutSx || cutSy) { + textTextureRenderer._context.translate(cutSx, cutSy); + } + // Original Lightining code ends here + canvasInternal.ready.then(() => { // everything is drawn + renderInfo.w = canvasInternal.w; + renderInfo.h = canvasInternal.h; + textTextureRenderer._canvas.width = canvasInternal.w; + textTextureRenderer._canvas.height = canvasInternal.h; + textTextureRenderer.renderInfo = renderInfo; + resolve(); + }); + }); + }); + }); + return drawPromise; + } + + loadFonts(fonts) { + let promises = []; + let fontResources = new Map(); + for (let font of fonts) { + let fontResource = sparkscene.create({t: "fontResource", url: font.url}); + promises.push(fontResource.ready); + fontResources.set(font.family, fontResource); + } + + // load fonts and then store a + // reference to them so they can be used + // in getFontSetting calls + Promise.all(promises) + .then(() => this._fontResources = fontResources); + + // continue to return promise/font object + // to maintain compatibility with SDK client + return { + promises: promises, + fontResources: fontResources + }; + } + + getFontSetting(textTextureRenderer) { + let fontResource = textTextureRenderer._context.font; + let fontFace = textTextureRenderer._settings.fontFace; + let fontStyle = textTextureRenderer._settings.fontStyle.toLowerCase(); + + if (this._fontResources !== undefined && this._fontResources.has(fontFace)) { + fontResource = this._fontResources.get(fontFace); + if (fontResource.needsStyleCoercion(fontStyle)) { + let url = fontResource.url; + fontResource = sparkscene.create({t: "fontResource", url: url, fontStyle: fontStyle}); + } + } + return fontResource; + } + + + static createMediaPlayer() + { + return SparkMediaplayer; + } +} + +const lightning$1 = lightning; + +lightning$1.Stage.platform = SparkPlatform; + +const Headers = fetch.Headers; + + +const events = ['timeupdate', 'error', 'ended', 'loadeddata', 'canplay', 'play', 'playing', 'pause', 'loadstart', 'seeking', 'seeked', 'encrypted']; + +class Mediaplayer extends lightning$1.Component { + + _construct(){ + this._skipRenderToTexture = false; + } + + static _template() { + return { + Video: { + VideoWrap: { + VideoTexture: { + visible: false, + pivot: 0.5, + texture: {type: lightning$1.textures.StaticTexture, options: {}} + } + } + } + }; + } + + set skipRenderToTexture (v) { + this._skipRenderToTexture = v; + } + + set textureMode(v) { + return this._textureMode = v; + } + + get textureMode() { + return this._textureMode; + } + + get videoView() { + return this.tag("Video"); + } + + _init() { + //re-use videotag if already there + const videoEls = document.getElementsByTagName('video'); + if (videoEls && videoEls.length > 0) + this.videoEl = videoEls[0]; + else { + this.videoEl = document.createElement('video'); + this.videoEl.setAttribute('id', 'video-player'); + this.videoEl.style.position = 'absolute'; + this.videoEl.style.zIndex = '1'; + this.videoEl.style.display = 'none'; + this.videoEl.setAttribute('width', '100%'); + this.videoEl.setAttribute('height', '100%'); + + this.videoEl.style.visibility = (this.textureMode) ? 'hidden' : 'visible'; + document.body.appendChild(this.videoEl); + } + if (this.textureMode && !this._skipRenderToTexture) { + this._createVideoTexture(); + } + + this.eventHandlers = []; + } + + _registerListeners() { + events.forEach(event => { + const handler = (e) => { + this.fire(event, {videoElement: this.videoEl, event: e}); + }; + this.eventHandlers.push(handler); + this.videoEl.addEventListener(event, handler); + }); + } + + _deregisterListeners() { + events.forEach((event, index) => { + this.videoEl.removeEventListener(event, this.eventHandlers[index]); + }); + this.eventHandlers = []; + } + + _attach() { + this._registerListeners(); + } + + _detach() { + this._deregisterListeners(); + } + + _createVideoTexture() { + const stage = this.stage; + + const gl = stage.gl; + const glTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, glTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + this.videoTexture.options = {source: glTexture, w: this.videoEl.width, h: this.videoEl.height}; + } + + _startUpdatingVideoTexture() { + if (this.textureMode && !this._skipRenderToTexture) { + const stage = this.stage; + if (!this._updateVideoTexture) { + this._updateVideoTexture = () => { + if (this.videoTexture.options.source && this.videoEl.videoWidth && this.active) { + const gl = stage.gl; + + const currentTime = (new Date()).getTime(); + + // When BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DEBUGUTILS is not set in WPE, webkitDecodedFrameCount will not be available. + // We'll fallback to fixed 30fps in this case. + const frameCount = this.videoEl.webkitDecodedFrameCount; + + const mustUpdate = (frameCount ? (this._lastFrame !== frameCount) : (this._lastTime < currentTime - 30)); + + if (mustUpdate) { + this._lastTime = currentTime; + this._lastFrame = frameCount; + try { + gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.videoEl); + this._lastFrame = this.videoEl.webkitDecodedFrameCount; + this.videoTextureView.visible = true; + + this.videoTexture.options.w = this.videoEl.videoWidth; + this.videoTexture.options.h = this.videoEl.videoHeight; + const expectedAspectRatio = this.videoTextureView.w / this.videoTextureView.h; + const realAspectRatio = this.videoEl.videoWidth / this.videoEl.videoHeight; + if (expectedAspectRatio > realAspectRatio) { + this.videoTextureView.scaleX = (realAspectRatio / expectedAspectRatio); + this.videoTextureView.scaleY = 1; + } else { + this.videoTextureView.scaleY = expectedAspectRatio / realAspectRatio; + this.videoTextureView.scaleX = 1; + } + } catch (e) { + console.error('texImage2d video', e); + this._stopUpdatingVideoTexture(); + this.videoTextureView.visible = false; + } + this.videoTexture.source.forceRenderUpdate(); + } + } + }; + } + if (!this._updatingVideoTexture) { + stage.on('frameStart', this._updateVideoTexture); + this._updatingVideoTexture = true; + } + } + } + + _stopUpdatingVideoTexture() { + if (this.textureMode) { + const stage = this.stage; + stage.removeListener('frameStart', this._updateVideoTexture); + this._updatingVideoTexture = false; + this.videoTextureView.visible = false; + + if (this.videoTexture.options.source) { + const gl = stage.gl; + gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source); + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + } + } + } + + updateSettings(settings = {}) { + // The Component that 'consumes' the media player. + this._consumer = settings.consumer; + + if (this._consumer && this._consumer.getMediaplayerSettings) { + // Allow consumer to add settings. + settings = Object.assign(settings, this._consumer.getMediaplayerSettings()); + } + + if (!lightning$1.Utils.equalValues(this._stream, settings.stream)) { + if (settings.stream && settings.stream.keySystem) { + navigator.requestMediaKeySystemAccess(settings.stream.keySystem.id, settings.stream.keySystem.config).then((keySystemAccess) => { + return keySystemAccess.createMediaKeys(); + }).then((createdMediaKeys) => { + return this.videoEl.setMediaKeys(createdMediaKeys); + }).then(() => { + if (settings.stream && settings.stream.src) + this.open(settings.stream.src); + }).catch(() => { + console.error('Failed to set up MediaKeys'); + }); + } else if (settings.stream && settings.stream.src) { + if(!window.Hls){ + window.Hls = class Hls{ + static isSupported(){ + console.warn("hls-light not included"); + return false; + } + }; + } + if (ux.Ui.hasOption("hls") && Hls.isSupported()) { + if (!this._hls) this._hls = new Hls({liveDurationInfinity: true}); + this._hls.loadSource(settings.stream.src); + this._hls.attachMedia(this.videoEl); + this.videoEl.style.display = "block"; + } else { + this.open(settings.stream.src); + } + } else { + this.close(); + } + this._stream = settings.stream; + } + + this._setHide(settings.hide); + this._setVideoArea(settings.videoPos); + } + + _setHide(hide) { + if (this.textureMode) { + this.tag("Video").setSmooth('alpha', hide ? 0 : 1); + } else { + this.videoEl.style.visibility = hide ? 'hidden' : 'visible'; + } + } + + open(url) { + console.log('Playing stream', url); + if (this.application.noVideo) { + console.log('noVideo option set, so ignoring: ' + url); + return; + } + if (this.videoEl.getAttribute('src') === url) return this.reload(); + this.videoEl.setAttribute('src', url); + + this.videoEl.style.display = 'block'; + } + + close() { + // We need to pause first in order to stop sound. + this.videoEl.pause(); + this.videoEl.removeAttribute('src'); + + // force load to reset everything without errors + this.videoEl.load(); + + this._clearSrc(); + + this.videoEl.style.display = 'none'; + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + this.videoEl.play(); + } + + doPause() { + this.videoEl.pause(); + } + + reload() { + var url = this.videoEl.getAttribute('src'); + this.close(); + this.videoEl.src = url; + } + + getPosition() { + return Promise.resolve(this.videoEl.currentTime); + } + + setPosition(pos) { + this.videoEl.currentTime = pos; + } + + getDuration() { + return Promise.resolve(this.videoEl.duration); + } + + seek(time, absolute = false) { + if(absolute) { + this.videoEl.currentTime = time; + } + else { + this.videoEl.currentTime += time; + } + } + + get videoTextureView() { + return this.tag("Video").tag("VideoTexture"); + } + + get videoTexture() { + return this.videoTextureView.texture; + } + + _setVideoArea(videoPos) { + if (lightning$1.Utils.equalValues(this._videoPos, videoPos)) { + return; + } + + this._videoPos = videoPos; + + if (this.textureMode) { + this.videoTextureView.patch({ + smooth: { + x: videoPos[0], + y: videoPos[1], + w: videoPos[2] - videoPos[0], + h: videoPos[3] - videoPos[1] + } + }); + } else { + const precision = this.stage.getRenderPrecision(); + this.videoEl.style.left = Math.round(videoPos[0] * precision) + 'px'; + this.videoEl.style.top = Math.round(videoPos[1] * precision) + 'px'; + this.videoEl.style.width = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px'; + this.videoEl.style.height = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px'; + } + } + + _fireConsumer(event, args) { + if (this._consumer) { + this._consumer.fire(event, args); + } + } + + _equalInitData(buf1, buf2) { + if (!buf1 || !buf2) return false; + if (buf1.byteLength != buf2.byteLength) return false; + const dv1 = new Int8Array(buf1); + const dv2 = new Int8Array(buf2); + for (let i = 0 ; i != buf1.byteLength ; i++) + if (dv1[i] != dv2[i]) return false; + return true; + } + + error(args) { + this._fireConsumer('$mediaplayerError', args); + this._setState(""); + return ""; + } + + loadeddata(args) { + this._fireConsumer('$mediaplayerLoadedData', args); + } + + play(args) { + this._fireConsumer('$mediaplayerPlay', args); + } + + playing(args) { + this._fireConsumer('$mediaplayerPlaying', args); + this._setState("Playing"); + } + + canplay(args) { + this.videoEl.play(); + this._fireConsumer('$mediaplayerStart', args); + } + + loadstart(args) { + this._fireConsumer('$mediaplayerLoad', args); + } + + seeked(args) { + this._fireConsumer('$mediaplayerSeeked', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + + seeking(args) { + this._fireConsumer('$mediaplayerSeeking', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + + durationchange(args) { + this._fireConsumer('$mediaplayerDurationChange', args); + } + + encrypted(args) { + const video = args.videoElement; + const event = args.event; + // FIXME: Double encrypted events need to be properly filtered by Gstreamer + if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { + this._previousInitData = event.initData; + this._fireConsumer('$mediaplayerEncrypted', args); + } + } + + static _states() { + return [ + class Playing extends this { + $enter() { + this._startUpdatingVideoTexture(); + } + $exit() { + this._stopUpdatingVideoTexture(); + } + timeupdate() { + this._fireConsumer('$mediaplayerProgress', { + currentTime: this.videoEl.currentTime, + duration: this.videoEl.duration || 1 + }); + } + ended(args) { + this._fireConsumer('$mediaplayerEnded', args); + this._setState(""); + } + pause(args) { + this._fireConsumer('$mediaplayerPause', args); + this._setState("Playing.Paused"); + } + _clearSrc() { + this._fireConsumer('$mediaplayerStop', {}); + this._setState(""); + } + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } + +} + +class NoopMediaplayer extends lightning$1.Component { + + static _template() { + return { + Video: { + w: 1920, h: 1080 + } + }; + } + + open(url) { + console.log('Playing stream', url); + } + + close() { + } + + playPause() { + if (this.isPlaying()) { + this.doPause(); + } else { + this.doPlay(); + } + } + + isPlaying() { + return (this._getState() === "Playing"); + } + + doPlay() { + } + + doPause() { + } + + reload() { + } + + getPosition() { + return Promise.resolve(0); + } + + setPosition(pos) { + } + + getDuration() { + return Promise.resolve(0); + } + + seek(time, absolute = false) { + } + + updateSettings(settings = {}) { + } + + static _states() { + return [ + class Playing extends this { + static _states() { + return [ + class Paused extends this { + } + ] + } + } + ] + } + +} + +class ScaledImageTexture extends lightning$1.textures.ImageTexture { + + constructor(stage) { + super(stage); + + this._scalingOptions = undefined; + this.precision = 1; + } + + set scalingOptions(options) { + if (!lightning$1.Utils.equalValues(this._scalingOptions, options)) { + this._scalingOptions = options; + this._changed(); + } + } + + _getLookupId() { + const opts = this._scalingOptions; + return `${this._src}-${opts.type}-${opts.width}-${opts.height}`; + } + + _getSourceLoader() { + let src = this._src; + if (this.stage.getOption('srcBasePath')) { + var fc = src.charCodeAt(0); + if ((src.indexOf("//") === -1) && ((fc >= 65 && fc <= 90) || (fc >= 97 && fc <= 122) || fc == 46)) { + // Alphabetical or dot: prepend base path. + src = this.stage.getOption('srcBasePath') + src; + } + } + + if (this.stage.application.useImageServer) { + src = this._getImageServerSrc(src); + } else { + this.resizeMode = ScaledImageTexture._convertScalingOptions(this._scalingOptions); + } + + const platform = this.stage.platform; + return function(cb) { + return platform.loadSrcTexture({src: src, hasAlpha: this._hasAlpha}, cb); + } + } + + static _convertScalingOptions(options) { + const opts = lightning$1.Utils.clone(options); + switch(options.type) { + case "crop": + opts.type = "cover"; + break; + case "fit": + case "parent": + case "exact": + case "height": + case "portrait": + case "width": + case "landscape": + case "auto": + default: + opts.type = "contain"; + break; + } + opts.w = opts.w || opts.width || 0; + opts.h = opts.h || opts.height || 0; + return opts; + } + + get precision() { + return this._customPrecision; + } + + set precision(v) { + this._customPrecision = v; + super.precision = this.stage.getRenderPrecision() * this._customPrecision; + } + + _getImageServerSrc(src) { + if (this._scalingOptions && (this._precision !== 1)) { + const opts = lightning$1.Utils.clone(this._scalingOptions); + if (opts.width) { + opts.width = Math.round(opts.width * this._precision); + } + + if (opts.height) { + opts.height = Math.round(opts.height * this._precision); + } + src = ScaledImageTexture.getImageUrl(src, opts); + } else { + src = ScaledImageTexture.getImageUrl(src, this._scalingOptions); + } + return src; + } + + static getImageUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/image" + this.getQueryString(url, opts); + } + + static _getCdnProtocol() { + return lightning$1.Utils.isWeb && location.protocol === "https:" ? "https" : "http"; + } + + static getQueryString(url, opts, key = "url") { + let str = `?operator=${encodeURIComponent('metrological')}`; + const keys = Object.keys(opts); + keys.forEach(key => { + str += "&" + encodeURIComponent(key) + "=" + encodeURIComponent("" + opts[key]); + }); + str += `&${key}=${encodeURIComponent(url)}`; + return str; + } + + getNonDefaults() { + const obj = super.getNonDefaults(); + if (this._src) { + obj.src = this._src; + } + return obj; + } + +} + +class Ui extends lightning$1.Application { + + constructor(options) { + options.defaultFontFace = options.defaultFontFace || "RobotoRegular"; + super(options); + this._options = options; + } + + static _template() { + let mediaPlayerType = NoopMediaplayer; + if (lightning$1.Utils.isWeb) { + mediaPlayerType = Mediaplayer; + } +/* + else if (lightning$1.Utils.isSpark) { + mediaPlayerType = lightning$1.Stage.platform.createMediaPlayer(); + } +*/ + return { + Mediaplayer: {type: mediaPlayerType, textureMode: Ui.hasOption('texture')}, + AppWrapper: {} + }; + } + + static set staticFilesPath(path) { + this._staticFilesPath = path; + } + + get useImageServer() { + return !Ui.hasOption("noImageServer"); + } + + get mediaplayer() { + return this.tag("Mediaplayer"); + } + + _active() { + this.tag('Mediaplayer').skipRenderToTexture = this._options.skipRenderToTexture; + } + + startApp(appClass) { + this._setState("App.Loading", [appClass]); + } + + stopApp() { + } + + _handleBack() { + if (lightning$1.Utils.isWeb) { + window.close(); + } + } + + loadPlatformFonts(fonts) { + if (lightning$1.Utils.isNode && !lightning$1.Utils.isSpark) { + // Font loading not supported. Fonts should be installed in Linux system and then they can be picked up by cairo. + return Promise.resolve(); + } + + if (lightning$1.Utils.isSpark) { + let ret = this.stage.platform.loadFonts(fonts); + return Promise.all(ret.promises).then(() => {return ret.fontResources}); + } + } + + static loadFonts(fonts) { + const fontFaces = fonts.map(({family, url, descriptors}) => new FontFace(family, `url(${url})`, descriptors)); + fontFaces.forEach(fontFace => { + document.fonts.add(fontFace); + }); + return Promise.all(fontFaces.map(ff => ff.load())).then(() => {return fontFaces}); + } + + static getPath(relPath) { + return this._staticFilesPath + "static-ux/" + relPath; + } + + static getFonts() { + return [ + {family: 'RobotoRegular', url: Ui.getPath('fonts/roboto-regular.ttf'), descriptors: {}}, + {family: 'Material-Icons', url: Ui.getPath('fonts/Material-Icons.ttf'), descriptors: {}} + ] + } + + static _states() { + return [ + class App extends this { + stopApp() { + this._setState(""); + } + static _states() { + return [ + class Loading extends this { + $enter(context, appClass) { + this._startApp(appClass); + } + _startApp(appClass) { + this._currentApp = { + type: appClass, + fontFaces: [] + }; + + // Preload fonts. + const fonts = this._currentApp.type.getFonts().concat(Ui.getFonts()); + let fn = lightning$1.Utils.isWeb ? Ui.loadFonts(fonts): this.loadPlatformFonts(fonts); + fn.then((fontFaces) => { + this._currentApp.fontFaces = fontFaces; + }).catch((e) => { + console.warn('Font loading issues: ' + e); + }).finally(()=>{ + this._done(); + }); + } + _done() { + this._setState("App.Started"); + } + }, + class Started extends this { + $enter() { + this.tag("AppWrapper").children = [{ref: "App", type: this._currentApp.type}]; + } + $exit() { + this.tag("AppWrapper").children = []; + } + } + ] + } + } + ] + } + + _getFocused() { + return this.tag("App"); + } + + _setFocusSettings(settings) { + settings.clearColor = this.stage.getOption('clearColor'); + settings.mediaplayer = { + consumer: null, + stream: null, + hide: false, + videoPos: [0, 0, 1920, 1080] + }; + } + + _handleFocusSettings(settings) { + if (this._clearColor !== settings.clearColor) { + this._clearColor = settings.clearColor; + this.stage.setClearColor(settings.clearColor); + } + + if (this.tag("Mediaplayer").attached) { + this.tag("Mediaplayer").updateSettings(settings.mediaplayer); + } + } + + static getProxyUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/proxy" + this.getQueryString(url, opts); + } + + static getImage(url, opts = {}) { + return {type: ScaledImageTexture, src: url, scalingOptions: opts}; + } + + static getImageUrl(url, opts = {}) { + throw new Error("{src: Ui.getImageUrl(...)} is deprecated. Please use {texture: Ui.getImage(...)} instead."); + } + + static getQrUrl(url, opts = {}) { + return this._getCdnProtocol() + "://cdn.metrological.com/qr" + this.getQueryString(url, opts, "q"); + } + + static _getCdnProtocol() { + return lightning$1.Utils.isWeb && location.protocol === "https:" ? "https" : "http"; + } + + static hasOption(name) { + if (lightning$1.Utils.isNode) { + return false; + } + + return new URL(document.location.href).searchParams.has(name); + } + + static getOption(name) { + if (lightning$1.Utils.isNode) { + return undefined; + } + + return new URL(document.location.href).searchParams.get(name); + } + + static getQueryString(url, opts, key = "url") { + let str = `?operator=${encodeURIComponent(this.getOption('operator') || 'metrological')}`; + const keys = Object.keys(opts); + keys.forEach(key => { + str += "&" + encodeURIComponent(key) + "=" + encodeURIComponent("" + opts[key]); + }); + str += `&${key}=${encodeURIComponent(url)}`; + return str; + } + + +} + +Ui._staticFilesPath = "./"; + +class App extends lightning$1.Component { + + static g(c) { + return c.seekAncestorByType(this); + } + + /** + * Returns all fonts to be preloaded before entering this app. + * @returns {{family: string, url: string, descriptors: {}}[]} + */ + static getFonts() { + return []; + } + + getPath(relPath) { + return App.getPath(this.constructor, relPath); + } + + static getPath(relPath) { + return Ui._staticFilesPath + "static/" + relPath; + } + + static get identifier() { + throw new Error("Please supply an identifier in the App definition file."); + } + +} + +class PlayerButton extends lightning$1.Component { + + static _template() { + const o = this.options; + return { + w: o.w, h: o.h, + Background: {x: -1, y: -1, texture: lightning$1.Tools.getRoundRect(o.w, o.h, 4, 0, 0, true), color: o.colors.deselected}, + Icon: {x: o.w/2, y: o.h/2, mount: 0.5, color: o.colors.selected} + }; + } + + set icon(source) { + this.tag("Icon").src = Ui.getPath(`tools/player/img/${source}`); + } + + set active(v) { + this.alpha = v ? 1 : 0.3; + } + + get active() { + return this.alpha === 1; + } + + static _states() { + return [ + class Selected extends this { + $enter() { + this.tag("Background").color = this.constructor.options.colors.selected; + this.tag("Icon").color = this.constructor.options.colors.deselected; + } + $exit() { + this.tag("Background").color = this.constructor.options.colors.deselected; + this.tag("Icon").color = this.constructor.options.colors.selected; + } + } + ] + } + + _focus() { + this._setState("Selected"); + } + + _unfocus() { + this._setState(""); + } + + static get options() { + if (!this._options) { + this._options = this._buildOptions(); + } + return this._options; + } + + static _buildOptions() { + return { + colors: { + selected: 0xFFFFFFFF, + deselected: 0xFF606060 + }, + w: 60, + h: 60 + }; + } + +} + +class PlayerControls extends lightning$1.Component { + + static _template() { + return { + Buttons: { + Previous: {type: this.PlayerButton, icon: "prev.png"}, + Play: {type: this.PlayerButton, icon: "play.png"}, + Next: {type: this.PlayerButton, icon: "next.png"} + }, + Title: {text: {fontSize: 46, lineHeight: 56, maxLines: 1, shadow: true}, y: 2} + }; + } + + static get PlayerButton() { + return PlayerButton; + } + + showButtons(previous, next) { + const o = this.constructor.options; + let buttons = []; + if (previous) buttons.push("Previous"); + buttons = buttons.concat(o.buttons); + if (next) buttons.push("Next"); + this._setActiveButtons(buttons); + } + + set title(title) { + this.tag("Title").text = title || ""; + } + + get _activeButtonIndex() { + let button = this.tag("Buttons").getByRef(this._getState()); + if (!button.active) { + button = this.tag("Play"); + } + return this._activeButtons.indexOf(button); + } + + get _activeButton() { + return this._activeButtons[this._activeButtonIndex]; + } + + _setActiveButtons(buttons) { + const o = this.constructor.options; + + let x = 0; + this._activeButtons = []; + this.tag("Buttons").children.map(button => { + button.active = (buttons.indexOf(button.ref) !== -1); + button.x = x; + if (button.active) { + this._activeButtons.push(button); + } + x += button.renderWidth + o.margin; + }); + this.tag("Title").x = x + 20; + + + this._checkActiveButton(); + } + + _setup() { + this._setState("Play"); + } + + _init() { + this.showButtons(false, false); + this._setState("Play"); + } + + _checkActiveButton() { + // After changing the active buttons, make sure that an active button is selected. + let index = this._activeButtonIndex; + if (index === -1) { + if (this._index >= this._activeButtons.length) { + this._index = this._activeButtons.length - 1; + } + } + this._setState(this._activeButtons[index].ref); + } + + _handleLeft() { + let index = this._activeButtonIndex; + if (index > 0) { + index--; + } + this._setState(this._activeButtons[index].ref); + } + + _handleRight() { + let index = this._activeButtonIndex; + if (index < this._activeButtons.length - 1) { + index++; + } + this._setState(this._activeButtons[index].ref); + } + + _handleEnter() { + this.signal('press' + this._activeButton.ref); + } + + + set paused(v) { + this.tag("Play").icon = v ? "play.png" : "pause.png"; + } + + static _states() { + return [ + class Previous extends this { + }, + class Play extends this { + }, + class Next extends this { + } + ] + } + + _getFocused() { + return this.tag(this._getState()); + } + + static get options() { + if (!this._options) { + this._options = this._buildOptions(); + } + return this._options; + } + + static _buildOptions() { + return { + buttons: ["Play"], + margin: 10 + }; + } + +} + +class PlayerProgress extends lightning$1.Component { + + static _template() { + return { + Progress: { + forceZIndexContext: true, + Total: { + x: -1, y: -1, texture: lightning$1.Tools.getRoundRect(1720, 10, 4), color: 0xFF606060, + Scroller: { + x: 0, y: 6, mount: 0.5, w: 16, h: 16, zIndex: 2, + Shadow: { + texture: lightning$1.Tools.getShadowRect(16, 16, 8), + mount: 0.5, + x: 8, + y: 8, + color: 0xFF000000 + }, + Main: {texture: lightning$1.Tools.getRoundRect(16, 16, 8), mount: 0.5, x: 8, y: 8, color: 0xFFF1F1F1} + } + }, + Active: {x: -1, y: -1, color: 0xFFF1F1F1}, + CurrentTime: { + x: 0, + y: 21, + text: {fontSize: 28, lineHeight: 34, maxLines: 1, shadow: true, text: "00:00"} + }, + Duration: { + x: 1720, + mountX: 1, + y: 21, + text: {fontSize: 28, lineHeight: 34, maxLines: 1, shadow: true, text: "00:00"} + } + } + }; + } + + set _progress(v) { + const now = Date.now(); + let estimation = 0; + if (!this._last || (this._last < now - 1000)) { + estimation = 500; + } else { + estimation = now - this._last; + } + this._last = now; + const x = v * 1720; + + estimation *= 0.001; + this.tag("Total").setSmooth('x', x, {timingFunction: 'linear', duration: estimation}); + this.tag("Total").setSmooth('texture.x', x, {timingFunction: 'linear', duration: estimation}); + this.tag("Active").setSmooth('texture.w', Math.max(x, 0.0001) /* force clipping */, { + timingFunction: 'linear', + duration: estimation + }); + } + + setProgress(currentTime, duration) { + this._progress = currentTime / Math.max(duration, 1); + this.tag("CurrentTime").text = Player.formatTime(currentTime); + this.tag("Duration").text = Player.formatTime(duration); + } + + static formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + seconds = Math.floor(seconds); + const parts = []; + if (hours) parts.push(hours); + parts.push(minutes); + parts.push(seconds); + return parts.map(number => (number < 10 ? "0" + number : "" + number)).join(":"); + } + + _alter() { + } + + _setup() { + this._alter(); + } + + _init() { + this.tag("Active").texture = { + type: lightning$1.textures.SourceTexture, + textureSource: this.tag("Total").texture.source + }; + } + +} + +class Player extends lightning$1.Component { + + static _template() { + return { + Gradient: { + x: 0, + y: 1080, + h: 300, + w: 1920, + mountY: 1, + colorTop: 0x00101010, + colorBottom: 0xE6101010, + rect: true + }, + Controls: { + x: 99, + y: 890, + type: this.PlayerControls, + signals: {pressPlay: true, pressPrevious: true, pressNext: "_pressNext"} + }, + Progress: {x: 99, y: 970, type: this.PlayerProgress} + }; + } + + static get PlayerControls() { + return PlayerControls; + } + + static get PlayerProgress() { + return PlayerProgress; + } + + _setItem(item) { + this.tag("Progress").setProgress(0, 0); + this._item = item; + this._stream = item.stream; + this.tag("Controls").title = item.title; + + this._index = this._items.indexOf(item); + this.tag("Controls").showButtons(this._index > 0, this._index < this._items.length - 1); + + this.application.updateFocusSettings(); + } + + static formatTime(seconds) { + const hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + seconds = Math.floor(seconds); + const parts = []; + if (hours) parts.push(hours); + parts.push(minutes); + parts.push(seconds); + return parts.map(number => (number < 10 ? "0" + number : "" + number)).join(":"); + } + + _setInterfaceTimeout() { + if (this._timeout) { + clearTimeout(this._timeout); + } + this._timeout = setTimeout(() => { + this._hide(); + }, 8000); + } + + _init() { + this._setState("Controls"); + } + + _focus() { + this._setInterfaceTimeout(); + } + + _unfocus() { + clearTimeout(this._timeout); + } + + $mediaplayerEnded() { + this._pressNext(); + } + + play({item, items = [item]}) { + this._items = items; + this._setItem(item); + return !!this._stream; + } + + pressPrevious() { + const index = this._index - 1; + if (index < 0) { + this._index = this._items.length - 1; + } + this._setItem(this._items[index]); + } + + _pressNext() { + if (!this._items.length) { + return this.signal('playerStop'); + } + const index = (this._index + 1) % this._items.length; + this._setItem(this._items[index]); + } + + pressPlay() { + this.application.mediaplayer.playPause(); + } + + $mediaplayerPause() { + this.tag("Controls").paused = true; + } + + $mediaplayerPlay() { + this.tag("Controls").paused = false; + } + + $mediaplayerStop() { + this.signal('playerStop'); + } + + $mediaplayerProgress({currentTime, duration}) { + this.tag("Progress").setProgress(currentTime, duration); + } + + _captureKey() { + this._setInterfaceTimeout(); + return false; + } + + _hide() { + this._setState("Hidden"); + } + + static _states() { + return [ + class Hidden extends this { + $enter({prevState}) { + this._prevState = prevState; + this.setSmooth('alpha', 0); + } + $exit() { + this._setInterfaceTimeout(); + this.setSmooth('alpha', 1); + } + _captureKey() { + this._setState(this._prevState); + } + }, + class Controls extends this { + } + ]; + } + + _getFocused() { + return this.tag("Controls"); + } + + _setFocusSettings(settings) { + settings.mediaplayer.consumer = this; + } + + getMediaplayerSettings() { + if (this._stream.link) { + // Backwards compatibility. + this._stream.src = this._stream.link; + } + + return { + stream: this._stream + }; + } + + +} + +const obj = { + Player, + PlayerButton, + PlayerControls, + PlayerProgress +}; + +class Light3dComponent extends lightning$1.Component { + + constructor(stage) { + super(stage); + + this.patch({ + __create: true, + Main: { + x: -1, + y: -1, + shader: {type: lightning$1.shaders.Light3d, fudge: 0.3}, + renderToTexture: true, + Wrapper: { + x: 1, + y: 1, + clipping: true, + Content: {} + } + } + }); + + this._shaderZ = 0; + this._shaderZ0 = 0; + this._shaderZ1 = 0; + + this._shaderRx = 0; + this._shaderRx0 = 0; + this._shaderRx1 = 0; + + this._shaderRy = 0; + this._shaderRy0 = 0; + this._shaderRy1 = 0; + + this._focusedZ = -150; + this._createAnimations(); + + this.transition('lightShader.strength', {duration: 0.2}); + this.transition('lightShader.ambient', {duration: 0.2}); + } + + get focusedZ() { + return this._focusedZ; + } + + set focusedZ(v) { + this._focusedZ = v; + this._createAnimations(); + } + + _createAnimations() { + this._anims = { + neutral: this.animation({ + duration: 0.4, actions: [ + {p: 'shaderZ0', merger: lightning$1.StageUtils.mergeNumbers, v: {0: 0, 0.5: -140, 1: -150}} + ] + }), + left: this._createAnimation('x', -1, 0), + right: this._createAnimation('x', 1, 1), + up: this._createAnimation('y', -1, 0), + down: this._createAnimation('y', 1, 0) + }; + } + + _createAnimation(axis, sign, idx) { + return this.animation({ + duration: 0.4, stopDuration: 0.2, actions: [ + {p: 'shaderR' + axis + idx, merger: lightning$1.StageUtils.mergeNumbers, v: {0: 0, 0.3: -0.20 * sign, 1: 0}}, + { + p: 'shaderZ' + idx, + merger: lightning$1.StageUtils.mergeNumbers, + v: {0: 0, 0.5: this._focusedZ + 10, 1: this._focusedZ} + } + ] + }); + } + + set w(v) { + this.tag('Main').w = v + 2; + this.tag('Wrapper').w = v; + } + + set h(v) { + this.tag('Main').h = v + 2; + this.tag('Wrapper').h = v; + } + + get lightShader() { + return this.tag('Main').shader; + } + + set lightShader(v) { + this.tag('Main').shader = v; + } + + get content() { + return this.tag('Content'); + } + + set content(v) { + this.tag('Content').patch(v, true); + } + + _recalc() { + this.tag('Main').shader.rx = this._shaderRx0 + this._shaderRx1 + this._shaderRx; + this.tag('Main').shader.ry = this._shaderRy0 + this._shaderRy1 + this._shaderRy; + this.tag('Main').shader.z = this._shaderZ0 + this._shaderZ1 + this._shaderZ; + this.tag('Main').shader.pivotZ = this._shaderZ0 + this._shaderZ1 + this._shaderZ; + } + + get shaderZ() { + return this._shaderZ; + } + + set shaderZ(v) { + this._shaderZ = v; + this._recalc(); + } + + get shaderZ0() { + return this._shaderZ0; + } + + set shaderZ0(v) { + this._shaderZ0 = v; + this._recalc(); + } + + get shaderZ1() { + return this._shaderZ1; + } + + set shaderZ1(v) { + this._shaderZ1 = v; + this._recalc(); + } + + get shaderRx() { + return this._shaderRx; + } + + set shaderRx(v) { + this._shaderRx = v; + this._recalc(); + } + + get shaderRx0() { + return this._shaderRx0; + } + + set shaderRx0(v) { + this._shaderRx0 = v; + this._recalc(); + } + + get shaderRx1() { + return this._shaderRx1; + } + + set shaderRx1(v) { + this._shaderRx1 = v; + this._recalc(); + } + + get shaderRy() { + return this._shaderRy; + } + + set shaderRy(v) { + this._shaderRy = v; + this._recalc(); + } + + get shaderRy0() { + return this._shaderRy0; + } + + set shaderRy0(v) { + this._shaderRy0 = v; + this._recalc(); + } + + get shaderRy1() { + return this._shaderRy1; + } + + set shaderRy1(v) { + this._shaderRy1 = v; + this._recalc(); + } + + leftEnter() { + this._anims['left'].start(); + this._enable3dShader(); + } + + leftExit() { + this.neutralExit(); + } + + rightEnter() { + this._anims['right'].start(); + this._enable3dShader(); + } + + rightExit() { + this.neutralExit(); + } + + upEnter() { + this._anims['up'].start(); + this._enable3dShader(); + } + + upExit() { + this.neutralExit(); + } + + downEnter() { + this._anims['down'].start(); + this._enable3dShader(); + } + + downExit() { + this.neutralExit(); + } + + neutralEnter() { + this._anims['neutral'].start(); + this._enable3dShader(); + } + + neutralExit() { + this._anims['up'].stop(); + this._anims['down'].stop(); + this._anims['left'].stop(); + this._anims['right'].stop(); + this._anims['neutral'].stop(); + this._disable3dShader(); + } + + _enable3dShader() { + this.patch({smooth: {'lightShader.strength': 0.4, 'lightShader.ambient': 0.6}}); + } + + _disable3dShader() { + this.patch({smooth: {'lightShader.strength': 0, 'lightShader.ambient': 1}}); + } + + +} + +const obj$1 = { + Light3dComponent +}; + +const template = { + keyWidth: 74, + keyHeight: 74, + horizontalSpacing: 8, + verticalSpacing: 12, + layouts: { + 'ABC': { + rows: [ + { + keys: [ + {c: 'A'}, + {c: 'B'}, + {c: 'C'}, + {c: 'D'}, + {c: 'E'}, + {c: 'F'}, + {c: 'G'}, + {action: 'backspace', w: 148, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {c: 'H'}, + {c: 'I'}, + {c: 'J'}, + {c: 'K'}, + {c: 'L'}, + {c: 'M'}, + {c: 'N'}, + {action: 'toggleToLayout', toLayout: '#123', w: 148, c: '#123'} + ] + }, + { + keys: [ + {c: 'O'}, + {c: 'P'}, + {c: 'Q'}, + {c: 'R'}, + {c: 'S'}, + {c: 'T'}, + {c: 'U'} + ] + }, + { + keys: [ + {c: 'V'}, + {c: 'W'}, + {c: 'X'}, + {c: 'Y'}, + {c: 'Z'}, + {c: '-'}, + {c: '\''} + ] + }, + { + keys: [ + {action: 'space', c: 'space', w: 183}, + {action: 'delete', c: 'delete', w: 183}, + {action: 'ok', c: 'ok', w: 183} + ] + } + ] + }, + '#123': { + rows: [ + { + keys: [ + {c: '1'}, + {c: '2'}, + {c: '3'}, + {c: '&'}, + {c: '#'}, + {c: '('}, + {c: ')'}, + {action: 'backspace', w: 148, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {c: '4'}, + {c: '5'}, + {c: '6'}, + {c: '@'}, + {c: '!'}, + {c: '?'}, + {c: ':'}, + {action: 'toggleToLayout', toLayout: 'ABC', w: 148, c: 'ABC'} + ] + }, + { + keys: [ + {c: '7'}, + {c: '8'}, + {c: '9'}, + {c: '0'}, + {c: '.'}, + {c: '_'}, + {c: '\"'} + ] + }, + { + keys: [ + {action: 'space', c: 'space', w: 183}, + {action: 'delete', c: 'delete', w: 183}, + {action: 'ok', c: 'ok', w: 183} + ] + } + ] + } + } +}; + +class KeyboardButton extends lightning$1.Component { + static _template() { + return { + Background: {colorTop: 0x80e8e8e8, colorBottom: 0x80d1d1d1}, + Content: {} + }; + } + + set action(v) { + this._action = v; + } + + get action() { + return this._action; + } + + get c() { + return this.key.c; + } + + set key(v) { + this._key = v; + if(this.active) { + this._update(); + } + } + + _update() { + this.patch(this._getPatch(this._key)); + } + + _getPatch(key) { + let content = key.patch || {text: {text: key.c, fontFace: 'RobotoRegular', textAlign: 'center', fontSize: 36}}; + return { + Background: {texture: lightning$1.Tools.getRoundRect(this.w, this.h, 7, 0, 0xffffffff, true, 0xffffffff)}, + Content: {mountX: 0.5, mountY: 0.4, x: this.w/2, y: this.h/2, ...content} + }; + } + + get key() { + return this._key; + } + + _focus() { + this.patch({ + Background: {smooth: {colorTop: 0xff3777ee, colorBottom: 0xff2654a8}} + }); + } + + _unfocus() { + this.patch({ + Background: {smooth: {colorTop: 0x80e8e8e8, colorBottom: 0x80d1d1d1}} + }); + } + + _firstActive() { + this._update(); + } +} + +class Keyboard extends lightning$1.Component { + static _template() { + return { + + }; + } + + _construct() { + this._template = template; + } + + set template(v) { + this._template = v; + } + + get keyboardTemplate() { + return this._template; + } + + get keyboardButton() { + return KeyboardButton; + } + + get maxCharacters() { + return 40; + } + + set value(v) { + if(v.length < this.maxCharacters) { + this._value = v; + this.signal('valueChanged', {value: v}); + } + } + + get value() { + return this._value; + } + + get rows() { + return this.children; + } + + get rowLength() { + return this.rows[this.rowIndex].children.length; + } + + get currentKey() { + return this.children[this.rowIndex].children[this.colIndex] || null; + } + + set layout(layout) { + this._layout = layout; + this._update(); + } + + get layout() { + return this._layout; + } + + _getFocused() { + return this.currentKey; + } + + _navigate(dir, value) { + dir = (dir === 'up' || dir === 'down') ? 'vertical' : 'horizontal'; + if(dir === 'horizontal' && this.colIndex + value < this.rowLength && this.colIndex + value > -1) { + this.previous = null; + return this.colIndex += value; + } + else if(dir === 'vertical' && this.rowIndex + value < this.rows.length && this.rowIndex + value > -1) { + const currentColIndex = this.colIndex; + const targetRow = this.rowIndex + value; + if(this.previous && this.previous.row === targetRow) { + const tmp = this.previous.col; + this.previous.col = this.colIndex; + this.colIndex = tmp; + } + else { + const targetRow = this.children[(this.rowIndex + value)]; + const targetItems = targetRow.children; + const ck = this.currentKey; + let target = 0; + for(let i = 0; i < targetItems.length; i++) { + const ckx = this.children[this.rowIndex].x + ck.x; + const tix = targetRow.x + targetItems[i].x; + target = i; + if((ckx >= tix && ckx <= tix + targetItems[i].w) || (tix >= ckx && tix <= ckx + ck.w)) { + break; + } + } + this.colIndex = target; + } + this.previous = {col: currentColIndex, row: this.rowIndex}; + return this.rowIndex += value; + } + return false; + } + + _update() { + if(this._layout && this.keyboardTemplate.layouts[this._layout] === undefined) { + console.error(`Configured layout "${this.layout}" does not exist. Reverting to "${Object.keys(this.keyboardTemplate.layouts)[0]}"`); + this._layout = null; + } + if(!this._layout) { + this._layout = Object.keys(this.keyboardTemplate.layouts)[0]; + } + const {keyWidth, keyHeight, horizontalSpacing = 0, verticalSpacing = 0, layouts} = this.keyboardTemplate; + + this.children = layouts[this._layout].rows.map((row, rowIndex) => { + let keyOffset = 0; + const {x = 0, rowVerticalSpacing = verticalSpacing, rowHorizontalSpacing = horizontalSpacing, keys = []} = row; + return {y: keyHeight * rowIndex + (rowIndex * rowVerticalSpacing), x, + children: keys.map((key) => { + key = Object.assign({action: 'input'}, key); + const prevOffset = keyOffset; + const {w = keyWidth, h = keyHeight, action, toLayout} = key; + keyOffset += w + rowHorizontalSpacing; + return {key, action, toLayout, x: prevOffset, w, h, type: this.keyboardButton} + }) + }; + }); + } + + reset() { + this.colIndex = 0; + this.rowIndex = 0; + this._value = ''; + this.previous = null; + } + + _init() { + this.reset(); + this._update(); + } + + _handleRight() { + return this._navigate('right', 1); + } + + _handleLeft() { + return this._navigate('left', -1); + } + + _handleUp() { + return this._navigate('up', -1); + } + + _handleDown() { + return this._navigate('down', 1); + } + + _handleEnter() { + const key = this.currentKey; + switch(key.action) { + case 'input': + this.value += key.c; + break; + case 'backspace': + this.value = this.value.slice(0, -1); + break + case 'space': + if(this.value.length > 0){ + this.value += ' '; + } + break; + case 'delete': + this.value = ''; + break; + case 'toggleToLayout': + this.layout = key.toLayout; + break; + default: + this.signal(key.action, key); + break; + } + } +} + +const template$1 = { + keyWidth: 64, + keyHeight: 84, + horizontalSpacing: 8, + verticalSpacing: 12, + layouts: { + 'ABC': { + rows: [ + { + keys: [ + {c: 'Q'}, + {c: 'W'}, + {c: 'E'}, + {c: 'R'}, + {c: 'T'}, + {c: 'Y'}, + {c: 'U'}, + {c: 'I'}, + {c: 'O'}, + {c: 'P'} + ] + }, + { + x: 34, + keys: [ + {c: 'A'}, + {c: 'S'}, + {c: 'D'}, + {c: 'F'}, + {c: 'G'}, + {c: 'H'}, + {c: 'J'}, + {c: 'K'}, + {c: 'L'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'abc', c: 'Aa', w: 98}, + {c: 'Z'}, + {c: 'X'}, + {c: 'C'}, + {c: 'V'}, + {c: 'B'}, + {c: 'N'}, + {c: 'M'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', w: 136, c: '#123'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + 'abc': { + rows: [ + { + keys: [ + {c: 'q'}, + {c: 'w'}, + {c: 'e'}, + {c: 'r'}, + {c: 't'}, + {c: 'y'}, + {c: 'u'}, + {c: 'i'}, + {c: 'o'}, + {c: 'p'} + ] + }, + { + x: 34, + keys: [ + {c: 'a'}, + {c: 's'}, + {c: 'd'}, + {c: 'f'}, + {c: 'g'}, + {c: 'h'}, + {c: 'j'}, + {c: 'k'}, + {c: 'l'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', c: 'aA', w: 98}, + {c: 'z'}, + {c: 'x'}, + {c: 'c'}, + {c: 'v'}, + {c: 'b'}, + {c: 'n'}, + {c: 'm'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', w: 136, c: '#123'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + '#123': { + rows: [ + { + keys: [ + {c: '1'}, + {c: '2'}, + {c: '3'}, + {c: '4'}, + {c: '5'}, + {c: '6'}, + {c: '7'}, + {c: '8'}, + {c: '9'}, + {c: '0'} + ] + }, + { + x: 34, + keys: [ + {c: '@'}, + {c: '#'}, + {c: '€'}, + {c: '_'}, + {c: '&'}, + {c: '-'}, + {c: '+'}, + {c: '('}, + {c: ')'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '{&=', c: '{&=', w: 98}, + {c: '*'}, + {c: '\"'}, + {c: '\''}, + {c: ':'}, + {c: ';'}, + {c: '!'}, + {c: '?'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', w: 136, c: 'ABC'}, + {c: ','}, + {action: 'space', c: '', w: 276}, + {c: '.'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + }, + '{&=': { + rows: [ + { + keys: [ + {c: '~'}, + {c: '\`'}, + {c: '|'}, + {c: '\u2022'}, + {c: '√'}, + {c: 'π'}, + {c: '\u00f7'}, + {c: '\u00d7'}, + {c: '¶'}, + {c: '∆'} + ] + }, + { + keys: [ + {c: '£'}, + {c: '¥'}, + {c: '€'}, + {c: '¢'}, + {c: '^'}, + {c: '°'}, + {c: '='}, + {c: '{'}, + {c: '}'}, + {c: 'a'} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: '#123', c: '#123', w: 98}, + {c: '%'}, + {c: '©'}, + {c: '®'}, + {c: '™'}, + {c: '\u2713'}, + {c: '['}, + {c: ']'}, + {action: 'backspace', w: 98, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + }, + { + keys: [ + {action: 'toggleToLayout', toLayout: 'ABC', w: 136, c: 'ABC'}, + {c: '<'}, + {action: 'space', c: '', w: 276}, + {c: '>'}, + {action: 'hideKeyboard', w: 136, patch: {mountY: 0.33, text: {text: '', fontFace: 'Material-Icons', fontSize: 55}}} + ] + } + ] + } + } +}; + +const obj$2 = { + Keyboard, + KeyboardButton, + SimpleKeyboardTemplate: template, + AdvancedKeyboardTemplate: template$1 +}; + +class ItemList extends lightning$1.Component { + static _template() { + return { + Wrapper: { + flex: {direction: 'row'} + } + }; + } + + set items(items) { + this.tag('Wrapper').children = items; + this._index = 0; + if(items.length > 0) { + this._setState('Filled'); + } + else { + this._setState('Empty'); + } + } + + get items() { + return this.tag('Wrapper').children + } + + get currentItem() { + return this.items[this._index]; + } + + get length() { + return this.items.length; + } + + set orientation(v) { + this._orientation = v; + if(v === 'horizontal') { + this.tag('Wrapper').patch({flex: {direction: 'row'}}); + } + else { + this.tag('Wrapper').patch({flex: {direction: 'column'}}); + } + } + + get orientation() { + return this._orientation || 'horizontal'; + } + + set jump(bool) { + this._jump = bool; + } + + get jump() { + return this._jump || false; + } + + set jumpToStart(bool) { + this._jumpToStart = bool; + } + + get jumpToStart() { + return this._jumpToStart !== undefined ? this._jumpToStart : this.jump; + } + + set jumpToEnd(bool) { + this._jumpToEnd = bool; + } + + get jumpToEnd() { + return this._jumpToEnd !== undefined ? this._jumpToEnd : this.jump; + } + + _navigate(dir) { + const ori = this.orientation; + if(((dir === 'right' || dir === 'left') && ori === 'horizontal') || ((dir === 'up' || dir === 'down') && ori === 'vertical')) { + const length = this.items.length; + const currentIndex = this._index; + let targetIndex = currentIndex + 1; + if(dir === 'left' || dir === 'up') { + targetIndex = currentIndex - 1; + } + + if(targetIndex > -1 && targetIndex < length) { + this._index = targetIndex; + } + else if(this.jump || (this.jumpToStart || this.jumpToEnd)) { + if(targetIndex < 0 && this.jumpToEnd) { + this._index = targetIndex + length; + } + else if(targetIndex === length && this.jumpToStart){ + this._index = 0; + } + } + else { + return false; + } + + if(currentIndex !== this._index) { + this.indexChanged({index: this._index, previousIndex: currentIndex}); + } + } + return false; + } + + setIndex(targetIndex) { + if(targetIndex > -1 && targetIndex < this.items.length) { + const currentIndex = this._index; + this._index = targetIndex; + this.indexChanged({index: this._index, previousIndex: currentIndex}); + } + } + + indexChanged(event) { + this.signal('indexChanged', event); + } + + _getFocused() { + return this; + } + + _construct() { + this._index = 0; + } + + _init() { + this._setState('Empty'); + } + + static _states() { + return [ + class Empty extends this { + }, + class Filled extends this { + _getFocused() { + return this.currentItem; + } + _handleRight() { + return this._navigate('right'); + } + + _handleLeft() { + return this._navigate('left'); + } + + _handleUp() { + return this._navigate('up'); + } + + _handleDown() { + return this._navigate('down'); + } + } + ] + } +} + +const obj$3 = { + ItemList +}; + +class Slider extends lightning$1.Component { + static _template() { + return { + Wrapper: { + flex: {direction: 'row'} + } + } + } + + set items(items) { + this._reset(); + this.tag('Wrapper').children = items; + this.scrollToFocus(true); + if(items.length > 0) { + this._setState('Filled'); + } + else { + this._setState('Empty'); + } + } + + get items() { + return this.tag('Wrapper').children; + } + + get currentItem() { + return this.items[this._index]; + } + + get index() { + return this._index; + } + + set orientation(v) { + this._orientation = v; + if(v === 'horizontal') { + this.tag('Wrapper').patch({flex: {direction: 'row'}}); + } + else { + this.tag('Wrapper').patch({flex: {direction: 'column'}}); + } + } + + get orientation() { + return this._orientation || 'horizontal'; + } + + set margin(v) { + this._margin = v; + } + + get margin() { + return this._margin || 0; + } + + set marginStart(v) { + this._marginStart = v; + } + + get marginStart() { + return this._marginStart || this.margin; + } + + set marginEnd(v) { + this._marginEnd = v; + } + + get marginEnd() { + return this._marginEnd || this.margin; + } + + set jump(bool) { + this._jump = bool; + } + + get jump() { + return this._jump || false; + } + + set jumpToStart(bool) { + this._jumpToStart = bool; + } + + get jumpToStart() { + return this._jumpToStart !== undefined ? this._jumpToStart : this.jump; + } + + set jumpToEnd(bool) { + this._jumpToEnd = bool; + } + + get jumpToEnd() { + return this._jumpToEnd !== undefined ? this._jumpToEnd : this.jump; + } + + get scrollTransitionSettings() { + return this._scrollTransitionSettings; + } + + set scrollTransition(v) { + this._scrollTransitionSettings.patch(v); + } + + get scrollTransition() { + return this._scrollTransition; + } + + get viewportSize() { + return this.orientation === 'horizontal' ? this.w : this.h; + } + + _getItemCenterPosition(item) { + if(this.orientation === 'horizontal') { + return item.finalX + (item.finalW * 0.5); + } + return item.finalY + (item.finalH * 0.5); + } + + _getScrollPosition(position) { + const s = this._fullSize; + + const viewportSize = this.viewportSize; + const marginStart = this.marginStart; + const marginEnd = this.marginEnd; + + const maxDistanceStart = 0.5 * viewportSize - marginStart; + const maxDistanceEnd = 0.5 * viewportSize - marginEnd; + if((position < maxDistanceStart) || (s < viewportSize - (marginStart + marginEnd))) { + position = maxDistanceStart; + } + else if(position > s - maxDistanceEnd) { + position = s - maxDistanceEnd; + } + return position - 0.5 * viewportSize; + } + + _navigate(dir) { + const ori = this.orientation; + if(((dir === 'right' || dir === 'left') && ori === 'horizontal') || ((dir === 'up' || dir === 'down') && ori === 'vertical')) { + const length = this.items.length; + const currentIndex = this._index; + let targetIndex = currentIndex + 1; + if(dir === 'left' || dir === 'up') { + targetIndex = currentIndex - 1; + } + + if(targetIndex > -1 && targetIndex < length) { + this._index = targetIndex; + } + else if(this.jump || (this.jumpToStart || this.jumpToEnd)) { + if(targetIndex < 0 && this.jumpToEnd) { + this._index = targetIndex + length; + } + else if(targetIndex === length && this.jumpToStart){ + this._index = 0; + } + } + + if(currentIndex !== this._index) { + this.indexChanged({index: this._index, previousIndex: currentIndex, length: this.items.length}); + } + this.scrollToFocus(); + } + return false; + } + + scrollToFocus(immediate) { + if(this.currentItem) { + const focusPosition = this._getItemCenterPosition(this.currentItem); + const scrollPosition = this._getScrollPosition(focusPosition); + if(this._scrollTransition.isRunning()) { + this._scrollTransition.reset(-scrollPosition, 0.1); + } + else { + this._scrollTransition.start(-scrollPosition); + } + if(immediate) { + this._scrollTransition.finish(); + } + } + } + + setIndex(targetIndex, immediate = false) { + if(targetIndex > -1 && targetIndex < this.items.length) { + const currentIndex = this._index; + this._index = targetIndex; + this.indexChanged({index: this._index, previousIndex: currentIndex, immediate}); + this.scrollToFocus(immediate); + } + } + + indexChanged(event) { + this.signal('indexChanged', event); + } + + _getFocused() { + return this; + } + + _reset() { + this._index = 0; + } + + _construct() { + this._index = 0; + this._scrollTransitionSettings = this.stage.transitions.createSettings({}); + } + + _init() { + const wrapper = this.tag('Wrapper'); + const or = this.orientation === 'horizontal' ? 'x' : 'y'; + wrapper.transition(or, this._scrollTransitionSettings); + this._scrollTransition = wrapper.transition(or); + wrapper.onAfterUpdate = () => { + if(this.orientation === 'horizontal') { + this._fullSize = wrapper.finalW; + } + else { + this._fullSize = wrapper.finalH; + } + }; + this._setState('Empty'); + } + + static _states() { + return [ + class Empty extends this { + }, + class Filled extends this { + _getFocused() { + return this.currentItem; + } + _handleRight() { + return this._navigate('right'); + } + + _handleLeft() { + return this._navigate('left'); + } + + _handleUp() { + return this._navigate('up'); + } + + _handleDown() { + return this._navigate('down'); + } + } + ] + } +} + +const obj$4 = { + Slider +}; + +const tools = { + player: obj, + effects: obj$1, + keyboard: obj$2, + itemlist: obj$3, + slider: obj$4 +}; + +// Exposes the ux namespace for apps. + +const ux$1 = { + Ui, + App, + tools +}; + +if (typeof window !== "undefined") { + window.ux = ux$1; +} + +class DevLauncher { + + constructor() { + } + + launch(appType, lightningOptions, options = {}) { + this._appType = appType; + this._options = options; + return this._start(lightningOptions); + } + + _handleKey(event) { + this._ui._receiveKeydown(event); + } + + _start(lightningOptions = {}) { + this._openFirewall(); + this._lightningOptions = this._getLightningOptions(lightningOptions); + return this._startApp(); + } + + _startApp() { + ux$1.Ui.staticFilesPath = __dirname + "/"; + + this._ui = new ux$1.Ui(this._lightningOptions); + this._ui.startApp(this._appType); + } + + _loadInspector() { + if (this._options.useInspector) { + /* Attach the inspector to create a fake DOM that shows where lightning elements can be found. */ + return this.loadScript(DevLauncher._uxPath + "../wpe-lightning/devtools/lightning-inspect.js"); + } else { + return Promise.resolve(); + } + } + + _openFirewall() { + // Fetch app store to ensure that proxy/image servers firewall is opened. + //fetch(`http://widgets.metrological.com/${encodeURIComponent(ux.Ui.getOption('operator') || 'metrological')}/nl/test`).then(() => {}); + } + + _getLightningOptions(customOptions = {}) { + let options = {stage: {w: 1920, h: 1080}, debug: false, keys: this._getNavigationKeys()}; + + const config = options.stage; + if (customOptions.h === 720) { + config['w'] = 1280; + config['h'] = 720; + config['precision'] = 0.6666666667; + } else { + config['w'] = 1920; + config['h'] = 1080; + + config.useImageWorker = true; + } + + options = lightning$1.tools.ObjMerger.merge(options, customOptions); + + return options; + } + + _getNavigationKeys() { + return { + 8: "Back", + 13: "Enter", + 27: "Menu", + 37: "Left", + 38: "Up", + 39: "Right", + 40: "Down", + 174: "ChannelDown", + 175: "ChannelUp", + 178: "Stop", + 250: "PlayPause", + 191: "Search", // Use "/" for keyboard + 409: "Search" + }; + } +} + +const Headers$1 = fetch.Headers; + + +class MyApp extends ux$1.App { + static _template() { + return { + BackgroundImage: { src: MyApp.getPath("background.png"), scaleX:2, scaleY:2}, + MainImage: { src: MyApp.getPath("rockies.jpeg")} + }; + } + + + _handleUp(){ + this.tag("BackgroundImage").scaleX = 1; + this.tag("BackgroundImage").scaleY = 1; + } + _handleDown(){ + this.tag("BackgroundImage").scaleX = 2; + this.tag("BackgroundImage").scaleY = 2; + } + +} + +MyApp.COLORS = { + BACKGROUND: 0xff282e32 +}; + +const launcher = new DevLauncher(); + +sparkview.on('onKeyDown', function(e) { + console.log('webgl onKeyDown keyCode:', e.keyCode); + launcher._handleKey(e); +}); + +var keymaps = { + 49: "Up", + 50: "Down" +}; +launcher.launch(MyApp, {debug:false, h:sparkscene.h, keys:keymaps}, {useInspector: false}); diff --git a/static-ux/fonts/Material-Icons.ttf b/static-ux/fonts/Material-Icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7015564ad166a3e9d88c82f17829f0cc01ebe29a GIT binary patch literal 128180 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 ohyj3!`#ee%L;DTjx@8!5k5~VH0QmdE^#A|> literal 0 HcmV?d00001 diff --git a/static-ux/fonts/roboto-regular.ttf b/static-ux/fonts/roboto-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..210c7f2bbfaa6ee2864b3c533761f36b80fd66dc GIT binary patch literal 140752 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νz5T%Sm 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 literal 0 HcmV?d00001 diff --git a/static-ux/tools/player/img/next.png b/static-ux/tools/player/img/next.png new file mode 100644 index 0000000000000000000000000000000000000000..9bdcafe6ea97bea2bbcf920e5b766ae85c640703 GIT binary patch literal 452 zcmV;#0XzPQP){1&<*5Jw0Z~ar zK~y-)tyN1-LO~GhdQLPFy7$H#cmxx@gl7;8YveRC*9&Y>da_H8Z;dFqDEI$=^c=;oE!95W;5&VYdwF6bZ?V zNpVL++TQzHk|rep?>+w&fxd{Gm|3fc{UoDdn`ASD@H7|C0$A9atAlZ=QjjCRztxH#c@C`uC%sN>;c}PQ&Bd literal 0 HcmV?d00001 diff --git a/static-ux/tools/player/img/pause.png b/static-ux/tools/player/img/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..32ef4fdf699c70efcf483c3259348f75ee4762ef GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eL!3HGH8OdY;DYhhUcNd2LAh=-f^2tCE&H|6f zVg?3oVGw3ym^DWND9B#o>Fdh=h?AYyO!LUg3#)-bTAnVBAr_~TQxXyqe*FLczn)Da wAtfcHW#h(9W5Wp?Zl*n15?2jYHJUImESSP!AQRp68>oxH)78&qol`;+0GahHaR2}S literal 0 HcmV?d00001 diff --git a/static-ux/tools/player/img/play.png b/static-ux/tools/player/img/play.png new file mode 100644 index 0000000000000000000000000000000000000000..b359f1887ddac2ac0c161371d7dfe7a82c739853 GIT binary patch literal 440 zcmV;p0Z0CcP)Px$a!Eu%R5%f(luJs)KoEu}At;6fiic$HP#)RJlaiTIG#KeT;`)haWKP`1bV6_pqG>NRSt%mkzIo%I1KU(9%8 zWo8k|)N9m9nF*d>qY)~@-mnr3^t@A>CBU?_zSA3qqJlZ4&7ot`+xkG{&dI4pQrigz z5qV?%|_+qeUAiXR*KweAv(;u#h4fuq_=Wz!KA49wqgU!&$qd?j~fNR-Jt izX9MxJMNVPBjFdmVwzB59a=^J0000*05odWI?zN2Ve)+NVgM&WV_fn zl7k@FVW{QI=&8C@-9t%2C6dI7lGHJ?%Me1r7*I3Y1@I&(CHWEX2T4jMlgS9cv!t{- zTVL6!nH>QbN$P&RTWjlXI-PD7MKO?c_S2itqEx#3MV{xc%YXpTsC+Zq1~8U%(8NIa zEhg@M3*eJ)DEjV@e69B{SVzM&S%n(BX|DXjZ$w!ik3Qdxv2jGz8 zTeP0eBUP=jKFKRdohZ;+8d6DPNqf$Vqox1Ojhs@R)|o50+1L3ClDI}@oyjo@)>o3G!eitBqRu;46Ne>ke!OA z4P-h$t*50Fj&9=R?EQ12%#BZG?dH0S*UMdU}0=+nqp{>WNBt$ w9h{SxoNaC5=xFR@X=1AD;^gY4Yhq$xs%v5CWTSSbWZfeK?07>^SivR!s literal 0 HcmV?d00001 diff --git a/static/._rockies.jpeg b/static/._rockies.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..351ed930e0e169e580337f336fa383fa65208990 GIT binary patch literal 301 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@oyjo@)>o3G!eitBqRu;46Ne>ke!OA z4P-h$t*50Fj&9=R?EQ12%#BZG?dH0S*UMdU}0=+nqp{>WNBt$ w9h{SxoNaC5=xFR@X=1AD;^gY4Yhq$xs%v5CWTSSbWZfeK?07>^SivR!s literal 0 HcmV?d00001 diff --git a/static/background.png b/static/background.png new file mode 100644 index 0000000000000000000000000000000000000000..b5256f50e198f0a2cb456fb7fb0cd202e2c568ba GIT binary patch literal 440067 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

#WX)tbift+K>q{;PhzapMl*pn0a-EWL%9Qf6MZ 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 z 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 z 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

I`#e#V<-+CbID#VWHC6C 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�n`v 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Πqj^~0 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 z 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 b#hw2EROS9-+%Pkg00000NkvXXu0mjf?3na< literal 0 HcmV?d00001 diff --git a/static/ll_image.png b/static/ll_image.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc06924c588aa7a32fdc3147ca89923cee2c219 GIT binary patch literal 15573 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 W5~#L^r~iv9gGh)paO6Jm;gQiz}(W^MNL~# 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

IYB1{ZLWD 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 z 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