mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 14:42:13 -05:00
Merge branch 'develop' into greenkeeper/react-style-proptype-3.2.1
This commit is contained in:
commit
325e8e6c07
4 changed files with 194 additions and 63 deletions
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@scratch/paper": "~0.11.6",
|
"@scratch/paper": "~0.11.6",
|
||||||
"autoprefixer": "7.2.5",
|
"autoprefixer": "8.1.0",
|
||||||
"babel-cli": "6.26.0",
|
"babel-cli": "6.26.0",
|
||||||
"babel-core": "^6.23.1",
|
"babel-core": "^6.23.1",
|
||||||
"babel-eslint": "^8.2.1",
|
"babel-eslint": "^8.2.1",
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
"eslint-plugin-import": "^2.9.0",
|
"eslint-plugin-import": "^2.9.0",
|
||||||
"eslint-plugin-react": "^7.2.1",
|
"eslint-plugin-react": "^7.2.1",
|
||||||
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
|
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
|
||||||
"html-webpack-plugin": "2.30.1",
|
"html-webpack-plugin": "3.0.6",
|
||||||
"jest": "^22.2.2",
|
"jest": "^22.2.2",
|
||||||
"keymirror": "0.1.1",
|
"keymirror": "0.1.1",
|
||||||
"lodash.bindall": "4.4.0",
|
"lodash.bindall": "4.4.0",
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"react-intl-redux": "0.7.0",
|
"react-intl-redux": "0.7.0",
|
||||||
"react-popover": "0.5.4",
|
"react-popover": "0.5.4",
|
||||||
"react-redux": "5.0.7",
|
"react-redux": "5.0.7",
|
||||||
"react-responsive": "4.0.3",
|
"react-responsive": "4.0.5",
|
||||||
"react-style-proptype": "3.2.1",
|
"react-style-proptype": "3.2.1",
|
||||||
"react-test-renderer": "^16.0.0",
|
"react-test-renderer": "^16.0.0",
|
||||||
"react-tooltip": "3.4.0",
|
"react-tooltip": "3.4.0",
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Blobbiness {
|
||||||
// Segment brush has performance issues at low threshold, but broad brush has weird corners
|
// Segment brush has performance issues at low threshold, but broad brush has weird corners
|
||||||
// which get more obvious the bigger it is
|
// which get more obvious the bigger it is
|
||||||
static get THRESHOLD () {
|
static get THRESHOLD () {
|
||||||
return 9;
|
return 30 / paper.view.zoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,7 +110,6 @@ class Blobbiness {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tool.onMouseDrag = function (event) {
|
this.tool.onMouseDrag = function (event) {
|
||||||
blob.resizeCursorIfNeeded(event.point);
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
if (blob.brush === Blobbiness.BROAD) {
|
if (blob.brush === Blobbiness.BROAD) {
|
||||||
blob.broadBrushHelper.onBroadMouseDrag(event, blob.tool, blob.options);
|
blob.broadBrushHelper.onBroadMouseDrag(event, blob.tool, blob.options);
|
||||||
|
@ -125,7 +124,6 @@ class Blobbiness {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tool.onMouseUp = function (event) {
|
this.tool.onMouseUp = function (event) {
|
||||||
blob.resizeCursorIfNeeded(event.point);
|
|
||||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||||
|
|
||||||
let lastPath;
|
let lastPath;
|
||||||
|
@ -143,11 +141,9 @@ class Blobbiness {
|
||||||
blob.mergeBrush(lastPath);
|
blob.mergeBrush(lastPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
blob.cursorPreview.visible = false;
|
blob.cursorPreview.remove();
|
||||||
|
blob.cursorPreview = null;
|
||||||
blob.onUpdateSvg();
|
blob.onUpdateSvg();
|
||||||
blob.cursorPreview.visible = true;
|
|
||||||
blob.cursorPreview.bringToFront();
|
|
||||||
blob.cursorPreview.position = event.point;
|
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
blob.brush = null;
|
blob.brush = null;
|
||||||
|
@ -432,8 +428,10 @@ class Blobbiness {
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivateTool () {
|
deactivateTool () {
|
||||||
this.cursorPreview.remove();
|
if (this.cursorPreview) {
|
||||||
this.cursorPreview = null;
|
this.cursorPreview.remove();
|
||||||
|
this.cursorPreview = null;
|
||||||
|
}
|
||||||
this.tool.remove();
|
this.tool.remove();
|
||||||
this.tool = null;
|
this.tool = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/
|
// Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/
|
||||||
import paper from '@scratch/paper';
|
import paper from '@scratch/paper';
|
||||||
import {styleBlob} from '../../helper/style-path';
|
import {styleBlob} from '../../helper/style-path';
|
||||||
|
import log from '../../log/log';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broad brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens
|
* Broad brush functions to add as listeners on the mouse. Call them when the corresponding mouse event happens
|
||||||
|
@ -14,99 +15,232 @@ import {styleBlob} from '../../helper/style-path';
|
||||||
*/
|
*/
|
||||||
class BroadBrushHelper {
|
class BroadBrushHelper {
|
||||||
constructor () {
|
constructor () {
|
||||||
|
// Direction vector of the last mouse move
|
||||||
|
this.lastVec = null;
|
||||||
|
// End point of the last mouse move
|
||||||
this.lastPoint = null;
|
this.lastPoint = null;
|
||||||
this.secondLastPoint = null;
|
// The path of the brush stroke we are building
|
||||||
this.finalPath = null;
|
this.finalPath = null;
|
||||||
|
// Number of points of finalPath that have already been processed
|
||||||
|
this.smoothed = 0;
|
||||||
|
// Number of steps to wait before performing another amortized smooth
|
||||||
|
this.smoothingThreshold = 20;
|
||||||
|
// Mouse moves since mouse down
|
||||||
|
this.steps = 0;
|
||||||
|
// End caps round out corners and are not merged into the path until the end.
|
||||||
|
this.endCaps = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
onBroadMouseDown (event, tool, options) {
|
onBroadMouseDown (event, tool, options) {
|
||||||
tool.minDistance = options.brushSize / 2;
|
this.steps = 0;
|
||||||
|
this.smoothed = 0;
|
||||||
|
tool.minDistance = Math.min(5, Math.max(2 / paper.view.zoom, options.brushSize / 2));
|
||||||
tool.maxDistance = options.brushSize;
|
tool.maxDistance = options.brushSize;
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
this.finalPath = new paper.Path();
|
this.finalPath = new paper.Path.Circle({
|
||||||
|
center: event.point,
|
||||||
|
radius: options.brushSize / 2
|
||||||
|
});
|
||||||
styleBlob(this.finalPath, options);
|
styleBlob(this.finalPath, options);
|
||||||
this.finalPath.add(event.point);
|
this.lastPoint = event.point;
|
||||||
this.lastPoint = this.secondLastPoint = event.point;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBroadMouseDrag (event, tool, options) {
|
onBroadMouseDrag (event, tool, options) {
|
||||||
|
this.steps++;
|
||||||
const step = (event.delta).normalize(options.brushSize / 2);
|
const step = (event.delta).normalize(options.brushSize / 2);
|
||||||
|
|
||||||
// Move the first point out away from the drag so that the end of the path is rounded
|
// Add an end cap if the mouse has changed direction very quickly
|
||||||
if (this.finalPath.segments && this.finalPath.segments.length === 1) {
|
if (this.lastVec) {
|
||||||
const removedPoint = this.finalPath.removeSegment(0).point;
|
const angle = this.lastVec.getDirectedAngle(step);
|
||||||
// Add handles to round the end caps
|
if (Math.abs(angle) > 126) {
|
||||||
const handleVec = step.clone();
|
// This will cause us to skip simplifying this sharp angle. Running simplify on
|
||||||
handleVec.length = options.brushSize / 2;
|
// sharp angles causes the stroke to blob outwards.
|
||||||
handleVec.angle += 90;
|
this.simplify(1);
|
||||||
this.finalPath.add(new paper.Segment(removedPoint.subtract(step), -handleVec, handleVec));
|
this.smoothed++;
|
||||||
|
|
||||||
|
// If the angle is large, the broad brush tends to leave behind a flat edge.
|
||||||
|
// This code makes a shape to fill in that flat edge with a rounded cap.
|
||||||
|
const circ = new paper.Path.Circle(this.lastPoint, options.brushSize / 2);
|
||||||
|
circ.fillColor = options.fillColor;
|
||||||
|
const rect = new paper.Path.Rectangle(
|
||||||
|
this.lastPoint.subtract(new paper.Point(-options.brushSize / 2, 0)),
|
||||||
|
this.lastPoint.subtract(new paper.Point(options.brushSize / 2, this.lastVec.length))
|
||||||
|
);
|
||||||
|
rect.fillColor = options.fillColor;
|
||||||
|
rect.rotate(this.lastVec.angle - 90, this.lastPoint);
|
||||||
|
const rect2 = new paper.Path.Rectangle(
|
||||||
|
event.point.subtract(new paper.Point(-options.brushSize / 2, 0)),
|
||||||
|
event.point.subtract(new paper.Point(options.brushSize / 2, event.delta.length))
|
||||||
|
);
|
||||||
|
rect2.fillColor = options.fillColor;
|
||||||
|
rect2.rotate(step.angle - 90, event.point);
|
||||||
|
this.endCaps.push(this.union(circ, this.union(rect, rect2)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.lastVec = event.delta;
|
||||||
step.angle += 90;
|
step.angle += 90;
|
||||||
|
|
||||||
|
// Move the first point out away from the drag so that the end of the path is rounded
|
||||||
|
if (this.steps === 1) {
|
||||||
|
// Replace circle with path
|
||||||
|
this.finalPath.remove();
|
||||||
|
this.finalPath = new paper.Path();
|
||||||
|
const handleVec = event.delta.normalize(options.brushSize / 2);
|
||||||
|
this.finalPath.add(new paper.Segment(
|
||||||
|
this.lastPoint.subtract(handleVec),
|
||||||
|
handleVec.rotate(-90),
|
||||||
|
handleVec.rotate(90)
|
||||||
|
));
|
||||||
|
styleBlob(this.finalPath, options);
|
||||||
|
this.finalPath.insert(0, new paper.Segment(this.lastPoint.subtract(step)));
|
||||||
|
this.finalPath.add(new paper.Segment(this.lastPoint.add(step)));
|
||||||
|
}
|
||||||
const top = event.middlePoint.add(step);
|
const top = event.middlePoint.add(step);
|
||||||
const bottom = event.middlePoint.subtract(step);
|
const bottom = event.middlePoint.subtract(step);
|
||||||
|
|
||||||
if (this.finalPath.segments.length > 3) {
|
|
||||||
this.finalPath.removeSegment(this.finalPath.segments.length - 1);
|
|
||||||
this.finalPath.removeSegment(0);
|
|
||||||
}
|
|
||||||
this.finalPath.add(top);
|
this.finalPath.add(top);
|
||||||
this.finalPath.add(event.point.add(step));
|
this.finalPath.add(event.point.add(step));
|
||||||
this.finalPath.insert(0, bottom);
|
this.finalPath.insert(0, bottom);
|
||||||
this.finalPath.insert(0, event.point.subtract(step));
|
this.finalPath.insert(0, event.point.subtract(step));
|
||||||
if (this.finalPath.segments.length === 5) {
|
|
||||||
// Flatten is necessary to prevent smooth from getting rid of the effect
|
if (this.finalPath.segments.length > this.smoothed + (this.smoothingThreshold * 2)) {
|
||||||
// of the handles on the first point.
|
this.simplify(1);
|
||||||
this.finalPath.flatten(Math.min(5, options.brushSize / 5));
|
|
||||||
}
|
}
|
||||||
this.finalPath.smooth();
|
|
||||||
this.lastPoint = event.point;
|
this.lastPoint = event.point;
|
||||||
this.secondLastPoint = event.lastPoint;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplify the path so that it looks almost the same while trying to have a reasonable number of handles.
|
||||||
|
* Without this, there would be 2 handles for every mouse move, which would make the path produced basically
|
||||||
|
* uneditable. This version of simplify keeps track of how much of the path has already been simplified to
|
||||||
|
* avoid repeating work.
|
||||||
|
* @param {number} threshold The simplify algorithm must try to stay within this distance of the actual line.
|
||||||
|
* The algorithm will be faster and able to remove more points the higher this number is.
|
||||||
|
* Note that 1 is about the lowest this algorithm can do (the result is about the same when 1 is
|
||||||
|
* passed in as when 0 is passed in)
|
||||||
|
*/
|
||||||
|
simplify (threshold) {
|
||||||
|
// Length of the current path
|
||||||
|
const length = this.finalPath.segments.length;
|
||||||
|
// Number of new points added to front and end of path since last simplify
|
||||||
|
const newPoints = Math.floor((length - this.smoothed) / 2) + 1;
|
||||||
|
|
||||||
|
// Where to cut. Don't go past the rounded start of the line (so there's always a tempPathMid)
|
||||||
|
const firstCutoff = Math.min(newPoints + 1, Math.floor((length / 2)));
|
||||||
|
const lastCutoff = Math.max(length - 1 - newPoints, Math.floor(length / 2) + 1);
|
||||||
|
if (firstCutoff <= 1 || lastCutoff >= length - 1) {
|
||||||
|
// Entire path is simplified already
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Cut the path into 3 segments: the 2 ends where the new points are, and the middle, which will be
|
||||||
|
// staying the same
|
||||||
|
const tempPath1 = new paper.Path(this.finalPath.segments.slice(1, firstCutoff));
|
||||||
|
const tempPathMid = new paper.Path(this.finalPath.segments.slice(firstCutoff, lastCutoff));
|
||||||
|
const tempPath2 = new paper.Path(this.finalPath.segments.slice(lastCutoff, length - 1));
|
||||||
|
|
||||||
|
// Run simplify on the new ends. We need to graft the old handles back onto the newly
|
||||||
|
// simplified paths, since simplify removes the in handle from the start of the path, and
|
||||||
|
// the out handle from the end of the path it's simplifying.
|
||||||
|
const oldPath1End = tempPath1.segments[tempPath1.segments.length - 1];
|
||||||
|
const oldPath2End = tempPath2.segments[0];
|
||||||
|
tempPath1.simplify(threshold);
|
||||||
|
tempPath2.simplify(threshold);
|
||||||
|
const newPath1End = tempPath1.segments[tempPath1.segments.length - 1];
|
||||||
|
const newPath2End = tempPath2.segments[0];
|
||||||
|
newPath1End.handleOut = oldPath1End.handleOut;
|
||||||
|
newPath2End.handleIn = oldPath2End.handleIn;
|
||||||
|
|
||||||
|
// Delete the old contents of finalPath and replace it with the newly simplified segments, concatenated
|
||||||
|
this.finalPath.removeSegments(1, this.finalPath.segments.length - 1);
|
||||||
|
this.finalPath.insertSegments(1, tempPath1.segments.concat(tempPathMid.segments).concat(tempPath2.segments));
|
||||||
|
|
||||||
|
// Remove temp paths
|
||||||
|
tempPath1.remove();
|
||||||
|
tempPath2.remove();
|
||||||
|
tempPathMid.remove();
|
||||||
|
|
||||||
|
// Update how many points have been smoothed so far so that we don't redo work when
|
||||||
|
// simplify is called next time.
|
||||||
|
this.smoothed = Math.max(2, this.finalPath.segments.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like paper.Path.unite, but it removes the original 2 paths
|
||||||
|
* @param {paper.Path} path1 to merge
|
||||||
|
* @param {paper.Path} path2 to merge
|
||||||
|
* @return {paper.Path} merged path. Original paths 1 and 2 will be removed from the view.
|
||||||
|
*/
|
||||||
|
union (path1, path2) {
|
||||||
|
const temp = path1.unite(path2);
|
||||||
|
path1.remove();
|
||||||
|
path2.remove();
|
||||||
|
return temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
onBroadMouseUp (event, tool, options) {
|
onBroadMouseUp (event, tool, options) {
|
||||||
|
// If there was only a single click, draw a circle.
|
||||||
|
if (this.steps === 0) {
|
||||||
|
this.endCaps.length = 0;
|
||||||
|
return this.finalPath;
|
||||||
|
}
|
||||||
|
|
||||||
// If the mouse up is at the same point as the mouse drag event then we need
|
// If the mouse up is at the same point as the mouse drag event then we need
|
||||||
// the second to last point to get the right direction vector for the end cap
|
// the second to last point to get the right direction vector for the end cap
|
||||||
if (event.point.equals(this.lastPoint)) {
|
if (!event.point.equals(this.lastPoint)) {
|
||||||
this.lastPoint = this.secondLastPoint;
|
const step = event.delta.normalize(options.brushSize / 2);
|
||||||
}
|
|
||||||
// If the points are still equal, then there was no drag, so just draw a circle.
|
|
||||||
if (event.point.equals(this.lastPoint)) {
|
|
||||||
this.finalPath.remove();
|
|
||||||
this.finalPath = new paper.Path.Circle({
|
|
||||||
center: event.point,
|
|
||||||
radius: options.brushSize / 2
|
|
||||||
});
|
|
||||||
styleBlob(this.finalPath, options);
|
|
||||||
} else {
|
|
||||||
const step = (event.point.subtract(this.lastPoint)).normalize(options.brushSize / 2);
|
|
||||||
step.angle += 90;
|
step.angle += 90;
|
||||||
const handleVec = step.clone();
|
|
||||||
handleVec.length = options.brushSize / 2;
|
|
||||||
|
|
||||||
const top = event.point.add(step);
|
const top = event.point.add(step);
|
||||||
const bottom = event.point.subtract(step);
|
const bottom = event.point.subtract(step);
|
||||||
this.finalPath.add(top);
|
this.finalPath.add(top);
|
||||||
this.finalPath.insert(0, bottom);
|
this.finalPath.insert(0, bottom);
|
||||||
|
|
||||||
// Simplify before adding end cap so cap doesn't get warped
|
|
||||||
this.finalPath.simplify(1);
|
|
||||||
|
|
||||||
// Add end cap
|
|
||||||
step.angle -= 90;
|
|
||||||
this.finalPath.add(new paper.Segment(event.point.add(step), handleVec, -handleVec));
|
|
||||||
this.finalPath.closed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simplify before adding end cap so cap doesn't get warped
|
||||||
|
this.simplify(1);
|
||||||
|
const handleVec = event.delta.normalize(options.brushSize / 2);
|
||||||
|
this.finalPath.add(new paper.Segment(
|
||||||
|
event.point.add(handleVec),
|
||||||
|
handleVec.rotate(90),
|
||||||
|
handleVec.rotate(-90)
|
||||||
|
));
|
||||||
|
this.finalPath.closePath();
|
||||||
|
|
||||||
// Resolve self-crossings
|
// Resolve self-crossings
|
||||||
const newPath =
|
const newPath =
|
||||||
this.finalPath
|
this.finalPath
|
||||||
.resolveCrossings()
|
.resolveCrossings()
|
||||||
.reorient(true /* nonZero */, true /* clockwise */)
|
.reorient(true /* nonZero */, true /* clockwise */)
|
||||||
.reduce({simplify: true});
|
.reduce({simplify: true});
|
||||||
newPath.copyAttributes(this.finalPath);
|
if (newPath !== this.finalPath) {
|
||||||
newPath.fillColor = this.finalPath.fillColor;
|
newPath.copyAttributes(this.finalPath);
|
||||||
this.finalPath = newPath;
|
newPath.fillColor = this.finalPath.fillColor;
|
||||||
|
this.finalPath.remove();
|
||||||
|
this.finalPath = newPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to merge end caps
|
||||||
|
for (const cap of this.endCaps) {
|
||||||
|
const temp = this.union(this.finalPath, cap);
|
||||||
|
if (temp.area >= this.finalPath.area &&
|
||||||
|
!(temp instanceof paper.CompoundPath && !(this.finalPath instanceof paper.CompoundPath))) {
|
||||||
|
this.finalPath = temp;
|
||||||
|
} else {
|
||||||
|
// If the union of the two shapes is smaller than the original shape,
|
||||||
|
// or it caused the path to become a compound path,
|
||||||
|
// then there must have been a glitch with paperjs's unite function.
|
||||||
|
// In this case, skip merging that segment. It's not great, but it's
|
||||||
|
// better than losing the whole path for instance. (Unfortunately, this
|
||||||
|
// happens reasonably often to scribbles, and this code doesn't catch
|
||||||
|
// all of the failures.)
|
||||||
|
this.finalPath.insertAbove(temp);
|
||||||
|
temp.remove();
|
||||||
|
log.warn('Skipping a merge.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.endCaps.length = 0;
|
||||||
|
|
||||||
return this.finalPath;
|
return this.finalPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,10 @@ class SegmentBrushHelper {
|
||||||
this.finalPath = null;
|
this.finalPath = null;
|
||||||
this.firstCircle = null;
|
this.firstCircle = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSegmentMouseDown (event, tool, options) {
|
onSegmentMouseDown (event, tool, options) {
|
||||||
if (event.event.button > 0) return; // only first mouse button
|
if (event.event.button > 0) return; // only first mouse button
|
||||||
|
|
||||||
tool.minDistance = 1;
|
tool.minDistance = 2 / paper.view.zoom;
|
||||||
tool.maxDistance = options.brushSize;
|
tool.maxDistance = options.brushSize;
|
||||||
|
|
||||||
this.firstCircle = new paper.Path.Circle({
|
this.firstCircle = new paper.Path.Circle({
|
||||||
|
|
Loading…
Reference in a new issue