/* * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. * http://paperjs.org/ * * Copyright (c) 2011 - 2014, Juerg Lehni & Jonathan Puckey * http://scratchdisk.com/ & http://jonathanpuckey.com/ * * Distributed under the MIT license. See LICENSE file for details. * * All rights reserved. */ /** * @name Style * * @class Style is used for changing the visual styles of items * contained within a Paper.js project and is returned by * {@link Item#style} and {@link Project#currentStyle}. * * All properties of Style are also reflected directly in {@link Item}, * i.e.: {@link Item#fillColor}. * * To set multiple style properties in one go, you can pass an object to * {@link Item#style}. This is a convenient way to define a style once and * apply it to a series of items: * * @classexample {@paperscript} // Styling paths * * var path = new Path.Circle(new Point(80, 50), 30); * path.style = { * fillColor: new Color(1, 0, 0), * strokeColor: 'black', * strokeWidth: 5 * }; * * @classexample {@paperscript} // Styling text items * var text = new PointText(view.center); * text.content = 'Hello world.'; * text.style = { * fontFamily: 'Courier New', * fontWeight: 'bold', * fontSize: 20, * fillColor: 'red', * justification: 'center' * }; * * @classexample {@paperscript} // Styling groups * var path1 = new Path.Circle({ * center: [100, 50], * radius: 30 * }); * * var path2 = new Path.Rectangle({ * from: [170, 20], * to: [230, 80] * }); * * var group = new Group(path1, path2); * * // All styles set on a group are automatically * // set on the children of the group: * group.style = { * strokeColor: 'black', * dashArray: [4, 10], * strokeWidth: 4, * strokeCap: 'round' * }; * */ var Style = Base.extend(new function() { // windingRule / resolution / fillOverprint / strokeOverprint are currently // not supported. var defaults = { // Paths fillColor: undefined, strokeColor: undefined, strokeWidth: 1, strokeCap: 'butt', strokeJoin: 'miter', miterLimit: 10, dashOffset: 0, dashArray: [], windingRule: 'nonzero', // Shadows shadowColor: undefined, shadowBlur: 0, shadowOffset: new Point(), // Selection selectedColor: undefined, // Characters fontFamily: 'sans-serif', fontWeight: 'normal', fontSize: 12, font: 'sans-serif', // deprecated, links to fontFamily leading: null, // Paragraphs justification: 'left' }; var flags = { strokeWidth: /*#=*/ Change.STROKE, strokeCap: /*#=*/ Change.STROKE, strokeJoin: /*#=*/ Change.STROKE, miterLimit: /*#=*/ Change.STROKE, fontFamily: /*#=*/ Change.GEOMETRY, fontWeight: /*#=*/ Change.GEOMETRY, fontSize: /*#=*/ Change.GEOMETRY, font: /*#=*/ Change.GEOMETRY, // deprecated, links to fontFamily leading: /*#=*/ Change.GEOMETRY, justification: /*#=*/ Change.GEOMETRY }; var item = {}, fields = { _defaults: defaults, // Override default fillColor for text items _textDefaults: new Base(defaults, { fillColor: new Color() // black }) }; Base.each(defaults, function(value, key) { var isColor = /Color$/.test(key), part = Base.capitalize(key), flag = flags[key], set = 'set' + part, get = 'get' + part; // Define getters and setters to be injected into this class. // This is how style values are handled: // - Style values are all stored in this._values // - The style object starts with an empty _values object, with fallback // on _defaults through code in the getter below. // - Only the styles that are explicitely set on the object get defined // in _values. // - 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; // Only unify styles on children of Groups, excluding CompoundPaths. if (children && children.length > 0 && this._item._type !== 'compound-path') { for (var i = 0, l = children.length; i < l; i++) children[i]._style[set](value); } else { var old = this._values[key]; if (old != value) { if (isColor) { if (old) delete old._owner; if (value && value.constructor === Color) { // Clone color if it already has an owner. // NOTE: If value is not a Color, it is only // converted and cloned in the getter further down. if (value._owner) value = value.clone(); value._owner = this._item; } } // 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, // additional flags come from flags, as used for STROKE: if (this._item) this._item._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 // they all have the same style. // If true is passed for dontMerge, don't merge children styles if (!children || children.length === 0 || arguments[0] || this._item instanceof CompoundPath) { var value = this._values[key]; if (value === undefined) { value = this._defaults[key]; if (value && value.clone) value = value.clone(); this._values[key] = value; } else if (isColor && !(value && value.constructor === Color)) { // Convert to a Color and stored result of conversion. this._values[key] = value = Color.read( [value], 0, 0, { readNull: true, clone: true }); if (value) value._owner = this._item; } return value; } for (var i = 0, l = children.length; i < l; i++) { var childValue = children[i]._style[get](); if (i === 0) { value = childValue; } else if (!Base.equals(value, childValue)) { // If there is another item with a different // style, the style is not defined: return undefined; } } return value; }; // Inject style getters and setters into the Item class, which redirect // calls to the linked style object. item[get] = function() { return this._style[get](); }; item[set] = function(value) { this._style[set](value); }; }); Item.inject(item); return fields; }, /** @lends Style# */{ _class: 'Style', initialize: function Style(style, _item) { // We keep values in a separate object that we can iterate over. this._values = {}; this._item = _item; if (_item instanceof TextItem) this._defaults = this._textDefaults; if (style) this.set(style); }, set: function(style) { // If the passed style object is also a Style, clone its clonable // fields rather than simply copying them. var isStyle = style instanceof Style, // Use the other stlyle's _values object for iteration values = isStyle ? style._values : style; if (values) { for (var key in values) { if (key in this._defaults) { var value = values[key]; // Delegate to setter, so Group styles work too. this[key] = value && isStyle && value.clone ? value.clone() : value; } } } }, equals: function(style) { return style === this || style && this._class === style._class && Base.equals(this._values, style._values) || false; }, // DOCS: Style#hasFill() hasFill: function() { return !!this.getFillColor(); }, // DOCS: Style#hasStroke() hasStroke: function() { return !!this.getStrokeColor() && this.getStrokeWidth() > 0; }, // DOCS: Style#hasShadow() hasShadow: function() { return !!this.getShadowColor() && this.getShadowBlur() > 0; }, // Overrides getFontStyle: function() { var size = 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; return this.getFontWeight() + ' ' + size + (/[a-z]/i.test(size + '') ? ' ' : 'px ') + this.getFontFamily(); }, /** * @private * @deprecated use {@link #getFontFamily()} instead. */ getFont: '#getFontFamily', /** * @private * @deprecated use {@link #setFontFamily(font)} instead. */ setFont: '#setFontFamily', 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; } // DOCS: why isn't the example code showing up? /** * Style objects don't need to be created directly. Just pass an object to * {@link Item#style} or {@link Project#currentStyle}, it will be converted * to a Style object internally. * * @name Style#initialize * @param {Object} style */ /** * {@grouptitle Stroke Style} * * The color of the stroke. * * @name Style#strokeColor * @property * @type Color * * @example {@paperscript} * // Setting the stroke color of a path: * * // Create a circle shaped path at { x: 80, y: 50 } * // with a radius of 35: * var circle = new Path.Circle(new Point(80, 50), 35); * * // Set its stroke color to RGB red: * circle.strokeColor = new Color(1, 0, 0); */ /** * The width of the stroke. * * @name Style#strokeWidth * @property * @default 1 * @type Number * * @example {@paperscript} * // Setting an item's stroke width: * * // Create a circle shaped path at { x: 80, y: 50 } * // with a radius of 35: * var circle = new Path.Circle(new Point(80, 50), 35); * * // Set its stroke color to black: * circle.strokeColor = 'black'; * * // Set its stroke width to 10: * circle.strokeWidth = 10; */ /** * The shape to be used at the end of open {@link Path} items, when they * have a stroke. * * @name Style#strokeCap * @property * @default 'butt' * @type String('round', 'square', 'butt') * * @example {@paperscript height=200} * // A look at the different stroke caps: * * var line = new Path(new Point(80, 50), new Point(420, 50)); * line.strokeColor = 'black'; * line.strokeWidth = 20; * * // Select the path, so we can see where the stroke is formed: * line.selected = true; * * // Set the stroke cap of the line to be round: * line.strokeCap = 'round'; * * // Copy the path and set its stroke cap to be square: * var line2 = line.clone(); * line2.position.y += 50; * line2.strokeCap = 'square'; * * // Make another copy and set its stroke cap to be butt: * var line2 = line.clone(); * line2.position.y += 100; * line2.strokeCap = 'butt'; */ /** * The shape to be used at the corners of paths when they have a stroke. * * @name Style#strokeJoin * @property * @default 'miter' * @type String('miter', 'round', 'bevel') * * @example {@paperscript height=120} * // A look at the different stroke joins: * var path = new Path(); * path.add(new Point(80, 100)); * path.add(new Point(120, 40)); * path.add(new Point(160, 100)); * path.strokeColor = 'black'; * path.strokeWidth = 20; * * // Select the path, so we can see where the stroke is formed: * path.selected = true; * * var path2 = path.clone(); * path2.position.x += path2.bounds.width * 1.5; * path2.strokeJoin = 'round'; * * var path3 = path2.clone(); * path3.position.x += path3.bounds.width * 1.5; * path3.strokeJoin = 'bevel'; */ /** * The dash offset of the stroke. * * @name Style#dashOffset * @property * @default 0 * @type Number */ /** * Specifies an array containing the dash and gap lengths of the stroke. * * @example {@paperscript} * var path = new Path.Circle(new Point(80, 50), 40); * path.strokeWidth = 2; * path.strokeColor = 'black'; * * // Set the dashed stroke to [10pt dash, 4pt gap]: * path.dashArray = [10, 4]; * * @name Style#dashArray * @property * @default [] * @type Array */ /** * The miter limit of the stroke. When two line segments meet at a sharp * angle and miter joins have been specified for {@link #strokeJoin}, it is * possible for the miter to extend far beyond the {@link #strokeWidth} of * the path. The miterLimit imposes a limit on the ratio of the miter length * to the {@link #strokeWidth}. * * @name Style#miterLimit * @property * @default 10 * @type Number */ /** * {@grouptitle Fill Style} * * The fill color. * * @name Style#fillColor * @property * @type Color * * @example {@paperscript} * // Setting the fill color of a path to red: * * // Create a circle shaped path at { x: 80, y: 50 } * // with a radius of 35: * var circle = new Path.Circle(new Point(80, 50), 35); * * // Set the fill color of the circle to RGB red: * circle.fillColor = new Color(1, 0, 0); */ /** * {@grouptitle Shadow Style} * * The shadow color. * * @property * @name Style#shadowColor * @type Color * * @example {@paperscript} * // Creating a circle with a black shadow: * * var circle = new Path.Circle({ * center: [80, 50], * radius: 35, * fillColor: 'white', * // Set the shadow color of the circle to RGB black: * shadowColor: new Color(0, 0, 0), * // Set the shadow blur radius to 12: * shadowBlur: 12, * // Offset the shadow by { x: 5, y: 5 } * shadowOffset: new Point(5, 5) * }); */ /** * The shadow's blur radius. * * @property * @default 0 * @name Style#shadowBlur * @type Number */ /** * The shadow's offset. * * @property * @default 0 * @name Style#shadowOffset * @type Point */ /** * {@grouptitle Selection Style} * * The color the item is highlighted with when selected. If the item does * not specify its own color, the color defined by its layer is used instead. * * @name Style#selectedColor * @property * @type Color */ /** * {@grouptitle Character Style} * * The font-family to be used in text content. * * @name Style#fontFamily * @default 'sans-serif' * @type String */ /** * * The font-weight to be used in text content. * * @name Style#fontWeight * @default 'normal' * @type String|Number */ /** * The font size of text content, as {@Number} in pixels, or as {@String} * with optional units {@code 'px'}, {@code 'pt'} and {@code 'em'}. * * @name Style#fontSize * @default 10 * @type Number|String */ /** * * The font-family to be used in text content, as one {@String}. * @deprecated use {@link #fontFamily} instead. * * @name Style#font * @default 'sans-serif' * @type String */ /** * The text leading of text content. * * @name Style#leading * @default fontSize * 1.2 * @type Number|String */ /** * {@grouptitle Paragraph Style} * * The justification of text paragraphs. * * @name Style#justification * @default 'left' * @type String('left', 'right', 'center') */ });