Merge branch 'doubleUpstreamDevelop' into mergePaper

This commit is contained in:
DD 2018-10-23 10:16:40 -04:00
commit ab7fa1e091
52 changed files with 1475 additions and 380 deletions

View file

@ -1,20 +1,21 @@
<!-- <!--
Questions: This issues database is used to keep track of bugs and new features.
https://groups.google.com/group/paperjs For questions and support, please visit the Gitter channel instead:
https://gitter.im/paperjs/paper.js https://gitter.im/Vincit/objection.js
--> -->
# Description/Steps to reproduce # Description/Steps to reproduce
<!-- <!--
If feature: A description of the feature If a feature: A description of the feature.
If bug: Steps to reproduce If a bug: The steps to reproduce the issue.
--> -->
# Link to reproduction test-case # Link to reproduction test-case
<!-- <!--
Link to a test-case for reproduction Please link to a test-case for reproduction, ideally as a repository with a
`packages.json` that installs all required dependencies to reduce confusion.
Note: Failure to provide a test-case for reproduction purposes will result in Note: Failure to provide a test-case for reproduction purposes will result in
the issue being closed. the issue being closed.
@ -23,16 +24,16 @@ the issue being closed.
# Expected result # Expected result
<!-- <!--
Also include actual results if bug Also include actual results when reporting a bug.
--> -->
# Additional information # Additional information
<!-- <!--
Please include the versions of Operating System and Browser that the issue is Please include the type and versions of Operating System, Node, as well as
encountered on. the underlying database that the issue is encountered on.
Examples: Examples:
macOS 10.12.6, Chrome 60.0.3112.113 macOS 10.12.6, Node 8.9.0, PostgreSQL 10.0
Windows 10 Pro 10586.962, Edge 25.10586.672.0 Windows 10 Pro 10586.962, Node 8.8.1, SQLite 3
--> -->

View file

