From 0aa73d90c5d18274d82c9fc14a3825f159f3320a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Mon, 17 Mar 2014 16:41:57 +0100 Subject: [PATCH] Change the way context dependent font sizes are handled by delegating handling to view. Closes #425 --- src/core/PaperScope.js | 2 +- src/item/Item.js | 24 +++++++++++------ src/item/Raster.js | 10 +++---- src/project/Project.js | 23 ++++++++++------ src/style/Style.js | 60 ++++++++++++++++++++++++++---------------- src/svg/SVGExport.js | 2 +- src/svg/SVGImport.js | 2 +- src/text/PointText.js | 51 +++++++++++++---------------------- src/ui/CanvasView.js | 37 ++++++++++++++++++++++---- src/ui/View.js | 14 +++++----- 10 files changed, 135 insertions(+), 90 deletions(-) diff --git a/src/core/PaperScope.js b/src/core/PaperScope.js index 87ef3880..76e321c9 100644 --- a/src/core/PaperScope.js +++ b/src/core/PaperScope.js @@ -118,7 +118,7 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{ * @bean */ getView: function() { - return this.project && this.project.view; + return this.project && this.project.getView(); }, /** diff --git a/src/item/Item.js b/src/item/Item.js index 9de5223c..7441cf45 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -103,7 +103,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ if (point) matrix.translate(point); matrix._owner = this; - this._style = new Style(project._currentStyle, this); + this._style = new Style(project._currentStyle, this, project); // If _project is already set, the item was already moved into the DOM // hierarchy. Used by Layer, where it's added to project.layers instead if (!this._project) { @@ -154,7 +154,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ install: function(type) { // If the view requires counting of installed mouse events, // increase the counters now according to mouseFlags - var counters = this._project.view._eventCounters; + var counters = this.getView()._eventCounters; if (counters) { for (var key in mouseFlags) { counters[key] = (counters[key] || 0) @@ -165,7 +165,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ uninstall: function(type) { // If the view requires counting of installed mouse events, // decrease the counters now according to mouseFlags - var counters = this._project.view._eventCounters; + var counters = this.getView()._eventCounters; if (counters) { for (var key in mouseFlags) counters[key] -= mouseFlags[key][type] || 0; @@ -194,7 +194,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ }, _animateItem: function(animate) { - this._project.view._animateItem(this, animate); + this.getView()._animateItem(this, animate); }, _serialize: function(options, dictionary) { @@ -1225,6 +1225,15 @@ var Item = Base.extend(Callback, /** @lends Item# */{ this._installEvents(true); }, + /** + * The view that this item belongs to. + * @type View + * @bean + */ + getView: function() { + return this._project.getView(); + }, + /** * Overrides Callback#_installEvents to also call _installEvents on all * children. @@ -1566,8 +1575,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ */ rasterize: function(resolution) { var bounds = this.getStrokeBounds(), - view = this._project.view, - scale = (resolution || view && view.getResolution() || 72) / 72, + scale = (resolution || this.getView().getResolution()) / 72, // Floor top-left corner and ceil bottom-right corner, to never // blur or cut pixels. topLeft = bounds.getTopLeft().floor(), @@ -1686,7 +1694,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ // chain already to determine the rough bounds. var matrix = this._matrix, parentTotalMatrix = options._totalMatrix, - view = this._project.view, + view = this.getView(), // Keep the accumulated matrices up to this item in options, so we // can keep calculating the correct _tolerancePadding values. totalMatrix = options._totalMatrix = parentTotalMatrix @@ -1694,7 +1702,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{ // If this is the first one in the recursion, factor in the // zoom of the view and the globalMatrix of the item. : this.getGlobalMatrix().clone().preConcatenate( - view ? view._matrix : new Matrix()), + view._matrix), // Calculate the transformed padding as 2D size that describes the // transformed tolerance circle / ellipse. Make sure it's never 0 // since we're using it for division. diff --git a/src/item/Raster.js b/src/item/Raster.js index 36b46bd5..62161fe6 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -298,13 +298,13 @@ var Raster = Item.extend(/** @lends Raster# */{ image; function loaded() { - var view = that._project.view; - if (view) + var view = that.getView(); + if (view) { paper = view._scope; - that.setImage(image); - that.fire('load'); - if (view) + that.setImage(image); + that.fire('load'); view.update(); + } } /*#*/ if (__options.environment == 'browser') { diff --git a/src/project/Project.js b/src/project/Project.js index 80645a4d..dd171a43 100644 --- a/src/project/Project.js +++ b/src/project/Project.js @@ -43,20 +43,23 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * Note that when working with PaperScript, a project is automatically * created for us and the {@link PaperScope#project} variable points to it. * - * @param {View|HTMLCanvasElement} view Either a view object or an HTML - * Canvas element that should be wrapped in a newly created view. + * @param {HTMLCanvasElement} element an HTML anvas element that should be + * used as the element for the view. */ - initialize: function Project(view) { + initialize: function Project(element) { // Activate straight away by passing true to PaperScopeItem constructor, // so paper.project is set, as required by Layer and DoumentView // constructors. PaperScopeItem.call(this, true); this.layers = []; this.symbols = []; - this._currentStyle = new Style(); + this._currentStyle = new Style(null, null, this); this.activeLayer = new Layer(); - if (view) - this.view = view instanceof View ? view : View.create(view); + // If no view is provided, we create a 1x1 px canvas view just so we + // have something to do size calculations with. + // (e.g. PointText#_getBounds) + this._view = View.create(this, + element || CanvasProvider.getCanvas(1, 1)); this._selectedItems = {}; this._selectedItemCount = 0; // See Item#draw() for an explanation of _updateVersion @@ -110,8 +113,8 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ remove: function remove() { if (!remove.base.call(this)) return false; - if (this.view) - this.view.remove(); + if (this._view) + this._view.remove(); return true; }, @@ -119,7 +122,11 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * The reference to the project's view. * @name Project#view * @type View + * @bean */ + getView: function() { + return this._view; + }, /** * The currently active path style. All selected items and newly diff --git a/src/style/Style.js b/src/style/Style.js index a5e7196d..afbb2a28 100644 --- a/src/style/Style.js +++ b/src/style/Style.js @@ -136,10 +136,11 @@ var Style = Base.extend(new function() { // - Color values are not stored as converted colors immediately. The // raw value is stored, and conversion only happens in the getter. fields[set] = function(value) { - var children = this._item && this._item._children; + var owner = this._owner, + children = owner && owner._children; // Only unify styles on children of Groups, excluding CompoundPaths. if (children && children.length > 0 - && this._item._type !== 'compound-path') { + && !(owner instanceof CompoundPath)) { for (var i = 0, l = children.length; i < l; i++) children[i]._style[set](value); } else { @@ -154,28 +155,29 @@ var Style = Base.extend(new function() { // converted and cloned in the getter further down. if (value._owner) value = value.clone(); - value._owner = this._item; + value._owner = owner; } } // Note: We do not convert the values to Colors in the // setter. This only happens once the getter is called. this._values[key] = value; - // Notify the item of the style change STYLE is always set, + // Notify the owner of the style change STYLE is always set, // additional flags come from flags, as used for STROKE: - if (this._item) - this._item._changed(flag || /*#=*/ Change.STYLE); + if (owner) + owner._changed(flag || /*#=*/ Change.STYLE); } } }; fields[get] = function(_dontMerge) { - var value, - children = this._item && this._item._children; - // If this item has children, walk through all of them and see if + var owner = this._owner, + children = owner && owner._children, + value; + // If the owner has children, walk through all of them and see if // they all have the same style. // If true is passed for _dontMerge, don't merge children styles if (!children || children.length === 0 || _dontMerge - || this._item instanceof CompoundPath) { + || owner instanceof CompoundPath) { var value = this._values[key]; if (value === undefined) { value = this._defaults[key]; @@ -187,7 +189,7 @@ var Style = Base.extend(new function() { this._values[key] = value = Color.read([value], 0, { readNull: true, clone: true }); if (value) - value._owner = this._item; + value._owner = owner; } return value; } @@ -196,7 +198,7 @@ var Style = Base.extend(new function() { if (i === 0) { value = childValue; } else if (!Base.equals(value, childValue)) { - // If there is another item with a different + // If there is another child with a different // style, the style is not defined: return undefined; } @@ -220,11 +222,12 @@ var Style = Base.extend(new function() { }, /** @lends Style# */{ _class: 'Style', - initialize: function Style(style, _item) { + initialize: function Style(style, _owner, _project) { // We keep values in a separate object that we can iterate over. this._values = {}; - this._item = _item; - if (_item instanceof TextItem) + this._owner = _owner; + this._project = _owner && _owner._project || _project || paper.project; + if (_owner instanceof TextItem) this._defaults = this._textDefaults; if (style) this.set(style); @@ -269,17 +272,27 @@ var Style = Base.extend(new function() { return !!this.getShadowColor() && this.getShadowBlur() > 0; }, + /** + * The view that this style belongs to. + * @type View + * @bean + */ + getView: function() { + return this._project.getView(); + }, + // Overrides getFontStyle: function() { - var size = this.getFontSize(); + var fontSize = this.getFontSize(); // To prevent an obscure iOS 7 crash, we have to convert the size to a // string first before passing it to the regular expression. - // This nonsensical statement would also prevent the bug, prooving that - // the issue is not the regular expression itself, but something deeper - // down in the optimizer: if (size === 0) size = 0; + // The following nonsensical statement would also prevent the bug, + // prooving that the issue is not the regular expression itself, but + // something deeper down in the optimizer: + // `if (size === 0) size = 0;` return this.getFontWeight() - + ' ' + size + (/[a-z]/i.test(size + '') ? ' ' : 'px ') + + ' ' + fontSize + (/\w/i.test(fontSize + '') ? ' ' : 'px ') + this.getFontFamily(); }, @@ -297,8 +310,11 @@ var Style = Base.extend(new function() { getLeading: function getLeading() { // Override leading to return fontSize * 1.2 by default. - var leading = getLeading.base.call(this); - return leading != null ? leading : this.getFontSize() * 1.2; + var leading = getLeading.base.call(this), + fontSize = this.getFontSize(); + if (/pt|em|%|px/.test(fontSize)) + fontSize = this.getView().getPixelSize(fontSize); + return leading != null ? leading : fontSize * 1.2; } // DOCS: why isn't the example code showing up? diff --git a/src/svg/SVGExport.js b/src/svg/SVGExport.js index 21eb7588..bc64ff45 100644 --- a/src/svg/SVGExport.js +++ b/src/svg/SVGExport.js @@ -404,7 +404,7 @@ new function() { exportSVG: function(options) { options = setOptions(options); var layers = this.layers, - size = this.view.getSize(), + size = this.getView().getSize(), node = createElement('svg', { x: 0, y: 0, diff --git a/src/svg/SVGImport.js b/src/svg/SVGImport.js index 42d3237f..aa419e42 100644 --- a/src/svg/SVGImport.js +++ b/src/svg/SVGImport.js @@ -531,7 +531,7 @@ new function() { paper = scope; var item = importSVG(svg, isRoot, options), onLoad = options.onLoad, - view = scope.project && scope.project.view; + view = scope.project && scope.getView(); if (onLoad) onLoad.call(this, item); view.update(); diff --git a/src/text/PointText.js b/src/text/PointText.js index febad30b..e48d5143 100644 --- a/src/text/PointText.js +++ b/src/text/PointText.js @@ -88,7 +88,6 @@ var PointText = TextItem.extend(/** @lends PointText# */{ lines = this._lines, leading = style.getLeading(), shadowColor = ctx.shadowColor; - ctx.font = style.getFontStyle(); ctx.textAlign = style.getJustification(); for (var i = 0, l = lines.length; i < l; i++) { @@ -103,36 +102,24 @@ var PointText = TextItem.extend(/** @lends PointText# */{ ctx.strokeText(line, 0, 0); ctx.translate(0, leading); } - } -}, new function() { - var measureCtx = null; + }, - return { - _getBounds: function(getter, matrix) { - // Create an in-memory canvas on which to do the measuring - if (!measureCtx) - measureCtx = CanvasProvider.getContext(1, 1); - var style = this._style, - lines = this._lines, - count = lines.length, - justification = style.getJustification(), - leading = style.getLeading(), - x = 0; - // Measure the real width of the text. Unfortunately, there is no - // sane way to measure text height with canvas - measureCtx.font = style.getFontStyle(); - var width = 0; - for (var i = 0; i < count; i++) - width = Math.max(width, measureCtx.measureText(lines[i]).width); - // Adjust for different justifications - if (justification !== 'left') - x -= width / (justification === 'center' ? 2: 1); - // Until we don't have baseline measuring, assume 1 / 4 leading as a - // rough guess: - var bounds = new Rectangle(x, - count ? - 0.75 * leading : 0, - width, count * leading); - return matrix ? matrix._transformBounds(bounds, bounds) : bounds; - } - }; + _getBounds: function(getter, matrix) { + var style = this._style, + lines = this._lines, + numLines = lines.length, + justification = style.getJustification(), + leading = style.getLeading(), + width = this.getView().getTextWidth(style.getFontStyle(), lines), + x = 0; + // Adjust for different justifications. + if (justification !== 'left') + x -= width / (justification === 'center' ? 2: 1); + // Until we don't have baseline measuring, assume 1 / 4 leading as a + // rough guess: + var bounds = new Rectangle(x, + numLines ? - 0.75 * leading : 0, + width, numLines * leading); + return matrix ? matrix._transformBounds(bounds, bounds) : bounds; + } }); diff --git a/src/ui/CanvasView.js b/src/ui/CanvasView.js index ed88d459..f0018b39 100644 --- a/src/ui/CanvasView.js +++ b/src/ui/CanvasView.js @@ -31,7 +31,7 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ * @name CanvasView#initialize * @param {Size} size the size of the canvas to be created */ - initialize: function CanvasView(canvas) { + initialize: function CanvasView(project, canvas) { // Handle canvas argument if (!(canvas instanceof HTMLCanvasElement)) { // See if the arguments describe the view size: @@ -56,7 +56,7 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ this._pixelRatio = deviceRatio / backingStoreRatio; } /*#*/ } // __options.environment == 'browser' - View.call(this, canvas); + View.call(this, project, canvas); }, _setViewSize: function(size) { @@ -77,13 +77,40 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ } }, + /** + * Converts the provide size in any of the units allowed in the browser to + * pixels, by the use of the context.font property. + */ + getPixelSize: function(size) { + var ctx = this._context, + prevFont = ctx.font; + ctx.font = size + ' serif'; + size = parseFloat(ctx.font); + ctx.font = prevFont; + return size; + }, + + getTextWidth: function(font, lines) { + var ctx = this._context, + prevFont = ctx.font, + width = 0; + ctx.font = font; + // Measure the real width of the text. Unfortunately, there is no sane + // way to measure text height with canvas. + for (var i = 0, l = lines.length; i < l; i++) + width = Math.max(width, ctx.measureText(lines[i]).width); + ctx.font = prevFont; + return width; + }, + /** * Updates the view if there are changes. * * @function */ update: function() { - if (!this._project._needsUpdate) + var project = this._project; + if (!project || !project._needsUpdate) return false; // Initial tests conclude that clearing the canvas using clearRect // is always faster than setting canvas.width = canvas.width @@ -91,8 +118,8 @@ var CanvasView = View.extend(/** @lends CanvasView# */{ var ctx = this._context, size = this._viewSize; ctx.clearRect(0, 0, size.width + 1, size.height + 1); - this._project.draw(ctx, this._matrix, this._pixelRatio); - this._project._needsUpdate = false; + project.draw(ctx, this._matrix, this._pixelRatio); + project._needsUpdate = false; return true; } }, new function() { // Item based mouse handling: diff --git a/src/ui/View.js b/src/ui/View.js index 6a8d2439..cd7d3182 100644 --- a/src/ui/View.js +++ b/src/ui/View.js @@ -22,11 +22,11 @@ var View = Base.extend(Callback, /** @lends View# */{ _class: 'View', - initialize: function View(element) { + initialize: function View(project, element) { // Store reference to the currently active global paper scope, and the // active project, which will be represented by this view - this._scope = paper; - this._project = paper.project; + this._project = project; + this._scope = project._scope; this._element = element; var size; /*#*/ if (__options.environment == 'browser') { @@ -139,8 +139,8 @@ var View = Base.extend(Callback, /** @lends View# */{ View._views.splice(View._views.indexOf(this), 1); delete View._viewsById[this._id]; // Unlink from project - if (this._project.view == this) - this._project.view = null; + if (this._project._view === this) + this._project._view = null; /*#*/ if (__options.environment == 'browser') { // Uninstall event handlers again for this view. DomEvent.remove(this._element, this._viewEvents); @@ -658,14 +658,14 @@ var View = Base.extend(Callback, /** @lends View# */{ _viewsById: {}, _id: 0, - create: function(element) { + create: function(project, element) { /*#*/ if (__options.environment == 'browser') { if (typeof element === 'string') element = document.getElementById(element); /*#*/ } // __options.environment == 'browser' // Factory to provide the right View subclass for a given element. // Produces only CanvasViews for now: - return new CanvasView(element); + return new CanvasView(project, element); } } }, new function() {