UNPKG

blossom

Version:

Modern, Cross-Platform Application Framework

471 lines (379 loc) 14.5 kB
// ========================================================================== // Project: Blossom - Modern, Cross-Platform Application Framework // Copyright: ©2012 Fohr Motion Picture Studios. All rights reserved. // License: Licensed under the GPLv3 license (see BLOSSOM-LICENSE). // ========================================================================== /*globals sc_assert */ sc_require('surfaces/leaf'); var base03 = "#002b36"; var base02 = "#073642"; var base01 = "#586e75"; var base00 = "#657b83"; var base0 = "#839496"; var base1 = "#93a1a1"; var base2 = "#eee8d5"; var base3 = "#fdf6e3"; var yellow = "#b58900"; var orange = "#cb4b16"; var red = "#dc322f"; var magenta = "#d33682"; var violet = "#6c71c4"; var blue = "#268bd2"; var cyan = "#2aa198"; var green = "#859900"; var white = "white"; /** @class `SC.View` implements an view controller, composed of a layer tree with associated behaviors. @extends SC.Surface @since Blossom 1.0 */ SC.View = SC.LeafSurface.extend({ // .......................................................... // PSURFACE SUPPORT // __tagName__: 'canvas', __useContentSize__: true, // we need our width and height attributes set init: function() { arguments.callee.base.apply(this, arguments); // We need to observe layers for changes; set that up now. if (!this.layers) { this.layers = []; this._sc_layersDidChange(); // Also handle providing layer classes to instantiate. } else { var layers = this.layers, ary = []; for (var idx=0, len=layers.length; idx<len; ++idx) { var layer = layers[idx]; if (layer.isClass) layer = layer.create(); sc_assert(layer.kindOf(SC.Layer)); ary.push(layer); } this.layers = ary; this._sc_layersDidChange(); } var canvas = document.createElement('canvas'), context = canvas.getContext('2d'), bounds = this.get('frame'); this._sc_hitTestCanvas = canvas; context.__sc_canvas__ = canvas; canvas.width = bounds[2]/*width*/; canvas.height = bounds[3]/*height*/; if (this.get('isHitTestOnly')) { var defineRect = SC.Layer._sc_defineRect, K = SC.K; // Override those drawing operations that take time, but that we'll // never see the visible effects of. SC.mixin(context, { stroke: K, fill: K, drawImage: K, fillText: K, strokeText: K, fillRect: defineRect, strokeRect: defineRect }); } }, layers: [], // .......................................................... // LAYER TREE SUPPORT // // When the subsurfaces property changes, we need to observe it's members // for changes. _sc_layers: null, _sc_layersDidChange: function() { // console.log("SC.View#_sc_layersDidChange()"); var cur = this.get('layers'), last = this._sc_layers, func = this._sc_layersMembersDidChange; if (last === cur) return this; // nothing to do // teardown old observer sc_assert(last? last.isEnumerable : true); if (last) last.removeObserver('[]', this, func); // save new cached values sc_assert(cur && cur.constructor.prototype === Array.prototype); this._sc_subsurfaces = cur; // setup new observers sc_assert(cur? cur.isEnumerable : true); if (cur) cur.addObserver('[]', this, func); // process the changes this._sc_layersMembersDidChange(); }.observes('layers'), _sc_layersMembersDidChange: function() { // console.log("SC.View#_sc_layersMembersDidChange()"); var layers = this.get('layers'); // FIXME: Do this smarter! for (var idx=0, len=layers.length; idx<len; ++idx) { layers[idx].set('surface', this); } this.triggerLayoutAndRendering(); }, // .......................................................... // RENDERING SUPPORT // performRenderingIfNeeded: function(timestamp) { // console.log('SC.Surface#performRenderingIfNeeded()'); var needsLayout = this.__needsLayout__, needsDisplay = this.__needsRendering__, isVisible = this.get('isVisible'); var benchKey = 'SC.Surface#performRenderingIfNeeded()', displayKey = 'SC.Surface#performRenderingIfNeeded(): needsDisplay'; SC.Benchmark.start(benchKey); var hasDirtyLayer = false, idx, len, layers = this.get('layers'); function checkLayer(layer) { if (layer.__needsRendering__) { hasDirtyLayer = true; return; } else { var idx, len, layers = layer.get('sublayers'); for (idx=0, len=layers.length; idx<len; ++idx) { checkLayer(layers[idx]); if (hasDirtyLayer) break; } } } for (idx=0, len=layers.length; idx<len; ++idx) { checkLayer(layers[idx]); if (hasDirtyLayer) break; } SC.Benchmark.start(displayKey); if (this.get('isPresentInViewport')) { if (hasDirtyLayer || this.__needsRendering__) { if (this.updateDisplay) this.updateDisplay(); this.__needsRendering__ = false; } } // else leave it set to true, we'll update it when it again becomes // visible in the viewport SC.Benchmark.end(displayKey); SC.Benchmark.end(benchKey); }, triggerLayoutAndRendering: function() { // console.log('SC.View#triggerLayoutAndRendering()', SC.guidFor(this)); arguments.callee.base.apply(this, arguments); var layers = this.get('layers'); // FIXME: Do this smarter! for (var idx=0, len=layers.length; idx<len; ++idx) { layers[idx].triggerLayoutAndRendering(); } }, updateLayout: function() { // console.log('SC.View#updateLayout()', SC.guidFor(this)); var benchKey = 'SC.View#updateLayout()'; SC.Benchmark.start(benchKey); var layers = this.get('layers'), layer, textLayersNeedingLayout = []; for (var idx=0, len=layers.length; idx<len; ++idx) { layer = layers[idx]; layer.updateLayout(textLayersNeedingLayout); // if (layer.updateTextLayout && layer.__needsTextLayout__) { // textLayersNeedingLayout.push(layer); // } } var frame = this.get('frame'), hitTestCanvas = this._sc_hitTestCanvas, ctx = hitTestCanvas.getContext('2d'); hitTestCanvas.width = frame[2]/*width*/; hitTestCanvas.height = frame[3]/*height*/; for (idx=0, len=textLayersNeedingLayout.length; idx<len; ++idx) { textLayersNeedingLayout[idx].updateTextLayout(ctx); } SC.Benchmark.end(benchKey); }, clearBackground: false, _sc_backgroundColor: base3, didCreateElement: function(canvas) { // console.log('SC.View#didCreateElement()', SC.guidFor(this)); arguments.callee.base.apply(this, arguments); var ctx = canvas.getContext('2d'); // Enables ctx.width and ctx.height to work. ctx.__sc_canvas__ = canvas; this._sc_context = ctx; this.triggerRendering(); }, _sc_scrollTranslation: null, updateDisplay: function() { // console.log('SC.View#updateDisplay()', SC.guidFor(this)); var benchKey = 'SC.ViewSurface#updateDisplay()', updateKey = 'SC.ViewSurface#updateDisplay() - update', renderKey = 'SC.ViewSurface#updateDisplay() - render', copyKey = 'SC.ViewSurface#updateDisplay() - copy'; SC.Benchmark.start(benchKey); var ctx = this._sc_context; sc_assert(ctx); sc_assert(document.getElementById(ctx.__sc_canvas__.id)); // Clear the background if requested. if (this.get('clearBackground')) ctx.clearRect(0, 0, ctx.w, ctx.h); else { // We need to draw the background color, even though it is also // applied with CSS, in order to get correct anti-aliasing. ctx.fillStyle = this.get('backgroundColor'); ctx.fillRect(0, 0, ctx.w, ctx.h); } if (this.willRenderLayers) { ctx.save(); this.willRenderLayers(ctx); ctx.restore(); } // // Re-cache layers that need updating. // SC.Benchmark.start(updateKey); // var layers = this.get('layers'); // for (var idx=0, len=layers.length; idx<len; ++idx) { // layers[idx].updateDisplay(); // } // SC.Benchmark.end(updateKey); var scrollTranslation = this._sc_scrollTranslation; if (scrollTranslation) { ctx.save(); ctx.translate(scrollTranslation[0]/*x*/, scrollTranslation[1]/*y*/); } // // Composite layers into view's canvas. // SC.Benchmark.start(copyKey); // for (idx=0, len=layers.length; idx<len; ++idx) { // layers[idx].copyIntoContext(ctx); // } // SC.Benchmark.end(copyKey); // Render layers into view's canvas directly. SC.Benchmark.start(renderKey); var layers = this.get('layers'); for (var idx=0, len=layers.length; idx<len; ++idx) { layers[idx].renderIntoContext(ctx); } SC.Benchmark.end(renderKey); if (scrollTranslation) ctx.restore(); if (this.didRenderLayers) { ctx.save(); this.didRenderLayers(ctx); ctx.restore(); } SC.Benchmark.end(benchKey); }, /** Finds the layer that is hit by this event, and returns its behavior if it exists. Otherwise, returns the receiver. */ targetResponderForEvent: function(evt) { // console.log('SC.ViewSurface#targetResponderForEvent(', evt, ')'); var context = this._sc_hitTestCanvas.getContext('2d'), hitLayer = null, zIndex = -1, boundingRect, x, y; // debugger; boundingRect = evt.target.getBoundingClientRect(); x = evt.clientX - boundingRect.left + window.pageXOffset; y = evt.clientY - boundingRect.top + window.pageYOffset; // console.log('*****', evt.type, x, y, '*****'); function spaces(depth) { console.log(depth); var ret = "", idx, len; for (idx = 0, len = depth; idx<len; ++idx) ret += "--"; return ret; } function hitTestLayer(layer, point, depth) { // debugger; // console.log(spaces(depth), "on entry:", point.x, point.y); if (!layer.get('isVisible')) return; context.save(); // Prevent this layer and any sublayer from drawing paths outside our // bounds. layer.renderBoundsPath(context); context.clip(); // Make sure the layer's transform is current. if (layer._sc_transformFromSuperlayerToLayerIsDirty) { layer._sc_computeTransformFromSuperlayerToLayer(); } // Apply the sublayer's transform from our layer (it's superlayer). var t = layer._sc_transformFromSuperlayerToLayer; context.transform(t[0], t[1], t[2], t[3], t[4], t[5]); SC.PointApplyAffineTransformTo(point, t, point); var frame = layer.get('frame'); // console.log(spaces(depth), 'frame:', frame.x, frame.y, frame.width, frame.height); // console.log(spaces(depth), 'transformed:', point.x, point.y); // First, test our sublayers. var sublayers = layer.get('sublayers'), idx = sublayers.length; depth++; while (idx--) { hitTestLayer(sublayers[idx], SC.MakePoint(point), depth); } // Only test ourself if (a) no hit has been found, or (b) our zIndex is // higher than whatever hit has been found so far. var layerZ = layer.get('zIndex'); if (!hitLayer || zIndex < layerZ) { if (layer.hitsPointInContext(x, y, context)) { evt.hitPoint = SC.MakePoint(x - point[0]/*x*/, y - point[1]/*y*/); hitLayer = layer; zIndex = layerZ; } } context.restore(); } // Next, begin the hit testing process. When this completes, hitLayer // will contain the layer that was hit with the highest zIndex. var layers = this.get('layers'), idx = layers.length; while (idx--) { hitTestLayer(layers[idx], SC.MakePoint(), 0); } // We don't need to test `layer`, because we already know it was hit when // this method is called by SC.RootResponder. if (!hitLayer) return this; else { // If we hit a layer, remember it so our view knows. evt.layer = hitLayer; // Try and find the behavior attached to this layer. var behavior = hitLayer.get('behavior'); while (!behavior && hitLayer) { hitLayer = hitLayer.get('superlayer'); if (hitLayer) behavior = hitLayer.get('behavior'); } return behavior || this; } }, /** This updates the `layer` and `hitPoint` properties on evt as if the layer was clicked by the mouse. The layer must be part of a layer tree in this surface, the event must be a mouse event, and the surface must have been active (part of SC.app's `surfaces` or the `ui` surface) *before* the event occurred. */ updateEventForLayer: function(evt, layer) { sc_assert(layer); sc_assert(layer.kindOf(SC.Layer)); sc_assert(evt); sc_assert(evt instanceof SC.Event); var psurface, boundingRect, x, y, parents, superlayer, type = evt.type; if (layer.get('surface') !== this) return; // If we're not currently an active surface, there's nothing to do. psurface = SC.psurfaces[this.__id__]; if (!psurface) return; // Must be a mouse event. if (type !== 'mousedown' && type !== 'mousemove' && type !== 'mouseup') return; evt.layer = layer; boundingRect = psurface.__element__.getBoundingClientRect(); x = evt.clientX - boundingRect.left + window.pageXOffset; y = evt.clientY - boundingRect.top + window.pageYOffset; // Gather the superlayers (with the layer first). parents = [layer]; superlayer = layer.get('superlayer'); while (superlayer) { parents.push(superlayer); superlayer = superlayer.get('superlayer'); } var len = parents.length, point = SC.MakePoint(); // Transform point from top-most superlayer on down to the layer. while (len--) { layer = parents[len]; // Make sure the layer's transform is current. if (layer._sc_transformFromSuperlayerToLayerIsDirty) { layer._sc_computeTransformFromSuperlayerToLayer(); } var t = layer._sc_transformFromSuperlayerToLayer; SC.PointApplyAffineTransformTo(point, t, point); } // Adjust for the surface. point[0]/*x*/ = x-point[0]/*x*/; point[1]/*y*/ = y-point[1]/*y*/; // point has now been transformed to layer's coordinate system evt.hitPoint = point; } });