@ -4,21 +4,23 @@
#### Related issues #### Related issues
<!-- <!--
Please use the following link syntaxes: Please list related issues and discussion by using the following syntax:
- relates to #49 (to reference issues in the current repository) - Relates to #49
(to reference issues in the Objection.js repository)
- Relates to https://github.com/tgriesser/knex/issues/100
(to reference issues in a related repository)
--> -->
- relates to <link_to_referenced_issue> - Relates to <link_to_referenced_issue>
### Checklist ### Checklist
<!-- <!--
- Please mark your choice with an "x" (i.e. [x], see - Please mark your choice with an "x" (i.e. [x], see
https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments) https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments)
- PR's without test coverage will be closed. - PR's without test coverage will be closed.
--> -->
- [ ] New tests added or existing tests modified to cover all changes - [ ] New tests added or existing tests modified to cover all changes
- [ ] Code conforms with the [style - [ ] Code conforms with the JSHint rules (`npm run jshint` passes)
guide](https://github.com/paperjs/paper.js/blob/develop/RULES.md)

View file

@ -1,7 +1,14 @@
language: node_js language: node_js
# Follow https://github.com/nodejs/LTS to decide when to remove a version # Follow https://github.com/nodejs/LTS to decide when to remove a version
node_js: node_js:
- '6' - stable
- 9
- 8
# 6.13 and 6.14 causing unreasonable errors in SymbolDefinition.initialize.
# https://travis-ci.org/paperjs/paper.js/jobs/434854796
# To avoid these versions, we specify version to 6.12 as temporary solution.
# See https://github.com/paperjs/paper.js/issues/1523
- 6.12
sudo: false sudo: false
env: env:
matrix: matrix:
@ -39,14 +46,3 @@ script:
- gulp test - gulp test
- gulp zip - gulp zip
- '[ "${TRAVIS_BRANCH}" = "develop" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ] && travis/deploy-prebuilt.sh || true' - '[ "${TRAVIS_BRANCH}" = "develop" ] && [ "${TRAVIS_NODE_VERSION}" = "stable" ] && travis/deploy-prebuilt.sh || true'
before_deploy:
- npm --no-git-tag-version version 0.11.$(date +%Y%m%d%H%M%S)
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
- git config --global user.name $(git log --pretty=format:"%an" -n1)
deploy:
- provider: npm
on:
branch: develop
skip_cleanup: true
email: $NPM_EMAIL
api_key: $NPM_TOKEN

View file

@ -14,3 +14,5 @@
- Andrew Wagenheim <abwagenheim@gmail.com> - Andrew Wagenheim <abwagenheim@gmail.com>
- Scott Kieronski <baroes0239@gmail.com> - Scott Kieronski <baroes0239@gmail.com>
- DD Liu <liudi@media.mit.edu> - DD Liu <liudi@media.mit.edu>
- Samuel Asensi <asensi.samuel@gmail.com>
- Takahiro Nishino <sapics.dev@gmail.com>

View file

@ -1,5 +1,61 @@
# Change Log # Change Log
## `0.11.8`
### News
This is the first release in quite a while, and it was made possible thanks to
two new people on the team:
A warm welcome to [@sasensi](https://github.com/sasensi) and
[@sapics](https://github.com/sapics), the two new and very active maintainers /
contributors! :tada:
Their efforts mean that many issues are finally getting proper attention and
solid fixes, as we are paving the way for the upcoming release of `1.0.0`. Here
the fixes and additions from the past two weeks:
### Fixed
- Prevent `paper` object from polluting the global scope (#1544).
- Make sure `Path#arcTo()` always passes through the provide through point
(#1477).
- Draw shadows on `Raster` images (#1437).
- Fix boolean operation edge case (#1506, #1513, #1515).
- Handle closed paths with only one segment in `Path#flatten()` (#1338).
- Remove memory leak on gradient colors (#1499).
- Support alpha channel in CSS colors (#1468, #1539, #1565).
- Improve color CSS string parsing and documentation.
- Improve caching of item positions (#1503).
- Always draw selected position in global coordinates system (#1545).
- Prevent empty `Symbol` items from causing issues with transformations (#1561).
- Better detect when a cached global matrix is not valid anymore (#1448).
- Correctly draw selected position when item is in a group with matrix not
applied (#1535).
- Improve handling of huge amounts of segments in paths (#1493).
- Do not trigger error messages about passive event listeners on Chrome (#1501).
- Fix errors with event listeners on mobile (#1533).
- Prevent first mouse drag event from being emitted twice (#1553).
- Support optional arguments in translate and rotate statements in SVG Import
(#1487).
- Make sure SVG import always applies imported attributes (#1416).
- Correctly handle `Raster` images positions in SVG import (#1328).
- Improve documentation for `Shape#toPath()` (#1374).
- Improve documentation of hit test coordinate system (#1430).
- Add documentation for `Item#locked` (#1436).
- Support Webpack bundling in Node.js server (#1482).
- Travis CI: Get unit tests to run correctly again.
- Travis CI: Remove Node 4 and add Node 9.
### Added
- `Curve#getTimesWithTangent()` and `Path#getOffsetsWithTangent()` as a way to
get the curve-times / offsets where the path is tangential to a given vector.
- `Raster#smoothing` to control if pixels should be blurred or repeated when a
raster is scaled up (#1521).
- Allow `PaperScript`to export from executed code, supporting `export default`,
named exports, as well as `module.exports`.
## `0.11.5` ## `0.11.5`
### Fixed ### Fixed

View file

@ -4,12 +4,14 @@ If you want to work with Paper.js, simply download the latest "stable" version
from [http://paperjs.org/download/](http://paperjs.org/download/) from [http://paperjs.org/download/](http://paperjs.org/download/)
- Website: <http://paperjs.org/> - Website: <http://paperjs.org/>
- Discussion forum: <http://groups.google.com/group/paperjs> - Discussion forum: <https://groups.google.com/group/paperjs>
- Mainline source code: <https://github.com/paperjs/paper.js> - Mainline source code: <https://github.com/paperjs/paper.js>
- Twitter: [@paperjs](http://twitter.com/paperjs) - Twitter: [@paperjs](https://twitter.com/paperjs)
- Latest releases: <http://paperjs.org/download/> - Latest releases: <http://paperjs.org/download/>
- Pre-built development versions: [`prebuilt/module`](https://github.com/paperjs/paper.js/tree/prebuilt/module) - Pre-built development versions:
and [`prebuilt/dist`](https://github.com/paperjs/paper.js/tree/prebuilt/dist) branches. [`prebuilt/module`](https://github.com/paperjs/paper.js/tree/prebuilt/module)
and [`prebuilt/dist`](https://github.com/paperjs/paper.js/tree/prebuilt/dist)
branches.
## Installing Paper.js ## Installing Paper.js
@ -51,9 +53,9 @@ generally not recommended to install Node.js through OS-supplied package
managers, as the its development cycles move fast and these versions are often managers, as the its development cycles move fast and these versions are often
out-of-date. out-of-date.
On macOS, [Homebrew](http://brew.sh/) is a good option if one version of On macOS, [Homebrew](https://brew.sh/) is a good option if one version of
Node.js that is kept up to date with `brew upgrade` is enough: Node.js that is kept up to date with `brew upgrade` is enough:
<http://treehouse.github.io/installation-guides/mac/node-mac.html> <https://treehouse.github.io/installation-guides/mac/node-mac.html>
[NVM](https://github.com/creationix/nvm) can be used instead to install and [NVM](https://github.com/creationix/nvm) can be used instead to install and
maintain multiple versions of Node.js on the same platform, as often required by maintain multiple versions of Node.js on the same platform, as often required by
@ -63,7 +65,7 @@ different projects:
Homebrew is recommended on macOS also if you intend to install Paper.js with Homebrew is recommended on macOS also if you intend to install Paper.js with
rendering to the Canvas on Node.js, as described in the next paragraph. rendering to the Canvas on Node.js, as described in the next paragraph.
For Linux, see <http://nodejs.org/download/> to locate 32-bit and 64-bit Node.js For Linux, see <https://nodejs.org/download/> to locate 32-bit and 64-bit Node.js
binaries as well as sources, or use NVM, as described in the paragraph above. binaries as well as sources, or use NVM, as described in the paragraph above.
### Installing Paper.js for Node.js ### Installing Paper.js for Node.js
@ -81,11 +83,11 @@ different one:
SVG importing and exporting through [jsdom](https://github.com/tmpvar/jsdom). SVG importing and exporting through [jsdom](https://github.com/tmpvar/jsdom).
In order to install `paper-jsdom-canvas`, you need the [Cairo Graphics In order to install `paper-jsdom-canvas`, you need the [Cairo Graphics
library](http://cairographics.org/) installed in your system: library](https://cairographics.org/) installed in your system:
##### Installing Cairo and Pango on macOS: ##### Installing Cairo and Pango on macOS:
The easiest way to install Cairo is through [Homebrew](http://brew.sh/), by The easiest way to install Cairo is through [Homebrew](https://brew.sh/), by
issuing the command: issuing the command:
brew install cairo pango brew install cairo pango
@ -160,7 +162,7 @@ run:
### Setting Up For Building ### Setting Up For Building
As of 2016, Paper.js uses [Gulp.js](http://gulpjs.com/) for building, and has a As of 2016, Paper.js uses [Gulp.js](https://gulpjs.com/) for building, and has a
couple of dependencies as Bower and NPM modules. Read the chapter [Installing couple of dependencies as Bower and NPM modules. Read the chapter [Installing
Node.js, NPM and Bower](#installing-nodejs-npm-and-bower) if you still need to Node.js, NPM and Bower](#installing-nodejs-npm-and-bower) if you still need to
install these. install these.
@ -248,7 +250,7 @@ Your docs will then be located at `dist/docs`.
### Testing ### Testing
Paper.js was developed and tested from day 1 using proper unit testing through Paper.js was developed and tested from day 1 using proper unit testing through
jQuery's [Qunit](http://docs.jquery.com/Qunit). To run the tests after any jQuery's [Qunit](https://qunitjs.com/). To run the tests after any
change to the library's source, simply open `index.html` inside the `test` change to the library's source, simply open `index.html` inside the `test`
folder in your web browser. There should be a green bar at the top, meaning all folder in your web browser. There should be a green bar at the top, meaning all
tests have passed. If the bar is red, some tests have not passed. These will be tests have passed. If the bar is red, some tests have not passed. These will be
@ -274,24 +276,30 @@ And to test both the PhantomJS and Node.js environments together, simply run:
gulp test gulp test
### Contributing ### Contributing [![Open Source Helpers](https://www.codetriage.com/paperjs/paper.js/badges/users.svg)](https://www.codetriage.com/paperjs/paper.js)
The main Paper.js source tree is hosted on GitHub, thus you should create a fork The main Paper.js source tree is hosted on GitHub, thus you should create a fork
of the repository in which you perform development. See of the repository in which you perform development. See
<http://help.github.com/forking/>. <https://help.github.com/articles/fork-a-repo/>.
We prefer that you send a [pull request on GitHub We prefer that you send a
](http://help.github.com/pull-requests/) which will then be merged into the [pull request on GitHub](https://help.github.com/articles/about-pull-requests/)
official main line repository. You need to sign the Paper.js CLA to be able to which will then be merged into the official main line repository.
contribute (see below). You need to sign the Paper.js CLA to be able to contribute (see below).
Also, in your first contribution, add yourself to the end of `AUTHORS.md` (which Also, in your first contribution, add yourself to the end of `AUTHORS.md` (which
of course is optional). of course is optional).
In addition to contributing code you can also triage issues which may include
reproducing bug reports or asking for vital information, such as version numbers
or reproduction instructions. If you would like to start triaging issues, one
easy way to get started is to
[subscribe to paper.js on CodeTriage](https://www.codetriage.com/paperjs/paper.js).
**Get the source (for contributing):** **Get the source (for contributing):**
If you want to contribute to the project you will have to [make a If you want to contribute to the project you will have to [make a
fork](http://help.github.com/forking/). Then do this: fork](https://help.github.com/articles/fork-a-repo/). Then do this:
git clone --recursive git@github.com:yourusername/paper.js.git git clone --recursive git@github.com:yourusername/paper.js.git
cd paper.js cd paper.js
@ -304,11 +312,11 @@ To then fetch changes from upstream, run
#### Creating and Submitting a Patch #### Creating and Submitting a Patch
As mentioned above, we prefer that you send a As mentioned above, we prefer that you send a
[pull request](http://help.github.com/pull-requests/) on GitHub: [pull request](https://help.github.com/articles/about-pull-requests/) on GitHub:
1. Create a fork of the upstream repository by visiting 1. Create a fork of the upstream repository by visiting
<https://github.com/paperjs/paper.js/fork>. If you feel insecure, here's a <https://github.com/paperjs/paper.js/fork>. If you feel insecure, here's a
great guide: <http://help.github.com/forking/> great guide: <https://help.github.com/articles/fork-a-repo/>
2. Clone of your repository: `git clone 2. Clone of your repository: `git clone
https://yourusername@github.com/yourusername/paper.js.git` https://yourusername@github.com/yourusername/paper.js.git`
@ -326,7 +334,7 @@ As mentioned above, we prefer that you send a
repository's site at GitHub (i.e. https://github.com/yourusername/paper.js) repository's site at GitHub (i.e. https://github.com/yourusername/paper.js)
and press the "Pull Request" button. Make sure you are creating the pull and press the "Pull Request" button. Make sure you are creating the pull
request to the `develop` branch, not the `master` branch. Here's a good guide request to the `develop` branch, not the `master` branch. Here's a good guide
on pull requests: <http://help.github.com/pull-requests/> on pull requests: <https://help.github.com/articles/about-pull-requests/>
##### Use one topic branch per feature: ##### Use one topic branch per feature:
@ -335,7 +343,7 @@ together into your `develop` branch (or develop everything in your `develop`
branch and then cherry-pick-and-merge into the different topic branches). Git branch and then cherry-pick-and-merge into the different topic branches). Git
provides for an extremely flexible workflow, which in many ways causes more provides for an extremely flexible workflow, which in many ways causes more
confusion than it helps you when new to collaborative software development. The confusion than it helps you when new to collaborative software development. The
guides provided by GitHub at <http://help.github.com/> are a really good guides provided by GitHub at <https://help.github.com/> are a really good
starting point and reference. If you are fixing an issue, a convenient way to starting point and reference. If you are fixing an issue, a convenient way to
name the branch is to use the issue number as a prefix, like this: `git checkout name the branch is to use the issue number as a prefix, like this: `git checkout
-tb issue-937-feature-add-text-styling`. -tb issue-937-feature-add-text-styling`.
@ -343,7 +351,7 @@ name the branch is to use the issue number as a prefix, like this: `git checkout
#### Contributor License Agreement #### Contributor License Agreement
Before we can accept any contributions to Paper.js, you need to sign this Before we can accept any contributions to Paper.js, you need to sign this
[CLA](http://en.wikipedia.org/wiki/Contributor_License_Agreement): [CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement):
[Contributor License Agreement](https://spreadsheets.google.com/a/paperjs.org/spreadsheet/embeddedform?formkey=dENxd0JBVDY2REo3THVuRmh4YjdWRlE6MQ) [Contributor License Agreement](https://spreadsheets.google.com/a/paperjs.org/spreadsheet/embeddedform?formkey=dENxd0JBVDY2REo3THVuRmh4YjdWRlE6MQ)
@ -352,10 +360,11 @@ Before we can accept any contributions to Paper.js, you need to sign this
> defend the project should there be a legal dispute regarding the software at > defend the project should there be a legal dispute regarding the software at
> some future time. > some future time.
For a list of authors and contributors, please see [AUTHORS For a list of authors and contributors, please see
](https://github.com/paperjs/paper.js/blob/master/AUTHORS.md). [AUTHORS](https://github.com/paperjs/paper.js/blob/master/AUTHORS.md).
## License ## License
Distributed under the MIT license. See [LICENSE Distributed under the MIT license. See
](https://github.com/paperjs/paper.js/blob/master/LICENSE.txt) for details. [LICENSE](https://github.com/paperjs/paper.js/blob/master/LICENSE.txt)
fo details.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Path Tangents</title>
<link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../../dist/paper-full.js"></script>
<script type="text/paperscript" canvas="canvas">
// draw path
var path = new Path('M185.972,83.103c18.542-27.813,41.083-63.865,72.633-78.412c40.768-18.787,56.519,22.783,48.118,55.174c-7.694,29.634-23.246,56.887-33.519,85.651c-2.092,5.856-12.005,28.226,1.46,28.226c13.623,0,30.341-20.748,38.719-29.609c13.322-14.092,25.403-29.262,36.591-45.092c18.532,0,21.893,16.679,15.512,30.659c-15.041,32.952-45.633,61.693-74.315,82.812c-20.822,15.332-62.421,39.657-85.61,14.639c-26.43-28.497,5.643-88.375,16.151-117.448c15.172-41.98-26.439-5.818-37.874,8.852c-17.928,22.999-16.922,77.719-18.303,106.529c-21.793,21.793-63.141,0.942-66.759-26.731c-2.207-16.876,2.573-34.851,7.098-50.965c4.793-17.07,17.809-38.034,17.809-55.889c0-15.34-20.016,2.606-23.117,5.779c-12.837,13.14-18.843,22.942-21.953,41.102c-3.221,18.811-1.106,85.684-22.392,87.86c-29.787,3.014-51.93-20.085-55.6-48.556c-2.067-16.034,1.385-33.132,5.247-48.637c2.243-9.004,5.006-17.888,8.197-26.599c-4.147-9.616-4.988-20.426-4.988-30.78c0-33.391,44.299-71.678,77.411-65.772c31.311,5.585,6.408,61.28-0.642,76.722c16.999-29.448,43.73-77.256,83.217-77.256C211.992,5.36,197.721,59.599,185.972,83.103z');
path.position = view.center;
path.fitBounds(view.bounds.scale(0.7));
path.fillColor = 'orange';
// create a marking layer to put temporary items
var layer = new Layer();
layer.activate();
// init drawing with a vertical vector
drawTangentsToVector(new Point(0, 1));
function onMouseMove(event) {
// draw tangents to vector between view center and mouse pointer
drawTangentsToVector(event.point - view.center);
}
function drawTangentsToVector(vector) {
// adapt vector length for display
vector.length = 50;
// remove existing marking items
layer.removeChildren();
// draw a line at view center to show vector direction
var line = new Path.Line({
from: view.center + vector,
to: view.center - vector,
strokeColor: 'black',
strokeWidth: 2
});
// get path times where path is tangent to vector
var offsets = path.getOffsetsWithTangent(vector);
for (var i = 0; i < offsets.length; i++) {
var point = path.getPointAt(offsets[i]);
// draw a circle around point
new Path.Circle({
center: point,
radius: 5,
strokeColor: 'red'
});
// draw a line showing tangent
new Path.Line({
from: point + vector,
to: point - vector,
strokeColor: 'red'
});
// draw a line showing point precisely on the path
new Path.Line({
from: point + vector.rotate(90).normalize(10),
to: point - vector.rotate(90).normalize(10),
strokeColor: 'red'
});
}
}
</script>
</head>
<body>
<canvas id='canvas' resize></canvas>
</body>
</html>

@ -1 +1 @@
Subproject commit 59faf79f13765dfd1fd8ec302d5627bbe548b2fa Subproject commit 2533ac8e1863262f3c28cd29bc940c6d2ecdf147

View file

@ -92,7 +92,7 @@ packages.forEach(function(name) {
gulp.task('publish:packages:' + name, ['publish:version'], function() { gulp.task('publish:packages:' + name, ['publish:version'], function() {
var path = 'packages/' + name, var path = 'packages/' + name,
opts = { cwd: path }; opts = { cwd: path };
gulp.src(['package.json'], opts) return gulp.src(['package.json'], opts)
.pipe(jsonEditor({ .pipe(jsonEditor({
version: options.version, version: options.version,
dependencies: { dependencies: {

View file

@ -29,7 +29,7 @@ gulp.task('test:phantom', ['minify:acorn'], function() {
return gulp.src('index.html', { cwd: 'test' }) return gulp.src('index.html', { cwd: 'test' })
.pipe(qunits({ .pipe(qunits({
checkGlobals: true, checkGlobals: true,
timeout: 20 timeout: 40
})); }));
}); });

View file

@ -1,6 +1,6 @@
{ {
"name": "@scratch/paper", "name": "@scratch/paper",
"version": "0.11.5", "version": "0.11.8",
"description": "The Swiss Army Knife of Vector Graphics Scripting", "description": "The Swiss Army Knife of Vector Graphics Scripting",
"license": "MIT", "license": "MIT",
"homepage": "http://paperjs.org", "homepage": "http://paperjs.org",
@ -9,10 +9,7 @@
"url": "https://github.com/paperjs/paper.js" "url": "https://github.com/paperjs/paper.js"
}, },
"bugs": "https://github.com/paperjs/paper.js/issues", "bugs": "https://github.com/paperjs/paper.js/issues",
"contributors": [ "contributors": ["Jürg Lehni <juerg@scratchdisk.com> (http://scratchdisk.com)", "Jonathan Puckey <jonathan@studiomoniker.com> (http://studiomoniker.com)"],
"Jürg Lehni <juerg@scratchdisk.com> (http://scratchdisk.com)",
"Jonathan Puckey <jonathan@studiomoniker.com> (http://studiomoniker.com)"
],
"main": "dist/paper-full.js", "main": "dist/paper-full.js",
"scripts": { "scripts": {
"precommit": "gulp jshint --branch develop", "precommit": "gulp jshint --branch develop",
@ -25,14 +22,7 @@
"jshint": "gulp jshint", "jshint": "gulp jshint",
"test": "gulp test" "test": "gulp test"
}, },
"files": [ "files": ["AUTHORS.md", "CHANGELOG.md", "dist/", "examples/", "LICENSE.txt", "README.md"],
"AUTHORS.md",
"CHANGELOG.md",
"dist/",
"examples/",
"LICENSE.txt",
"README.md"
],
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=4.0.0"
}, },
@ -42,7 +32,7 @@
"del": "^2.2.1", "del": "^2.2.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-cached": "^1.1.0", "gulp-cached": "^1.1.0",
"gulp-git-streamed": "^2.4.0", "gulp-git-streamed": "^2.8.1",
"gulp-jshint": "^2.0.0", "gulp-jshint": "^2.0.0",
"gulp-json-editor": "^2.2.1", "gulp-json-editor": "^2.2.1",
"gulp-prepro": "^2.4.0", "gulp-prepro": "^2.4.0",
@ -79,21 +69,5 @@
"./dist/node/self.js": false, "./dist/node/self.js": false,
"./dist/node/extend.js": false "./dist/node/extend.js": false
}, },
"keywords": [ "keywords": ["vector", "graphic", "graphics", "2d", "geometry", "bezier", "curve", "curves", "path", "paths", "canvas", "svg", "paper", "paper.js", "paperjs"]
"vector",
"graphic",
"graphics",
"2d",
"geometry",
"bezier",
"curve",
"curves",
"path",
"paths",
"canvas",
"svg",
"paper",
"paper.js",
"paperjs"
]
} }

@ -1 +1 @@
Subproject commit afd2bbbf1cea00f1f94ff89c8a3dd370888ac705 Subproject commit f601084fc319734d0bf47da700d6b6bff95260ba

@ -1 +1 @@
Subproject commit f6794e0749cfb65d5138f3512fc0eee755bc1829 Subproject commit a07b7d149f02e980dfd837cd595f5000a9d5e052

View file

@ -131,7 +131,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{
if (owner._applyMatrix) { if (owner._applyMatrix) {
owner.transform(null, true); owner.transform(null, true);
} else { } else {
owner._changed(/*#=*/Change.GEOMETRY); owner._changed(/*#=*/Change.MATRIX);
} }
} }
}, },
@ -672,12 +672,11 @@ var Matrix = Base.extend(/** @lends Matrix# */{
}, },
/** /**
* Attempts to decompose the affine transformation described by this matrix * Decomposes the affine transformation described by this matrix into
* into `scaling`, `rotation` and `skewing`, and returns an object with * `scaling`, `rotation` and `skewing`, and returns an object with
* these properties if it succeeded, `null` otherwise. * these properties.
* *
* @return {Object} the decomposed matrix, or `null` if decomposition is not * @return {Object} the decomposed matrix
* possible
*/ */
decompose: function() { decompose: function() {
// http://dev.w3.org/csswg/css3-2d-transforms/#matrix-decomposition // http://dev.w3.org/csswg/css3-2d-transforms/#matrix-decomposition
@ -795,7 +794,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{
* @see #decompose() * @see #decompose()
*/ */
getScaling: function() { getScaling: function() {
return (this.decompose() || {}).scaling; return this.decompose().scaling;
}, },
/** /**
@ -806,7 +805,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{
* @see #decompose() * @see #decompose()
*/ */
getRotation: function() { getRotation: function() {
return (this.decompose() || {}).rotation; return this.decompose().rotation;
}, },
/** /**

View file

@ -515,8 +515,7 @@ statics: /** @lends Base */{
if (create) { if (create) {
res = create(type, args, isFirst || _isRoot); res = create(type, args, isFirst || _isRoot);
} else { } else {
res = Base.create(type.prototype); res = new type(args);
type.apply(res, args);
} }
} }
} else if (Base.isPlainObject(json)) { } else if (Base.isPlainObject(json)) {
@ -572,6 +571,27 @@ statics: /** @lends Base */{
}); });
}, },
/**
* Utility function for pushing a large amount of items to an array.
*/
push: function(list, items) {
var itemsLength = items.length;
// It seems for "small" amounts of items, this performs better,
// but once it reaches a certain amount, some browsers start crashing:
if (itemsLength < 4096) {
list.push.apply(list, items);
} else {
// Use a loop as the best way to handle big arrays (see #1493).
// Set new array length once before the loop for better performance.
var startLength = list.length;
list.length += itemsLength;
for (var i = 0; i < itemsLength; i++) {
list[startLength + i] = items[i];
}
}
return list;
},
/** /**
* Utility function for adding and removing items from a list of which each * Utility function for adding and removing items from a list of which each
* entry keeps a reference to its index in the list in the private _index * entry keeps a reference to its index in the list in the private _index
@ -588,14 +608,14 @@ statics: /** @lends Base */{
items[i]._index = index + i; items[i]._index = index + i;
if (append) { if (append) {
// Append them all at the end by using push // Append them all at the end by using push
list.push.apply(list, items); Base.push(list, items);
// Nothing removed, and nothing to adjust above // Nothing removed, and nothing to adjust above
return []; return [];
} else { } else {
// Insert somewhere else and/or remove // Insert somewhere else and/or remove
var args = [index, remove]; var args = [index, remove];
if (items) if (items)
args.push.apply(args, items); Base.push(args, items);
var removed = list.splice.apply(list, args); var removed = list.splice.apply(list, args);
// Erase the indices of the removed items // Erase the indices of the removed items
for (var i = 0, l = removed.length; i < l; i++) for (var i = 0, l = removed.length; i < l; i++)

View file

@ -203,8 +203,11 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{
* @param {Object} [option] the compilation options * @param {Object} [option] the compilation options
*/ */
execute: function(code, options) { execute: function(code, options) {
paper.PaperScript.execute(code, this, options); /*#*/ if (__options.paperScript) {
View.updateFocus(); var exports = paper.PaperScript.execute(code, this, options);
View.updateFocus();
return exports;
/*#*/ }
}, },
/** /**
@ -246,9 +249,10 @@ var PaperScope = Base.extend(/** @lends PaperScope# */{
* Sets up an empty project for us. If a canvas is provided, it also creates * Sets up an empty project for us. If a canvas is provided, it also creates
* a {@link View} for it, both linked to this scope. * a {@link View} for it, both linked to this scope.
* *
* @param {HTMLCanvasElement|String} element the HTML canvas element this * @param {HTMLCanvasElement|String|Size} element the HTML canvas element
* scope should be associated with, or an ID string by which to find the * this scope should be associated with, or an ID string by which to find
* element. * the element, or the size of the canvas to be created for usage in a web
* worker.
*/ */
setup: function(element) { setup: function(element) {
// Make sure this is the active scope, so the created project and view // Make sure this is the active scope, so the created project and view

View file

@ -284,6 +284,40 @@ Base.exports.PaperScript = function() {
} }
} }
break; break;
case 'ExportDefaultDeclaration':
// Convert `export default` to `module.exports = ` statements:
replaceCode({
range: [node.start, node.declaration.start]
}, 'module.exports = ');
break;
case 'ExportNamedDeclaration':
// Convert named exports to `module.exports.NAME = NAME;`
// statements both for new declarations and existing specifiers:
var declaration = node.declaration;
var specifiers = node.specifiers;
if (declaration) {
var declarations = declaration.declarations;
if (declarations) {
declarations.forEach(function(dec) {
replaceCode(dec, 'module.exports.' + getCode(dec));
});
replaceCode({
range: [
node.start,
declaration.start + declaration.kind.length
]
}, '');
}
} else if (specifiers) {
var exports = specifiers.map(function(specifier) {
var name = getCode(specifier);
return 'module.exports.' + name + ' = ' + name + '; ';
}).join('');
if (exports) {
replaceCode(node, exports);
}
}
break;
} }
} }
@ -364,7 +398,11 @@ Base.exports.PaperScript = function() {
}; };
} }
// Now do the parsing magic // Now do the parsing magic
walkAST(parse(code, { ranges: true, preserveParens: true })); walkAST(parse(code, {
ranges: true,
preserveParens: true,
sourceType: 'module'
}));
if (map) { if (map) {
if (offsetCode) { if (offsetCode) {
// Adjust the line offset of the resulting code if required. // Adjust the line offset of the resulting code if required.
@ -404,8 +442,7 @@ Base.exports.PaperScript = function() {
* @param {String} code the PaperScript code * @param {String} code the PaperScript code
* @param {PaperScope} scope the scope for which the code is executed * @param {PaperScope} scope the scope for which the code is executed
* @param {Object} [option] the compilation options * @param {Object} [option] the compilation options
* @return {Object} an object holding the compiled PaperScript translated * @return the exports defined in the executed code
* into JavaScript code along with source-maps and other information.
*/ */
function execute(code, scope, options) { function execute(code, scope, options) {
// Set currently active scope. // Set currently active scope.
@ -450,21 +487,26 @@ Base.exports.PaperScript = function() {
expose({ __$__: __$__, $__: $__, paper: scope, view: view, tool: tool }, expose({ __$__: __$__, $__: $__, paper: scope, view: view, tool: tool },
true); true);
expose(scope); expose(scope);
// Add a fake `module.exports` object so PaperScripts can export things.
code = 'var module = { exports: {} }; ' + code;
// Finally define the handler variable names as parameters and compose // Finally define the handler variable names as parameters and compose
// the string describing the properties for the returned object at the // the string describing the properties for the returned exports object
// end of the code execution, so we can retrieve their values from the // at the end of the code execution, so we can retrieve their values
// function call. // from the function call.
handlers = Base.each(handlers, function(key) { var exports = Base.each(handlers, function(key) {
// Check for each handler explicitly and only return them if they // Check for each handler explicitly and only export them if they
// seem to exist. // seem to exist.
if (new RegExp('\\s+' + key + '\\b').test(code)) { if (new RegExp('\\s+' + key + '\\b').test(code)) {
params.push(key); params.push(key);
this.push(key + ': ' + key); this.push('module.exports.' + key + ' = ' + key + ';');
} }
}, []).join(', '); }, []).join('\n');
// We need an additional line that returns the handlers in one object. // Add the setting of the exported handlers to the end of the code.
if (handlers) if (exports) {
code += '\nreturn { ' + handlers + ' };'; code += '\n' + exports;
}
// End by returning `module.exports` at the end of the generated code:
code += '\nreturn module.exports;';
var agent = paper.agent; var agent = paper.agent;
if (document && (agent.chrome if (document && (agent.chrome
|| agent.firefox && agent.versionNumber < 40)) { || agent.firefox && agent.versionNumber < 40)) {
@ -481,39 +523,42 @@ Base.exports.PaperScript = function() {
if (agent.firefox) if (agent.firefox)
code = '\n' + code; code = '\n' + code;
script.appendChild(document.createTextNode( script.appendChild(document.createTextNode(
'paper._execute = function(' + params + ') {' + code + '\n}' 'document.__paperscript__ = function(' + params + ') {' +
code +
'\n}'
)); ));
head.appendChild(script); head.appendChild(script);
func = paper._execute; func = document.__paperscript__;
delete paper._execute; delete document.__paperscript__;
head.removeChild(script); head.removeChild(script);
} else { } else {
func = Function(params, code); func = Function(params, code);
} }
var res = func.apply(scope, args) || {}; var exports = func && func.apply(scope, args);
var obj = exports || {};
// Now install the 'global' tool and view handlers, and we're done! // Now install the 'global' tool and view handlers, and we're done!
Base.each(toolHandlers, function(key) { Base.each(toolHandlers, function(key) {
var value = res[key]; var value = obj[key];
if (value) if (value)
tool[key] = value; tool[key] = value;
}); });
if (view) { if (view) {
if (res.onResize) if (obj.onResize)
view.setOnResize(res.onResize); view.setOnResize(obj.onResize);
// Emit resize event directly, so any user // Emit resize event directly, so any user
// defined resize handlers are called. // defined resize handlers are called.
view.emit('resize', { view.emit('resize', {
size: view.size, size: view.size,
delta: new Point() delta: new Point()
}); });
if (res.onFrame) if (obj.onFrame)
view.setOnFrame(res.onFrame); view.setOnFrame(obj.onFrame);
// Automatically request an update at the end. This is only needed // Automatically request an update at the end. This is only needed
// if the script does not actually produce anything yet, and the // if the script does not actually produce anything yet, and the
// used canvas contains previous content. // used canvas contains previous content.
view.requestUpdate(); view.requestUpdate();
} }
return compiled; return exports;
} }
function loadScript(script) { function loadScript(script) {

View file

@ -84,14 +84,14 @@ var DomElement = new function() {
}, },
/** /**
* Checks if element is invisibile (display: none, ...). * Checks if element is invisible (display: none, ...).
*/ */
isInvisible: function(el) { isInvisible: function(el) {
return DomElement.getSize(el).equals(new Size(0, 0)); return DomElement.getSize(el).equals(new Size(0, 0));
}, },
/** /**
* Checks if element is visibile in current viewport. * Checks if element is visible in current viewport.
*/ */
isInView: function(el) { isInView: function(el) {
// See if the viewport bounds intersect with the windows rectangle // See if the viewport bounds intersect with the windows rectangle

View file

@ -23,8 +23,20 @@ var DomEvent = /** @lends DomEvent */{
for (var type in events) { for (var type in events) {
var func = events[type], var func = events[type],
parts = type.split(/[\s,]+/g); parts = type.split(/[\s,]+/g);
for (var i = 0, l = parts.length; i < l; i++) for (var i = 0, l = parts.length; i < l; i++) {
el.addEventListener(parts[i], func, false); var name = parts[i];
// For touchstart/touchmove events on document, we need to
// explicitly declare that the event is not passive (can be
// prevented). Otherwise chrome browser would ignore
// `event.preventDefault()` calls and omit warnings.
// See #1501 and:
// https://www.chromestatus.com/features/5093566007214080
var options = (
el === document
&& (name === 'touchstart' || name === 'touchmove')
) ? { passive: false } : false;
el.addEventListener(name, func, options);
}
} }
} }
}, },

View file

@ -16,7 +16,7 @@
// NOTE: Do not create local variable `var paper` since it would shield the // NOTE: Do not create local variable `var paper` since it would shield the
// global one in the whole scope. // global one in the whole scope.
paper = new (PaperScope.inject(Base.exports, { var paper = new (PaperScope.inject(Base.exports, {
Base: Base, Base: Base,
Numerical: Numerical, Numerical: Numerical,
Key: Key, Key: Key,

View file

@ -16,7 +16,6 @@
// Node.js,only the files included in such a way see each other's variables in // Node.js,only the files included in such a way see each other's variables in
// their shared scope. // their shared scope.
/* global document:true, window:true */
// Set up a local `window` variable valid across the full the paper.js scope, // Set up a local `window` variable valid across the full the paper.js scope,
// pointing to the native window in browsers and the one provided by JSDom in // pointing to the native window in browsers and the one provided by JSDom in
// Node.js // Node.js

View file

@ -17,27 +17,29 @@ var ChangeFlag = {
// A change in the item's children // A change in the item's children
CHILDREN: 0x2, CHILDREN: 0x2,
// A change of the item's place in the scene graph (removed, inserted, // A change of the item's place in the scene graph (removed, inserted,
// moved). // moved)
INSERTION: 0x4, INSERTION: 0x4,
// Item geometry (path, bounds) // Item geometry (path, bounds)
GEOMETRY: 0x8, GEOMETRY: 0x8,
// The item's matrix has changed
MATRIX: 0x10,
// Only segment(s) have changed, and affected curves have already been // Only segment(s) have changed, and affected curves have already been
// notified. This is to implement an optimization in _changed() calls. // notified. This is to implement an optimization in _changed() calls
SEGMENTS: 0x10, SEGMENTS: 0x20,
// Stroke geometry (excluding color) // Stroke geometry (excluding color)
STROKE: 0x20, STROKE: 0x40,
// Fill style or stroke color / dash // Fill style or stroke color / dash
STYLE: 0x40, STYLE: 0x80,
// Item attributes: visible, blendMode, locked, name, opacity, clipMask ... // Item attributes: visible, blendMode, locked, name, opacity, clipMask ...
ATTRIBUTE: 0x80, ATTRIBUTE: 0x100,
// Text content // Text content
CONTENT: 0x100, CONTENT: 0x200,
// Raster pixels // Raster pixels
PIXELS: 0x200, PIXELS: 0x400,
// Clipping in one of the child items // Clipping in one of the child items
CLIPPING: 0x400, CLIPPING: 0x800,
// The view has been transformed // The view has been transformed
VIEW: 0x800 VIEW: 0x1000
}; };
// Shortcuts to often used ChangeFlag values including APPEARANCE // Shortcuts to often used ChangeFlag values including APPEARANCE
@ -48,6 +50,7 @@ var Change = {
// Changing the insertion can change the appearance through parent's matrix. // Changing the insertion can change the appearance through parent's matrix.
INSERTION: ChangeFlag.INSERTION | ChangeFlag.APPEARANCE, INSERTION: ChangeFlag.INSERTION | ChangeFlag.APPEARANCE,
GEOMETRY: ChangeFlag.GEOMETRY | ChangeFlag.APPEARANCE, GEOMETRY: ChangeFlag.GEOMETRY | ChangeFlag.APPEARANCE,
MATRIX: ChangeFlag.MATRIX | ChangeFlag.GEOMETRY | ChangeFlag.APPEARANCE,
SEGMENTS: ChangeFlag.SEGMENTS | ChangeFlag.GEOMETRY | ChangeFlag.APPEARANCE, SEGMENTS: ChangeFlag.SEGMENTS | ChangeFlag.GEOMETRY | ChangeFlag.APPEARANCE,
STROKE: ChangeFlag.STROKE | ChangeFlag.STYLE | ChangeFlag.APPEARANCE, STROKE: ChangeFlag.STROKE | ChangeFlag.STYLE | ChangeFlag.APPEARANCE,
STYLE: ChangeFlag.STYLE | ChangeFlag.APPEARANCE, STYLE: ChangeFlag.STYLE | ChangeFlag.APPEARANCE,

View file

@ -216,8 +216,10 @@ new function() { // Injection scope for various item event handlers
if (flags & /*#=*/ChangeFlag.GEOMETRY) { if (flags & /*#=*/ChangeFlag.GEOMETRY) {
// Clear cached bounds, position and decomposed matrix whenever // Clear cached bounds, position and decomposed matrix whenever
// geometry changes. // geometry changes.
this._bounds = this._position = this._decomposed = this._bounds = this._position = this._decomposed = undefined;
this._globalMatrix = undefined; }
if (flags & /*#=*/ChangeFlag.MATRIX) {
this._globalMatrix = undefined;
} }
if (cacheParent if (cacheParent
&& (flags & /*#=*/(ChangeFlag.GEOMETRY | ChangeFlag.STROKE))) { && (flags & /*#=*/(ChangeFlag.GEOMETRY | ChangeFlag.STROKE))) {
@ -411,7 +413,7 @@ new function() { // Injection scope for various item event handlers
flags = { flags = {
// #locked does not change appearance, all others do: // #locked does not change appearance, all others do:
locked: /*#=*/ChangeFlag.ATTRIBUTE, locked: /*#=*/ChangeFlag.ATTRIBUTE,
// #visible changes apperance // #visible changes appearance
visible: /*#=*/(Change.ATTRIBUTE | Change.GEOMETRY) visible: /*#=*/(Change.ATTRIBUTE | Change.GEOMETRY)
}; };
this['get' + part] = function() { this['get' + part] = function() {
@ -433,12 +435,39 @@ new function() { // Injection scope for various item event handlers
// injection scope above. // injection scope above.
/** /**
* Specifies whether the item is locked. * Specifies whether the item is locked. When set to `true`, item
* interactions with the mouse are disabled.
* *
* @name Item#locked * @name Item#locked
* @type Boolean * @type Boolean
* @default false * @default false
* @ignore *
* @example {@paperscript}
* var unlockedItem = new Path.Circle({
* center: view.center - [35, 0],
* radius: 30,
* fillColor: 'springgreen',
* onMouseDown: function() {
* this.fillColor = Color.random();
* }
* });
*
* var lockedItem = new Path.Circle({
* center: view.center + [35, 0],
* radius: 30,
* fillColor: 'crimson',
* locked: true,
* // This event won't be triggered because the item is locked.
* onMouseDown: function() {
* this.fillColor = Color.random();
* }
* });
*
* new PointText({
* content: 'Click on both circles to see which one is locked.',
* point: view.center - [0, 35],
* justification: 'center'
* });
*/ */
/** /**
@ -473,7 +502,7 @@ new function() { // Injection scope for various item event handlers
* light', 'color-dodge', 'color-burn', 'darken', 'lighten', * light', 'color-dodge', 'color-burn', 'darken', 'lighten',
* 'difference', 'exclusion', 'hue', 'saturation', 'luminosity', * 'difference', 'exclusion', 'hue', 'saturation', 'luminosity',
* 'color', 'add', 'subtract', 'average', 'pin-light', 'negation', * 'color', 'add', 'subtract', 'average', 'pin-light', 'negation',
* 'source- over', 'source-in', 'source-out', 'source-atop', * 'source-over', 'source-in', 'source-out', 'source-atop',
* 'destination-over', 'destination-in', 'destination-out', * 'destination-over', 'destination-in', 'destination-out',
* 'destination-atop', 'lighter', 'darker', 'copy', 'xor' * 'destination-atop', 'lighter', 'darker', 'copy', 'xor'
* @default 'normal' * @default 'normal'
@ -746,20 +775,13 @@ new function() { // Injection scope for various item event handlers
getPosition: function(_dontLink) { getPosition: function(_dontLink) {
// Cache position value. // Cache position value.
// Pass true for _dontLink in getCenter(), so receive back a normal point // Pass true for _dontLink in getCenter(), so receive back a normal point
var position = this._position, var ctor = _dontLink ? Point : LinkedPoint;
ctor = _dontLink ? Point : LinkedPoint;
// Do not cache LinkedPoints directly, since we would not be able to // Do not cache LinkedPoints directly, since we would not be able to
// use them to calculate the difference in #setPosition, as when it is // use them to calculate the difference in #setPosition, as when it is
// modified, it would hold new values already and only then cause the // modified, it would hold new values already and only then cause the
// calling of #setPosition. // calling of #setPosition.
if (!position) { var position = this._position ||
// If an pivot point is provided, use it to determine position (this._position = this._getPositionFromBounds());
// 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'); return new ctor(position.x, position.y, this, 'setPosition');
}, },
@ -770,6 +792,22 @@ new function() { // Injection scope for various item event handlers
this.translate(Point.read(arguments).subtract(this.getPosition(true))); this.translate(Point.read(arguments).subtract(this.getPosition(true)));
}, },
/**
* Internal method used to calculate position either from pivot point or
* bounds.
* @param {Rectangle} bounds if provided, these bounds are used instead of
* calling getBounds()
* @return {Point} the transformed pivot point or the center of the bounds
* @private
*/
_getPositionFromBounds: function(bounds) {
// If an pivot point is provided, use it to determine position
// based on the matrix. Otherwise use the center of the bounds.
return this._pivot
? this._matrix._transformPoint(this._pivot)
: (bounds || this.getBounds()).getCenter(true);
},
/** /**
* The item's pivot point specified in the item coordinate system, defining * The item's pivot point specified in the item coordinate system, defining
* the point around which all transformations are hinging. This is also the * the point around which all transformations are hinging. This is also the
@ -1204,17 +1242,33 @@ new function() { // Injection scope for various item event handlers
* @type Matrix * @type Matrix
*/ */
getGlobalMatrix: function(_dontClone) { getGlobalMatrix: function(_dontClone) {
var matrix = this._globalMatrix, var matrix = this._globalMatrix;
updateVersion = this._project._updateVersion; if (matrix) {
// If #_globalMatrix is out of sync, recalculate it now. // If there's a cached global matrix for this item, check if all its
if (matrix && matrix._updateVersion !== updateVersion) // parents also have one. If it's missing in any of its parents, it
matrix = null; // means the child's cached version isn't valid anymore.
// For better performance, we also use the occasion of this loop to
// clear cached version of items parents.
var parent = this._parent;
var parents = [];
while (parent) {
if (!parent._globalMatrix) {
matrix = null;
// Also clear global matrix of item's parents.
for (var i = 0, l = parents.length; i < l; i++) {
parents[i]._globalMatrix = null;
}
break;
}
parents.push(parent);
parent = parent._parent;
}
}
if (!matrix) { if (!matrix) {
matrix = this._globalMatrix = this._matrix.clone(); matrix = this._globalMatrix = this._matrix.clone();
var parent = this._parent; var parent = this._parent;
if (parent) if (parent)
matrix.prepend(parent.getGlobalMatrix(true)); matrix.prepend(parent.getGlobalMatrix(true));
matrix._updateVersion = updateVersion;
} }
return _dontClone ? matrix : matrix.clone(); return _dontClone ? matrix : matrix.clone();
}, },
@ -1901,6 +1955,7 @@ new function() { // Injection scope for hit-test functions shared with project
* fills for paths * fills for paths
* *
* @param {Point} point the point where the hit-test should be performed * @param {Point} point the point where the hit-test should be performed
* (in global coordinates system).
* @param {Object} [options={ fill: true, stroke: true, segments: true, * @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }] * tolerance: settings.hitTolerance }]
* @return {HitResult} a hit result object describing what exactly was hit * @return {HitResult} a hit result object describing what exactly was hit
@ -1918,6 +1973,7 @@ new function() { // Injection scope for hit-test functions shared with project
* @name Item#hitTestAll * @name Item#hitTestAll
* @function * @function
* @param {Point} point the point where the hit-test should be performed * @param {Point} point the point where the hit-test should be performed
* (in global coordinates system).
* @param {Object} [options={ fill: true, stroke: true, segments: true, * @param {Object} [options={ fill: true, stroke: true, segments: true,
* tolerance: settings.hitTolerance }] * tolerance: settings.hitTolerance }]
* @return {HitResult[]} hit result objects for all hits, describing what * @return {HitResult[]} hit result objects for all hits, describing what
@ -2667,6 +2723,8 @@ new function() { // Injection scope for hit-test functions shared with project
var owner = this._getOwner(), var owner = this._getOwner(),
project = this._project, project = this._project,
index = this._index; index = this._index;
if (this._style)
this._style._dispose();
if (owner) { if (owner) {
// Handle named children separately from index: // Handle named children separately from index:
if (this._name) if (this._name)
@ -3429,19 +3487,19 @@ new function() { // Injection scope for hit-test functions shared with project
var _matrix = this._matrix, var _matrix = this._matrix,
// If no matrix is provided, or the matrix is the identity, we might // If no matrix is provided, or the matrix is the identity, we might
// still have some work to do in case _applyMatrix is true // still have some work to do in case _applyMatrix is true
transform = matrix && !matrix.isIdentity(), transformMatrix = matrix && !matrix.isIdentity(),
applyMatrix = (_applyMatrix || this._applyMatrix) applyMatrix = (_applyMatrix || this._applyMatrix)
// Don't apply _matrix if the result of concatenating with // Don't apply _matrix if the result of concatenating with
// matrix would be identity. // matrix would be identity.
&& ((!_matrix.isIdentity() || transform) && ((!_matrix.isIdentity() || transformMatrix)
// Even if it's an identity matrix, we still need to // Even if it's an identity matrix, we still need to
// recursively apply the matrix to children. // recursively apply the matrix to children.
|| _applyMatrix && _applyRecursively && this._children); || _applyMatrix && _applyRecursively && this._children);
// Bail out if there is nothing to do. // Bail out if there is nothing to do.
if (!transform && !applyMatrix) if (!transformMatrix && !applyMatrix)
return this; return this;
// Simply prepend the internal matrix with the passed one: // Simply prepend the internal matrix with the passed one:
if (transform) { if (transformMatrix) {
// Keep a backup of the last valid state before the matrix becomes // Keep a backup of the last valid state before the matrix becomes
// non-invertible. This is then used again in setBounds to restore. // non-invertible. This is then used again in setBounds to restore.
if (!matrix.isInvertible() && _matrix.isInvertible()) if (!matrix.isInvertible() && _matrix.isInvertible())
@ -3487,13 +3545,13 @@ new function() { // Injection scope for hit-test functions shared with project
// on matrix we can calculate and set them again, so preserve them. // on matrix we can calculate and set them again, so preserve them.
var bounds = this._bounds, var bounds = this._bounds,
position = this._position; position = this._position;
if (transform || applyMatrix) { if (transformMatrix || applyMatrix) {
this._changed(/*#=*/Change.GEOMETRY); this._changed(/*#=*/Change.MATRIX);
} }
// Detect matrices that contain only translations and scaling // Detect matrices that contain only translations and scaling
// and transform the cached _bounds and _position without having to // and transform the cached _bounds and _position without having to
// fully recalculate each time. // fully recalculate each time.
var decomp = transform && bounds && matrix.decompose(); var decomp = transformMatrix && bounds && matrix.decompose();
if (decomp && decomp.skewing.isZero() && decomp.rotation % 90 === 0) { if (decomp && decomp.skewing.isZero() && decomp.rotation % 90 === 0) {
// Transform the old bound by looping through all the cached // Transform the old bound by looping through all the cached
// bounds in _bounds and transform each. // bounds in _bounds and transform each.
@ -3515,11 +3573,12 @@ new function() { // Injection scope for hit-test functions shared with project
// If we have cached bounds, try to determine _position as its // If we have cached bounds, try to determine _position as its
// center. Use _boundsOptions do get the cached default bounds. // center. Use _boundsOptions do get the cached default bounds.
var cached = bounds[this._getBoundsCacheKey( var cached = bounds[this._getBoundsCacheKey(
this._boundsOptions || {})]; this._boundsOptions || {})];
if (cached) { if (cached) {
this._position = cached.rect.getCenter(true); // use this method to handle pivot case (see #1503)
this._position = this._getPositionFromBounds(cached.rect);
} }
} else if (transform && position && this._pivot) { } else if (transformMatrix && position && this._pivot) {
// If the item has a pivot defined, it means that the default // If the item has a pivot defined, it means that the default
// position defined as the center of the bounds won't shift with // position defined as the center of the bounds won't shift with
// arbitrary transformations and we can therefore update _position: // arbitrary transformations and we can therefore update _position:
@ -4260,8 +4319,6 @@ new function() { // Injection scope for hit-test functions shared with project
// Only keep track of transformation if told so. See Project#draw() // Only keep track of transformation if told so. See Project#draw()
matrices.push(globalMatrix); matrices.push(globalMatrix);
if (param.updateMatrix) { if (param.updateMatrix) {
// Update the cached _globalMatrix and keep it versioned.
globalMatrix._updateVersion = updateVersion;
this._globalMatrix = globalMatrix; this._globalMatrix = globalMatrix;
} }
@ -4291,8 +4348,12 @@ new function() { // Injection scope for hit-test functions shared with project
// Apply the parent's global matrix to the calculation of correct // Apply the parent's global matrix to the calculation of correct
// bounds. // bounds.
var bounds = this.getStrokeBounds(viewMatrix); var bounds = this.getStrokeBounds(viewMatrix);
if (!bounds.width || !bounds.height) if (!bounds.width || !bounds.height) {
// Item won't be drawn so its global matrix need to be removed
// from the stack (#1561).
matrices.pop();
return; return;
}
// Store previous offset and save the main context, so we can // Store previous offset and save the main context, so we can
// draw onto it later. // draw onto it later.
prevOffset = param.offset; prevOffset = param.offset;
@ -4417,7 +4478,11 @@ new function() { // Injection scope for hit-test functions shared with project
if (itemSelected) if (itemSelected)
this._drawSelected(ctx, mx, selectionItems); this._drawSelected(ctx, mx, selectionItems);
if (positionSelected) { if (positionSelected) {
var point = this.getPosition(true), // Convert position from the parent's coordinates system to the
// global one:
var pos = this.getPosition(true),
parent = this._parent,
point = parent ? parent.localToGlobal(pos) : pos,
x = point.x, x = point.x,
y = point.y; y = point.y;
ctx.beginPath(); ctx.beginPath();

View file

@ -44,9 +44,10 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* Note that when working with PaperScript, a project is automatically * Note that when working with PaperScript, a project is automatically
* created for us and the {@link PaperScope#project} variable points to it. * created for us and the {@link PaperScope#project} variable points to it.
* *
* @param {HTMLCanvasElement|String} element the HTML canvas element that * @param {HTMLCanvasElement|String|Size} element the HTML canvas element
* should be used as the element for the view, or an ID string by which to * that should be used as the element for the view, or an ID string by which
* find the element. * to find the element, or the size of the canvas to be created for usage in
* a web worker.
*/ */
initialize: function Project(element) { initialize: function Project(element) {
// Activate straight away by passing true to PaperScopeItem constructor, // Activate straight away by passing true to PaperScopeItem constructor,

View file

@ -30,6 +30,7 @@ var Raster = Item.extend(/** @lends Raster# */{
}, },
// Prioritize `crossOrigin` over `source`: // Prioritize `crossOrigin` over `source`:
_prioritize: ['crossOrigin'], _prioritize: ['crossOrigin'],
_smoothing: true,
// TODO: Implement type, width, height. // TODO: Implement type, width, height.
// TODO: Have SymbolItem & Raster inherit from a shared class? // TODO: Have SymbolItem & Raster inherit from a shared class?
@ -419,6 +420,30 @@ var Raster = Item.extend(/** @lends Raster# */{
image.crossOrigin = crossOrigin; image.crossOrigin = crossOrigin;
}, },
/**
* Specifies if the raster should be smoothed when scaled up or if the
* pixels should be scaled up by repeating the nearest neighboring pixels.
*
* @bean
* @type Boolean
* @default true
*
* @example {@paperscript}
* var raster = new Raster({
* source: 'http://assets.paperjs.org/images/marilyn.jpg',
* smoothing: false
* });
* raster.scale(5);
*/
getSmoothing: function() {
return this._smoothing;
},
setSmoothing: function(smoothing) {
this._smoothing = smoothing;
this._changed(/*#=*/Change.ATTRIBUTE);
},
// DOCS: document Raster#getElement // DOCS: document Raster#getElement
getElement: function() { getElement: function() {
// Only return the internal element if the content is actually ready. // Only return the internal element if the content is actually ready.
@ -734,12 +759,23 @@ var Raster = Item.extend(/** @lends Raster# */{
} }
}, },
_draw: function(ctx) { _draw: function(ctx, param, viewMatrix) {
var element = this.getElement(); var element = this.getElement();
if (element) { if (element) {
// Handle opacity for Rasters separately from the rest, since // Handle opacity for Rasters separately from the rest, since
// Rasters never draw a stroke. See Item#draw(). // Rasters never draw a stroke. See Item#draw().
ctx.globalAlpha = this._opacity; ctx.globalAlpha = this._opacity;
// Call _setStyles() to make sure shadow is drawn (#1437).
this._setStyles(ctx, param, viewMatrix);
// Set context smoothing value according to raster property.
// There's no need to restore original value after drawing due to
// the call to ctx.restore() in Item#draw() after this method call.
DomElement.setPrefixed(
ctx, 'imageSmoothingEnabled', this._smoothing
);
ctx.drawImage(element, ctx.drawImage(element,
-this._size.width / 2, -this._size.height / 2); -this._size.width / 2, -this._size.height / 2);
} }

View file

@ -162,7 +162,7 @@ var Shape = Item.extend(/** @lends Shape# */{
* @param {Boolean} [insert=true] specifies whether the new path should be * @param {Boolean} [insert=true] specifies whether the new path should be
* inserted into the scene graph. When set to `true`, it is inserted * inserted into the scene graph. When set to `true`, it is inserted
* above the shape item * above the shape item
* @return {Shape} the newly created path item with the same geometry as * @return {Path} the newly created path item with the same geometry as
* this shape item * this shape item
* @see Path#toShape(insert) * @see Path#toShape(insert)
*/ */

View file

@ -14,7 +14,7 @@
// the browser, avoiding the step of having to manually preprocess it after each // the browser, avoiding the step of having to manually preprocess it after each
// change. This is very useful during development of the library itself. // change. This is very useful during development of the library itself.
if (typeof window === 'object') { if (typeof window === 'object') {
// Browser based loading through Prepro.js: // Browser based loading through Prepro.js:
if (!window.include) { if (!window.include) {
// Get the last script tag and assume it's the one that loaded this file // Get the last script tag and assume it's the one that loaded this file
// then get its src attribute and figure out the location of our root. // then get its src attribute and figure out the location of our root.
@ -36,6 +36,13 @@ if (typeof window === 'object') {
// the code the 2nd time around. // the code the 2nd time around.
load(root + 'src/load.js'); load(root + 'src/load.js');
} else { } else {
// Some native javascript classes have name collisions with Paper.js
// classes. Store them to be able to use them later in tests.
NativeClasses = {
Event: Event,
MouseEvent: MouseEvent
};
include('options.js'); include('options.js');
// Load constants.js, required by the on-the-fly preprocessing: // Load constants.js, required by the on-the-fly preprocessing:
include('constants.js'); include('constants.js');

View file

@ -19,7 +19,7 @@ module.exports = function(self, requireName) {
var Canvas; var Canvas;
try { try {
Canvas = require('canvas'); Canvas = require('canvas');
} catch(e) { } catch(error) {
// Remove `self.window`, so we still have the global `self` reference, // Remove `self.window`, so we still have the global `self` reference,
// but no `window` object: // but no `window` object:
// - On the browser, this corresponds to a worker context. // - On the browser, this corresponds to a worker context.

View file

@ -17,7 +17,7 @@ var path = require('path');
// Determine the name by which name the module was required (either 'paper', // Determine the name by which name the module was required (either 'paper',
// 'paper-jsdom' or 'paper-jsdom-canvas'), and use this to determine if error // 'paper-jsdom' or 'paper-jsdom-canvas'), and use this to determine if error
// exceptions should be thrown or if loading should fail silently. // exceptions should be thrown or if loading should fail silently.
var parent = module.parent.parent, var parent = module.parent && module.parent.parent,
requireName = parent && path.basename(path.dirname(parent.filename)); requireName = parent && path.basename(path.dirname(parent.filename));
requireName = /^paper/.test(requireName) ? requireName : 'paper'; requireName = /^paper/.test(requireName) ? requireName : 'paper';

View file

@ -17,7 +17,7 @@
// The paper.js version. // The paper.js version.
// NOTE: Adjust value here before calling `gulp publish`, which then updates and // NOTE: Adjust value here before calling `gulp publish`, which then updates and
// publishes the various JSON package files automatically. // publishes the various JSON package files automatically.
var version = '0.11.5'; var version = '0.11.8';
// If this file is loaded in the browser, we're in load.js mode. // If this file is loaded in the browser, we're in load.js mode.
var load = typeof window === 'object'; var load = typeof window === 'object';

View file

@ -213,8 +213,9 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
getCurves: function() { getCurves: function() {
var children = this._children, var children = this._children,
curves = []; curves = [];
for (var i = 0, l = children.length; i < l; i++) for (var i = 0, l = children.length; i < l; i++) {
curves.push.apply(curves, children[i].getCurves()); Base.push(curves, children[i].getCurves());
}
return curves; return curves;
}, },

View file

@ -1147,6 +1147,21 @@ statics: /** @lends Curve */{
*/ */
getParameterAt: '#getTimeAt', getParameterAt: '#getTimeAt',
/**
* Calculates the curve-time parameters where the curve is tangential to
* provided tangent. Note that tangents at the start or end are included.
*
* @param {Point} tangent the tangent to which the curve must be tangential
* @return {Number[]} at most two curve-time parameters, where the curve is
* tangential to the given tangent
*/
getTimesWithTangent: function (/* tangent */) {
var tangent = Point.read(arguments);
return tangent.isZero()
? []
: Curve.getTimesWithTangent(this.getValues(), tangent);
},
/** /**
* Calculates the curve offset at the specified curve-time parameter on * Calculates the curve offset at the specified curve-time parameter on
* the curve. * the curve.
@ -1686,7 +1701,7 @@ new function() { // Scope for methods that require private functions
* http://math.stackexchange.com/questions/1954845/bezier-curvature-extrema * http://math.stackexchange.com/questions/1954845/bezier-curvature-extrema
* *
* @param {Number[]} v the curve values array * @param {Number[]} v the curve values array
* @returns {Number[]} the roots of all found peaks * @return {Number[]} the roots of all found peaks
*/ */
getPeaks: function(v) { getPeaks: function(v) {
var x0 = v[0], y0 = v[1], var x0 = v[0], y0 = v[1],
@ -2128,7 +2143,7 @@ new function() { // Scope for bezier intersection using fat-line clipping
// Flatten the list of location arrays to one array and return it. // Flatten the list of location arrays to one array and return it.
locations = []; locations = [];
for (var i = 0, l = arrays.length; i < l; i++) { for (var i = 0, l = arrays.length; i < l; i++) {
locations.push.apply(locations, arrays[i]); Base.push(locations, arrays[i]);
} }
return locations; return locations;
} }
@ -2230,6 +2245,56 @@ new function() { // Scope for bezier intersection using fat-line clipping
return pairs; return pairs;
} }
/**
* Internal method to calculates the curve-time parameters where the curve
* is tangential to provided tangent.
* Tangents at the start or end are included.
*
* @param {Number[]} v curve values
* @param {Point} tangent the tangent to which the curve must be tangential
* @return {Number[]} at most two curve-time parameters, where the curve is
* tangential to the given tangent
*/
function getTimesWithTangent(v, tangent) {
// Algorithm adapted from: https://stackoverflow.com/a/34837312/7615922
var x0 = v[0], y0 = v[1],
x1 = v[2], y1 = v[3],
x2 = v[4], y2 = v[5],
x3 = v[6], y3 = v[7],
normalized = tangent.normalize(),
tx = normalized.x,
ty = normalized.y,
ax = 3 * x3 - 9 * x2 + 9 * x1 - 3 * x0,
ay = 3 * y3 - 9 * y2 + 9 * y1 - 3 * y0,
bx = 6 * x2 - 12 * x1 + 6 * x0,
by = 6 * y2 - 12 * y1 + 6 * y0,
cx = 3 * x1 - 3 * x0,
cy = 3 * y1 - 3 * y0,
den = 2 * ax * ty - 2 * ay * tx,
times = [];
if (Math.abs(den) < Numerical.CURVETIME_EPSILON) {
var num = ax * cy - ay * cx,
den = ax * by - ay * bx;
if (den != 0) {
var t = -num / den;
if (t >= 0 && t <= 1) times.push(t);
}
} else {
var delta = (bx * bx - 4 * ax * cx) * ty * ty +
(-2 * bx * by + 4 * ay * cx + 4 * ax * cy) * tx * ty +
(by * by - 4 * ay * cy) * tx * tx,
k = bx * ty - by * tx;
if (delta >= 0 && den != 0) {
var d = Math.sqrt(delta),
t0 = -(k + d) / den,
t1 = (-k + d) / den;
if (t0 >= 0 && t0 <= 1) times.push(t0);
if (t1 >= 0 && t1 <= 1) times.push(t1);
}
}
return times;
}
return /** @lends Curve# */{ return /** @lends Curve# */{
/** /**
* Returns all intersections between two {@link Curve} objects as an * Returns all intersections between two {@link Curve} objects as an
@ -2252,7 +2317,8 @@ new function() { // Scope for bezier intersection using fat-line clipping
getOverlaps: getOverlaps, getOverlaps: getOverlaps,
// Exposed for use in boolean offsetting // Exposed for use in boolean offsetting
getIntersections: getIntersections, getIntersections: getIntersections,
getCurveLineIntersections: getCurveLineIntersections getCurveLineIntersections: getCurveLineIntersections,
getTimesWithTangent: getTimesWithTangent
} }
}; };
}); });

View file

@ -404,8 +404,8 @@ var Path = PathItem.extend(/** @lends Path# */{
this._updateSelection(segment, 0, segment._selection); this._updateSelection(segment, 0, segment._selection);
} }
if (append) { if (append) {
// Append them all at the end by using push // Append them all at the end.
segments.push.apply(segments, segs); Base.push(segments, segs);
} else { } else {
// Insert somewhere else // Insert somewhere else
segments.splice.apply(segments, [index, 0].concat(segs)); segments.splice.apply(segments, [index, 0].concat(segs));
@ -1012,7 +1012,7 @@ var Path = PathItem.extend(/** @lends Path# */{
* path.strokeColor = 'black'; * path.strokeColor = 'black';
* *
* // Split the path half-way: * // Split the path half-way:
* var path2 = path.splitAt(path2.length / 2); * var path2 = path.splitAt(path.length / 2);
* *
* // Give the resulting path a red stroke-color * // Give the resulting path a red stroke-color
* // and move it 20px to the right: * // and move it 20px to the right:
@ -1896,7 +1896,7 @@ var Path = PathItem.extend(/** @lends Path# */{
return offset; return offset;
} }
return null; return null;
} },
/** /**
* Calculates the point on the path at the given offset. * Calculates the point on the path at the given offset.
@ -2124,6 +2124,42 @@ var Path = PathItem.extend(/** @lends Path# */{
* the beginning of the path and {@link Path#length} at the end * the beginning of the path and {@link Path#length} at the end
* @return {Number} the normal vector at the given offset * @return {Number} the normal vector at the given offset
*/ */
/**
* Calculates path offsets where the path is tangential to the provided
* tangent. Note that tangents at the start or end are included. Tangents at
* segment points are returned even if only one of their handles is
* collinear with the provided tangent.
*
* @param {Point} tangent the tangent to which the path must be tangential
* @return {Number[]} path offsets where the path is tangential to the
* provided tangent
*/
getOffsetsWithTangent: function(/* tangent */) {
var tangent = Point.read(arguments);
if (tangent.isZero()) {
return [];
}
var offsets = [];
var curveStart = 0;
var curves = this.getCurves();
for (var i = 0, l = curves.length; i < l; i++) {
var curve = curves[i];
// Calculate curves times at vector tangent...
var curveTimes = curve.getTimesWithTangent(tangent);
for (var j = 0, m = curveTimes.length; j < m; j++) {
// ...and convert them to path offsets...
var offset = curveStart + curve.getOffsetAtTime(curveTimes[j]);
// ...avoiding duplicates.
if (offsets.indexOf(offset) < 0) {
offsets.push(offset);
}
}
curveStart += curve.length;
}
return offsets;
}
}), }),
new function() { // Scope for drawing new function() { // Scope for drawing
@ -2519,7 +2555,7 @@ new function() { // PostScript-style drawing commands
} }
vector = from.subtract(center); vector = from.subtract(center);
extent = vector.getDirectedAngle(to.subtract(center)); extent = vector.getDirectedAngle(to.subtract(center));
var centerSide = line.getSide(center); var centerSide = line.getSide(center, true);
if (centerSide === 0) { if (centerSide === 0) {
// If the center is lying on the line, we might have gotten // If the center is lying on the line, we might have gotten
// the wrong sign for extent above. Use the sign of the side // the wrong sign for extent above. Use the sign of the side

View file

@ -95,7 +95,7 @@ var PathFlattener = Base.extend({
segment1 = segment2; segment1 = segment2;
} }
if (path._closed) if (path._closed)
addCurve(segment2, segments[0]); addCurve(segment2 || segment1, segments[0]);
this.curves = curves; this.curves = curves;
this.parts = parts; this.parts = parts;
this.length = length; this.length = length;

View file

@ -112,8 +112,8 @@ PathItem.inject(new function() {
function collect(paths) { function collect(paths) {
for (var i = 0, l = paths.length; i < l; i++) { for (var i = 0, l = paths.length; i < l; i++) {
var path = paths[i]; var path = paths[i];
segments.push.apply(segments, path._segments); Base.push(segments, path._segments);
curves.push.apply(curves, path.getCurves()); Base.push(curves, path.getCurves());
// See if all encountered segments in a path are overlaps, to // See if all encountered segments in a path are overlaps, to
// be able to separately handle fully overlapping paths. // be able to separately handle fully overlapping paths.
path._overlapsOnly = true; path._overlapsOnly = true;
@ -248,7 +248,7 @@ PathItem.inject(new function() {
* @param {Boolean} [clockwise] if provided, the orientation of the root * @param {Boolean} [clockwise] if provided, the orientation of the root
* paths will be set to the orientation specified by `clockwise`, * paths will be set to the orientation specified by `clockwise`,
* otherwise the orientation of the largest root child is used. * otherwise the orientation of the largest root child is used.
* @returns {Item[]} the reoriented paths * @return {Item[]} the reoriented paths
*/ */
function reorientPaths(paths, isInside, clockwise) { function reorientPaths(paths, isInside, clockwise) {
var length = paths && paths.length; var length = paths && paths.length;
@ -748,13 +748,26 @@ PathItem.inject(new function() {
// While subtracting, we need to omit this curve if it is // While subtracting, we need to omit this curve if it is
// contributing to the second operand and is outside the // contributing to the second operand and is outside the
// first operand. // first operand.
var wind = !(operator.subtract && path2 && ( var wind = null;
operand === path1 && if (operator.subtract && path2) {
path2._getWinding(pt, dir, true).winding || // Calculate path winding at point depending on operand.
operand === path2 && var pathWinding = operand === path1
!path1._getWinding(pt, dir, true).winding)) ? path2._getWinding(pt, dir, true)
? getWinding(pt, curves, dir, true) : path1._getWinding(pt, dir, true);
: { winding: 0, quality: 1 }; // Check if curve should be omitted.
if (operand === path1 && pathWinding.winding ||
operand === path2 && !pathWinding.winding) {
// Check if quality is not good enough...
if (pathWinding.quality < 1) {
// ...and if so, skip this point...
continue;
} else {
// ...otherwise, omit this curve.
wind = { winding: 0, quality: 1 };
}
}
}
wind = wind || getWinding(pt, curves, dir, true);
if (wind.quality > winding.quality) if (wind.quality > winding.quality)
winding = wind; winding = wind;
break; break;
@ -1223,7 +1236,7 @@ PathItem.inject(new function() {
clearCurveHandles(clearCurves); clearCurveHandles(clearCurves);
// Finally resolve self-intersections through tracePaths() // Finally resolve self-intersections through tracePaths()
paths = tracePaths(Base.each(paths, function(path) { paths = tracePaths(Base.each(paths, function(path) {
this.push.apply(this, path._segments); Base.push(this, path._segments);
}, [])); }, []));
} }
// Determine how to return the paths: First try to recycle the // Determine how to return the paths: First try to recycle the

View file

@ -53,60 +53,94 @@ var Color = Base.extend(new function() {
// Parsers of values for setters, by type and property // Parsers of values for setters, by type and property
var componentParsers = {}, var componentParsers = {},
// Cache and canvas context for color name lookup // Cache and canvas context for color name lookup
colorCache = {}, namedColors = {
// node-canvas appears to return wrong values for 'transparent'.
// Fix it by having it pre-cashed here:
transparent: [0, 0, 0, 0]
},
colorCtx; colorCtx;
// TODO: Implement hsv, etc. CSS parsing!
function fromCSS(string) { function fromCSS(string) {
var match = string.match(/^#(\w{1,2})(\w{1,2})(\w{1,2})$/), var match = string.match(
/^#([\da-f]{2})([\da-f]{2})([\da-f]{2})([\da-f]{2})?$/i
) || string.match(
/^#([\da-f])([\da-f])([\da-f])([\da-f])?$/i
),
type = 'rgb',
components; components;
if (match) { if (match) {
// Hex // Hex with optional alpha channel:
components = [0, 0, 0]; var amount = match[4] ? 4 : 3;
for (var i = 0; i < 3; i++) { components = new Array(amount);
for (var i = 0; i < amount; i++) {
var value = match[i + 1]; var value = match[i + 1];
components[i] = parseInt(value.length == 1 components[i] = parseInt(value.length == 1
? value + value : value, 16) / 255; ? value + value : value, 16) / 255;
} }
} else if (match = string.match(/^rgba?\((.*)\)$/)) { } else if (match = string.match(/^(rgb|hsl)a?\((.*)\)$/)) {
// RGB / RGBA // RGB / RGBA or HSL / HSLA
components = match[1].split(','); type = match[1];
for (var i = 0, l = components.length; i < l; i++) { components = match[2].split(/[,\s]+/g);
var value = +components[i]; var isHSL = type === 'hsl';
components[i] = i < 3 ? value / 255 : value; for (var i = 0, l = Math.min(components.length, 4); i < l; i++) {
} var component = components[i];
} else if (window) { // Use `parseFloat()` instead of `+value` to parse '\d+%' to
// Named // float for HSL:
var cached = colorCache[string]; var value = parseFloat(component);
if (!cached) { if (isHSL) {
// Use a canvas to draw to with the given name and then retrieve if (i === 0) {
// RGB values from. Build a cache for all the used colors. // handle 'deg', 'turn', 'rad' 'grad':
if (!colorCtx) { var unit = component.match(/([a-z]*)$/)[1];
colorCtx = CanvasProvider.getContext(1, 1); value *= ({
colorCtx.globalCompositeOperation = 'copy'; turn: 360,
rad: 180 / Math.PI,
grad: 0.9 // 360 / 400
}[unit] || 1);
} else if (i < 3) {
// Percentages to 0..1
value /= 100;
}
} else if (i < 3) {
// RGB color values to 0..1
value /= 255;
} }
// Set the current fillStyle to transparent, so that it will be components[i] = value;
// transparent instead of the previously set color in case the
// new color can not be interpreted.
colorCtx.fillStyle = 'rgba(0,0,0,0)';
// Set the fillStyle of the context to the passed name and fill
// the canvas with it, then retrieve the data for the drawn
// pixel:
colorCtx.fillStyle = string;
colorCtx.fillRect(0, 0, 1, 1);
var data = colorCtx.getImageData(0, 0, 1, 1).data;
cached = colorCache[string] = [
data[0] / 255,
data[1] / 255,
data[2] / 255
];
} }
components = cached.slice();
} else { } else {
// Web-workers can't resolve CSS color names, for now. // Named
components = [0, 0, 0]; var color = namedColors[string];
if (!color) {
if (window) {
// Use a canvas to draw with the given name, then retrieve
// RGB values and build a cache for all the used colors.
if (!colorCtx) {
colorCtx = CanvasProvider.getContext(1, 1);
colorCtx.globalCompositeOperation = 'copy';
}
// Set the current fillStyle to transparent, so that it will be
// transparent instead of the previously set color in case the
// new color can not be interpreted.
colorCtx.fillStyle = 'rgba(0,0,0,0)';
// Set the fillStyle of the context to the passed name and fill
// the canvas with it, then retrieve the data for the drawn
// pixel:
colorCtx.fillStyle = string;
colorCtx.fillRect(0, 0, 1, 1);
var data = colorCtx.getImageData(0, 0, 1, 1).data;
color = namedColors[string] = [
data[0] / 255,
data[1] / 255,
data[2] / 255
];
} else {
// Web-workers can't resolve CSS color names, for now.
// TODO: Find a way to make this work there too?
color = [0, 0, 0];
}
}
components = color.slice();
} }
return components; return [type, components];
} }
// For hsb-rgb conversion, used to lookup the right parameters in the // For hsb-rgb conversion, used to lookup the right parameters in the
@ -237,36 +271,40 @@ var Color = Base.extend(new function() {
// hsb and hsl. Handle this here separately, by testing for // hsb and hsl. Handle this here separately, by testing for
// overlaps and skipping conversion if the type is /hs[bl]/ // overlaps and skipping conversion if the type is /hs[bl]/
hasOverlap = /^(hue|saturation)$/.test(name), hasOverlap = /^(hue|saturation)$/.test(name),
// Produce value parser function for the given type / propeprty // Produce value parser function for the given type / property
// name combination. parser = componentParsers[type][index] = type === 'gradient'
parser = componentParsers[type][index] = name === 'gradient' ? name === 'gradient'
? function(value) { // gradient property of gradient color:
var current = this._components[0]; ? function(value) {
value = Gradient.read(Array.isArray(value) ? value var current = this._components[0];
: arguments, 0, { readNull: true }); value = Gradient.read(
if (current !== value) { Array.isArray(value)
if (current) ? value
current._removeOwner(this); : arguments, 0, { readNull: true }
if (value) );
value._addOwner(this); if (current !== value) {
if (current)
current._removeOwner(this);
if (value)
value._addOwner(this);
}
return value;
} }
return value; // all other (point) properties of gradient color:
} : function(/* value */) {
: type === 'gradient'
? function(/* value */) {
return Point.read(arguments, 0, { return Point.read(arguments, 0, {
readNull: name === 'highlight', readNull: name === 'highlight',
clone: true clone: true
}); });
} }
: function(value) { // Normal number component properties:
// NOTE: We don't clamp values here, they're only : function(value) {
// clamped once the actual CSS values are produced. // NOTE: We don't clamp values here, they're only
// Gotta love the fact that isNaN(null) is false, // clamped once the actual CSS values are produced.
// while isNaN(undefined) is true. // Gotta love the fact that isNaN(null) is false,
return value == null || isNaN(value) ? 0 : value; // while isNaN(undefined) is true.
}; return value == null || isNaN(value) ? 0 : +value;
};
this['get' + part] = function() { this['get' + part] = function() {
return this._type === type return this._type === type
|| hasOverlap && /^hs[bl]$/.test(this._type) || hasOverlap && /^hs[bl]$/.test(this._type)
@ -411,6 +449,25 @@ var Color = Base.extend(new function() {
* } * }
* }); * });
*/ */
/**
* Creates a Color object from a CSS string. All common CSS color string
* formats are supported:
* - Named colors (e.g. `'red'`, `'fuchsia'`, )
* - Hex strings (`'#ffff00'`, `'#ff0'`, )
* - RGB strings (`'rgb(255, 128, 0)'`, `'rgba(255, 128, 0, 0.5)'`, )
* - HSL strings (`'hsl(180deg, 20%, 50%)'`,
* `'hsla(3.14rad, 20%, 50%, 0.5)'`, )
*
* @name Color#initialize
* @param {String} color the color's CSS string representation
*
* @example {@paperscript}
* var circle = new Path.Circle({
* center: [80, 50],
* radius: 30,
* fillColor: new Color('rgba(255, 255, 0, 0.5)')
* });
*/
/** /**
* Creates a gradient Color object. * Creates a gradient Color object.
* *
@ -544,8 +601,9 @@ var Color = Base.extend(new function() {
if (values.length > length) if (values.length > length)
values = Base.slice(values, 0, length); values = Base.slice(values, 0, length);
} else if (argType === 'string') { } else if (argType === 'string') {
type = 'rgb'; var converted = fromCSS(arg);
components = fromCSS(arg); type = converted[0];
components = converted[1];
if (components.length === 4) { if (components.length === 4) {
alpha = components[3]; alpha = components[3];
components.length--; components.length--;

View file

@ -174,8 +174,10 @@ var Style = Base.extend(new function() {
if (isColor) { if (isColor) {
// The old value may be a native string or other color // The old value may be a native string or other color
// description that wasn't coerced to a color object yet // description that wasn't coerced to a color object yet
if (old && old._owner !== undefined) if (old && old._owner !== undefined) {
old._owner = undefined; old._owner = undefined;
old._canvasStyle = null;
}
if (value && value.constructor === Color) { if (value && value.constructor === Color) {
// Clone color if it already has an owner. // Clone color if it already has an owner.
// NOTE: If value is not a Color, it is only // NOTE: If value is not a Color, it is only
@ -305,6 +307,16 @@ var Style = Base.extend(new function() {
|| false; || false;
}, },
_dispose: function() {
var color;
color = this.getFillColor();
if (color) color._canvasStyle = null;
color = this.getStrokeColor();
if (color) color._canvasStyle = null;
color = this.getShadowColor();
if (color) color._canvasStyle = null;
},
// DOCS: Style#hasFill() // DOCS: Style#hasFill()
hasFill: function() { hasFill: function() {
var color = this.getFillColor(); var color = this.getFillColor();

View file

@ -222,9 +222,8 @@ new function() {
// half of its size. We also need to take the raster's matrix // half of its size. We also need to take the raster's matrix
// into account, which will be defined by the time the load // into account, which will be defined by the time the load
// event is called. // event is called.
var center = this._matrix._transformPoint( var center = getPoint(node).add(size.divide(2));
getPoint(node).add(size.divide(2))); this._matrix.append(new Matrix().translate(center));
this.translate(center);
}); });
return raster; return raster;
}, },
@ -300,48 +299,10 @@ new function() {
// TODO: Support for these is missing in Paper.js right now // TODO: Support for these is missing in Paper.js right now
// rotate: character rotation // rotate: character rotation
// lengthAdjust: // lengthAdjust:
var text = new PointText(getPoint(node).add(
// Scratch-specific: Do not use x/y attributes because they break multiline usage. getPoint(node, 'dx', 'dy')));
var fontSize = parseFloat(node.getAttribute("font-size")); text.setContent(node.textContent.trim() || '');
var alignmentBaseline = node.getAttribute("alignment-baseline"); return text;
if (node.childElementCount === 0) {
var text = new PointText();
text.setContent(node.textContent.trim() || '');
// Scratch-specific: Scratch2 SVGs are offset by 1 leading vertically.
// Scratch3 SVGs use <tspan> method for all text (below)
text.translate(0, text._style.getLeading());
if (!isNaN(fontSize)) text.setFontSize(fontSize);
return text;
} else {
// Scratch3 SVGs always use <tspan>'s for multiline string support.
// Does not support x/y attribute or tspan positioning beyond left justified.
var lines = [];
var spacing = 1.2;
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
lines.push(child.textContent);
var dyString = child.getAttribute('dy');
if (dyString) {
var dy = parseFloat(dyString);
if (!isNaN(dy)) {
if (dyString.endsWith('em')) {
spacing = dy;
} else if (dyString.endsWith('px') && !isNaN(fontSize)) {
spacing = dy / fontSize;
}
}
}
}
var text = new PointText();
if (!isNaN(fontSize)) text.setFontSize(fontSize);
text.setLeading(text.fontSize * spacing);
if (alignmentBaseline === 'text-before-edge') {
text.setContent(' '); // No content results in 0 height
text.translate(0, text.bounds.height);
}
text.setContent(lines.join('\n'));
return text;
}
} }
}; };
@ -375,12 +336,10 @@ new function() {
new Matrix(v[0], v[1], v[2], v[3], v[4], v[5])); new Matrix(v[0], v[1], v[2], v[3], v[4], v[5]));
break; break;
case 'rotate': case 'rotate':
var v2 = (typeof v[1] === 'number' && typeof v[2] !== 'number') ? 0 : v[2]; matrix.rotate(v[0], v[1] || 0, v[2] || 0);
matrix.rotate(v[0], v[1], v2);
break; break;
case 'translate': case 'translate':
var v1 = (typeof v[1] === 'number') ? v[1] : 0; matrix.translate(v[0], v[1] || 0);
matrix.translate(v[0], v1);
break; break;
case 'scale': case 'scale':
matrix.scale(v); matrix.scale(v);
@ -537,7 +496,7 @@ new function() {
// First see if the given attribute is defined. // First see if the given attribute is defined.
var attr = node.attributes[name], var attr = node.attributes[name],
value = attr && attr.value; value = attr && attr.value;
if (!value) { if (!value && node.style) {
// Fallback to using styles. See if there is a style, either set // Fallback to using styles. See if there is a style, either set
// directly on the object or applied to it through CSS rules. // directly on the object or applied to it through CSS rules.
// We also need to filter out inheritance from their parents. // We also need to filter out inheritance from their parents.
@ -561,25 +520,23 @@ new function() {
* @param {Item} item the item to apply the style and attributes to * @param {Item} item the item to apply the style and attributes to
*/ */
function applyAttributes(item, node, isRoot) { function applyAttributes(item, node, isRoot) {
if (node.style) { // SVG attributes can be set both as styles and direct node
// SVG attributes can be set both as styles and direct node // attributes, so we need to handle both.
// attributes, so we need to handle both. var parent = node.parentNode,
var parent = node.parentNode, styles = {
styles = { node: DomElement.getStyles(node) || {},
node: DomElement.getStyles(node) || {}, // Do not check for inheritance if this is root, to make the
// Do not check for inheritance if this is root, to make the // default SVG settings stick. Also detect defs parents, of
// default SVG settings stick. Also detect defs parents, of // which children need to explicitly inherit their styles.
// which children need to explicitly inherit their styles. parent: !isRoot && !/^defs$/i.test(parent.tagName)
parent: !isRoot && !/^defs$/i.test(parent.tagName) && DomElement.getStyles(parent) || {}
&& DomElement.getStyles(parent) || {} };
}; Base.each(attributes, function(apply, name) {
Base.each(attributes, function(apply, name) { var value = getAttribute(node, name, styles);
var value = getAttribute(node, name, styles); // 'clip-path' attribute returns a new item, support it here:
// 'clip-path' attribute returns a new item, support it here: item = value !== undefined
item = value !== undefined && apply(item, value, name, node, styles) || item;
&& apply(item, value, name, node, styles) || item; });
});
}
return item; return item;
} }

View file

@ -309,7 +309,9 @@ var Tool = PaperScopeItem.extend(/** @lends Tool# */{
// so there's always a delta. // so there's always a delta.
toolPoint = move ? tool._point : (tool._downPoint || pt); toolPoint = move ? tool._point : (tool._downPoint || pt);
if (move) { if (move) {
if (tool._moveCount && pt.equals(toolPoint)) { // After first move event was emitted, tool._moveCount = 0, so
// we need to include 0 in this check.
if (tool._moveCount >= 0 && pt.equals(toolPoint)) {
return false; return false;
} }
if (toolPoint && (minDistance != null || maxDistance != null)) { if (toolPoint && (minDistance != null || maxDistance != null)) {

View file

@ -525,11 +525,9 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @see #getScaling() * @see #getScaling()
*/ */
getZoom: function() { getZoom: function() {
var decomposed = this._decompose(), var scaling = this._decompose().scaling;
scaling = decomposed && decomposed.scaling; // Use average since it can be non-uniform.
// Use average since it can be non-uniform, and return 0 when it can't return (scaling.x + scaling.y) / 2;
// be decomposed.
return scaling ? (scaling.x + scaling.y) / 2 : 0;
}, },
setZoom: function(zoom) { setZoom: function(zoom) {
@ -545,8 +543,7 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @type Number * @type Number
*/ */
getRotation: function() { getRotation: function() {
var decomposed = this._decompose(); return this._decompose().rotation;
return decomposed && decomposed.rotation;
}, },
setRotation: function(rotation) { setRotation: function(rotation) {
@ -565,11 +562,8 @@ var View = Base.extend(Emitter, /** @lends View# */{
* @see #getZoom() * @see #getZoom()
*/ */
getScaling: function() { getScaling: function() {
var decomposed = this._decompose(), var scaling = this._decompose().scaling;
scaling = decomposed && decomposed.scaling; return new LinkedPoint(scaling.x, scaling.y, this, 'setScaling');
return scaling
? new LinkedPoint(scaling.x, scaling.y, this, 'setScaling')
: undefined;
}, },
setScaling: function(/* scaling */) { setScaling: function(/* scaling */) {
@ -1271,11 +1265,12 @@ new function() { // Injection scope for event handling on the browser
point, prevPoint) point, prevPoint)
// Next handle the hit-item, if it's different from the drag-item // Next handle the hit-item, if it's different from the drag-item
// and not a descendant of it (in which case it would already have // and not a descendant of it (in which case it would already have
// received an event in the call above). // received an event in the call above). Translate mousedrag to
// mousemove, since drag is handled above.
|| hitItem && hitItem !== dragItem || hitItem && hitItem !== dragItem
&& !hitItem.isDescendant(dragItem) && !hitItem.isDescendant(dragItem)
&& emitMouseEvent(hitItem, null, type, event, point, prevPoint, && emitMouseEvent(hitItem, null, type === 'mousedrag' ?
dragItem) 'mousemove' : type, event, point, prevPoint, dragItem)
// Lastly handle the mouse events on the view, if we're still here. // Lastly handle the mouse events on the view, if we're still here.
|| emitMouseEvent(view, dragItem || hitItem || view, type, event, || emitMouseEvent(view, dragItem || hitItem || view, type, event,
point, prevPoint)); point, prevPoint));
@ -1440,8 +1435,16 @@ new function() { // Injection scope for event handling on the browser
// which can call `preventDefault()` explicitly or return `false`. // which can call `preventDefault()` explicitly or return `false`.
// - If this is a unhandled mousedown event, but the view or tools // - If this is a unhandled mousedown event, but the view or tools
// respond to mouseup. // respond to mouseup.
if (called && !mouse.move || mouse.down && responds('mouseup')) //
// Some events are not cancelable anyway (like during a scroll
// inertia on mobile) so trying to prevent default in those case
// would result in no effect and an error.
if (
event.cancelable !== false
&& (called && !mouse.move || mouse.down && responds('mouseup'))
) {
event.preventDefault(); event.preventDefault();
}
}, },
/** /**
@ -1490,7 +1493,20 @@ new function() { // Injection scope for event handling on the browser
* Loops through all views and sets the focus on the first * Loops through all views and sets the focus on the first
* active one. * active one.
*/ */
updateFocus: updateFocus updateFocus: updateFocus,
/**
* Clear all events handling state informations. Made for testing
* purpose, to have a way to start with a fresh state before each
* test.
* @private
*/
_resetState: function() {
dragging = mouseDown = called = wasInView = false;
prevFocus = tempFocus = overView = downPoint = lastPoint =
downItem = overItem = dragItem = clickItem = clickTime =
dblClick = null;
}
} }
}; };
}); });

View file

@ -34,6 +34,16 @@ if (isNode) {
}); });
} }
// Some native javascript classes have name collisions with Paper.js classes.
// If they have not already been stored in src/load.js, we dot it now.
if (!isNode && typeof NativeClasses === 'undefined')
{
NativeClasses = {
Event: Event,
MouseEvent: MouseEvent
};
}
// The unit-tests expect the paper classes to be global. // The unit-tests expect the paper classes to be global.
paper.install(scope); paper.install(scope);
@ -48,12 +58,16 @@ console.error = function() {
errorHandler.apply(this, arguments); errorHandler.apply(this, arguments);
}; };
var currentProject;
QUnit.done(function(details) { QUnit.done(function(details) {
console.error = errorHandler; console.error = errorHandler;
// Clear all event listeners after final test.
if (currentProject) {
currentProject.remove();
}
}); });
var currentProject;
// NOTE: In order to "export" all methods into the shared Prepro.js scope when // NOTE: In order to "export" all methods into the shared Prepro.js scope when
// using node-qunit, we need to define global functions as: // using node-qunit, we need to define global functions as:
// `var name = function() {}`. `function name() {}` does not work! // `var name = function() {}`. `function name() {}` does not work!
@ -61,9 +75,16 @@ var test = function(testName, expected) {
return QUnit.test(testName, function(assert) { return QUnit.test(testName, function(assert) {
// Since tests can be asynchronous, remove the old project before // Since tests can be asynchronous, remove the old project before
// running the next test. // running the next test.
if (currentProject) if (currentProject) {
currentProject.remove(); currentProject.remove();
currentProject = new Project(); // This is needed for interactions tests, to make sure that test is
// run with a fresh state.
View._resetState();
}
// Instantiate project with 100x100 pixels canvas instead of default
// 1x1 to make interactions tests simpler by working with integers.
currentProject = new Project(new Size(100, 100));
expected(assert); expected(assert);
}); });
}; };
@ -550,3 +571,63 @@ var compareSVG = function(done, actual, expected, message, options) {
compare(); compare();
} }
}; };
//
// Interactions helpers
//
var MouseEventPolyfill = function(type, params) {
var mouseEvent = document.createEvent('MouseEvent');
mouseEvent.initMouseEvent(
type,
params.bubbles,
params.cancelable,
window,
0,
params.screenX,
params.screenY,
params.clientX,
params.clientY,
params.ctrlKey,
params.altKey,
params.shiftKey,
params.metaKey,
params.button,
params.relatedTarget
);
return mouseEvent;
};
MouseEventPolyfill.prototype = typeof NativeClasses !== 'undefined'
&& NativeClasses.Event.prototype || Event.prototype;
var triggerMouseEvent = function(type, point, target) {
// Depending on event type, events have to be triggered on different
// elements due to the event handling implementation (see `viewEvents`
// and `docEvents` in View.js). And we cannot rely on the fact that event
// will bubble from canvas to document, since the canvas used in tests is
// not inserted in DOM.
target = target || (type === 'mousedown' ? view._element : document);
// If `gulp load` was run, there is a name collision between paper Event /
// MouseEvent and native javascript classes. In this case, we need to use
// native classes stored in global NativeClasses object instead.
var constructor = typeof NativeClasses !== 'undefined'
&& NativeClasses.MouseEvent || MouseEvent;
// MouseEvent class does not exist in PhantomJS, so in that case, we need to
// use a polyfill method.
if (typeof constructor !== 'function') {
constructor = MouseEventPolyfill;
}
var event = new constructor(type, {
bubbles: true,
cancelable: true,
composed: true,
clientX: point.x,
clientY: point.y,
screenX: point.x,
screenY: point.y
});
target.dispatchEvent(event);
};

View file

@ -60,23 +60,39 @@ test('Creating Colors', function() {
equals(new Color('red'), new Color(1, 0, 0), equals(new Color('red'), new Color(1, 0, 0),
'Color from name (red)'); 'Color from name (red)');
equals(new Color('transparent'), new Color(0, 0, 0, 0),
'Color from name (transparent)');
equals(new Color('#ff0000'), new Color(1, 0, 0), equals(new Color('#ff0000'), new Color(1, 0, 0),
'Color from hex code'); 'Color from hex string');
equals(new Color('#FF3300'), new Color(1, 0.2, 0),
'Color from uppercase hex string');
equals(new Color('#f009'), new Color(1, 0, 0, .6),
'Color from 4 characters hex code with alpha');
equals(new Color('#ff000099'), new Color(1, 0, 0, .6),
'Color from 8 characters hex code with alpha');
equals(new Color('rgb(255, 0, 0)'), new Color(1, 0, 0), equals(new Color('rgb(255, 0, 0)'), new Color(1, 0, 0),
'Color from RGB code'); 'Color from rgb() string');
equals(new Color('rgba(255, 0, 0, 0.5)'), new Color(1, 0, 0, 0.5), equals(new Color('rgba(255, 0, 0, 0.5)'), new Color(1, 0, 0, 0.5),
'Color from RGBA code'); 'Color from rgba() string');
equals(new Color('hsl(180deg, 20%, 40%)'),
new Color({ hue: 180, saturation: 0.2, lightness: 0.4 }),
'Color from hsl() string');
equals(new Color({ red: 1, green: 0, blue: 1}), equals(new Color({ red: 1, green: 0, blue: 1}),
new Color(1, 0, 1), 'Color from rgb object literal'); new Color(1, 0, 1), 'Color from RGB object literal');
equals(new Color({ gray: 0.2 }), equals(new Color({ gray: 0.2 }),
new Color(0.2), 'Color from gray object literal'); new Color(0.2), 'Color from gray object literal');
equals(new Color({ hue: 0, saturation: 1, brightness: 1}), equals(new Color({ hue: 0, saturation: 1, brightness: 1}),
new Color(1, 0, 0).convert('hsb'), 'Color from hsb object literal'); new Color(1, 0, 0).convert('hsb'), 'Color from HSB object literal');
equals(new Color([1, 0, 0]), new Color(1, 0, 0), equals(new Color([1, 0, 0]), new Color(1, 0, 0),
'RGB Color from array'); 'RGB Color from array');

View file

@ -347,3 +347,25 @@ test('Curve#divideAt(offset)', function() {
return new Curve(point1, point2).divideAtTime(0.5).point1; return new Curve(point1, point2).divideAtTime(0.5).point1;
}, middle); }, middle);
}); });
test('Curve#getTimesWithTangent()', function() {
var curve = new Curve([0, 0], [100, 0], [0, -100], [200, 200]);
equals(curve.getTimesWithTangent(), [], 'should return empty array when called without argument');
equals(curve.getTimesWithTangent([1, 0]), [0], 'should return tangent at start');
equals(curve.getTimesWithTangent([-1, 0]), [0], 'should return the same when called with opposite direction vector');
equals(curve.getTimesWithTangent([0, 1]), [1], 'should return tangent at end');
equals(curve.getTimesWithTangent([1, 1]), [0.5], 'should return tangent at middle');
equals(curve.getTimesWithTangent([1, -1]), [], 'should return empty array when there is no tangent');
equals(
new Curve([0, 0], [100, 0], [500, -500], [-500, -500]).getTimesWithTangent([1, 0]).length,
2,
'should return 2 values for specific self-intersecting path case'
);
equals(
new Curve([0, 0], [100, 0], [0, -100], [0, -100]).getTimesWithTangent([1, 0]).length,
2,
'should return 2 values for specific parabollic path case'
);
});

347
test/tests/Interactions.js Normal file
View file

@ -0,0 +1,347 @@
/*
* 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.
*/
/**
* These tests are focused on user interactions.
* They trigger events and check callbacks.
* Warning: when running tests locally from `gulp test:browser` command, don't
* move your mouse over the window because that could perturbate tests
* execution.
*/
QUnit.module('Interactions');
//
// Mouse
//
test('Item#onMouseDown()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onMouseDown = function(event) {
equals(event.type, 'mousedown');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, null);
done();
};
triggerMouseEvent('mousedown', point);
});
test('Item#onMouseDown() with stroked item', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.strokeColor = 'red';
var point = new Point(0, 0);
item.onMouseDown = function(event) {
equals(event.type, 'mousedown');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, null);
done();
};
triggerMouseEvent('mousedown', point);
});
test('Item#onMouseDown() is not triggered when item is not filled', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.onMouseDown = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
expect(0);
});
test('Item#onMouseDown() is not triggered when item is not visible', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.visible = false;
item.onMouseDown = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
expect(0);
});
test('Item#onMouseDown() is not triggered when item is locked', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.locked = true;
item.onMouseDown = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
expect(0);
});
test('Item#onMouseDown() is not triggered when another item is in front', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var item2 = item.clone();
item.onMouseDown = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
expect(0);
});
test('Item#onMouseDown() is not triggered if event target is document', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.onMouseDown = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5), document);
expect(0);
});
test('Item#onMouseMove()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onMouseMove = function(event) {
equals(event.type, 'mousemove');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, null);
done();
};
triggerMouseEvent('mousemove', point);
});
test('Item#onMouseMove() is not re-triggered if point is the same', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
var counter = 0;
item.onMouseMove = function(event) {
equals(true, true);
};
triggerMouseEvent('mousemove', point);
triggerMouseEvent('mousemove', point);
expect(1);
});
test('Item#onMouseUp()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onMouseUp = function(event) {
equals(event.type, 'mouseup');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, new Point(0, 0));
done();
};
triggerMouseEvent('mousedown', point);
triggerMouseEvent('mouseup', point);
});
test('Item#onMouseUp() is only triggered after mouse down', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.onMouseUp = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mouseup', new Point(5, 5));
expect(0);
});
test('Item#onClick()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onClick = function(event) {
equals(event.type, 'click');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, new Point(0, 0));
done();
};
triggerMouseEvent('mousedown', point);
triggerMouseEvent('mouseup', point);
});
test('Item#onClick() is not triggered if up point is not on item', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.onClick = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
triggerMouseEvent('mouseup', new Point(15, 15));
expect(0);
});
test('Item#onClick() is not triggered if down point is not on item', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.onClick = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(15, 15));
triggerMouseEvent('mouseup', new Point(5, 5));
expect(0);
});
test('Item#onDoubleClick()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onDoubleClick = function(event) {
equals(event.type, 'doubleclick');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, new Point(0, 0));
done();
};
triggerMouseEvent('mousedown', point);
triggerMouseEvent('mouseup', point);
triggerMouseEvent('mousedown', point);
triggerMouseEvent('mouseup', point);
});
test('Item#onDoubleClick() is not triggered if both clicks are not on same item', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var item2 = item.clone().translate(5);
item.onDoubleClick = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
triggerMouseEvent('mouseup', new Point(5, 5));
triggerMouseEvent('mousedown', new Point(6, 6));
triggerMouseEvent('mouseup', new Point(6, 6));
expect(0);
});
test('Item#onDoubleClick() is not triggered if time between both clicks is too long', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onDoubleClick = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', point);
triggerMouseEvent('mouseup', point);
setTimeout(function() {
triggerMouseEvent('mousedown', point);
triggerMouseEvent('mouseup', point);
done();
}, 301);
expect(0);
});
test('Item#onMouseEnter()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point = new Point(5, 5);
item.onMouseEnter = function(event) {
equals(event.type, 'mouseenter');
equals(event.point, point);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, null);
done();
};
triggerMouseEvent('mousemove', point);
});
test('Item#onMouseEnter() is only re-triggered after mouse leave', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.onMouseEnter = function(event) {
equals(true, true);
};
// enter
triggerMouseEvent('mousemove', new Point(5, 5));
triggerMouseEvent('mousemove', new Point(6, 6));
triggerMouseEvent('mousemove', new Point(7, 7));
// leave
triggerMouseEvent('mousemove', new Point(11, 11));
// re-enter
triggerMouseEvent('mousemove', new Point(10, 10));
expect(2);
});
test('Item#onMouseLeave()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point1 = new Point(5, 5);
var point2 = new Point(15, 15);
item.onMouseLeave = function(event) {
equals(event.type, 'mouseleave');
equals(event.point, point2);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, null);
done();
};
triggerMouseEvent('mousemove', point1);
triggerMouseEvent('mousemove', point2);
});
test('Item#onMouseDrag()', function(assert) {
var done = assert.async();
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var point1 = new Point(5, 5);
var point2 = new Point(15, 15);
item.onMouseDrag = function(event) {
equals(event.type, 'mousedrag');
equals(event.point, point2);
equals(event.target, item);
equals(event.currentTarget, item);
equals(event.delta, new Point(10, 10));
done();
};
triggerMouseEvent('mousedown', point1);
triggerMouseEvent('mousemove', point2);
});
test('Item#onMouseDrag() is not triggered after mouse up', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
item.onMouseDrag = function(event) {
equals(true, true);
};
triggerMouseEvent('mousedown', new Point(5, 5));
triggerMouseEvent('mousemove', new Point(6, 6));
triggerMouseEvent('mouseup', new Point(7, 7));
triggerMouseEvent('mousemove', new Point(8, 8));
expect(1);
});
test('Item#onMouseDrag() is not triggered if mouse down was on another item', function(assert) {
var item = new Path.Rectangle(new Point(0, 0), new Size(10));
item.fillColor = 'red';
var item2 = item.clone().translate(10);
item2.onMouseDrag = function(event) {
throw 'this should not be called';
};
triggerMouseEvent('mousedown', new Point(5, 5));
triggerMouseEvent('mousemove', new Point(11, 11));
expect(0);
});

View file

@ -929,3 +929,19 @@ test('Item#scaling, #rotation', function() {
equals(shape2.bounds, expected, equals(shape2.bounds, expected,
'shape2.bounds, setting shape2.scaling before shape2.rotation'); 'shape2.bounds, setting shape2.scaling before shape2.rotation');
}); });
test('Item#position pivot point and caching (#1503)', function() {
var item = Path.Rectangle(new Point(0, 0), new Size(20));
item.pivot = new Point(0, 0);
var bounds = item.bounds;
item.translate(5, 5);
equals(item.position, new Point(5, 5));
});
test('Children global matrices are cleared after parent transformation', function() {
var item = Path.Rectangle(new Point(0, 0), new Size(100));
var group = new Group({ children: [item], applyMatrix: false });
equals(item.localToGlobal(item.getPointAt(0)), new Point(0, 100));
group.translate(100, 0);
equals(item.localToGlobal(item.getPointAt(0)), new Point(100, 100));
});

View file

@ -789,3 +789,12 @@ test('group.internalBounds with child and child.applyMatrix = false (#1250)', fu
equals(group.internalBounds, new Rectangle(0, 0, 250, 250), equals(group.internalBounds, new Rectangle(0, 0, 250, 250),
'group.internalBounds after scaling item1'); 'group.internalBounds after scaling item1');
}); });
test('#1561 item._globalMatrix on item after empty symbol', function(){
var symbol = new SymbolItem(new Path());
symbol.opacity = 0.5;
symbol.skew(10);
var item = new Path.Circle(new Point(0,0), 10);
view.update();
equals(item._globalMatrix, new Matrix());
});

View file

@ -249,7 +249,6 @@ test('After removing all segments of a selected path, it should still be selecte
}, true); }, true);
}); });
test('After simplifying a path using #simplify(), the path should stay fullySelected', function() { test('After simplifying a path using #simplify(), the path should stay fullySelected', function() {
var path = new Path(); var path = new Path();
for (var i = 0; i < 30; i++) { for (var i = 0; i < 30; i++) {
@ -451,6 +450,13 @@ test('Path#flatten(maxDistance)', function() {
}, true, 'The points of the last and before last segments should not be so close, that calling toString on them returns the same string value.'); }, true, 'The points of the last and before last segments should not be so close, that calling toString on them returns the same string value.');
}); });
test('Path#single segment closed path flatten (#1338)', function() {
var p = PathItem.create("m445.26701,223.69688c6.1738,8.7566 -7.05172,14.0468 0,0z");
p.strokeColor = "red";
p.flatten();
expect(0);
});
test('Path#curves after removing a segment - 1', function() { test('Path#curves after removing a segment - 1', function() {
var path = new paper.Path([0, 0], [1, 1], [2, 2]); var path = new paper.Path([0, 0], [1, 1], [2, 2]);
var prevCurves = path.curves.slice(); var prevCurves = path.curves.slice();
@ -611,3 +617,31 @@ test('Path#arcTo(from, through, to); where from, through and to all share the sa
} }
equals(error != null, true, 'We expect this arcTo() command to throw an error'); equals(error != null, true, 'We expect this arcTo() command to throw an error');
}); });
test('Path#getOffsetsWithTangent()', function() {
var path = new Path.Circle(new Point(0, 0), 50);
var length = path.length;
equals(path.getOffsetsWithTangent(), [], 'should return empty array when called without argument');
equals(path.getOffsetsWithTangent([1, 0]), [0.25 * length, 0.75 * length], 'should not return duplicates when tangent is at segment point');
equals(path.getOffsetsWithTangent([1, 1]).length, 2, 'should return 2 values when called on a circle with a diagonal vector');
});
test('Path#add() with a lot of segments (#1493)', function() {
var segments = [];
for (var i = 0; i < 100000; i++) {
segments.push(new Point(0, 0));
}
var path = new Path(segments);
path.clone();
expect(0);
});
test('Path#arcTo(through, to) is on through point side (#1477)', function() {
var p1 = new Point(16, 21.5);
var p2 = new Point(22.5, 15);
var p3 = new Point(16.000000000000004, 8.5);
var path = new Path();
path.add(p1);
path.arcTo(p2, p3);
equals(true, path.segments[1].point.x > p1.x);
});

View file

@ -1189,3 +1189,17 @@ test('Isolated edge-cases from @iconexperience\'s boolean-test suite', function(
compareBoolean(path1.intersect(path2), result[1], 'path1.intersect(path2); // Test ' + (i + 1)); compareBoolean(path1.intersect(path2), result[1], 'path1.intersect(path2); // Test ' + (i + 1));
} }
}); });
test('#1506', function () {
var path1 = new Path('M250,175c27.61424,0 50,22.38576 50,50c0,27.61424 -22.38576,50 -50,50c-9.10718,0 -17.64567,-2.43486 -25,-6.68911c14.94503,-8.64524 25,-24.80383 25,-43.31089c0,-18.50706 -10.05497,-34.66565 -25,-43.31089c7.35433,-4.25425 15.89282,-6.68911 25,-6.68911z');
var path2 = new Path('M250,225c0,-27.61424 22.38576,-50 50,-50c27.61424,0 50,22.38576 50,50c0,27.61424 -22.38576,50 -50,50c-27.61424,0 -50,-22.38576 -50,-50z');
var result = 'M250,175c9.10718,0 17.64567,2.43486 25,6.68911c-14.94503,8.64523 -25,24.80383 -25,43.31089c0,18.50706 10.05497,34.66566 25,43.31089c-7.35433,4.25425 -15.89282,6.68911 -25,6.68911c-9.10718,0 -17.64567,-2.43486 -25,-6.68911c14.94503,-8.64524 25,-24.80383 25,-43.31089c0,-18.50706 -10.05497,-34.66565 -25,-43.31089c7.35433,-4.25425 15.89282,-6.68911 25,-6.68911z';
compareBoolean(path1.subtract(path2), result);
});
test('#1513', function () {
var path1 = PathItem.create('M100,200v-100h200v100z');
var path2 = PathItem.create('M200,100c55.22847,0 100,44.77153 100,100h-200c0,-55.22847 44.77153,-100 100,-100z');
var result = 'M100,100h200v100c0,-55.22847 -44.77153,-100 -100,-100c-55.22847,0 -100,44.77153 -100,100z';
compareBoolean(path1.subtract(path2), result);
});

View file

@ -179,3 +179,30 @@ test('Raster#getAverageColor(path) with compound path', function() {
equals(raster.getAverageColor(compoundPath), new Color(1, 0, 0), null, equals(raster.getAverageColor(compoundPath), new Color(1, 0, 0), null,
{ tolerance: 1e-3 }); { tolerance: 1e-3 });
}); });
test('Raster#smoothing defaults to true', function() {
var raster = new Raster();
equals(raster.smoothing, true);
});
test('Raster#smoothing', function() {
var raster = new Raster({ smoothing: false });
equals(raster.smoothing, false);
raster.smoothing = true;
equals(raster.smoothing, true);
});
test('Raster#setSmoothing setting does not impact canvas context', function(assert) {
var done = assert.async();
var raster = new Raster('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABlJREFUeNpi+s/AwPCfgYmR4f9/hv8AAQYAHiAFAS8Lwy8AAAAASUVORK5CYII=');
var view = raster.view;
var context = view._context;
raster.onLoad = function() {
var originalValue = context.imageSmoothingEnabled;
raster.smoothing = false;
view.update();
equals(context.imageSmoothingEnabled, originalValue);
done();
};
});

View file

@ -114,6 +114,17 @@ test('Import SVG polyline', function() {
equals(imported, path); equals(imported, path);
}); });
test('Import SVG Image', function(assert) {
var done = assert.async();
var svg = '<?xml version="1.0" encoding="utf-8"?><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image style="overflow:visible;enable-background:new ;" width="300" height="67" id="e0" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" transform="matrix(0.2149 0 0 0.2149 304.7706 197.8176)"></image></svg>';
var imported = paper.project.importSVG(svg);
var raster = imported.children[0];
raster.on('load', function() {
equals(raster.matrix, new Matrix(0.2149, 0, 0, 0.2149, 337.0056, 205.01675));
done();
});
});
test('Import complex CompoundPath and clone', function() { test('Import complex CompoundPath and clone', function() {
var svg = '<svg xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M4,14h20v-2H4V14z M15,26h7v-2h-7V26z M15,22h9v-2h-9V22z M15,18h9v-2h-9V18z M4,26h9V16H4V26z M28,10V6H0v22c0,0,0,4,4,4 h25c0,0,3-0.062,3-4V10H28z M4,30c-2,0-2-2-2-2V8h24v20c0,0.921,0.284,1.558,0.676,2H4z"/></svg>'; var svg = '<svg xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M4,14h20v-2H4V14z M15,26h7v-2h-7V26z M15,22h9v-2h-9V22z M15,18h9v-2h-9V18z M4,26h9V16H4V26z M28,10V6H0v22c0,0,0,4,4,4 h25c0,0,3-0.062,3-4V10H28z M4,30c-2,0-2-2-2-2V8h24v20c0,0.921,0.284,1.558,0.676,2H4z"/></svg>';
var item = paper.project.importSVG(svg); var item = paper.project.importSVG(svg);

View file

@ -62,3 +62,8 @@
/*#*/ include('SvgExport.js'); /*#*/ include('SvgExport.js');
/*#*/ include('Numerical.js'); /*#*/ include('Numerical.js');
// There is no need to test interactions in node context.
if (!isNode) {
/*#*/ include('Interactions.js');
}