mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-22 07:19:57 -05:00
4205 lines
148 KiB
JavaScript
4205 lines
148 KiB
JavaScript
/*
|
|
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
|
|
* http://paperjs.org/
|
|
*
|
|
* Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey
|
|
* http://scratchdisk.com/ & http://jonathanpuckey.com/
|
|
*
|
|
* Distributed under the MIT license. See LICENSE file for details.
|
|
*
|
|
* All rights reserved.
|
|
*/
|
|
|
|
/**
|
|
* @name Item
|
|
*
|
|
* @class The Item type allows you to access and modify the items in
|
|
* Paper.js projects. Its functionality is inherited by different project
|
|
* item types such as {@link Path}, {@link CompoundPath}, {@link Group},
|
|
* {@link Layer} and {@link Raster}. They each add a layer of functionality that
|
|
* is unique to their type, but share the underlying properties and functions
|
|
* that they inherit from Item.
|
|
*/
|
|
var Item = Base.extend(Emitter, /** @lends Item# */{
|
|
statics: {
|
|
/**
|
|
* Override Item.extend() to merge the subclass' _serializeFields with
|
|
* the parent class' _serializeFields.
|
|
*
|
|
* @private
|
|
*/
|
|
extend: function extend(src) {
|
|
if (src._serializeFields)
|
|
src._serializeFields = new Base(
|
|
this.prototype._serializeFields, src._serializeFields);
|
|
return extend.base.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* An object constant that can be passed to Item#initialize() to avoid
|
|
* insertion into the DOM.
|
|
*
|
|
* @private
|
|
*/
|
|
NO_INSERT: { insert: false }
|
|
},
|
|
|
|
_class: 'Item',
|
|
// All items apply their matrix by default.
|
|
// Exceptions are Raster, PlacedSymbol, Clip and Shape.
|
|
_applyMatrix: true,
|
|
_canApplyMatrix: true,
|
|
_boundsSelected: false,
|
|
_selectChildren: false,
|
|
// Provide information about fields to be serialized, with their defaults
|
|
// that can be ommited.
|
|
_serializeFields: {
|
|
name: null,
|
|
applyMatrix: null,
|
|
matrix: new Matrix(),
|
|
pivot: null,
|
|
locked: false,
|
|
visible: true,
|
|
blendMode: 'normal',
|
|
opacity: 1,
|
|
guide: false,
|
|
selected: false,
|
|
clipMask: false,
|
|
data: {}
|
|
},
|
|
|
|
initialize: function Item() {
|
|
// Do nothing, but declare it for named constructors.
|
|
},
|
|
|
|
/**
|
|
* Private helper for #initialize() that tries setting properties from the
|
|
* passed props object, and apply the point translation to the internal
|
|
* matrix.
|
|
*
|
|
* @param {Object} props the properties to be applied to the item
|
|
* @param {Point} point the point by which to transform the internal matrix
|
|
* @return {Boolean} {@true if the properties were successfully be applied,
|
|
* or if none were provided}
|
|
*/
|
|
_initialize: function(props, point) {
|
|
// Define this Item's unique id. But allow the creation of internally
|
|
// used paths with no ids.
|
|
var hasProps = props && Base.isPlainObject(props),
|
|
internal = hasProps && props.internal === true,
|
|
matrix = this._matrix = new Matrix(),
|
|
// Allow setting another project than the currently active one.
|
|
project = hasProps && props.project || paper.project;
|
|
if (!internal)
|
|
this._id = UID.get();
|
|
// Inherit the applyMatrix setting from paper.settings.applyMatrix
|
|
this._applyMatrix = this._canApplyMatrix && paper.settings.applyMatrix;
|
|
// Handle matrix before everything else, to avoid issues with
|
|
// #addChild() calling _changed() and accessing _matrix already.
|
|
if (point)
|
|
matrix.translate(point);
|
|
matrix._owner = 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) {
|
|
// Do not insert into DOM if it's an internal path, if props.insert
|
|
// is false, or if the props are setting a different parent anyway.
|
|
if (internal || hasProps && props.insert === false) {
|
|
this._setProject(project);
|
|
} else if (hasProps && props.parent) {
|
|
this.setParent(props.parent);
|
|
} else {
|
|
// Create a new layer if there is no active one. This will
|
|
// automatically make it the new activeLayer.
|
|
(project._activeLayer || new Layer()).addChild(this);
|
|
}
|
|
}
|
|
// Filter out Item.NO_INSERT before _set(), for performance reasons.
|
|
if (hasProps && props !== Item.NO_INSERT)
|
|
// Filter out insert, parent and project properties as these were
|
|
// handled above.
|
|
this._set(props, { insert: true, project: true, parent: true },
|
|
// Don't check for plain object as that's done by hasProps.
|
|
true);
|
|
return hasProps;
|
|
},
|
|
|
|
_events: Base.each(['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick',
|
|
'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'],
|
|
function(name) {
|
|
this[name] = {
|
|
install: function(type) {
|
|
this.getView()._installEvent(type);
|
|
},
|
|
|
|
uninstall: function(type) {
|
|
this.getView()._uninstallEvent(type);
|
|
}
|
|
};
|
|
}, {
|
|
onFrame: {
|
|
install: function() {
|
|
this.getView()._animateItem(this, true);
|
|
},
|
|
|
|
uninstall: function() {
|
|
this.getView()._animateItem(this, false);
|
|
}
|
|
},
|
|
|
|
// Only for external sources, e.g. Raster
|
|
onLoad: {}
|
|
}
|
|
),
|
|
|
|
_serialize: function(options, dictionary) {
|
|
var props = {},
|
|
that = this;
|
|
|
|
function serialize(fields) {
|
|
for (var key in fields) {
|
|
var value = that[key];
|
|
// Style#leading is a special case, as its default value is
|
|
// dependent on the fontSize. Handle this here separately.
|
|
if (!Base.equals(value, key === 'leading'
|
|
? fields.fontSize * 1.2 : fields[key])) {
|
|
props[key] = Base.serialize(value, options,
|
|
// Do not use compact mode for data
|
|
key !== 'data', dictionary);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Serialize fields that this Item subclass defines first
|
|
serialize(this._serializeFields);
|
|
// Serialize style fields, but only if they differ from defaults.
|
|
// Do not serialize styles on Groups and Layers, since they just unify
|
|
// their children's own styles.
|
|
if (!(this instanceof Group))
|
|
serialize(this._style._defaults);
|
|
// There is no compact form for Item serialization, we always keep the
|
|
// class.
|
|
return [ this._class, props ];
|
|
},
|
|
|
|
/**
|
|
* Private notifier that is called whenever a change occurs in this item or
|
|
* its sub-elements, such as Segments, Curves, Styles, etc.
|
|
*
|
|
* @param {ChangeFlag} flags describes what exactly has changed
|
|
*/
|
|
_changed: function(flags) {
|
|
var symbol = this._parentSymbol,
|
|
cacheParent = this._parent || symbol,
|
|
project = this._project;
|
|
if (flags & /*#=*/ChangeFlag.GEOMETRY) {
|
|
// Clear cached bounds, position and decomposed matrix whenever
|
|
// geometry changes. Also clear _currentPath since it can be used
|
|
// both on compound-paths and clipping groups.
|
|
this._bounds = this._position = this._decomposed =
|
|
this._globalMatrix = this._currentPath = undefined;
|
|
}
|
|
if (cacheParent
|
|
&& (flags & /*#=*/(ChangeFlag.GEOMETRY | ChangeFlag.STROKE))) {
|
|
// Clear cached bounds of all items that this item contributes to.
|
|
// We call this on the parent, since the information is cached on
|
|
// the parent, see getBounds().
|
|
Item._clearBoundsCache(cacheParent);
|
|
}
|
|
if (flags & /*#=*/ChangeFlag.CHILDREN) {
|
|
// Clear cached bounds of all items that this item contributes to.
|
|
// Here we don't call this on the parent, since adding / removing a
|
|
// child triggers this notification on the parent.
|
|
Item._clearBoundsCache(this);
|
|
}
|
|
if (project) {
|
|
if (flags & /*#=*/ChangeFlag.APPEARANCE) {
|
|
project._needsUpdate = true;
|
|
}
|
|
// Have project keep track of changed items so they can be iterated.
|
|
// This can be used for example to update the SVG tree. Needs to be
|
|
// activated in Project
|
|
if (project._changes) {
|
|
var entry = project._changesById[this._id];
|
|
if (entry) {
|
|
entry.flags |= flags;
|
|
} else {
|
|
entry = { item: this, flags: flags };
|
|
project._changesById[this._id] = entry;
|
|
project._changes.push(entry);
|
|
}
|
|
}
|
|
}
|
|
// If this item is a symbol's definition, notify it of the change too
|
|
if (symbol)
|
|
symbol._changed(flags);
|
|
},
|
|
|
|
/**
|
|
* Sets those properties of the passed object literal on this item to
|
|
* the values defined in the object literal, if the item has property of the
|
|
* given name (or a setter defined for it).
|
|
*
|
|
* @param {Object} props
|
|
* @return {Item} the item itself
|
|
*
|
|
* @example {@paperscript}
|
|
* // Setting properties through an object literal
|
|
* var circle = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
*
|
|
* circle.set({
|
|
* strokeColor: 'red',
|
|
* strokeWidth: 10,
|
|
* fillColor: 'black',
|
|
* selected: true
|
|
* });
|
|
*/
|
|
set: function(props) {
|
|
if (props)
|
|
this._set(props);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* The unique id of the item.
|
|
*
|
|
* @type Number
|
|
* @bean
|
|
*/
|
|
getId: function() {
|
|
return this._id;
|
|
},
|
|
|
|
/**
|
|
* The class name of the item as a string.
|
|
*
|
|
* @name Item#className
|
|
* @type String('Group', 'Layer', 'Path', 'CompoundPath', 'Shape',
|
|
* 'Raster', 'PlacedSymbol', 'PointText')
|
|
*/
|
|
|
|
/**
|
|
* The name of the item. If the item has a name, it can be accessed by name
|
|
* through its parent's children list.
|
|
*
|
|
* @type String
|
|
* @bean
|
|
*
|
|
* @example {@paperscript}
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
|
|
* // Set the name of the path:
|
|
* path.name = 'example';
|
|
*
|
|
* // Create a group and add path to it as a child:
|
|
* var group = new Group();
|
|
* group.addChild(path);
|
|
*
|
|
* // The path can be accessed by name:
|
|
* group.children['example'].fillColor = 'red';
|
|
*/
|
|
getName: function() {
|
|
return this._name;
|
|
},
|
|
|
|
setName: function(name) {
|
|
// Note: Don't check if the name has changed and bail out if it has not,
|
|
// because setName is used internally also to update internal structures
|
|
// when an item is moved from one parent to another.
|
|
|
|
// If the item already had a name, remove the reference to it from the
|
|
// parent's children object:
|
|
if (this._name)
|
|
this._removeNamed();
|
|
// See if the name is a simple number, which we cannot support due to
|
|
// the named lookup on the children array.
|
|
if (name === (+name) + '')
|
|
throw new Error(
|
|
'Names consisting only of numbers are not supported.');
|
|
var parent = this._parent;
|
|
if (name && parent) {
|
|
var children = parent._children,
|
|
namedChildren = parent._namedChildren;
|
|
(namedChildren[name] = namedChildren[name] || []).push(this);
|
|
children[name] = this;
|
|
}
|
|
this._name = name || undefined;
|
|
this._changed(/*#=*/ChangeFlag.ATTRIBUTE);
|
|
},
|
|
|
|
/**
|
|
* The path style of the item.
|
|
*
|
|
* @name Item#getStyle
|
|
* @type Style
|
|
* @bean
|
|
*
|
|
* @example {@paperscript}
|
|
* // Applying several styles to an item in one go, by passing an object
|
|
* // to its style property:
|
|
* var circle = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 30
|
|
* });
|
|
* circle.style = {
|
|
* fillColor: 'blue',
|
|
* strokeColor: 'red',
|
|
* strokeWidth: 5
|
|
* };
|
|
*
|
|
* @example {@paperscript split=true height=100}
|
|
* // Copying the style of another item:
|
|
* var path = new Path.Circle({
|
|
* center: [50, 50],
|
|
* radius: 30,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* var path2 = new Path.Circle({
|
|
* center: new Point(180, 50),
|
|
* radius: 20
|
|
* });
|
|
*
|
|
* // Copy the path style of path:
|
|
* path2.style = path.style;
|
|
*
|
|
* @example {@paperscript}
|
|
* // Applying the same style object to multiple items:
|
|
* var myStyle = {
|
|
* fillColor: 'red',
|
|
* strokeColor: 'blue',
|
|
* strokeWidth: 4
|
|
* };
|
|
*
|
|
* var path = new Path.Circle({
|
|
* center: [50, 50],
|
|
* radius: 30
|
|
* });
|
|
* path.style = myStyle;
|
|
*
|
|
* var path2 = new Path.Circle({
|
|
* center: new Point(150, 50),
|
|
* radius: 20
|
|
* });
|
|
* path2.style = myStyle;
|
|
*/
|
|
getStyle: function() {
|
|
return this._style;
|
|
},
|
|
|
|
setStyle: function(style) {
|
|
// Don't access _style directly so Path#getStyle() can be overriden for
|
|
// CompoundPaths.
|
|
this.getStyle().set(style);
|
|
}
|
|
}, Base.each(['locked', 'visible', 'blendMode', 'opacity', 'guide'],
|
|
// Produce getter/setters for properties. We need setters because we want to
|
|
// call _changed() if a property was modified.
|
|
function(name) {
|
|
var part = Base.capitalize(name),
|
|
name = '_' + name;
|
|
this['get' + part] = function() {
|
|
return this[name];
|
|
};
|
|
this['set' + part] = function(value) {
|
|
if (value != this[name]) {
|
|
this[name] = value;
|
|
// #locked does not change appearance, all others do:
|
|
this._changed(name === '_locked'
|
|
? /*#=*/ChangeFlag.ATTRIBUTE : /*#=*/Change.ATTRIBUTE);
|
|
}
|
|
};
|
|
},
|
|
{}), /** @lends Item# */{
|
|
// Enforce creation of beans, as bean getters have hidden parameters.
|
|
// See #getPosition() below.
|
|
beans: true,
|
|
|
|
// Note: These properties have their getter / setters produced in the
|
|
// injection scope above.
|
|
|
|
/**
|
|
* Specifies whether the item is locked.
|
|
*
|
|
* @name Item#locked
|
|
* @type Boolean
|
|
* @default false
|
|
* @ignore
|
|
*/
|
|
_locked: false,
|
|
|
|
/**
|
|
* Specifies whether the item is visible. When set to `false`, the item
|
|
* won't be drawn.
|
|
*
|
|
* @name Item#visible
|
|
* @type Boolean
|
|
* @default true
|
|
*
|
|
* @example {@paperscript}
|
|
* // Hiding an item:
|
|
* var path = new Path.Circle({
|
|
* center: [50, 50],
|
|
* radius: 20,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Hide the path:
|
|
* path.visible = false;
|
|
*/
|
|
_visible: true,
|
|
|
|
/**
|
|
* The blend mode with which the item is composited onto the canvas. Both
|
|
* the standard canvas compositing modes, as well as the new CSS blend modes
|
|
* are supported. If blend-modes cannot be rendered natively, they are
|
|
* emulated. Be aware that emulation can have an impact on performance.
|
|
*
|
|
* @name Item#blendMode
|
|
* @type String('normal', 'multiply', 'screen', 'overlay', 'soft-light',
|
|
* 'hard-light', 'color-dodge', 'color-burn', 'darken', 'lighten',
|
|
* 'difference', 'exclusion', 'hue', 'saturation', 'luminosity', 'color',
|
|
* 'add', 'subtract', 'average', 'pin-light', 'negation', 'source-over',
|
|
* 'source-in', 'source-out', 'source-atop', 'destination-over',
|
|
* 'destination-in', 'destination-out', 'destination-atop', 'lighter',
|
|
* 'darker', 'copy', 'xor')
|
|
* @default 'normal'
|
|
*
|
|
* @example {@paperscript}
|
|
* // Setting an item's blend mode:
|
|
*
|
|
* // Create a white rectangle in the background
|
|
* // with the same dimensions as the view:
|
|
* var background = new Path.Rectangle(view.bounds);
|
|
* background.fillColor = 'white';
|
|
*
|
|
* var circle = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* var circle2 = new Path.Circle({
|
|
* center: new Point(120, 50),
|
|
* radius: 35,
|
|
* fillColor: 'blue'
|
|
* });
|
|
*
|
|
* // Set the blend mode of circle2:
|
|
* circle2.blendMode = 'multiply';
|
|
*/
|
|
_blendMode: 'normal',
|
|
|
|
/**
|
|
* The opacity of the item as a value between `0` and `1`.
|
|
*
|
|
* @name Item#opacity
|
|
* @type Number
|
|
* @default 1
|
|
*
|
|
* @example {@paperscript}
|
|
* // Making an item 50% transparent:
|
|
* var circle = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* var circle2 = new Path.Circle({
|
|
* center: new Point(120, 50),
|
|
* radius: 35,
|
|
* fillColor: 'blue',
|
|
* strokeColor: 'green',
|
|
* strokeWidth: 10
|
|
* });
|
|
*
|
|
* // Make circle2 50% transparent:
|
|
* circle2.opacity = 0.5;
|
|
*/
|
|
_opacity: 1,
|
|
|
|
// TODO: Implement guides
|
|
/**
|
|
* Specifies whether the item functions as a guide. When set to `true`, the
|
|
* item will be drawn at the end as a guide.
|
|
*
|
|
* @name Item#guide
|
|
* @type Boolean
|
|
* @default true
|
|
* @ignore
|
|
*/
|
|
_guide: false,
|
|
|
|
/**
|
|
* Specifies whether the item is selected. This will also return `true` for
|
|
* {@link Group} items if they are partially selected, e.g. groups
|
|
* containing selected or partially selected paths.
|
|
*
|
|
* Paper.js draws the visual outlines of selected items on top of your
|
|
* project. This can be useful for debugging, as it allows you to see the
|
|
* construction of paths, position of path curves, individual segment points
|
|
* and bounding boxes of symbol and raster items.
|
|
*
|
|
* @type Boolean
|
|
* @default false
|
|
* @bean
|
|
* @see Project#selectedItems
|
|
* @see Segment#selected
|
|
* @see Curve#selected
|
|
* @see Point#selected
|
|
*
|
|
* @example {@paperscript}
|
|
* // Selecting an item:
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
* path.selected = true; // Select the path
|
|
*/
|
|
isSelected: function() {
|
|
if (this._selectChildren) {
|
|
var children = this._children;
|
|
for (var i = 0, l = children.length; i < l; i++)
|
|
if (children[i].isSelected())
|
|
return true;
|
|
}
|
|
return this._selected;
|
|
},
|
|
|
|
setSelected: function(selected, noChildren) {
|
|
// Don't recursively call #setSelected() if it was called with
|
|
// noChildren set to true, see #setFullySelected().
|
|
if (!noChildren && this._selectChildren) {
|
|
var children = this._children;
|
|
for (var i = 0, l = children.length; i < l; i++)
|
|
children[i].setSelected(selected);
|
|
}
|
|
if ((selected = !!selected) ^ this._selected) {
|
|
this._selected = selected;
|
|
this._project._updateSelection(this);
|
|
this._changed(/*#=*/Change.ATTRIBUTE);
|
|
}
|
|
},
|
|
|
|
_selected: false,
|
|
|
|
isFullySelected: function() {
|
|
var children = this._children;
|
|
if (children && this._selected) {
|
|
for (var i = 0, l = children.length; i < l; i++)
|
|
if (!children[i].isFullySelected())
|
|
return false;
|
|
return true;
|
|
}
|
|
// If there are no children, this is the same as #selected
|
|
return this._selected;
|
|
},
|
|
|
|
setFullySelected: function(selected) {
|
|
var children = this._children;
|
|
if (children) {
|
|
for (var i = 0, l = children.length; i < l; i++)
|
|
children[i].setFullySelected(selected);
|
|
}
|
|
// Pass true for hidden noChildren argument
|
|
this.setSelected(selected, true);
|
|
},
|
|
|
|
/**
|
|
* Specifies whether the item defines a clip mask. This can only be set on
|
|
* paths, compound paths, and text frame objects, and only if the item is
|
|
* already contained within a clipping group.
|
|
*
|
|
* @type Boolean
|
|
* @default false
|
|
* @bean
|
|
*/
|
|
isClipMask: function() {
|
|
return this._clipMask;
|
|
},
|
|
|
|
setClipMask: function(clipMask) {
|
|
// On-the-fly conversion to boolean:
|
|
if (this._clipMask != (clipMask = !!clipMask)) {
|
|
this._clipMask = clipMask;
|
|
if (clipMask) {
|
|
this.setFillColor(null);
|
|
this.setStrokeColor(null);
|
|
}
|
|
this._changed(/*#=*/Change.ATTRIBUTE);
|
|
// Tell the parent the clipping mask has changed
|
|
if (this._parent)
|
|
this._parent._changed(/*#=*/ChangeFlag.CLIPPING);
|
|
}
|
|
},
|
|
|
|
_clipMask: false,
|
|
|
|
// TODO: get/setIsolated (print specific feature)
|
|
// TODO: get/setKnockout (print specific feature)
|
|
// TODO: get/setAlphaIsShape
|
|
|
|
/**
|
|
* A plain javascript object which can be used to store
|
|
* arbitrary data on the item.
|
|
*
|
|
* @type Object
|
|
* @bean
|
|
*
|
|
* @example
|
|
* var path = new Path();
|
|
* path.data.remember = 'milk';
|
|
*
|
|
* @example
|
|
* var path = new Path();
|
|
* path.data.malcolm = new Point(20, 30);
|
|
* console.log(path.data.malcolm.x); // 20
|
|
*
|
|
* @example
|
|
* var path = new Path();
|
|
* path.data = {
|
|
* home: 'Omicron Theta',
|
|
* found: 2338,
|
|
* pets: ['Spot']
|
|
* };
|
|
* console.log(path.data.pets.length); // 1
|
|
*
|
|
* @example
|
|
* var path = new Path({
|
|
* data: {
|
|
* home: 'Omicron Theta',
|
|
* found: 2338,
|
|
* pets: ['Spot']
|
|
* }
|
|
* });
|
|
* console.log(path.data.pets.length); // 1
|
|
*/
|
|
getData: function() {
|
|
if (!this._data)
|
|
this._data = {};
|
|
return this._data;
|
|
},
|
|
|
|
setData: function(data) {
|
|
this._data = data;
|
|
},
|
|
|
|
/**
|
|
* {@grouptitle Position and Bounding Boxes}
|
|
*
|
|
* The item's position within the parent item's coordinate system. By
|
|
* default, this is the {@link Rectangle#center} of the item's
|
|
* {@link #bounds} rectangle.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
*
|
|
* @example {@paperscript}
|
|
* // Changing the position of a path:
|
|
*
|
|
* // Create a circle at position { x: 10, y: 10 }
|
|
* var circle = new Path.Circle({
|
|
* center: new Point(10, 10),
|
|
* radius: 10,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Move the circle to { x: 20, y: 20 }
|
|
* circle.position = new Point(20, 20);
|
|
*
|
|
* // Move the circle 100 points to the right and 50 points down
|
|
* circle.position += new Point(100, 50);
|
|
*
|
|
* @example {@paperscript split=true height=100}
|
|
* // Changing the x coordinate of an item's position:
|
|
*
|
|
* // Create a circle at position { x: 20, y: 20 }
|
|
* var circle = new Path.Circle({
|
|
* center: new Point(20, 20),
|
|
* radius: 10,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Move the circle 100 points to the right
|
|
* circle.position.x += 100;
|
|
*/
|
|
getPosition: function(_dontLink) {
|
|
// Cache position value.
|
|
// Pass true for _dontLink in getCenter(), so receive back a normal point
|
|
var position = this._position,
|
|
ctor = _dontLink ? Point : LinkedPoint;
|
|
// Do not cache LinkedPoints directly, since we would not be able to
|
|
// use them to calculate the difference in #setPosition, as when it is
|
|
// modified, it would hold new values already and only then cause the
|
|
// calling of #setPosition.
|
|
if (!position) {
|
|
// If an pivot point is provided, use it to determine position
|
|
// based on the matrix. Otherwise use the center of the bounds.
|
|
var pivot = this._pivot;
|
|
position = this._position = pivot
|
|
? this._matrix._transformPoint(pivot)
|
|
: this.getBounds().getCenter(true);
|
|
}
|
|
return new ctor(position.x, position.y, this, 'setPosition');
|
|
},
|
|
|
|
setPosition: function(/* point */) {
|
|
// Calculate the distance to the current position, by which to
|
|
// translate the item. Pass true for _dontLink, as we do not need a
|
|
// LinkedPoint to simply calculate this distance.
|
|
this.translate(Point.read(arguments).subtract(this.getPosition(true)));
|
|
},
|
|
|
|
/**
|
|
* The item's pivot point specified in the item coordinate system, defining
|
|
* the point around which all transformations are hinging. This is also the
|
|
* reference point for {@link #position}. By default, it is set to `null`,
|
|
* meaning the {@link Rectangle#center} of the item's {@link #bounds}
|
|
* rectangle is used as pivot.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
* @default null
|
|
*/
|
|
getPivot: function(_dontLink) {
|
|
var pivot = this._pivot;
|
|
if (pivot) {
|
|
var ctor = _dontLink ? Point : LinkedPoint;
|
|
pivot = new ctor(pivot.x, pivot.y, this, 'setPivot');
|
|
}
|
|
return pivot;
|
|
},
|
|
|
|
setPivot: function(/* point */) {
|
|
// Clone existing points since we're caching internally.
|
|
this._pivot = Point.read(arguments, 0, { clone: true, readNull: true });
|
|
// No need for _changed() since the only thing this affects is _position
|
|
this._position = undefined;
|
|
},
|
|
|
|
_pivot: null,
|
|
}, Base.each(['bounds', 'strokeBounds', 'handleBounds', 'roughBounds',
|
|
'internalBounds', 'internalRoughBounds'],
|
|
function(key) {
|
|
// Produce getters for bounds properties. These handle caching, matrices
|
|
// and redirect the call to the private _getBounds, which can be
|
|
// overridden by subclasses, see below.
|
|
// Treat internalBounds and internalRoughBounds untransformed, as
|
|
// required by the code that uses these methods internally, but make
|
|
// sure they can be cached like all the others as well.
|
|
// Pass on the getter that these version actually use, untransformed,
|
|
// as internalGetter.
|
|
// NOTE: These need to be versions of other methods, as otherwise the
|
|
// cache gets messed up.
|
|
var getter = 'get' + Base.capitalize(key),
|
|
match = key.match(/^internal(.*)$/),
|
|
internalGetter = match ? 'get' + match[1] : null;
|
|
this[getter] = function(_matrix) {
|
|
var boundsGetter = this._boundsGetter,
|
|
// Allow subclasses to override _boundsGetter if they use the
|
|
// same calculations for multiple type of bounds.
|
|
// The default is getter:
|
|
name = !internalGetter && (typeof boundsGetter === 'string'
|
|
? boundsGetter : boundsGetter && boundsGetter[getter])
|
|
|| getter,
|
|
bounds = this._getCachedBounds(name, _matrix, this,
|
|
internalGetter);
|
|
// If we're returning 'bounds', create a LinkedRectangle that uses
|
|
// the setBounds() setter to update the Item whenever the bounds are
|
|
// changed:
|
|
return key === 'bounds'
|
|
? new LinkedRectangle(bounds.x, bounds.y, bounds.width,
|
|
bounds.height, this, 'setBounds')
|
|
: bounds;
|
|
};
|
|
},
|
|
/** @lends Item# */{
|
|
// Enforce creation of beans, as bean getters have hidden parameters.
|
|
// See _matrix parameter above.
|
|
beans: true,
|
|
|
|
/**
|
|
* Protected method used in all the bounds getters. It loops through all the
|
|
* children, gets their bounds and finds the bounds around all of them.
|
|
* Subclasses override it to define calculations for the various required
|
|
* bounding types.
|
|
*/
|
|
_getBounds: function(getter, matrix, cacheItem) {
|
|
// Note: We cannot cache these results here, since we do not get
|
|
// _changed() notifications here for changing geometry in children.
|
|
// But cacheName is used in sub-classes such as PlacedSymbol and Raster.
|
|
var children = this._children;
|
|
// TODO: What to return if nothing is defined, e.g. empty Groups?
|
|
// Scriptographer behaves weirdly then too.
|
|
if (!children || children.length === 0)
|
|
return new Rectangle();
|
|
// Call _updateBoundsCache() even when the group is currently empty
|
|
// (or only holds empty / invisible items), so future changes in these
|
|
// items will cause right handling of _boundsCache.
|
|
Item._updateBoundsCache(this, cacheItem);
|
|
var x1 = Infinity,
|
|
x2 = -x1,
|
|
y1 = x1,
|
|
y2 = x2;
|
|
for (var i = 0, l = children.length; i < l; i++) {
|
|
var child = children[i];
|
|
if (child._visible && !child.isEmpty()) {
|
|
var rect = child._getCachedBounds(getter,
|
|
matrix && matrix.chain(child._matrix), cacheItem);
|
|
x1 = Math.min(rect.x, x1);
|
|
y1 = Math.min(rect.y, y1);
|
|
x2 = Math.max(rect.x + rect.width, x2);
|
|
y2 = Math.max(rect.y + rect.height, y2);
|
|
}
|
|
}
|
|
return isFinite(x1)
|
|
? new Rectangle(x1, y1, x2 - x1, y2 - y1)
|
|
: new Rectangle();
|
|
},
|
|
|
|
setBounds: function(/* rect */) {
|
|
var rect = Rectangle.read(arguments),
|
|
bounds = this.getBounds(),
|
|
matrix = new Matrix(),
|
|
center = rect.getCenter();
|
|
// Read this from bottom to top:
|
|
// Translate to new center:
|
|
matrix.translate(center);
|
|
// Scale to new Size, if size changes and avoid divisions by 0:
|
|
if (rect.width != bounds.width || rect.height != bounds.height) {
|
|
matrix.scale(
|
|
bounds.width !== 0 ? rect.width / bounds.width : 1,
|
|
bounds.height !== 0 ? rect.height / bounds.height : 1);
|
|
}
|
|
// Translate to bounds center:
|
|
center = bounds.getCenter();
|
|
matrix.translate(-center.x, -center.y);
|
|
// Now execute the transformation
|
|
this.transform(matrix);
|
|
},
|
|
|
|
/**
|
|
* Private method that deals with the calling of _getBounds, recursive
|
|
* matrix concatenation and handles all the complicated caching mechanisms.
|
|
*/
|
|
_getCachedBounds: function(getter, matrix, cacheItem, internalGetter) {
|
|
// See if we can cache these bounds. We only cache the bounds
|
|
// transformed with the internally stored _matrix, (the default if no
|
|
// matrix is passed).
|
|
matrix = matrix && matrix.orNullIfIdentity();
|
|
// Do not transform by the internal matrix if there is a internalGetter.
|
|
var _matrix = internalGetter ? null : this._matrix.orNullIfIdentity(),
|
|
cache = (!matrix || matrix.equals(_matrix)) && getter;
|
|
// Note: This needs to happen before returning cached values, since even
|
|
// then, _boundsCache needs to be kept up-to-date.
|
|
Item._updateBoundsCache(this._parent || this._parentSymbol, cacheItem);
|
|
if (cache && this._bounds && this._bounds[cache])
|
|
return this._bounds[cache].clone();
|
|
// If we're caching bounds on this item, pass it on as cacheItem, so the
|
|
// children can setup the _boundsCache structures for it.
|
|
// getInternalBounds is getBounds untransformed. Do not replace earlier,
|
|
// so we can cache both separately, since they're not in the same
|
|
// transformation space!
|
|
var bounds = this._getBounds(internalGetter || getter,
|
|
matrix || _matrix, cacheItem);
|
|
// If we can cache the result, update the _bounds cache structure
|
|
// before returning
|
|
if (cache) {
|
|
if (!this._bounds)
|
|
this._bounds = {};
|
|
var cached = this._bounds[cache] = bounds.clone();
|
|
// Mark as internal, so Item#transform() won't transform it!
|
|
cached._internal = !!internalGetter;
|
|
}
|
|
return bounds;
|
|
},
|
|
|
|
statics: {
|
|
/**
|
|
* Set up a boundsCache structure that keeps track of items that keep
|
|
* cached bounds that depend on this item. We store this in the parent,
|
|
* for multiple reasons:
|
|
* The parent receives CHILDREN change notifications for when its
|
|
* children are added or removed and can thus clear the cache, and we
|
|
* save a lot of memory, e.g. when grouping 100 items and asking the
|
|
* group for its bounds. If stored on the children, we would have 100
|
|
* times the same structure.
|
|
*/
|
|
_updateBoundsCache: function(parent, item) {
|
|
if (parent) {
|
|
// Set-up the parent's boundsCache structure if it does not
|
|
// exist yet and add the item to it.
|
|
var id = item._id,
|
|
ref = parent._boundsCache = parent._boundsCache || {
|
|
// Use a hash-table for ids and an array for the list,
|
|
// so we can keep track of items that were added already
|
|
ids: {},
|
|
list: []
|
|
};
|
|
if (!ref.ids[id]) {
|
|
ref.list.push(item);
|
|
ref.ids[id] = item;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Clears cached bounds of all items that the children of this item are
|
|
* contributing to. See _updateBoundsCache() for an explanation why this
|
|
* information is stored on parents, not the children themselves.
|
|
*/
|
|
_clearBoundsCache: function(item) {
|
|
// This is defined as a static method so Symbol can used it too.
|
|
// Clear the position as well, since it's depending on bounds.
|
|
var cache = item._boundsCache;
|
|
if (cache) {
|
|
// Erase cache before looping, to prevent circular recursion.
|
|
item._bounds = item._position = item._boundsCache = undefined;
|
|
for (var i = 0, list = cache.list, l = list.length; i < l; i++){
|
|
var other = list[i];
|
|
if (other !== item) {
|
|
other._bounds = other._position = undefined;
|
|
// We need to recursively call _clearBoundsCache, as
|
|
// when the cache for the other item's children is not
|
|
// valid anymore, that propagates up the DOM tree.
|
|
if (other._boundsCache)
|
|
Item._clearBoundsCache(other);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The bounding rectangle of the item excluding stroke width.
|
|
*
|
|
* @name Item#bounds
|
|
* @type Rectangle
|
|
*/
|
|
|
|
/**
|
|
* The bounding rectangle of the item including stroke width.
|
|
*
|
|
* @name Item#strokeBounds
|
|
* @type Rectangle
|
|
*/
|
|
|
|
/**
|
|
* The bounding rectangle of the item including handles.
|
|
*
|
|
* @name Item#handleBounds
|
|
* @type Rectangle
|
|
*/
|
|
|
|
/**
|
|
* The rough bounding rectangle of the item that is sure to include all of
|
|
* the drawing, including stroke width.
|
|
*
|
|
* @name Item#roughBounds
|
|
* @type Rectangle
|
|
* @ignore
|
|
*/
|
|
}), /** @lends Item# */{
|
|
// Enforce creation of beans, as bean getters have hidden parameters.
|
|
// See #getGlobalMatrix() below.
|
|
beans: true,
|
|
|
|
_decompose: function() {
|
|
return this._decomposed = this._matrix.decompose();
|
|
},
|
|
|
|
/**
|
|
* The current rotation angle of the item, as described by its
|
|
* {@link #matrix}.
|
|
*
|
|
* @type Number
|
|
* @bean
|
|
*/
|
|
getRotation: function() {
|
|
var decomposed = this._decomposed || this._decompose();
|
|
return decomposed && decomposed.rotation;
|
|
},
|
|
|
|
setRotation: function(rotation) {
|
|
var current = this.getRotation();
|
|
if (current != null && rotation != null) {
|
|
// Preserve the cached _decomposed values over rotation, and only
|
|
// update the rotation property on it.
|
|
var decomposed = this._decomposed;
|
|
this.rotate(rotation - current);
|
|
decomposed.rotation = rotation;
|
|
this._decomposed = decomposed;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The current scale factor of the item, as described by its
|
|
* {@link #matrix}.
|
|
*
|
|
* @type Point
|
|
* @bean
|
|
*/
|
|
getScaling: function(_dontLink) {
|
|
var decomposed = this._decomposed || this._decompose(),
|
|
scaling = decomposed && decomposed.scaling,
|
|
ctor = _dontLink ? Point : LinkedPoint;
|
|
return scaling && new ctor(scaling.x, scaling.y, this, 'setScaling');
|
|
},
|
|
|
|
setScaling: function(/* scaling */) {
|
|
var current = this.getScaling();
|
|
if (current) {
|
|
// Clone existing points since we're caching internally.
|
|
var scaling = Point.read(arguments, 0, { clone: true }),
|
|
// See #setRotation() for preservation of _decomposed.
|
|
decomposed = this._decomposed;
|
|
this.scale(scaling.x / current.x, scaling.y / current.y);
|
|
decomposed.scaling = scaling;
|
|
this._decomposed = decomposed;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The item's transformation matrix, defining position and dimensions in
|
|
* relation to its parent item in which it is contained.
|
|
*
|
|
* @type Matrix
|
|
* @bean
|
|
*/
|
|
getMatrix: function() {
|
|
return this._matrix;
|
|
},
|
|
|
|
setMatrix: function() {
|
|
// Use Matrix#initialize to easily copy over values.
|
|
var matrix = this._matrix;
|
|
matrix.initialize.apply(matrix, arguments);
|
|
if (this._applyMatrix) {
|
|
// Directly apply the internal matrix. This will also call
|
|
// _changed() for us.
|
|
this.transform(null, true);
|
|
} else {
|
|
this._changed(/*#=*/Change.GEOMETRY);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The item's global transformation matrix in relation to the global project
|
|
* coordinate space. Note that the view's transformations resulting from
|
|
* zooming and panning are not factored in.
|
|
*
|
|
* @type Matrix
|
|
* @bean
|
|
*/
|
|
getGlobalMatrix: function(_dontClone) {
|
|
var matrix = this._globalMatrix,
|
|
updateVersion = this._project._updateVersion;
|
|
// If #_globalMatrix is out of sync, recalculate it now.
|
|
if (matrix && matrix._updateVersion !== updateVersion)
|
|
matrix = null;
|
|
if (!matrix) {
|
|
matrix = this._globalMatrix = this._matrix.clone();
|
|
var parent = this._parent;
|
|
if (parent)
|
|
matrix.preConcatenate(parent.getGlobalMatrix(true));
|
|
matrix._updateVersion = updateVersion;
|
|
}
|
|
return _dontClone ? matrix : matrix.clone();
|
|
},
|
|
|
|
/**
|
|
* Controls whether the transformations applied to the item (e.g. through
|
|
* {@link #transform(matrix)}, {@link #rotate(angle)},
|
|
* {@link #scale(scale)}, etc.) are stored in its {@link #matrix} property,
|
|
* or whether they are directly applied to its contents or children (passed
|
|
* on to the segments in {@link Path} items, the children of {@link Group}
|
|
* items, etc.).
|
|
*
|
|
* @type Boolean
|
|
* @default true
|
|
* @bean
|
|
*/
|
|
getApplyMatrix: function() {
|
|
return this._applyMatrix;
|
|
},
|
|
|
|
setApplyMatrix: function(apply) {
|
|
// Tell #transform() to apply the internal matrix if _applyMatrix
|
|
// can be set to true.
|
|
if (this._applyMatrix = this._canApplyMatrix && !!apply)
|
|
this.transform(null, true);
|
|
},
|
|
|
|
/**
|
|
* @bean
|
|
* @deprecated use {@link #applyMatrix} instead.
|
|
*/
|
|
getTransformContent: '#getApplyMatrix',
|
|
setTransformContent: '#setApplyMatrix',
|
|
}, /** @lends Item# */{
|
|
/**
|
|
* {@grouptitle Project Hierarchy}
|
|
* The project that this item belongs to.
|
|
*
|
|
* @type Project
|
|
* @bean
|
|
*/
|
|
getProject: function() {
|
|
return this._project;
|
|
},
|
|
|
|
_setProject: function(project, installEvents) {
|
|
if (this._project !== project) {
|
|
// Uninstall events before switching project, then install them
|
|
// again.
|
|
// NOTE: _installEvents handles all children too!
|
|
if (this._project)
|
|
this._installEvents(false);
|
|
this._project = project;
|
|
var children = this._children;
|
|
for (var i = 0, l = children && children.length; i < l; i++)
|
|
children[i]._setProject(project);
|
|
// We need to call _installEvents(true) again, but merge it with
|
|
// handling of installEvents argument below.
|
|
installEvents = true;
|
|
}
|
|
if (installEvents)
|
|
this._installEvents(true);
|
|
},
|
|
|
|
/**
|
|
* The view that this item belongs to.
|
|
* @type View
|
|
* @bean
|
|
*/
|
|
getView: function() {
|
|
return this._project.getView();
|
|
},
|
|
|
|
/**
|
|
* Overrides Emitter#_installEvents to also call _installEvents on all
|
|
* children.
|
|
*/
|
|
_installEvents: function _installEvents(install) {
|
|
_installEvents.base.call(this, install);
|
|
var children = this._children;
|
|
for (var i = 0, l = children && children.length; i < l; i++)
|
|
children[i]._installEvents(install);
|
|
},
|
|
|
|
/**
|
|
* The layer that this item is contained within.
|
|
*
|
|
* @type Layer
|
|
* @bean
|
|
*/
|
|
getLayer: function() {
|
|
var parent = this;
|
|
while (parent = parent._parent) {
|
|
if (parent instanceof Layer)
|
|
return parent;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* The item that this item is contained within.
|
|
*
|
|
* @type Item
|
|
* @bean
|
|
*
|
|
* @example
|
|
* var path = new Path();
|
|
*
|
|
* // New items are placed in the active layer:
|
|
* console.log(path.parent == project.activeLayer); // true
|
|
*
|
|
* var group = new Group();
|
|
* group.addChild(path);
|
|
*
|
|
* // Now the parent of the path has become the group:
|
|
* console.log(path.parent == group); // true
|
|
*
|
|
* @example // Setting the parent of the item to another item
|
|
* var path = new Path();
|
|
*
|
|
* // New items are placed in the active layer:
|
|
* console.log(path.parent == project.activeLayer); // true
|
|
*
|
|
* var group = new Group();
|
|
* group.parent = path;
|
|
*
|
|
* // Now the parent of the path has become the group:
|
|
* console.log(path.parent == group); // true
|
|
*
|
|
* // The path is now contained in the children list of group:
|
|
* console.log(group.children[0] == path); // true
|
|
*
|
|
* @example // Setting the parent of an item in the constructor
|
|
* var group = new Group();
|
|
*
|
|
* var path = new Path({
|
|
* parent: group
|
|
* });
|
|
*
|
|
* // The parent of the path is the group:
|
|
* console.log(path.parent == group); // true
|
|
*
|
|
* // The path is contained in the children list of group:
|
|
* console.log(group.children[0] == path); // true
|
|
*/
|
|
getParent: function() {
|
|
return this._parent;
|
|
},
|
|
|
|
setParent: function(item) {
|
|
return item.addChild(this);
|
|
},
|
|
|
|
/**
|
|
* The children items contained within this item. Items that define a
|
|
* {@link #name} can also be accessed by name.
|
|
*
|
|
* <b>Please note:</b> The children array should not be modified directly
|
|
* using array functions. To remove single items from the children list, use
|
|
* {@link Item#remove()}, to remove all items from the children list, use
|
|
* {@link Item#removeChildren()}. To add items to the children list, use
|
|
* {@link Item#addChild(item)} or {@link Item#insertChild(index,item)}.
|
|
*
|
|
* @type Item[]
|
|
* @bean
|
|
*
|
|
* @example {@paperscript}
|
|
* // Accessing items in the children array:
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
*
|
|
* // Create a group and move the path into it:
|
|
* var group = new Group();
|
|
* group.addChild(path);
|
|
*
|
|
* // Access the path through the group's children array:
|
|
* group.children[0].fillColor = 'red';
|
|
*
|
|
* @example {@paperscript}
|
|
* // Accessing children by name:
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
* // Set the name of the path:
|
|
* path.name = 'example';
|
|
*
|
|
* // Create a group and move the path into it:
|
|
* var group = new Group();
|
|
* group.addChild(path);
|
|
*
|
|
* // The path can be accessed by name:
|
|
* group.children['example'].fillColor = 'orange';
|
|
*
|
|
* @example {@paperscript}
|
|
* // Passing an array of items to item.children:
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
*
|
|
* var group = new Group();
|
|
* group.children = [path];
|
|
*
|
|
* // The path is the first child of the group:
|
|
* group.firstChild.fillColor = 'green';
|
|
*/
|
|
getChildren: function() {
|
|
return this._children;
|
|
},
|
|
|
|
setChildren: function(items) {
|
|
this.removeChildren();
|
|
this.addChildren(items);
|
|
},
|
|
|
|
/**
|
|
* The first item contained within this item. This is a shortcut for
|
|
* accessing `item.children[0]`.
|
|
*
|
|
* @type Item
|
|
* @bean
|
|
*/
|
|
getFirstChild: function() {
|
|
return this._children && this._children[0] || null;
|
|
},
|
|
|
|
/**
|
|
* The last item contained within this item.This is a shortcut for
|
|
* accessing `item.children[item.children.length - 1]`.
|
|
*
|
|
* @type Item
|
|
* @bean
|
|
*/
|
|
getLastChild: function() {
|
|
return this._children && this._children[this._children.length - 1]
|
|
|| null;
|
|
},
|
|
|
|
/**
|
|
* The next item on the same level as this item.
|
|
*
|
|
* @type Item
|
|
* @bean
|
|
*/
|
|
getNextSibling: function() {
|
|
return this._parent && this._parent._children[this._index + 1] || null;
|
|
},
|
|
|
|
/**
|
|
* The previous item on the same level as this item.
|
|
*
|
|
* @type Item
|
|
* @bean
|
|
*/
|
|
getPreviousSibling: function() {
|
|
return this._parent && this._parent._children[this._index - 1] || null;
|
|
},
|
|
|
|
/**
|
|
* The index of this item within the list of its parent's children.
|
|
*
|
|
* @type Number
|
|
* @bean
|
|
*/
|
|
getIndex: function() {
|
|
return this._index;
|
|
},
|
|
|
|
equals: function(item) {
|
|
// Note: We do not compare name and selected state.
|
|
// TODO: Consider not comparing locked and visible also?
|
|
return item === this || item && this._class === item._class
|
|
&& this._style.equals(item._style)
|
|
&& this._matrix.equals(item._matrix)
|
|
&& this._locked === item._locked
|
|
&& this._visible === item._visible
|
|
&& this._blendMode === item._blendMode
|
|
&& this._opacity === item._opacity
|
|
&& this._clipMask === item._clipMask
|
|
&& this._guide === item._guide
|
|
&& this._equals(item)
|
|
|| false;
|
|
},
|
|
|
|
/**
|
|
* A private helper for #equals(), to be overridden in sub-classes. When it
|
|
* is called, item is always defined, of the same class as `this` and has
|
|
* equal general state attributes such as matrix, style, opacity, etc.
|
|
*/
|
|
_equals: function(item) {
|
|
return Base.equals(this._children, item._children);
|
|
},
|
|
|
|
/**
|
|
* Clones the item within the same project and places the copy above the
|
|
* item.
|
|
*
|
|
* @param {Boolean} [insert=true] specifies whether the copy should be
|
|
* inserted into the DOM. When set to `true`, it is inserted above the
|
|
* original
|
|
* @return {Item} the newly cloned item
|
|
*
|
|
* @example {@paperscript}
|
|
* // Cloning items:
|
|
* var circle = new Path.Circle({
|
|
* center: [50, 50],
|
|
* radius: 10,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Make 20 copies of the circle:
|
|
* for (var i = 0; i < 20; i++) {
|
|
* var copy = circle.clone();
|
|
*
|
|
* // Distribute the copies horizontally, so we can see them:
|
|
* copy.position.x += i * copy.bounds.width;
|
|
* }
|
|
*/
|
|
clone: function(insert) {
|
|
var copy = new this.constructor(Item.NO_INSERT),
|
|
children = this._children;
|
|
// On items with children, for performance reasons due to the way that
|
|
// styles are currently "flattened" into existing children, we need to
|
|
// clone attributes first, then content.
|
|
// For all other items, it's the other way around, since applying
|
|
// attributes might have an impact on their content.
|
|
if (children)
|
|
copy.copyAttributes(this);
|
|
copy.copyContent(this);
|
|
if (!children)
|
|
copy.copyAttributes(this);
|
|
// Insert is true by default.
|
|
if (insert || insert === undefined)
|
|
copy.insertAbove(this);
|
|
// Make sure we're not overriding the original name in the same parent
|
|
var name = this._name,
|
|
parent = this._parent;
|
|
if (name && parent) {
|
|
var children = parent._children,
|
|
orig = name,
|
|
i = 1;
|
|
while (children[name])
|
|
name = orig + ' ' + (i++);
|
|
if (name !== orig)
|
|
copy.setName(name);
|
|
}
|
|
return copy;
|
|
},
|
|
|
|
/**
|
|
* Copies the content of the specified item over to this item.
|
|
*
|
|
* @param {Item} source the item to copy the content from
|
|
*/
|
|
copyContent: function(source) {
|
|
var children = source._children;
|
|
// Clone all children and add them to the copy. tell #addChild we're
|
|
// cloning, as needed by CompoundPath#insertChild().
|
|
for (var i = 0, l = children && children.length; i < l; i++) {
|
|
this.addChild(children[i].clone(false), true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Copies all attributes of the specified item over to this item. This
|
|
* includes its style, visibility, matrix, pivot, blend-mode, opacity,
|
|
* selection state, data, name, etc.
|
|
*
|
|
* @param {Item} source the item to copy the attributes from
|
|
* @param {Boolean} excludeMatrix whether to exclude the transformation
|
|
* matrix when copying all attributes
|
|
*/
|
|
copyAttributes: function(source, excludeMatrix) {
|
|
// Copy over style
|
|
this.setStyle(source._style);
|
|
// Only copy over these fields if they are actually defined in 'source',
|
|
// meaning the default value has been overwritten (default is on
|
|
// prototype).
|
|
var keys = ['_locked', '_visible', '_blendMode', '_opacity',
|
|
'_clipMask', '_guide'];
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
var key = keys[i];
|
|
if (source.hasOwnProperty(key))
|
|
this[key] = source[key];
|
|
}
|
|
// Use Matrix#initialize to easily copy over values.
|
|
if (!excludeMatrix)
|
|
this._matrix.initialize(source._matrix);
|
|
// We can't just set _applyMatrix as many item types won't allow it,
|
|
// e.g. creating a Shape in Path#toShape().
|
|
// Using the setter instead takes care of it.
|
|
// NOTE: This will also bake in the matrix that we just initialized,
|
|
// in case #applyMatrix is true.
|
|
this.setApplyMatrix(source._applyMatrix);
|
|
this.setPivot(source._pivot);
|
|
// Copy over the selection state, use setSelected so the item
|
|
// is also added to Project#selectedItems if it is selected.
|
|
this.setSelected(source._selected);
|
|
// Copy over data and name as well.
|
|
var data = source._data,
|
|
name = source._name;
|
|
this._data = data ? Base.clone(data) : null;
|
|
if (name)
|
|
this.setName(name);
|
|
},
|
|
|
|
/**
|
|
* When passed a project, copies the item to the project,
|
|
* or duplicates it within the same project. When passed an item,
|
|
* copies the item into the specified item.
|
|
*
|
|
* @param {Project|Layer|Group|CompoundPath} item the item or project to
|
|
* copy the item to
|
|
* @return {Item} the new copy of the item
|
|
*/
|
|
copyTo: function(itemOrProject) {
|
|
// Pass false fo insert, since we're inserting at a specific location.
|
|
return itemOrProject.addChild(this.clone(false));
|
|
},
|
|
|
|
/**
|
|
* Rasterizes the item into a newly created Raster object. The item itself
|
|
* is not removed after rasterization.
|
|
*
|
|
* @param {Number} [resolution=view.resolution] the resolution of the raster
|
|
* in pixels per inch (DPI). If not specified, the value of
|
|
* `view.resolution` is used.
|
|
* @return {Raster} the newly created raster item
|
|
*
|
|
* @example {@paperscript}
|
|
* // Rasterizing an item:
|
|
* var circle = new Path.Circle({
|
|
* center: [50, 50],
|
|
* radius: 5,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Create a rasterized version of the path:
|
|
* var raster = circle.rasterize();
|
|
*
|
|
* // Move it 100pt to the right:
|
|
* raster.position.x += 100;
|
|
*
|
|
* // Scale the path and the raster by 300%, so we can compare them:
|
|
* circle.scale(5);
|
|
* raster.scale(5);
|
|
*/
|
|
rasterize: function(resolution) {
|
|
var bounds = this.getStrokeBounds(),
|
|
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(),
|
|
bottomRight = bounds.getBottomRight().ceil(),
|
|
size = new Size(bottomRight.subtract(topLeft)),
|
|
raster = new Raster(Item.NO_INSERT);
|
|
if (!size.isZero()) {
|
|
var canvas = CanvasProvider.getCanvas(size.multiply(scale)),
|
|
ctx = canvas.getContext('2d'),
|
|
matrix = new Matrix().scale(scale).translate(topLeft.negate());
|
|
ctx.save();
|
|
matrix.applyToContext(ctx);
|
|
// See Project#draw() for an explanation of new Base()
|
|
this.draw(ctx, new Base({ matrices: [matrix] }));
|
|
ctx.restore();
|
|
// NOTE: We don't need to release the canvas since it belongs to the
|
|
// raster now!
|
|
raster.setCanvas(canvas);
|
|
}
|
|
raster.transform(new Matrix().translate(topLeft.add(size.divide(2)))
|
|
// Take resolution into account and scale back to original size.
|
|
.scale(1 / scale));
|
|
raster.insertAbove(this);
|
|
return raster;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item's geometry contains the given point.
|
|
*
|
|
* @example {@paperscript} // Click within and outside the star below
|
|
* // Create a star shaped path:
|
|
* var path = new Path.Star({
|
|
* center: [50, 50],
|
|
* points: 12,
|
|
* radius1: 20,
|
|
* radius2: 40,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Whenever the user presses the mouse:
|
|
* function onMouseDown(event) {
|
|
* // If the position of the mouse is within the path,
|
|
* // set its fill color to red, otherwise set it to
|
|
* // black:
|
|
* if (path.contains(event.point)) {
|
|
* path.fillColor = 'red';
|
|
* } else {
|
|
* path.fillColor = 'black';
|
|
* }
|
|
* }
|
|
*
|
|
* @param {Point} point The point to check for
|
|
*/
|
|
contains: function(/* point */) {
|
|
// See CompoundPath#_contains() for the reason for !!
|
|
return !!this._contains(
|
|
this._matrix._inverseTransform(Point.read(arguments)));
|
|
},
|
|
|
|
_contains: function(point) {
|
|
var children = this._children;
|
|
if (children) {
|
|
for (var i = children.length - 1; i >= 0; i--) {
|
|
if (children[i].contains(point))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
// We only implement it here for items with rectangular content,
|
|
// for anything else we need to override #contains()
|
|
return point.isInside(this.getInternalBounds());
|
|
},
|
|
|
|
// DOCS:
|
|
// TEST:
|
|
/**
|
|
* @param {Rectangle} rect the rectangle to check against
|
|
* @return {Boolean}
|
|
*/
|
|
isInside: function(/* rect */) {
|
|
return Rectangle.read(arguments).contains(this.getBounds());
|
|
},
|
|
|
|
// Internal helper function, used at the moment for intersects check only.
|
|
// TODO: Move #getIntersections() to Item, make it handle all type of items
|
|
// through _asPathItem(), and support Group items as well, taking nested
|
|
// matrices into account properly!
|
|
_asPathItem: function() {
|
|
// Creates a temporary rectangular path item with this item's bounds.
|
|
return new Path.Rectangle({
|
|
rectangle: this.getInternalBounds(),
|
|
matrix: this._matrix,
|
|
insert: false,
|
|
});
|
|
},
|
|
|
|
// DOCS:
|
|
// TEST:
|
|
/**
|
|
* @param {Item} item the item to check against
|
|
* @return {Boolean}
|
|
*/
|
|
intersects: function(item, _matrix) {
|
|
if (!(item instanceof Item))
|
|
return false;
|
|
// Tell getIntersections() to return as soon as some intersections are
|
|
// found, because all we care for here is there are some or none:
|
|
return this._asPathItem().getIntersections(item._asPathItem(), null,
|
|
_matrix, true).length > 0;
|
|
},
|
|
|
|
/**
|
|
* Perform a hit-test on the item (and its children, if it is a {@link
|
|
* Group} or {@link Layer}) at the location of the specified point.
|
|
*
|
|
* The options object allows you to control the specifics of the hit-test
|
|
* and may contain a combination of the following values:
|
|
*
|
|
* @option [options.tolerance={@link PaperScope#settings}.hitTolerance]
|
|
* {Number} the tolerance of the hit-test
|
|
* @option options.class {Function} only hit-test again a certain item class
|
|
* and its sub-classes: {@code Group, Layer, Path, CompoundPath, Shape,
|
|
* Raster, PlacedSymbol, PointText}, etc
|
|
* @option options.fill {Boolean} hit-test the fill of items
|
|
* @option options.stroke {Boolean} hit-test the stroke of path items,
|
|
* taking into account the setting of stroke color and width
|
|
* @option options.segments {Boolean} hit-test for {@link Segment#point} of
|
|
* {@link Path} items
|
|
* @option options.curves {Boolean} hit-test the curves of path items,
|
|
* without taking the stroke color or width into account
|
|
* @option options.handles {Boolean} hit-test for the handles ({@link
|
|
* Segment#handleIn} / {@link Segment#handleOut}) of path segments
|
|
* @option options.ends {Boolean} only hit-test for the first or last
|
|
* segment points of open path items
|
|
* @option options.bounds {Boolean} hit-test the corners and side-centers of
|
|
* the bounding rectangle of items ({@link Item#bounds})
|
|
* @option options.center {Boolean} hit-test the {@link Rectangle#center} of
|
|
* the bounding rectangle of items ({@link Item#bounds})
|
|
* @option options.guides {Boolean} hit-test items that have {@link
|
|
* Item#guide} set to `true`
|
|
* @option options.selected {Boolean} only hit selected items
|
|
*
|
|
* @param {Point} point The point where the hit-test should be performed
|
|
* @param {Object} [options={ fill: true, stroke: true, segments: true,
|
|
* tolerance: 2 }]
|
|
* @return {HitResult} a hit result object that contains more information
|
|
* about what exactly was hit or `null` if nothing was hit
|
|
*/
|
|
hitTest: function(/* point, options */) {
|
|
return this._hitTest(
|
|
Point.read(arguments),
|
|
HitResult.getOptions(Base.read(arguments)));
|
|
},
|
|
|
|
_hitTest: function(point, options) {
|
|
if (this._locked || !this._visible || this._guide && !options.guides
|
|
|| this.isEmpty())
|
|
return null;
|
|
|
|
// Check if the point is withing roughBounds + tolerance, but only if
|
|
// this item does not have children, since we'd have to travel up the
|
|
// chain already to determine the rough bounds.
|
|
var matrix = this._matrix,
|
|
parentTotalMatrix = options._totalMatrix,
|
|
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
|
|
? parentTotalMatrix.chain(matrix)
|
|
// If this is the first one in the recursion, factor in the
|
|
// zoom of the view and the globalMatrix of the item.
|
|
: this.getGlobalMatrix().preConcatenate(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.
|
|
tolerancePadding = options._tolerancePadding = new Size(
|
|
Path._getPenPadding(1, totalMatrix.inverted())
|
|
).multiply(
|
|
Math.max(options.tolerance, /*#=*/Numerical.TOLERANCE)
|
|
);
|
|
// Transform point to local coordinates.
|
|
point = matrix._inverseTransform(point);
|
|
// If the matrix is non-reversible, point will now be `null`:
|
|
if (!point || !this._children && !this.getInternalRoughBounds()
|
|
.expand(tolerancePadding.multiply(2))._containsPoint(point))
|
|
return null;
|
|
// Filter for type, guides and selected items if that's required.
|
|
var checkSelf = !(options.guides && !this._guide
|
|
|| options.selected && !this._selected
|
|
// Support legacy Item#type property to match hyphenated
|
|
// class-names.
|
|
|| options.type && options.type !== Base.hyphenate(this._class)
|
|
|| options.class && !(this instanceof options.class)),
|
|
that = this,
|
|
res;
|
|
|
|
function checkBounds(type, part) {
|
|
var pt = bounds['get' + part]();
|
|
// Since there are transformations, we cannot simply use a numerical
|
|
// tolerance value. Instead, we divide by a padding size, see above.
|
|
if (point.subtract(pt).divide(tolerancePadding).length <= 1)
|
|
return new HitResult(type, that,
|
|
{ name: Base.hyphenate(part), point: pt });
|
|
}
|
|
|
|
// Ignore top level layers by checking for _parent:
|
|
if (checkSelf && (options.center || options.bounds) && this._parent) {
|
|
// Don't get the transformed bounds, check against transformed
|
|
// points instead
|
|
var bounds = this.getInternalBounds();
|
|
if (options.center)
|
|
res = checkBounds('center', 'Center');
|
|
if (!res && options.bounds) {
|
|
// TODO: Move these into a private scope
|
|
var points = [
|
|
'TopLeft', 'TopRight', 'BottomLeft', 'BottomRight',
|
|
'LeftCenter', 'TopCenter', 'RightCenter', 'BottomCenter'
|
|
];
|
|
for (var i = 0; i < 8 && !res; i++)
|
|
res = checkBounds('bounds', points[i]);
|
|
}
|
|
}
|
|
|
|
var children = !res && this._children;
|
|
if (children) {
|
|
var opts = this._getChildHitTestOptions(options);
|
|
// Loop backwards, so items that get drawn last are tested first
|
|
for (var i = children.length - 1; i >= 0 && !res; i--)
|
|
res = children[i]._hitTest(point, opts);
|
|
}
|
|
if (!res && checkSelf)
|
|
res = this._hitTestSelf(point, options);
|
|
// Transform the point back to the outer coordinate system.
|
|
if (res && res.point)
|
|
res.point = matrix.transform(res.point);
|
|
// Restore totalMatrix for next child.
|
|
options._totalMatrix = parentTotalMatrix;
|
|
return res;
|
|
},
|
|
|
|
_getChildHitTestOptions: function(options) {
|
|
// This is overridden in CompoundPath, for treatment of type === 'path'.
|
|
return options;
|
|
},
|
|
|
|
_hitTestSelf: function(point, options) {
|
|
// The default implementation honly handles 'fill' through #_contains()
|
|
if (options.fill && this.hasFill() && this._contains(point))
|
|
return new HitResult('fill', this);
|
|
},
|
|
|
|
/**
|
|
* {@grouptitle Fetching and matching items}
|
|
*
|
|
* Checks whether the item matches the criteria described by the given
|
|
* object, by iterating over all of its properties and matching against
|
|
* their values through {@link #matches(name, compare)}.
|
|
*
|
|
* See {@link Project#getItems(match)} for a selection of illustrated
|
|
* examples.
|
|
*
|
|
* @name Item#matches
|
|
* @function
|
|
*
|
|
* @param {Object|Function} match the criteria to match against
|
|
* @return {Boolean} {@true if the item matches all the criteria}
|
|
* @see #getItems(match)
|
|
*/
|
|
/**
|
|
* Checks whether the item matches the given criteria. Extended matching is
|
|
* possible by providing a compare function or a regular expression.
|
|
* Matching points, colors only work as a comparison of the full object, not
|
|
* partial matching (e.g. only providing the x-coordinate to match all
|
|
* points with that x-value). Partial matching does work for
|
|
* {@link Item#data}.
|
|
*
|
|
* See {@link Project#getItems(match)} for a selection of illustrated
|
|
* examples.
|
|
*
|
|
* @name Item#matches
|
|
* @function
|
|
*
|
|
* @param {String} name the name of the state to match against
|
|
* @param {Object} compare the value, function or regular expression to
|
|
* compare against
|
|
* @return {Boolean} {@true if the item matches the state}
|
|
* @see #getItems(match)
|
|
*/
|
|
matches: function(name, compare) {
|
|
// matchObject() is used to match against objects in a nested manner.
|
|
// This is useful for matching against Item#data.
|
|
function matchObject(obj1, obj2) {
|
|
for (var i in obj1) {
|
|
if (obj1.hasOwnProperty(i)) {
|
|
var val1 = obj1[i],
|
|
val2 = obj2[i];
|
|
if (Base.isPlainObject(val1) && Base.isPlainObject(val2)) {
|
|
if (!matchObject(val1, val2))
|
|
return false;
|
|
} else if (!Base.equals(val1, val2)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
var type = typeof name;
|
|
if (type === 'object') {
|
|
// `name` is the match object, not a string
|
|
for (var key in name) {
|
|
if (name.hasOwnProperty(key) && !this.matches(key, name[key]))
|
|
return false;
|
|
}
|
|
return true;
|
|
} else if (type === 'function') {
|
|
return name(this);
|
|
} else if (name === 'match') {
|
|
return compare(this);
|
|
} else {
|
|
var value = /^(empty|editable)$/.test(name)
|
|
// Handle boolean test functions separately, by calling them
|
|
// to get the value.
|
|
? this['is' + Base.capitalize(name)]()
|
|
// Support legacy Item#type property to match hyphenated
|
|
// class-names.
|
|
// TODO: Remove after December 2016.
|
|
: name === 'type'
|
|
? Base.hyphenate(this._class)
|
|
: this[name];
|
|
if (name === 'class') {
|
|
if (typeof compare === 'function')
|
|
return this instanceof compare;
|
|
// Compare further with the _class property value instead.
|
|
value = this._class;
|
|
}
|
|
if (compare instanceof RegExp) {
|
|
return compare.test(value);
|
|
} else if (typeof compare === 'function') {
|
|
return !!compare(value);
|
|
} else if (Base.isPlainObject(compare)) {
|
|
return matchObject(compare, value);
|
|
}
|
|
return Base.equals(value, compare);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Fetch the descendants (children or children of children) of this item
|
|
* that match the properties in the specified object. Extended matching is
|
|
* possible by providing a compare function or regular expression. Matching
|
|
* points, colors only work as a comparison of the full object, not partial
|
|
* matching (e.g. only providing the x- coordinate to match all points with
|
|
* that x-value). Partial matching does work for {@link Item#data}.
|
|
*
|
|
* Matching items against a rectangular area is also possible, by setting
|
|
* either `match.inside` or `match.overlapping` to a rectangle describing
|
|
* the area in which the items either have to be fully or partly contained.
|
|
*
|
|
* See {@link Project#getItems(match)} for a selection of illustrated
|
|
* examples.
|
|
*
|
|
* @option [match.recursive=true] {Boolean} whether to loop recursively
|
|
* through all children, or stop at the current level
|
|
* @option match.match {Function} a match function to be called for each
|
|
* item, allowing the definition of more flexible item checks that are
|
|
* not bound to properties. If no other match properties are defined,
|
|
* this function can also be passed instead of the `match` object
|
|
* @option match.class {Function} the constructor function of the item type
|
|
* to match against
|
|
* @option match.inside {Rectangle} the rectangle in which the items need to
|
|
* be fully contained
|
|
* @option match.overlapping {Rectangle} the rectangle with which the items
|
|
* need to at least partly overlap
|
|
*
|
|
* @param {Object|Function} match the criteria to match against
|
|
* @return {Item[]} the list of matching descendant items
|
|
* @see #matches(match)
|
|
*/
|
|
getItems: function(match) {
|
|
return Item._getItems(this._children, match, this._matrix);
|
|
},
|
|
|
|
/**
|
|
* Fetch the first descendant (child or child of child) of this item
|
|
* that matches the properties in the specified object.
|
|
* Extended matching is possible by providing a compare function or
|
|
* regular expression. Matching points, colors only work as a comparison
|
|
* of the full object, not partial matching (e.g. only providing the x-
|
|
* coordinate to match all points with that x-value). Partial matching
|
|
* does work for {@link Item#data}.
|
|
* See {@link Project#getItems(match)} for a selection of illustrated
|
|
* examples.
|
|
*
|
|
* @param {Object|Function} match the criteria to match against
|
|
* @return {Item} the first descendant item matching the given criteria
|
|
* @see #getItems(match)
|
|
*/
|
|
getItem: function(match) {
|
|
return Item._getItems(this._children, match, this._matrix, null, true)
|
|
[0] || null;
|
|
},
|
|
|
|
statics: {
|
|
// NOTE: We pass children instead of item as first argument so the
|
|
// method can be used for Project#layers as well in Project.
|
|
_getItems: function _getItems(children, match, matrix, param,
|
|
firstOnly) {
|
|
if (!param) {
|
|
// Set up a couple of "side-car" values for the recursive calls
|
|
// of _getItems below, mainly related to the handling of
|
|
// inside / overlapping:
|
|
var obj = typeof match === 'object' && match,
|
|
overlapping = obj && obj.overlapping,
|
|
inside = obj && obj.inside,
|
|
// If overlapping is set, we also perform the inside check:
|
|
bounds = overlapping || inside,
|
|
rect = bounds && Rectangle.read([bounds]);
|
|
param = {
|
|
items: [], // The list to contain the results.
|
|
recursive: obj && obj.recursive !== false,
|
|
inside: !!inside,
|
|
overlapping: !!overlapping,
|
|
rect: rect,
|
|
path: overlapping && new Path.Rectangle({
|
|
rectangle: rect,
|
|
insert: false
|
|
})
|
|
};
|
|
if (obj) {
|
|
// Create a copy of the match object that doesn't contain
|
|
// these special properties:
|
|
match = Base.set({}, match, {
|
|
recursive: true, inside: true, overlapping: true
|
|
});
|
|
}
|
|
}
|
|
var items = param.items,
|
|
rect = param.rect;
|
|
matrix = rect && (matrix || new Matrix());
|
|
for (var i = 0, l = children && children.length; i < l; i++) {
|
|
var child = children[i],
|
|
childMatrix = matrix && matrix.chain(child._matrix),
|
|
add = true;
|
|
if (rect) {
|
|
var bounds = child.getBounds(childMatrix);
|
|
// Regardless of the setting of inside / overlapping, if the
|
|
// bounds don't even intersect, we can skip this child.
|
|
if (!rect.intersects(bounds))
|
|
continue;
|
|
if (!(rect.contains(bounds)
|
|
// First check the bounds, if the rect is fully
|
|
// contained, we are always overlapping, and don't
|
|
// need to perform further checks, otherwise perform
|
|
// a proper #intersects() check:
|
|
|| param.overlapping && (bounds.contains(rect)
|
|
|| param.path.intersects(child, childMatrix))))
|
|
add = false;
|
|
}
|
|
if (add && child.matches(match)) {
|
|
items.push(child);
|
|
if (firstOnly)
|
|
break;
|
|
}
|
|
if (param.recursive !== false) {
|
|
_getItems(child._children, match,
|
|
childMatrix, param,
|
|
firstOnly);
|
|
}
|
|
if (firstOnly && items.length > 0)
|
|
break;
|
|
}
|
|
return items;
|
|
}
|
|
}
|
|
}, /** @lends Item# */{
|
|
/**
|
|
* {@grouptitle Importing / Exporting JSON and SVG}
|
|
*
|
|
* Exports (serializes) the item with its content and child items to a JSON
|
|
* data string.
|
|
*
|
|
* @name Item#exportJSON
|
|
* @function
|
|
*
|
|
* @option [options.asString=true] {Boolean} whether the JSON is returned as
|
|
* a `Object` or a `String`
|
|
* @option [options.precision=5] {Number} the amount of fractional digits in
|
|
* numbers used in JSON data
|
|
*
|
|
* @param {Object} [options] the serialization options
|
|
* @return {String} the exported JSON data
|
|
*/
|
|
|
|
/**
|
|
* Imports (deserializes) the stored JSON data into this item. If the data
|
|
* describes an item of the same class or a parent class of the item, the
|
|
* data is imported into the item itself. If not, the imported item is added
|
|
* to this item's {@link Item#children} list. Note that not all type of
|
|
* items can have children.
|
|
*
|
|
* @param {String} json the JSON data to import from
|
|
*/
|
|
importJSON: function(json) {
|
|
// Try importing into `this`. If another item is returned, try adding
|
|
// it as a child (this won't be successful on some classes, returning
|
|
// null).
|
|
var res = Base.importJSON(json, this);
|
|
return res !== this
|
|
? this.addChild(res)
|
|
: res;
|
|
},
|
|
|
|
/**
|
|
* Exports the item with its content and child items as an SVG DOM.
|
|
*
|
|
* @name Item#exportSVG
|
|
* @function
|
|
*
|
|
* @option [options.asString=false] {Boolean} whether a SVG node or a
|
|
* `String` is to be returned
|
|
* @option [options.precision=5] {Number} the amount of fractional digits in
|
|
* numbers used in SVG data
|
|
* @option [options.matchShapes=false] {Boolean} whether path items should
|
|
* tried to be converted to SVG shape items (rect, circle, ellipse,
|
|
* line, polyline, polygon), if their geometries match
|
|
* @option [options.embedImages=true] {Boolean} whether raster images should
|
|
* be embedded as base64 data inlined in the xlink:href attribute, or
|
|
* kept as a link to their external URL.
|
|
*
|
|
* @param {Object} [options] the export options
|
|
* @return {SVGElement} the item converted to an SVG node
|
|
*/
|
|
|
|
/**
|
|
* Converts the provided SVG content into Paper.js items and adds them to
|
|
* the this item's children list. Note that the item is not cleared first.
|
|
* You can call {@link Item#removeChildren()} to do so.
|
|
*
|
|
* @name Item#importSVG
|
|
* @function
|
|
*
|
|
* @option [options.expandShapes=false] {Boolean} whether imported shape
|
|
* items should be expanded to path items
|
|
* @option [options.onLoad] {Function} the callback function to call once
|
|
* the SVG content is loaded from the given URL. Only required when
|
|
* loading from external files.
|
|
* @option [options.applyMatrix={@link PaperScope#settings}.applyMatrix]
|
|
* {Boolean} whether imported items should have their transformation
|
|
* matrices applied to their contents or not
|
|
*
|
|
* @param {SVGElement|String} svg the SVG content to import, either as a SVG
|
|
* DOM node, a string containing SVG content, or a string describing the
|
|
* URL of the SVG file to fetch.
|
|
* @param {Object} [options] the import options
|
|
* @return {Item} the newly created Paper.js item containing the converted
|
|
* SVG content
|
|
*/
|
|
/**
|
|
* Imports the provided external SVG file, converts it into Paper.js items
|
|
* and adds them to the this item's children list. Note that the item is not
|
|
* cleared first. You can call {@link Item#removeChildren()} to do so.
|
|
*
|
|
* @name Item#importSVG
|
|
* @function
|
|
*
|
|
* @param {SVGElement|String} svg the URL of the SVG file to fetch.
|
|
* @param {Function} onLoad the callback function to call once the SVG
|
|
* content is loaded from the given URL.
|
|
* @return {Item} the newly created Paper.js item containing the converted
|
|
* SVG content
|
|
*/
|
|
|
|
/**
|
|
* {@grouptitle Hierarchy Operations} Adds the specified item as a child of
|
|
* this item at the end of the its children list. You can use this function
|
|
* for groups, compound paths and layers.
|
|
*
|
|
* @param {Item} item the item to be added as a child
|
|
* @return {Item} the added item, or `null` if adding was not possible
|
|
*/
|
|
addChild: function(item, _preserve) {
|
|
return this.insertChild(undefined, item, _preserve);
|
|
},
|
|
|
|
/**
|
|
* Inserts the specified item as a child of this item at the specified index
|
|
* in its {@link #children} list. You can use this function for groups,
|
|
* compound paths and layers.
|
|
*
|
|
* @param {Number} index
|
|
* @param {Item} item the item to be inserted as a child
|
|
* @return {Item} the inserted item, or `null` if inserting was not possible
|
|
*/
|
|
insertChild: function(index, item, _preserve) {
|
|
var res = item ? this.insertChildren(index, [item], _preserve) : null;
|
|
return res && res[0];
|
|
},
|
|
|
|
/**
|
|
* Adds the specified items as children of this item at the end of the its
|
|
* children list. You can use this function for groups, compound paths and
|
|
* layers.
|
|
*
|
|
* @param {Item[]} items The items to be added as children
|
|
* @return {Item[]} the added items, or `null` if adding was not possible
|
|
*/
|
|
addChildren: function(items, _preserve) {
|
|
return this.insertChildren(this._children.length, items, _preserve);
|
|
},
|
|
|
|
/**
|
|
* Inserts the specified items as children of this item at the specified
|
|
* index in its {@link #children} list. You can use this function for
|
|
* groups, compound paths and layers.
|
|
*
|
|
* @param {Number} index
|
|
* @param {Item[]} items The items to be appended as children
|
|
* @return {Item[]} the inserted items, or `null` if inserted was not
|
|
* possible
|
|
*/
|
|
insertChildren: function(index, items, _preserve, _proto) {
|
|
// CompoundPath#insertChildren() requires _preserve and _type:
|
|
// _preserve avoids changing of the children's path orientation
|
|
// _proto enforces the prototype of the inserted items, as used by
|
|
// CompoundPath#insertChildren()
|
|
var children = this._children;
|
|
if (children && items && items.length > 0) {
|
|
// We need to clone items because it might be
|
|
// an Item#children array. Also, we're removing elements if they
|
|
// don't match _type. Use Array.prototype.slice because items can be
|
|
// an arguments object.
|
|
items = Array.prototype.slice.apply(items);
|
|
// Remove the items from their parents first, since they might be
|
|
// inserted into their own parents, affecting indices.
|
|
// Use the loop also to filter out wrong _type.
|
|
for (var i = items.length - 1; i >= 0; i--) {
|
|
var item = items[i];
|
|
if (_proto && !(item instanceof _proto)) {
|
|
items.splice(i, 1);
|
|
} else {
|
|
// If the item is removed and inserted it again further
|
|
/// above, the index needs to be adjusted accordingly.
|
|
var shift = item._parent === this && item._index < index;
|
|
// Notify parent of change. Don't notify item itself yet,
|
|
// as we're doing so when adding it to the new parent below.
|
|
if (item._remove(false, true) && shift)
|
|
index--;
|
|
}
|
|
}
|
|
Base.splice(children, items, index, 0);
|
|
var project = this._project,
|
|
// See #_remove() for an explanation of this:
|
|
notifySelf = project && project._changes;
|
|
for (var i = 0, l = items.length; i < l; i++) {
|
|
var item = items[i];
|
|
item._parent = this;
|
|
item._setProject(this._project, true);
|
|
// Setting the name again makes sure all name lookup structures
|
|
// are kept in sync.
|
|
if (item._name)
|
|
item.setName(item._name);
|
|
if (notifySelf)
|
|
this._changed(/*#=*/Change.INSERTION);
|
|
}
|
|
this._changed(/*#=*/Change.CHILDREN);
|
|
} else {
|
|
items = null;
|
|
}
|
|
return items;
|
|
},
|
|
|
|
// Private helper for #insertAbove() / #insertBelow()
|
|
_insertSibling: function(index, item, _preserve) {
|
|
return this._parent
|
|
? this._parent.insertChild(index, item, _preserve)
|
|
: null;
|
|
},
|
|
|
|
/**
|
|
* Inserts this item above the specified item.
|
|
*
|
|
* @param {Item} item the item above which it should be inserted
|
|
* @return {Item} the inserted item, or `null` if inserting was not possible
|
|
*/
|
|
insertAbove: function(item, _preserve) {
|
|
return item._insertSibling(item._index + 1, this, _preserve);
|
|
},
|
|
|
|
/**
|
|
* Inserts this item below the specified item.
|
|
*
|
|
* @param {Item} item the item below which it should be inserted
|
|
* @return {Item} the inserted item, or `null` if inserting was not possible
|
|
*/
|
|
insertBelow: function(item, _preserve) {
|
|
return item._insertSibling(item._index, this, _preserve);
|
|
},
|
|
|
|
/**
|
|
* Sends this item to the back of all other items within the same parent.
|
|
*/
|
|
sendToBack: function() {
|
|
// If there is no parent and the item is a layer, delegate to project
|
|
// instead.
|
|
return (this._parent || this instanceof Layer && this._project)
|
|
.insertChild(0, this);
|
|
},
|
|
|
|
/**
|
|
* Brings this item to the front of all other items within the same parent.
|
|
*/
|
|
bringToFront: function() {
|
|
// If there is no parent and the item is a layer, delegate to project
|
|
// instead.
|
|
return (this._parent || this instanceof Layer && this._project)
|
|
.addChild(this);
|
|
},
|
|
|
|
/**
|
|
* Inserts the specified item as a child of this item by appending it to
|
|
* the list of children and moving it above all other children. You can
|
|
* use this function for groups, compound paths and layers.
|
|
*
|
|
* @function
|
|
* @param {Item} item The item to be appended as a child
|
|
* @deprecated use {@link #addChild(item)} instead.
|
|
*/
|
|
appendTop: '#addChild',
|
|
|
|
/**
|
|
* Inserts the specified item as a child of this item by appending it to
|
|
* the list of children and moving it below all other children. You can
|
|
* use this function for groups, compound paths and layers.
|
|
*
|
|
* @param {Item} item The item to be appended as a child
|
|
* @deprecated use {@link #insertChild(index, item)} instead.
|
|
*/
|
|
appendBottom: function(item) {
|
|
return this.insertChild(0, item);
|
|
},
|
|
|
|
/**
|
|
* Moves this item above the specified item.
|
|
*
|
|
* @function
|
|
* @param {Item} item The item above which it should be moved
|
|
* @return {Boolean} {@true if it was moved}
|
|
* @deprecated use {@link #insertAbove(item)} instead.
|
|
*/
|
|
moveAbove: '#insertAbove',
|
|
|
|
/**
|
|
* Moves the item below the specified item.
|
|
*
|
|
* @function
|
|
* @param {Item} item the item below which it should be moved
|
|
* @return {Boolean} {@true if it was moved}
|
|
* @deprecated use {@link #insertBelow(item)} instead.
|
|
*/
|
|
moveBelow: '#insertBelow',
|
|
|
|
/**
|
|
* If this is a group, layer or compound-path with only one child-item,
|
|
* the child-item is moved outside and the parent is erased. Otherwise, the
|
|
* item itself is returned unmodified.
|
|
*
|
|
* @return {Item} the reduced item
|
|
*/
|
|
reduce: function() {
|
|
var children = this._children;
|
|
if (children && children.length === 1) {
|
|
var child = children[0].reduce();
|
|
// Make sure the reduced item has the same parent as the original.
|
|
if (this._parent) {
|
|
child.insertAbove(this);
|
|
this.remove();
|
|
} else {
|
|
child.remove();
|
|
}
|
|
child.copyAttributes(this);
|
|
return child;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Removes the item from its parent's named children list.
|
|
*/
|
|
_removeNamed: function() {
|
|
var parent = this._parent;
|
|
if (parent) {
|
|
var children = parent._children,
|
|
namedChildren = parent._namedChildren,
|
|
name = this._name,
|
|
namedArray = namedChildren[name],
|
|
index = namedArray ? namedArray.indexOf(this) : -1;
|
|
if (index !== -1) {
|
|
// Remove the named reference
|
|
if (children[name] == this)
|
|
delete children[name];
|
|
// Remove this entry
|
|
namedArray.splice(index, 1);
|
|
// If there are any items left in the named array, set
|
|
// the last of them to be this.parent.children[this.name]
|
|
if (namedArray.length) {
|
|
children[name] = namedArray[namedArray.length - 1];
|
|
} else {
|
|
// Otherwise delete the empty array
|
|
delete namedChildren[name];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes the item from its parent's children list.
|
|
*/
|
|
_remove: function(notifySelf, notifyParent) {
|
|
var parent = this._parent;
|
|
if (parent) {
|
|
if (this._name)
|
|
this._removeNamed();
|
|
if (this._index != null)
|
|
Base.splice(parent._children, null, this._index, 1);
|
|
this._installEvents(false);
|
|
// Notify self of the insertion change. We only need this
|
|
// notification if we're tracking changes for now.
|
|
if (notifySelf) {
|
|
var project = this._project;
|
|
if (project && project._changes)
|
|
this._changed(/*#=*/Change.INSERTION);
|
|
}
|
|
// Notify parent of changed children
|
|
if (notifyParent)
|
|
parent._changed(/*#=*/Change.CHILDREN);
|
|
this._parent = null;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Removes the item and all its children from the project. The item is not
|
|
* destroyed and can be inserted again after removal.
|
|
*
|
|
* @return {Boolean} {@true if the item was removed}
|
|
*/
|
|
remove: function() {
|
|
// Notify self and parent of change:
|
|
return this._remove(true, true);
|
|
},
|
|
|
|
/**
|
|
* Replaces this item with the provided new item which will takes its place
|
|
* in the project hierarchy instead.
|
|
*
|
|
* @return {Boolean} {@true if the item was replaced}
|
|
*/
|
|
replaceWith: function(item) {
|
|
var ok = item && item.insertBelow(this);
|
|
if (ok)
|
|
this.remove();
|
|
return ok;
|
|
},
|
|
|
|
/**
|
|
* Removes all of the item's {@link #children} (if any).
|
|
*
|
|
* @name Item#removeChildren
|
|
* @alias Item#clear
|
|
* @function
|
|
* @return {Item[]} an array containing the removed items
|
|
*/
|
|
/**
|
|
* Removes the children from the specified `from` index to the `to` index
|
|
* from the parent's {@link #children} array.
|
|
*
|
|
* @name Item#removeChildren
|
|
* @function
|
|
* @param {Number} from the beginning index, inclusive
|
|
* @param {Number} [to=children.length] the ending index, exclusive
|
|
* @return {Item[]} an array containing the removed items
|
|
*/
|
|
removeChildren: function(from, to) {
|
|
if (!this._children)
|
|
return null;
|
|
from = from || 0;
|
|
to = Base.pick(to, this._children.length);
|
|
// Use Base.splice(), which adjusts #_index for the items above, and
|
|
// deletes it for the removed items. Calling #_remove() afterwards is
|
|
// fine, since it only calls Base.splice() if #_index is set.
|
|
var removed = Base.splice(this._children, null, from, to - from);
|
|
for (var i = removed.length - 1; i >= 0; i--) {
|
|
// Don't notify parent each time, notify it separately after.
|
|
removed[i]._remove(true, false);
|
|
}
|
|
if (removed.length > 0)
|
|
this._changed(/*#=*/Change.CHILDREN);
|
|
return removed;
|
|
},
|
|
|
|
// DOCS Item#clear()
|
|
clear: '#removeChildren',
|
|
|
|
/**
|
|
* Reverses the order of the item's children
|
|
*/
|
|
reverseChildren: function() {
|
|
if (this._children) {
|
|
this._children.reverse();
|
|
// Adjust indices
|
|
for (var i = 0, l = this._children.length; i < l; i++)
|
|
this._children[i]._index = i;
|
|
this._changed(/*#=*/Change.CHILDREN);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* {@grouptitle Tests}
|
|
* Specifies whether the item has any content or not. The meaning of what
|
|
* content is differs from type to type. For example, a {@link Group} with
|
|
* no children, a {@link TextItem} with no text content and a {@link Path}
|
|
* with no segments all are considered empty.
|
|
*
|
|
* @return Boolean
|
|
*/
|
|
isEmpty: function() {
|
|
return !this._children || this._children.length === 0;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item is editable.
|
|
*
|
|
* @return {Boolean} {@true when neither the item, nor its parents are
|
|
* locked or hidden}
|
|
* @ignore
|
|
*/
|
|
// TODO: Item#isEditable is currently ignored in the documentation, as
|
|
// locking an item currently has no effect
|
|
isEditable: function() {
|
|
var item = this;
|
|
while (item) {
|
|
if (!item._visible || item._locked)
|
|
return false;
|
|
item = item._parent;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item is valid, i.e. it hasn't been removed.
|
|
*
|
|
* @return {Boolean} {@true if the item is valid}
|
|
*/
|
|
// TODO: isValid / checkValid
|
|
|
|
/**
|
|
* {@grouptitle Style Tests}
|
|
*
|
|
* Checks whether the item has a fill.
|
|
*
|
|
* @return {Boolean} {@true if the item has a fill}
|
|
*/
|
|
hasFill: function() {
|
|
return this.getStyle().hasFill();
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item has a stroke.
|
|
*
|
|
* @return {Boolean} {@true if the item has a stroke}
|
|
*/
|
|
hasStroke: function() {
|
|
return this.getStyle().hasStroke();
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item has a shadow.
|
|
*
|
|
* @return {Boolean} {@true if the item has a shadow}
|
|
*/
|
|
hasShadow: function() {
|
|
return this.getStyle().hasShadow();
|
|
},
|
|
|
|
/**
|
|
* Returns -1 if 'this' is above 'item', 1 if below, 0 if their order is not
|
|
* defined in such a way, e.g. if one is a descendant of the other.
|
|
*/
|
|
_getOrder: function(item) {
|
|
// Private method that produces a list of anchestors, starting with the
|
|
// root and ending with the actual element as the last entry.
|
|
function getList(item) {
|
|
var list = [];
|
|
do {
|
|
list.unshift(item);
|
|
} while (item = item._parent);
|
|
return list;
|
|
}
|
|
var list1 = getList(this),
|
|
list2 = getList(item);
|
|
for (var i = 0, l = Math.min(list1.length, list2.length); i < l; i++) {
|
|
if (list1[i] != list2[i]) {
|
|
// Found the position in the parents list where the two start
|
|
// to differ. Look at who's above who.
|
|
return list1[i]._index < list2[i]._index ? 1 : -1;
|
|
}
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* {@grouptitle Hierarchy Tests}
|
|
*
|
|
* Checks if the item contains any children items.
|
|
*
|
|
* @return {Boolean} {@true it has one or more children}
|
|
*/
|
|
hasChildren: function() {
|
|
return this._children && this._children.length > 0;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item and all its parents are inserted into the DOM or
|
|
* not.
|
|
*
|
|
* @return {Boolean} {@true if the item is inserted into the DOM}
|
|
*/
|
|
isInserted: function() {
|
|
return this._parent ? this._parent.isInserted() : false;
|
|
},
|
|
|
|
/**
|
|
* Checks if this item is above the specified item in the stacking order
|
|
* of the project.
|
|
*
|
|
* @param {Item} item The item to check against
|
|
* @return {Boolean} {@true if it is above the specified item}
|
|
*/
|
|
isAbove: function(item) {
|
|
return this._getOrder(item) === -1;
|
|
},
|
|
|
|
/**
|
|
* Checks if the item is below the specified item in the stacking order of
|
|
* the project.
|
|
*
|
|
* @param {Item} item The item to check against
|
|
* @return {Boolean} {@true if it is below the specified item}
|
|
*/
|
|
isBelow: function(item) {
|
|
return this._getOrder(item) === 1;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the specified item is the parent of the item.
|
|
*
|
|
* @param {Item} item The item to check against
|
|
* @return {Boolean} {@true if it is the parent of the item}
|
|
*/
|
|
isParent: function(item) {
|
|
return this._parent === item;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the specified item is a child of the item.
|
|
*
|
|
* @param {Item} item The item to check against
|
|
* @return {Boolean} {@true it is a child of the item}
|
|
*/
|
|
isChild: function(item) {
|
|
return item && item._parent === this;
|
|
},
|
|
|
|
/**
|
|
* Checks if the item is contained within the specified item.
|
|
*
|
|
* @param {Item} item The item to check against
|
|
* @return {Boolean} {@true if it is inside the specified item}
|
|
*/
|
|
isDescendant: function(item) {
|
|
var parent = this;
|
|
while (parent = parent._parent) {
|
|
if (parent == item)
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Checks if the item is an ancestor of the specified item.
|
|
*
|
|
* @param {Item} item the item to check against
|
|
* @return {Boolean} {@true if the item is an ancestor of the specified
|
|
* item}
|
|
*/
|
|
isAncestor: function(item) {
|
|
return item ? item.isDescendant(this) : false;
|
|
},
|
|
|
|
/**
|
|
* Checks if the item is an a sibling of the specified item.
|
|
*
|
|
* @param {Item} item the item to check against
|
|
* @return {Boolean} {@true if the item is aa sibling of the specified item}
|
|
*/
|
|
isSibling: function(item) {
|
|
return this._parent === item._parent;
|
|
},
|
|
|
|
/**
|
|
* Checks whether the item is grouped with the specified item.
|
|
*
|
|
* @param {Item} item
|
|
* @return {Boolean} {@true if the items are grouped together}
|
|
*/
|
|
isGroupedWith: function(item) {
|
|
var parent = this._parent;
|
|
while (parent) {
|
|
// Find group parents. Check for parent._parent, since don't want
|
|
// top level layers, because they also inherit from Group
|
|
if (parent._parent
|
|
&& /^(Group|Layer|CompoundPath)$/.test(parent._class)
|
|
&& item.isDescendant(parent))
|
|
return true;
|
|
// Keep walking up otherwise
|
|
parent = parent._parent;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// Document all style properties which get injected into Item by Style:
|
|
|
|
/**
|
|
* {@grouptitle Stroke Style}
|
|
*
|
|
* The color of the stroke.
|
|
*
|
|
* @name Item#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({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
*
|
|
* // Set its stroke color to RGB red:
|
|
* circle.strokeColor = new Color(1, 0, 0);
|
|
*/
|
|
|
|
/**
|
|
* The width of the stroke.
|
|
*
|
|
* @name Item#strokeWidth
|
|
* @property
|
|
* @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({
|
|
* center: [80, 50],
|
|
* radius: 35,
|
|
* strokeColor: 'red'
|
|
* });
|
|
*
|
|
* // Set its stroke width to 10:
|
|
* circle.strokeWidth = 10;
|
|
*/
|
|
|
|
/**
|
|
* The shape to be used at the beginning and end of open {@link Path} items,
|
|
* when they have a stroke.
|
|
*
|
|
* @name Item#strokeCap
|
|
* @property
|
|
* @default 'butt'
|
|
* @type String('round', 'square', 'butt')
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // A look at the different stroke caps:
|
|
*
|
|
* var line = new Path({
|
|
* segments: [[80, 50], [420, 50]],
|
|
* strokeColor: 'black',
|
|
* strokeWidth: 20,
|
|
* 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 segments and corners of {@link Path} items
|
|
* when they have a stroke.
|
|
*
|
|
* @name Item#strokeJoin
|
|
* @property
|
|
* @default 'miter'
|
|
* @type String('miter', 'round', 'bevel')
|
|
*
|
|
*
|
|
* @example {@paperscript height=120}
|
|
* // A look at the different stroke joins:
|
|
* var path = new Path({
|
|
* segments: [[80, 100], [120, 40], [160, 100]],
|
|
* strokeColor: 'black',
|
|
* strokeWidth: 20,
|
|
* // Select the path, in order to see where the stroke is formed:
|
|
* 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 Item#dashOffset
|
|
* @property
|
|
* @default 0
|
|
* @type Number
|
|
*/
|
|
|
|
/**
|
|
* Specifies whether the stroke is to be drawn taking the current affine
|
|
* transformation into account (the default behavior), or whether it should
|
|
* appear as a non-scaling stroke.
|
|
*
|
|
* @name Item#strokeScaling
|
|
* @property
|
|
* @default true
|
|
* @type Boolean
|
|
*/
|
|
|
|
/**
|
|
* Specifies an array containing the dash and gap lengths of the stroke.
|
|
*
|
|
* @example {@paperscript}
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 40,
|
|
* strokeWidth: 2,
|
|
* strokeColor: 'black'
|
|
* });
|
|
*
|
|
* // Set the dashed stroke to [10pt dash, 4pt gap]:
|
|
* path.dashArray = [10, 4];
|
|
*
|
|
* @name Item#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 Item#strokeJoin}, it is possible for the miter to
|
|
* extend far beyond the {@link Item#strokeWidth} of the path. The
|
|
* miterLimit imposes a limit on the ratio of the miter length to the
|
|
* {@link Item#strokeWidth}.
|
|
*
|
|
* @name Item#miterLimit
|
|
* @property
|
|
* @default 10
|
|
* @type Number
|
|
*/
|
|
|
|
/**
|
|
* {@grouptitle Fill Style}
|
|
*
|
|
* The fill color of the item.
|
|
*
|
|
* @name Item#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({
|
|
* center: [80, 50],
|
|
* radius: 35
|
|
* });
|
|
*
|
|
* // Set the fill color of the circle to RGB red:
|
|
* circle.fillColor = new Color(1, 0, 0);
|
|
*/
|
|
|
|
/**
|
|
* The fill-rule with which the shape gets filled. Please note that only
|
|
* modern browsers support fill-rules other than `'nonzero'`.
|
|
*
|
|
* @name Item#fillRule
|
|
* @property
|
|
* @default 'nonzero'
|
|
* @type String('nonzero', 'evenodd')
|
|
*/
|
|
|
|
/**
|
|
* {@grouptitle Shadow Style}
|
|
*
|
|
* The shadow color.
|
|
*
|
|
* @property
|
|
* @name Item#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 Item#shadowBlur
|
|
* @type Number
|
|
*/
|
|
|
|
/**
|
|
* The shadow's offset.
|
|
*
|
|
* @property
|
|
* @default 0
|
|
* @name Item#shadowOffset
|
|
* @type Point
|
|
*/
|
|
|
|
// TODO: Find a better name than selectedColor. It should also be used for
|
|
// guides, etc.
|
|
/**
|
|
* {@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 Item#selectedColor
|
|
* @property
|
|
* @type Color
|
|
*/
|
|
|
|
/**
|
|
* {@grouptitle Transform Functions}
|
|
*
|
|
* Translates (moves) the item by the given offset point.
|
|
*
|
|
* @param {Point} delta the offset to translate the item by
|
|
*/
|
|
translate: function(/* delta */) {
|
|
var mx = new Matrix();
|
|
return this.transform(mx.translate.apply(mx, arguments));
|
|
},
|
|
|
|
/**
|
|
* Rotates the item by a given angle around the given point.
|
|
*
|
|
* Angles are oriented clockwise and measured in degrees.
|
|
*
|
|
* @param {Number} angle the rotation angle
|
|
* @param {Point} [center={@link Item#position}]
|
|
* @see Matrix#rotate
|
|
*
|
|
* @example {@paperscript}
|
|
* // Rotating an item:
|
|
*
|
|
* // Create a rectangle shaped path with its top left
|
|
* // point at {x: 80, y: 25} and a size of {width: 50, height: 50}:
|
|
* var path = new Path.Rectangle(new Point(80, 25), new Size(50, 50));
|
|
* path.fillColor = 'black';
|
|
*
|
|
* // Rotate the path by 30 degrees:
|
|
* path.rotate(30);
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Rotating an item around a specific point:
|
|
*
|
|
* // Create a rectangle shaped path with its top left
|
|
* // point at {x: 175, y: 50} and a size of {width: 100, height: 100}:
|
|
* var topLeft = new Point(175, 50);
|
|
* var size = new Size(100, 100);
|
|
* var path = new Path.Rectangle(topLeft, size);
|
|
* path.fillColor = 'black';
|
|
*
|
|
* // Draw a circle shaped path in the center of the view,
|
|
* // to show the rotation point:
|
|
* var circle = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 5,
|
|
* fillColor: 'white'
|
|
* });
|
|
*
|
|
* // Each frame rotate the path 3 degrees around the center point
|
|
* // of the view:
|
|
* function onFrame(event) {
|
|
* path.rotate(3, view.center);
|
|
* }
|
|
*/
|
|
rotate: function(angle /*, center */) {
|
|
return this.transform(new Matrix().rotate(angle,
|
|
Point.read(arguments, 1, { readNull: true })
|
|
|| this.getPosition(true)));
|
|
}
|
|
}, Base.each(['scale', 'shear', 'skew'], function(name) {
|
|
this[name] = function() {
|
|
// See Matrix#scale for explanation of this:
|
|
var point = Point.read(arguments),
|
|
center = Point.read(arguments, 0, { readNull: true });
|
|
return this.transform(new Matrix()[name](point,
|
|
center || this.getPosition(true)));
|
|
};
|
|
}, /** @lends Item# */{
|
|
/**
|
|
* Scales the item by the given value from its center point, or optionally
|
|
* from a supplied point.
|
|
*
|
|
* @name Item#scale
|
|
* @function
|
|
* @param {Number} scale the scale factor
|
|
* @param {Point} [center={@link Item#position}]
|
|
*
|
|
* @example {@paperscript}
|
|
* // Scaling an item from its center point:
|
|
*
|
|
* // Create a circle shaped path at { x: 80, y: 50 }
|
|
* // with a radius of 20:
|
|
* var circle = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 20,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Scale the path by 150% from its center point
|
|
* circle.scale(1.5);
|
|
*
|
|
* @example {@paperscript}
|
|
* // Scaling an item from a specific point:
|
|
*
|
|
* // Create a circle shaped path at { x: 80, y: 50 }
|
|
* // with a radius of 20:
|
|
* var circle = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 20,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Scale the path 150% from its bottom left corner
|
|
* circle.scale(1.5, circle.bounds.bottomLeft);
|
|
*/
|
|
/**
|
|
* Scales the item by the given values from its center point, or optionally
|
|
* from a supplied point.
|
|
*
|
|
* @name Item#scale
|
|
* @function
|
|
* @param {Number} hor the horizontal scale factor
|
|
* @param {Number} ver the vertical scale factor
|
|
* @param {Point} [center={@link Item#position}]
|
|
*
|
|
* @example {@paperscript}
|
|
* // Scaling an item horizontally by 300%:
|
|
*
|
|
* // Create a circle shaped path at { x: 100, y: 50 }
|
|
* // with a radius of 20:
|
|
* var circle = new Path.Circle({
|
|
* center: [100, 50],
|
|
* radius: 20,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Scale the path horizontally by 300%
|
|
* circle.scale(3, 1);
|
|
*/
|
|
|
|
// TODO: Add test for item shearing, as it might be behaving oddly.
|
|
/**
|
|
* Shears the item by the given value from its center point, or optionally
|
|
* by a supplied point.
|
|
*
|
|
* @name Item#shear
|
|
* @function
|
|
* @param {Point} shear the horziontal and vertical shear factors as a point
|
|
* @param {Point} [center={@link Item#position}]
|
|
* @see Matrix#shear
|
|
*/
|
|
/**
|
|
* Shears the item by the given values from its center point, or optionally
|
|
* by a supplied point.
|
|
*
|
|
* @name Item#shear
|
|
* @function
|
|
* @param {Number} hor the horizontal shear factor
|
|
* @param {Number} ver the vertical shear factor
|
|
* @param {Point} [center={@link Item#position}]
|
|
* @see Matrix#shear
|
|
*/
|
|
|
|
/**
|
|
* Skews the item by the given angles from its center point, or optionally
|
|
* by a supplied point.
|
|
*
|
|
* @name Item#skew
|
|
* @function
|
|
* @param {Point} skew the horziontal and vertical skew angles in degrees
|
|
* @param {Point} [center={@link Item#position}]
|
|
* @see Matrix#shear
|
|
*/
|
|
/**
|
|
* Skews the item by the given angles from its center point, or optionally
|
|
* by a supplied point.
|
|
*
|
|
* @name Item#skew
|
|
* @function
|
|
* @param {Number} hor the horizontal skew angle in degrees
|
|
* @param {Number} ver the vertical sskew angle in degrees
|
|
* @param {Point} [center={@link Item#position}]
|
|
* @see Matrix#shear
|
|
*/
|
|
}), /** @lends Item# */{
|
|
/**
|
|
* Transform the item.
|
|
*
|
|
* @param {Matrix} matrix the matrix by which the item shall be transformed
|
|
*/
|
|
// TODO: Implement flags:
|
|
// @param {String[]} flags Array of any of the following: 'objects',
|
|
// 'children', 'fill-gradients', 'fill-patterns', 'stroke-patterns',
|
|
// 'lines'. Default: ['objects', 'children']
|
|
transform: function(matrix, _applyMatrix, _applyRecursively,
|
|
_setApplyMatrix) {
|
|
// If no matrix is provided, or the matrix is the identity, we might
|
|
// still have some work to do in case _applyMatrix is true
|
|
if (matrix && matrix.isIdentity())
|
|
matrix = null;
|
|
var _matrix = this._matrix,
|
|
applyMatrix = (_applyMatrix || this._applyMatrix)
|
|
// Don't apply _matrix if the result of concatenating with
|
|
// matrix would be identity.
|
|
&& ((!_matrix.isIdentity() || matrix)
|
|
// Even if it's an identity matrix, we still need to
|
|
// recursively apply the matrix to children.
|
|
|| _applyMatrix && _applyRecursively && this._children);
|
|
// Bail out if there is nothing to do.
|
|
if (!matrix && !applyMatrix)
|
|
return this;
|
|
// Simply preconcatenate the internal matrix with the passed one:
|
|
if (matrix)
|
|
_matrix.preConcatenate(matrix);
|
|
// Call #_transformContent() now, if we need to directly apply the
|
|
// internal _matrix transformations to the item's content.
|
|
// Application is not possible on Raster, PointText, PlacedSymbol, since
|
|
// the matrix is where the actual transformation state is stored.
|
|
if (applyMatrix = applyMatrix && this._transformContent(_matrix,
|
|
_applyRecursively, _setApplyMatrix)) {
|
|
// When the _matrix could be applied, we also need to transform
|
|
// color styles (only gradients so far) and pivot point:
|
|
var pivot = this._pivot,
|
|
style = this._style,
|
|
// pass true for _dontMerge so we don't recursively transform
|
|
// styles on groups' children.
|
|
fillColor = style.getFillColor(true),
|
|
strokeColor = style.getStrokeColor(true);
|
|
if (pivot)
|
|
_matrix._transformPoint(pivot, pivot, true);
|
|
if (fillColor)
|
|
fillColor.transform(_matrix);
|
|
if (strokeColor)
|
|
strokeColor.transform(_matrix);
|
|
// Reset the internal matrix to the identity transformation if it
|
|
// was possible to apply it.
|
|
_matrix.reset(true);
|
|
// Set the internal _applyMatrix flag to true if we're told to do so
|
|
if (_setApplyMatrix && this._canApplyMatrix)
|
|
this._applyMatrix = true;
|
|
}
|
|
// Calling _changed will clear _bounds and _position, but depending
|
|
// on matrix we can calculate and set them again, so preserve them.
|
|
var bounds = this._bounds,
|
|
position = this._position;
|
|
// We always need to call _changed since we're caching bounds on all
|
|
// items, including Group.
|
|
this._changed(/*#=*/Change.GEOMETRY);
|
|
// Detect matrices that contain only translations and scaling
|
|
// and transform the cached _bounds and _position without having to
|
|
// fully recalculate each time.
|
|
var decomp = bounds && matrix && matrix.decompose();
|
|
if (decomp && !decomp.shearing && decomp.rotation % 90 === 0) {
|
|
// Transform the old bound by looping through all the cached bounds
|
|
// in _bounds and transform each.
|
|
for (var key in bounds) {
|
|
var rect = bounds[key];
|
|
// If these are internal bounds, only transform them if this
|
|
// item applied its matrix.
|
|
if (applyMatrix || !rect._internal)
|
|
matrix._transformBounds(rect, rect);
|
|
}
|
|
// If we have cached bounds, update _position again as its
|
|
// center. We need to take into account _boundsGetter here too, in
|
|
// case another getter is assigned to it, e.g. 'getStrokeBounds'.
|
|
var getter = this._boundsGetter,
|
|
rect = bounds[getter && getter.getBounds || getter || 'getBounds'];
|
|
if (rect)
|
|
this._position = rect.getCenter(true);
|
|
this._bounds = bounds;
|
|
} else if (matrix && position) {
|
|
// Transform position as well.
|
|
this._position = matrix._transformPoint(position, position);
|
|
}
|
|
// Allow chaining here, since transform() is related to Matrix functions
|
|
return this;
|
|
},
|
|
|
|
_transformContent: function(matrix, applyRecursively, setApplyMatrix) {
|
|
var children = this._children;
|
|
if (children) {
|
|
for (var i = 0, l = children.length; i < l; i++)
|
|
children[i].transform(matrix, true, applyRecursively,
|
|
setApplyMatrix);
|
|
return true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Converts the specified point from global project coordinate space to the
|
|
* item's own local coordinate space.
|
|
*
|
|
* @param {Point} point the point to be transformed
|
|
* @return {Point} the transformed point as a new instance
|
|
*/
|
|
globalToLocal: function(/* point */) {
|
|
return this.getGlobalMatrix(true)._inverseTransform(
|
|
Point.read(arguments));
|
|
},
|
|
|
|
/**
|
|
* Converts the specified point from the item's own local coordinate space
|
|
* to the global project coordinate space.
|
|
*
|
|
* @param {Point} point the point to be transformed
|
|
* @return {Point} the transformed point as a new instance
|
|
*/
|
|
localToGlobal: function(/* point */) {
|
|
return this.getGlobalMatrix(true)._transformPoint(
|
|
Point.read(arguments));
|
|
},
|
|
|
|
/**
|
|
* Converts the specified point from the parent's coordinate space to
|
|
* item's own local coordinate space.
|
|
*
|
|
* @param {Point} point the point to be transformed
|
|
* @return {Point} the transformed point as a new instance
|
|
*/
|
|
parentToLocal: function(/* point */) {
|
|
return this._matrix._inverseTransform(Point.read(arguments));
|
|
},
|
|
|
|
/**
|
|
* Converts the specified point from the item's own local coordinate space
|
|
* to the parent's coordinate space.
|
|
*
|
|
* @param {Point} point the point to be transformed
|
|
* @return {Point} the transformed point as a new instance
|
|
*/
|
|
localToParent: function(/* point */) {
|
|
return this._matrix._transformPoint(Point.read(arguments));
|
|
},
|
|
|
|
/**
|
|
* Transform the item so that its {@link #bounds} fit within the specified
|
|
* rectangle, without changing its aspect ratio.
|
|
*
|
|
* @param {Rectangle} rectangle
|
|
* @param {Boolean} [fill=false]
|
|
*
|
|
* @example {@paperscript height=100}
|
|
* // Fitting an item to the bounding rectangle of another item's bounding
|
|
* // rectangle:
|
|
*
|
|
* // Create a rectangle shaped path with its top left corner
|
|
* // at {x: 80, y: 25} and a size of {width: 75, height: 50}:
|
|
* var path = new Path.Rectangle({
|
|
* point: [80, 25],
|
|
* size: [75, 50],
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Create a circle shaped path with its center at {x: 80, y: 50}
|
|
* // and a radius of 30.
|
|
* var circlePath = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 30,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Fit the circlePath to the bounding rectangle of
|
|
* // the rectangular path:
|
|
* circlePath.fitBounds(path.bounds);
|
|
*
|
|
* @example {@paperscript height=100}
|
|
* // Fitting an item to the bounding rectangle of another item's bounding
|
|
* // rectangle with the fill parameter set to true:
|
|
*
|
|
* // Create a rectangle shaped path with its top left corner
|
|
* // at {x: 80, y: 25} and a size of {width: 75, height: 50}:
|
|
* var path = new Path.Rectangle({
|
|
* point: [80, 25],
|
|
* size: [75, 50],
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Create a circle shaped path with its center at {x: 80, y: 50}
|
|
* // and a radius of 30.
|
|
* var circlePath = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 30,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Fit the circlePath to the bounding rectangle of
|
|
* // the rectangular path:
|
|
* circlePath.fitBounds(path.bounds, true);
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Fitting an item to the bounding rectangle of the view
|
|
* var path = new Path.Circle({
|
|
* center: [80, 50],
|
|
* radius: 30,
|
|
* fillColor: 'red'
|
|
* });
|
|
*
|
|
* // Fit the path to the bounding rectangle of the view:
|
|
* path.fitBounds(view.bounds);
|
|
*/
|
|
fitBounds: function(rectangle, fill) {
|
|
// TODO: Think about passing options with various ways of defining
|
|
// fitting.
|
|
rectangle = Rectangle.read(arguments);
|
|
var bounds = this.getBounds(),
|
|
itemRatio = bounds.height / bounds.width,
|
|
rectRatio = rectangle.height / rectangle.width,
|
|
scale = (fill ? itemRatio > rectRatio : itemRatio < rectRatio)
|
|
? rectangle.width / bounds.width
|
|
: rectangle.height / bounds.height,
|
|
newBounds = new Rectangle(new Point(),
|
|
new Size(bounds.width * scale, bounds.height * scale));
|
|
newBounds.setCenter(rectangle.getCenter());
|
|
this.setBounds(newBounds);
|
|
},
|
|
|
|
/**
|
|
* {@grouptitle Event Handlers}
|
|
*
|
|
* Item level handler function to be called on each frame of an animation.
|
|
* The function receives an event object which contains information about
|
|
* the frame event:
|
|
*
|
|
* @option event.count {Number} the number of times the frame event was
|
|
* fired
|
|
* @option event.time {Number} the total amount of time passed since the
|
|
* first frame event in seconds
|
|
* @option event.delta {Number} the time passed in seconds since the last
|
|
* frame event
|
|
*
|
|
* @see View#onFrame
|
|
* @example {@paperscript}
|
|
* // Creating an animation:
|
|
*
|
|
* // Create a rectangle shaped path with its top left point at:
|
|
* // {x: 50, y: 25} and a size of {width: 50, height: 50}
|
|
* var path = new Path.Rectangle(new Point(50, 25), new Size(50, 50));
|
|
* path.fillColor = 'black';
|
|
*
|
|
* path.onFrame = function(event) {
|
|
* // Every frame, rotate the path by 3 degrees:
|
|
* this.rotate(3);
|
|
* }
|
|
*
|
|
* @name Item#onFrame
|
|
* @property
|
|
* @type Function
|
|
*/
|
|
|
|
/**
|
|
* The function to be called when the mouse button is pushed down on the
|
|
* item. The function receives a {@link MouseEvent} object which contains
|
|
* information about the mouse event.
|
|
*
|
|
* @name Item#onMouseDown
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // Press the mouse button down on the circle shaped path, to make it red:
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse is pressed on the item,
|
|
* // set its fill color to red:
|
|
* path.onMouseDown = function(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*
|
|
* @example {@paperscript}
|
|
* // Press the mouse on the circle shaped paths to remove them:
|
|
*
|
|
* // Loop 30 times:
|
|
* for (var i = 0; i < 30; i++) {
|
|
* // Create a circle shaped path at a random position
|
|
* // in the view:
|
|
* var path = new Path.Circle({
|
|
* center: Point.random() * view.size,
|
|
* radius: 25,
|
|
* fillColor: 'black',
|
|
* strokeColor: 'white'
|
|
* });
|
|
*
|
|
* // When the mouse is pressed on the item, remove it:
|
|
* path.onMouseDown = function(event) {
|
|
* this.remove();
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* The function to be called when the mouse button is released over the item.
|
|
* The function receives a {@link MouseEvent} object which contains
|
|
* information about the mouse event.
|
|
*
|
|
* @name Item#onMouseUp
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // Release the mouse button over the circle shaped path, to make it red:
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse is released over the item,
|
|
* // set its fill color to red:
|
|
* path.onMouseUp = function(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* The function to be called when the mouse clicks on the item. The function
|
|
* receives a {@link MouseEvent} object which contains information about the
|
|
* mouse event.
|
|
*
|
|
* @name Item#onClick
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // Click on the circle shaped path, to make it red:
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse is clicked on the item,
|
|
* // set its fill color to red:
|
|
* path.onClick = function(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*
|
|
* @example {@paperscript}
|
|
* // Click on the circle shaped paths to remove them:
|
|
*
|
|
* // Loop 30 times:
|
|
* for (var i = 0; i < 30; i++) {
|
|
* // Create a circle shaped path at a random position
|
|
* // in the view:
|
|
* var path = new Path.Circle({
|
|
* center: Point.random() * view.size,
|
|
* radius: 25,
|
|
* fillColor: 'black',
|
|
* strokeColor: 'white'
|
|
* });
|
|
*
|
|
* // When the mouse clicks on the item, remove it:
|
|
* path.onClick = function(event) {
|
|
* this.remove();
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* The function to be called when the mouse double clicks on the item. The
|
|
* function receives a {@link MouseEvent} object which contains information
|
|
* about the mouse event.
|
|
*
|
|
* @name Item#onDoubleClick
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // Double click on the circle shaped path, to make it red:
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse is double clicked on the item,
|
|
* // set its fill color to red:
|
|
* path.onDoubleClick = function(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*
|
|
* @example {@paperscript}
|
|
* // Double click on the circle shaped paths to remove them:
|
|
*
|
|
* // Loop 30 times:
|
|
* for (var i = 0; i < 30; i++) {
|
|
* // Create a circle shaped path at a random position
|
|
* // in the view:
|
|
* var path = new Path.Circle({
|
|
* center: Point.random() * view.size,
|
|
* radius: 25,
|
|
* fillColor: 'black',
|
|
* strokeColor: 'white'
|
|
* });
|
|
*
|
|
* // When the mouse is double clicked on the item, remove it:
|
|
* path.onDoubleClick = function(event) {
|
|
* this.remove();
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* The function to be called repeatedly when the mouse moves on top of the
|
|
* item. The function receives a {@link MouseEvent} object which contains
|
|
* information about the mouse event.
|
|
*
|
|
* @name Item#onMouseMove
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // Move over the circle shaped path, to change its opacity:
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse moves on top of the item, set its opacity
|
|
* // to a random value between 0 and 1:
|
|
* path.onMouseMove = function(event) {
|
|
* this.opacity = Math.random();
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* The function to be called when the mouse moves over the item. This
|
|
* function will only be called again, once the mouse moved outside of the
|
|
* item first. The function receives a {@link MouseEvent} object which
|
|
* contains information about the mouse event.
|
|
*
|
|
* @name Item#onMouseEnter
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // When you move the mouse over the item, its fill color is set to red.
|
|
* // When you move the mouse outside again, its fill color is set back
|
|
* // to black.
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse enters the item, set its fill color to red:
|
|
* path.onMouseEnter = function(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*
|
|
* // When the mouse leaves the item, set its fill color to black:
|
|
* path.onMouseLeave = function(event) {
|
|
* this.fillColor = 'black';
|
|
* }
|
|
* @example {@paperscript}
|
|
* // When you click the mouse, you create new circle shaped items. When you
|
|
* // move the mouse over the item, its fill color is set to red. When you
|
|
* // move the mouse outside again, its fill color is set back
|
|
* // to black.
|
|
*
|
|
* function enter(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*
|
|
* function leave(event) {
|
|
* this.fillColor = 'black';
|
|
* }
|
|
*
|
|
* // When the mouse is pressed:
|
|
* function onMouseDown(event) {
|
|
* // Create a circle shaped path at the position of the mouse:
|
|
* var path = new Path.Circle(event.point, 25);
|
|
* path.fillColor = 'black';
|
|
*
|
|
* // When the mouse enters the item, set its fill color to red:
|
|
* path.onMouseEnter = enter;
|
|
*
|
|
* // When the mouse leaves the item, set its fill color to black:
|
|
* path.onMouseLeave = leave;
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* The function to be called when the mouse moves out of the item.
|
|
* The function receives a {@link MouseEvent} object which contains
|
|
* information about the mouse event.
|
|
*
|
|
* @name Item#onMouseLeave
|
|
* @property
|
|
* @type Function
|
|
*
|
|
* @example {@paperscript}
|
|
* // Move the mouse over the circle shaped path and then move it out
|
|
* // of it again to set its fill color to red:
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse leaves the item, set its fill color to red:
|
|
* path.onMouseLeave = function(event) {
|
|
* this.fillColor = 'red';
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* {@grouptitle Event Handling}
|
|
*
|
|
* Attaches an event handler to the item.
|
|
*
|
|
* @name Item#on
|
|
* @function
|
|
* @param {String('mousedown', 'mouseup', 'mousedrag', 'click',
|
|
* 'doubleclick', 'mousemove', 'mouseenter', 'mouseleave')} type the event
|
|
* type
|
|
* @param {Function} function The function to be called when the event
|
|
* occurs
|
|
* @return {Item} this item itself, so calls can be chained
|
|
*
|
|
* @example {@paperscript}
|
|
* // Change the fill color of the path to red when the mouse enters its
|
|
* // shape and back to black again, when it leaves its shape.
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // When the mouse enters the item, set its fill color to red:
|
|
* path.on('mouseenter', function() {
|
|
* this.fillColor = 'red';
|
|
* });
|
|
*
|
|
* // When the mouse leaves the item, set its fill color to black:
|
|
* path.on('mouseleave', function() {
|
|
* this.fillColor = 'black';
|
|
* });
|
|
*/
|
|
/**
|
|
* Attaches one or more event handlers to the item.
|
|
*
|
|
* @name Item#on
|
|
* @function
|
|
* @param {Object} object an object literal containing one or more of the
|
|
* following properties: {@code mousedown, mouseup, mousedrag, click,
|
|
* doubleclick, mousemove, mouseenter, mouseleave}
|
|
* @return {Item} this item itself, so calls can be chained
|
|
*
|
|
* @example {@paperscript}
|
|
* // Change the fill color of the path to red when the mouse enters its
|
|
* // shape and back to black again, when it leaves its shape.
|
|
*
|
|
* // Create a circle shaped path at the center of the view:
|
|
* var path = new Path.Circle({
|
|
* center: view.center,
|
|
* radius: 25
|
|
* });
|
|
* path.fillColor = 'black';
|
|
*
|
|
* // When the mouse enters the item, set its fill color to red:
|
|
* path.on({
|
|
* mouseenter: function(event) {
|
|
* this.fillColor = 'red';
|
|
* },
|
|
* mouseleave: function(event) {
|
|
* this.fillColor = 'black';
|
|
* }
|
|
* });
|
|
* @example {@paperscript}
|
|
* // When you click the mouse, you create new circle shaped items. When you
|
|
* // move the mouse over the item, its fill color is set to red. When you
|
|
* // move the mouse outside again, its fill color is set black.
|
|
*
|
|
* var pathHandlers = {
|
|
* mouseenter: function(event) {
|
|
* this.fillColor = 'red';
|
|
* },
|
|
* mouseleave: function(event) {
|
|
* this.fillColor = 'black';
|
|
* }
|
|
* }
|
|
*
|
|
* // When the mouse is pressed:
|
|
* function onMouseDown(event) {
|
|
* // Create a circle shaped path at the position of the mouse:
|
|
* var path = new Path.Circle({
|
|
* center: event.point,
|
|
* radius: 25,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Attach the handers inside the object literal to the path:
|
|
* path.on(pathHandlers);
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Detach an event handler from the item.
|
|
*
|
|
* @name Item#off
|
|
* @function
|
|
* @param {String('mousedown', 'mouseup', 'mousedrag', 'click',
|
|
* 'doubleclick', 'mousemove', 'mouseenter', 'mouseleave')} type the event
|
|
* type
|
|
* @param {Function} function The function to be detached
|
|
* @return {Item} this item itself, so calls can be chained
|
|
*/
|
|
/**
|
|
* Detach one or more event handlers to the item.
|
|
*
|
|
* @name Item#off
|
|
* @function
|
|
* @param {Object} object an object literal containing one or more of the
|
|
* following properties: {@code mousedown, mouseup, mousedrag, click,
|
|
* doubleclick, mousemove, mouseenter, mouseleave}
|
|
* @return {Item} this item itself, so calls can be chained
|
|
*/
|
|
|
|
/**
|
|
* Emit an event on the item.
|
|
*
|
|
* @name Item#emit
|
|
* @function
|
|
* @param {String('mousedown', 'mouseup', 'mousedrag', 'click',
|
|
* 'doubleclick', 'mousemove', 'mouseenter', 'mouseleave')} type the event
|
|
* type
|
|
* @param {Object} event an object literal containing properties describing
|
|
* the event
|
|
* @return {Boolean} {@true if the event had listeners}
|
|
*/
|
|
|
|
/**
|
|
* Check if the item has one or more event handlers of the specified type.
|
|
*
|
|
* @name Item#responds
|
|
* @function
|
|
* @param {String('mousedown', 'mouseup', 'mousedrag', 'click',
|
|
* 'doubleclick', 'mousemove', 'mouseenter', 'mouseleave')} type the event
|
|
* type
|
|
* @return {Boolean} {@true if the item has one or more event handlers of
|
|
* the specified type}
|
|
*/
|
|
|
|
/**
|
|
* Private method that sets Path related styles on the canvas context.
|
|
* Not defined in Path as it is required by other classes too,
|
|
* e.g. PointText.
|
|
*/
|
|
_setStyles: function(ctx) {
|
|
// We can access internal properties since we're only using this on
|
|
// items without children, where styles would be merged.
|
|
var style = this._style,
|
|
fillColor = style.getFillColor(),
|
|
strokeColor = style.getStrokeColor(),
|
|
shadowColor = style.getShadowColor();
|
|
if (fillColor)
|
|
ctx.fillStyle = fillColor.toCanvasStyle(ctx);
|
|
if (strokeColor) {
|
|
var strokeWidth = style.getStrokeWidth();
|
|
if (strokeWidth > 0) {
|
|
ctx.strokeStyle = strokeColor.toCanvasStyle(ctx);
|
|
ctx.lineWidth = strokeWidth;
|
|
var strokeJoin = style.getStrokeJoin(),
|
|
strokeCap = style.getStrokeCap(),
|
|
miterLimit = style.getMiterLimit();
|
|
if (strokeJoin)
|
|
ctx.lineJoin = strokeJoin;
|
|
if (strokeCap)
|
|
ctx.lineCap = strokeCap;
|
|
if (miterLimit)
|
|
ctx.miterLimit = miterLimit;
|
|
if (paper.support.nativeDash) {
|
|
var dashArray = style.getDashArray(),
|
|
dashOffset = style.getDashOffset();
|
|
if (dashArray && dashArray.length) {
|
|
if ('setLineDash' in ctx) {
|
|
ctx.setLineDash(dashArray);
|
|
ctx.lineDashOffset = dashOffset;
|
|
} else {
|
|
ctx.mozDash = dashArray;
|
|
ctx.mozDashOffset = dashOffset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (shadowColor) {
|
|
var shadowBlur = style.getShadowBlur();
|
|
if (shadowBlur > 0) {
|
|
ctx.shadowColor = shadowColor.toCanvasStyle(ctx);
|
|
ctx.shadowBlur = shadowBlur;
|
|
var offset = this.getShadowOffset();
|
|
ctx.shadowOffsetX = offset.x;
|
|
ctx.shadowOffsetY = offset.y;
|
|
}
|
|
}
|
|
},
|
|
|
|
draw: function(ctx, param, parentStrokeMatrix) {
|
|
// Each time the project gets drawn, it's _updateVersion is increased.
|
|
// Keep the _updateVersion of drawn items in sync, so we have an easy
|
|
// way to know for which selected items we need to draw selection info.
|
|
var updateVersion = this._updateVersion = this._project._updateVersion;
|
|
// Now bail out if no actual drawing is required.
|
|
if (!this._visible || this._opacity === 0)
|
|
return;
|
|
// Keep calculating the current global matrix, by keeping a history
|
|
// and pushing / popping as we go along.
|
|
var matrices = param.matrices,
|
|
viewMatrix = param.viewMatrix,
|
|
matrix = this._matrix,
|
|
globalMatrix = matrices[matrices.length - 1].chain(matrix);
|
|
// If this item is not invertible, do not draw it, since it would cause
|
|
// empty ctx.currentPath and mess up caching. It appears to also be a
|
|
// good idea generally to not draw in such circumstances, e.g. SVG
|
|
// handles it the same way.
|
|
if (!globalMatrix.isInvertible())
|
|
return;
|
|
|
|
// Since globalMatrix does not take the view's matrix into account (we
|
|
// could have multiple views with different zooms), we may have to
|
|
// pre-concatenate the view's matrix.
|
|
// Note that it's only provided if it isn't the identity matrix.
|
|
function getViewMatrix(matrix) {
|
|
return viewMatrix ? viewMatrix.chain(matrix) : matrix;
|
|
}
|
|
|
|
// Only keep track of transformation if told so. See Project#draw()
|
|
matrices.push(globalMatrix);
|
|
if (param.updateMatrix) {
|
|
// Update the cached _globalMatrix and keep it versioned.
|
|
globalMatrix._updateVersion = updateVersion;
|
|
this._globalMatrix = globalMatrix;
|
|
}
|
|
|
|
// If the item has a blendMode or is defining an opacity, draw it on
|
|
// a temporary canvas first and composite the canvas afterwards.
|
|
// Paths with an opacity < 1 that both define a fillColor
|
|
// and strokeColor also need to be drawn on a temporary canvas
|
|
// first, since otherwise their stroke is drawn half transparent
|
|
// over their fill.
|
|
// Exclude Raster items since they never draw a stroke and handle
|
|
// opacity by themselves (they also don't call _setStyles)
|
|
var blendMode = this._blendMode,
|
|
opacity = this._opacity,
|
|
normalBlend = blendMode === 'normal',
|
|
nativeBlend = BlendMode.nativeModes[blendMode],
|
|
// Determine if we can draw directly, or if we need to draw into a
|
|
// separate canvas and then composite onto the main canvas.
|
|
direct = normalBlend && opacity === 1
|
|
|| param.dontStart // e.g. CompoundPath
|
|
|| param.clip
|
|
// If native blending is possible, see if the item allows it
|
|
|| (nativeBlend || normalBlend && opacity < 1)
|
|
&& this._canComposite(),
|
|
pixelRatio = param.pixelRatio || 1,
|
|
mainCtx, itemOffset, prevOffset;
|
|
if (!direct) {
|
|
// Apply the parent's global matrix to the calculation of correct
|
|
// bounds.
|
|
var bounds = this.getStrokeBounds(getViewMatrix(globalMatrix));
|
|
if (!bounds.width || !bounds.height)
|
|
return;
|
|
// Store previous offset and save the main context, so we can
|
|
// draw onto it later.
|
|
prevOffset = param.offset;
|
|
// Floor the offset and ceil the size, so we don't cut off any
|
|
// antialiased pixels when drawing onto the temporary canvas.
|
|
itemOffset = param.offset = bounds.getTopLeft().floor();
|
|
// Set ctx to the context of the temporary canvas, so we draw onto
|
|
// it, instead of the mainCtx.
|
|
mainCtx = ctx;
|
|
ctx = CanvasProvider.getContext(bounds.getSize().ceil().add(1)
|
|
.multiply(pixelRatio));
|
|
if (pixelRatio !== 1)
|
|
ctx.scale(pixelRatio, pixelRatio);
|
|
}
|
|
ctx.save();
|
|
// Get the transformation matrix for non-scaling strokes.
|
|
var strokeMatrix = parentStrokeMatrix
|
|
? parentStrokeMatrix.chain(matrix)
|
|
// pass `true` for dontMerge
|
|
: !this.getStrokeScaling(true) && getViewMatrix(globalMatrix),
|
|
// If we're drawing into a separate canvas and a clipItem is defined
|
|
// for the current rendering loop, we need to draw the clip item
|
|
// again.
|
|
clip = !direct && param.clipItem,
|
|
// If we're drawing with a strokeMatrix, the CTM is reset either way
|
|
// so we don't need to set it, except when we also have to draw a
|
|
// clipItem.
|
|
transform = !strokeMatrix || clip;
|
|
// If drawing directly, handle opacity and native blending now,
|
|
// otherwise we will do it later when the temporary canvas is composited.
|
|
if (direct) {
|
|
ctx.globalAlpha = opacity;
|
|
if (nativeBlend)
|
|
ctx.globalCompositeOperation = blendMode;
|
|
} else if (transform) {
|
|
// Translate the context so the topLeft of the item is at (0, 0)
|
|
// on the temporary canvas.
|
|
ctx.translate(-itemOffset.x, -itemOffset.y);
|
|
}
|
|
// Apply globalMatrix when drawing into temporary canvas.
|
|
if (transform)
|
|
(direct ? matrix : getViewMatrix(globalMatrix)).applyToContext(ctx);
|
|
if (clip)
|
|
param.clipItem.draw(ctx, param.extend({ clip: true }));
|
|
if (strokeMatrix) {
|
|
// Reset the transformation but take HiDPI pixel ratio into account.
|
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
// Also offset again when drawing non-directly.
|
|
// NOTE: Don't use itemOffset since offset might be from the parent,
|
|
// e.g. CompoundPath
|
|
var offset = param.offset;
|
|
if (offset)
|
|
ctx.translate(-offset.x, -offset.y);
|
|
}
|
|
this._draw(ctx, param, strokeMatrix);
|
|
ctx.restore();
|
|
matrices.pop();
|
|
if (param.clip && !param.dontFinish)
|
|
ctx.clip();
|
|
// If a temporary canvas was created, composite it onto the main canvas:
|
|
if (!direct) {
|
|
// Use BlendMode.process even for processing normal blendMode with
|
|
// opacity.
|
|
BlendMode.process(blendMode, ctx, mainCtx, opacity,
|
|
// Calculate the pixel offset of the temporary canvas to the
|
|
// main canvas. We also need to factor in the pixel-ratio.
|
|
itemOffset.subtract(prevOffset).multiply(pixelRatio));
|
|
// Return the temporary context, so it can be reused
|
|
CanvasProvider.release(ctx);
|
|
// Restore previous offset.
|
|
param.offset = prevOffset;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Checks the _updateVersion of the item to see if it got drawn in the draw
|
|
* loop. If the version is out of sync, the item is either not in the DOM
|
|
* anymore or is invisible.
|
|
*/
|
|
_isUpdated: function(updateVersion) {
|
|
var parent = this._parent;
|
|
// For compound-paths, we need to use the _updateVersion of the parent,
|
|
// because when using the ctx.currentPath optimization, the children
|
|
// don't have to get drawn on each frame and thus won't change their
|
|
// _updateVersion.
|
|
if (parent instanceof CompoundPath)
|
|
return parent._isUpdated(updateVersion);
|
|
// In case a parent is visible but isn't drawn (e.g. opacity == 0), the
|
|
// _updateVersion of all its children will not be updated, but the
|
|
// children should still be considered updated, and selections should be
|
|
// drawn for them. Excluded are only items with _visible == false:
|
|
var updated = this._updateVersion === updateVersion;
|
|
if (!updated && parent && parent._visible
|
|
&& parent._isUpdated(updateVersion)) {
|
|
this._updateVersion = updateVersion;
|
|
updated = true;
|
|
}
|
|
return updated;
|
|
},
|
|
|
|
_drawSelection: function(ctx, matrix, size, selectedItems, updateVersion) {
|
|
if ((this._drawSelected || this._boundsSelected)
|
|
&& this._isUpdated(updateVersion)) {
|
|
// Allow definition of selected color on a per item and per
|
|
// layer level, with a fallback to #009dec
|
|
var color = this.getSelectedColor(true)
|
|
|| this.getLayer().getSelectedColor(true),
|
|
mx = matrix.chain(this.getGlobalMatrix(true));
|
|
ctx.strokeStyle = ctx.fillStyle = color
|
|
? color.toCanvasStyle(ctx) : '#009dec';
|
|
if (this._drawSelected)
|
|
this._drawSelected(ctx, mx, selectedItems);
|
|
if (this._boundsSelected) {
|
|
var half = size / 2,
|
|
coords = mx._transformCorners(this.getInternalBounds());
|
|
// Now draw a rectangle that connects the transformed
|
|
// bounds corners, and draw the corners.
|
|
ctx.beginPath();
|
|
for (var i = 0; i < 8; i++)
|
|
ctx[i === 0 ? 'moveTo' : 'lineTo'](coords[i], coords[++i]);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
for (var i = 0; i < 8; i++)
|
|
ctx.fillRect(coords[i] - half, coords[++i] - half,
|
|
size, size);
|
|
}
|
|
}
|
|
},
|
|
|
|
_canComposite: function() {
|
|
return false;
|
|
}
|
|
}, Base.each(['down', 'drag', 'up', 'move'], function(name) {
|
|
this['removeOn' + Base.capitalize(name)] = function() {
|
|
var hash = {};
|
|
hash[name] = true;
|
|
return this.removeOn(hash);
|
|
};
|
|
}, /** @lends Item# */{
|
|
/**
|
|
* {@grouptitle Remove On Event}
|
|
*
|
|
* Removes the item when the events specified in the passed options object
|
|
* occur.
|
|
*
|
|
* @option options.move {Boolean) remove the item when the next {@link
|
|
* Tool#onMouseMove} event is fired.
|
|
*
|
|
* @option options.drag {Boolena) remove the item when the next {@link
|
|
* Tool#onMouseDrag} event is fired.
|
|
*
|
|
* @option options.down {Boolean) remove the item when the next {@link
|
|
* Tool#onMouseDown} event is fired.
|
|
*
|
|
* @option options.up {Boolean) remove the item when the next {@link
|
|
* Tool#onMouseUp} event is fired.
|
|
*
|
|
* @name Item#removeOn
|
|
* @function
|
|
* @param {Object} options
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Click and drag below:
|
|
* function onMouseDrag(event) {
|
|
* // Create a circle shaped path at the mouse position,
|
|
* // with a radius of 10:
|
|
* var path = new Path.Circle({
|
|
* center: event.point,
|
|
* radius: 10,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Remove the path on the next onMouseDrag or onMouseDown event:
|
|
* path.removeOn({
|
|
* drag: true,
|
|
* down: true
|
|
* });
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Removes the item when the next {@link Tool#onMouseMove} event is fired.
|
|
*
|
|
* @name Item#removeOnMove
|
|
* @function
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Move your mouse below:
|
|
* function onMouseMove(event) {
|
|
* // Create a circle shaped path at the mouse position,
|
|
* // with a radius of 10:
|
|
* var path = new Path.Circle({
|
|
* center: event.point,
|
|
* radius: 10,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // On the next move event, automatically remove the path:
|
|
* path.removeOnMove();
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Removes the item when the next {@link Tool#onMouseDown} event is fired.
|
|
*
|
|
* @name Item#removeOnDown
|
|
* @function
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Click a few times below:
|
|
* function onMouseDown(event) {
|
|
* // Create a circle shaped path at the mouse position,
|
|
* // with a radius of 10:
|
|
* var path = new Path.Circle({
|
|
* center: event.point,
|
|
* radius: 10,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Remove the path, next time the mouse is pressed:
|
|
* path.removeOnDown();
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Removes the item when the next {@link Tool#onMouseDrag} event is fired.
|
|
*
|
|
* @name Item#removeOnDrag
|
|
* @function
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Click and drag below:
|
|
* function onMouseDrag(event) {
|
|
* // Create a circle shaped path at the mouse position,
|
|
* // with a radius of 10:
|
|
* var path = new Path.Circle({
|
|
* center: event.point,
|
|
* radius: 10,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // On the next drag event, automatically remove the path:
|
|
* path.removeOnDrag();
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Removes the item when the next {@link Tool#onMouseUp} event is fired.
|
|
*
|
|
* @name Item#removeOnUp
|
|
* @function
|
|
*
|
|
* @example {@paperscript height=200}
|
|
* // Click a few times below:
|
|
* function onMouseDown(event) {
|
|
* // Create a circle shaped path at the mouse position,
|
|
* // with a radius of 10:
|
|
* var path = new Path.Circle({
|
|
* center: event.point,
|
|
* radius: 10,
|
|
* fillColor: 'black'
|
|
* });
|
|
*
|
|
* // Remove the path, when the mouse is released:
|
|
* path.removeOnUp();
|
|
* }
|
|
*/
|
|
// TODO: implement Item#removeOnFrame
|
|
removeOn: function(obj) {
|
|
for (var name in obj) {
|
|
if (obj[name]) {
|
|
var key = 'mouse' + name,
|
|
project = this._project,
|
|
sets = project._removeSets = project._removeSets || {};
|
|
sets[key] = sets[key] || {};
|
|
sets[key][this._id] = this;
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
}));
|