Skip to content

Instantly share code, notes, and snippets.

@jviereck
Created February 17, 2010 20:54
Show Gist options
  • Save jviereck/307001 to your computer and use it in GitHub Desktop.
Save jviereck/307001 to your computer and use it in GitHub Desktop.
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Bespin.
*
* The Initial Developer of the Original Code is
* Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Bespin Team ([email protected])
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
var SC = require('sproutcore/runtime').SC;
var Rect = require('utils/rect');
/**
* @class
*
* This class provides support for manual scrolling and positioning for canvas-
* based elements. Getting these elements to play nicely with SproutCore is
* tricky and error-prone, so all canvas-based views should consider deriving
* from this class. Derived views should implement drawRect() in order to
* perform the appropriate canvas drawing logic.
*
* The actual size of the canvas is always the size of the container the canvas
* view is placed in.
*/
exports.CanvasView = SC.View.extend({
_cvCanvasContext: null,
_cvCanvasDom: null,
_cvCanvasId: null,
_cvInvalidRects: null,
_cvLastRedrawTime: null,
_cvPreviousClippingFrame: null,
_cvRedrawTimer: null,
_cvFullRepaint: null,
updateOnDownScale: true,
contentOfScrollView: false,
_cvClippingFrameChanged: function() {
// False positives here are very common, so check to make sure before
// we take the slow path.
var previousClippingFrame = this._cvPreviousClippingFrame;
var clippingFrame = this.get('clippingFrame');
if (previousClippingFrame === null ||
!SC.rectsEqual(clippingFrame, previousClippingFrame)) {
this.clippingFrameChanged();
this._cvPreviousClippingFrame = clippingFrame;
}
}.observes('clippingFrame'),
_cvGetContext: function() {
if (this._cvCanvasContext === null) {
this._cvCanvasContext = this._cvCanvasDom.getContext('2d');
}
return this._cvCanvasContext;
},
_cvLayoutChanged: function() {
this._cvResizeToFit();
}.observes('layout'),
_cvParentViewFrameChanged: function() {
this._cvResizeToFit();
},
_cvResizeToFit: function() {
var parentFrame = this.getPath('parentView.frame');
var frame = this.get('frame');
var frameWidth = frame.width, frameHeight = frame.height;
var parentWidth = parentFrame.width, parentHeight = parentFrame.height;
if (frameWidth >= parentWidth && frameHeight >= parentHeight) {
return;
}
this.set('layout', {
left: frame.x,
top: frame.y,
width: Math.max(frameWidth, parentWidth),
height: Math.max(frameHeight, parentHeight)
});
},
/**
* Subclasses can override this method to provide custom behavior whenever
* the clipping frame changes. The default implementation simply
* invalidates the entire visible area.
*/
clippingFrameChanged: function() {
var needsDisplay = true;
if (this.contentOfScrollView) {
var clippingFrame = this.get('clippingFrame');
var parentFrame = this.getPath('parentView.frame');
var rect = {
width: Math.max(clippingFrame.width, parentFrame.width),
height: Math.max(clippingFrame.height, parentFrame.height)
}
console.log('clippingFrameChanged', this.name, clippingFrame.width, rect.width);
needsDisplay = !this._cvChangeCanvasSize(rect);
}
if (needsDisplay) {
var previousClippingFrame = this._cvPreviousClippingFrame;
var clippingFrame = this.get('clippingFrame');
if (!previousClippingFrame || this.updateOnDownScale || previousClippingFrame.x != clippingFrame.x || previousClippingFrame.y != clippingFrame.y) {
this.setNeedsDisplay();
}
}
},
layoutStyle: { left: "0px", top: "0px" },
/**
* @property{Number}
*
* The minimum delay between canvas redraws in milliseconds, equal to 1000
* divided by the desired number of frames per second.
*/
minimumRedrawDelay: 1000.0 / 30.0,
init: function() {
arguments.callee.base.apply(this, arguments);
this._cvInvalidRects = [];
this.get('parentView').addObserver('frame', this,
this._cvParentViewFrameChanged);
},
setCursor: function(cssCursor) {
return this._cvCanvasDom.style.cursor = cssCursor;
},
/**
* Calls drawRect() on all the invalid rects to redraw the canvas contents.
* Generally, you should not need to call this function unless you override
* the default implementations of didCreateLayer() or render().
*
* @return True on a successful render, or false if the rendering wasn't
* done because the view wasn't visible.
*/
redraw: function() {
var frame = this.get('frame');
if (frame.x < 0) {
this.computeFrameWithParentFrame(null);
}
var context = this._cvGetContext();
context.save();
context.translate(frame.x, frame.y);
if (this._cvFullRepaint) {
var _frame = this.get('frame');
this._cvInvalidRects = [
{
x: 0,
y: 0,
width: _frame.width,
height: _frame.height
}];
this._cvFullRepaint = false;
} else {
this._cvInvalidRects = Rect.merge(this._cvInvalidRects);
}
var clippingFrame = this.get('clippingFrame');
this._cvInvalidRects.forEach(function(rect) {
context.save();
rect = SC.intersectRects(rect, clippingFrame);
if (rect.width !== 0 && rect.height !== 0) {
var x = rect.x, y = rect.y;
var width = rect.width, height = rect.height;
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + width, y);
context.lineTo(x + width, y + height);
context.lineTo(x, y + height);
context.closePath();
context.clip();
this.drawRect(rect, context);
}
context.restore();
}, this);
context.restore();
this._cvInvalidRects = [];
this._cvLastRedrawTime = new Date().getTime();
return true;
},
renderLayout: function(context, firstTime) {
arguments.callee.base.apply(this, arguments);
var parentFrame = this.getPath('parentView.frame');
if (firstTime) {
context.attr('width', parentFrame.width);
context.attr('height', parentFrame.height);
return;
}
if (this.name) {
console.log('renderLayout', this.name, parentFrame.width, parentFrame.height);
}
this._cvChangeCanvasSize(parentFrame);
},
_cvChangeCanvasSize: function(rect) {
var canvas = this._cvCanvasDom;
if (!canvas) {
return;
}
var widthChanged = canvas.width != rect.width;
var heightChanged = canvas.height != rect.height;
if (widthChanged) {
canvas.width = rect.width;
if (this.name) {
console.log('change width of', this.name, rect.width);
}
}
if (heightChanged) {
canvas.height = rect.height;
}
if (widthChanged || heightChanged) {
this.setNeedsDisplay();
return true;
} else {
return false;
}
},
/**
* Subclasses should override this method to perform any drawing that they
* need to.
*
* @param rect{Rect} The rectangle to draw in.
* @param context{CanvasRenderingContext2D} The 2D graphics context, taken
* from the canvas.
*/
drawRect: function(rect, context) {
// empty
},
didCreateLayer: function() {
arguments.callee.base.apply(this, arguments);
this._cvCanvasDom = this.$("#" + this._cvCanvasId)[0];
this.redraw();
},
render: function(context, firstTime) {
arguments.callee.base.apply(this, arguments);
if (firstTime) {
var parentFrame = this.getPath('parentView.frame');
var canvasContext = context.begin("canvas");
this._cvCanvasId = SC.guidFor(canvasContext);
canvasContext.id(this._cvCanvasId);
canvasContext.attr("width", "" + parentFrame.width);
canvasContext.attr("height", "" + parentFrame.height);
canvasContext.push("canvas tag not supported by your browser");
canvasContext.end();
return;
}
// The render() method can actually get called multiple times during a
// run loop, because other calls to render() can be queued after the
// first render() runs. This workaround forces tryRedraw() to be called
// only once per run loop.
SC.RunLoop.currentRunLoop.invokeLast(this, this.tryRedraw);
},
/**
* Invalidates the entire visible region of the canvas.
*/
setNeedsDisplay: function(rect) {
this._cvFullRepaint = true;
this.set('layerNeedsUpdate', true);
},
/**
* Invalidates the given rect of the canvas, and schedules that portion of
* the canvas to be redrawn at the end of the run loop.
*/
setNeedsDisplayInRect: function(rect) {
if (this._cvFullRepaint !== true) {
this._cvInvalidRects.push(rect);
this.set('layerNeedsUpdate', true);
}
},
tryRedraw: function(context, firstTime) {
var now = new Date().getTime();
var lastRedrawTime = this._cvLastRedrawTime;
var minimumRedrawDelay = this.get('minimumRedrawDelay');
if (lastRedrawTime === null ||
now - lastRedrawTime >= minimumRedrawDelay) {
this.redraw();
return;
}
var redrawTimer = this._cvRedrawTimer;
if (redrawTimer !== null && redrawTimer.get('isValid')) {
return; // already scheduled
}
this._cvRedrawTimer = SC.Timer.schedule({
target: this,
action: this.redraw,
interval: minimumRedrawDelay,
repeats: false
});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment