mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 14:42:13 -05:00
Add end caps to round out flat edges. Significantly increase the threshold for using broad brush helper now that its able to handle end caps better. Also make the threshold depend on zoom level.
This commit is contained in:
parent
f9a9aa8feb
commit
0943d8d0a1
3 changed files with 68 additions and 29 deletions
|
@ -23,7 +23,7 @@ class Blobbiness {
|
|||
// Segment brush has performance issues at low threshold, but broad brush has weird corners
|
||||
// which get more obvious the bigger it is
|
||||
static get THRESHOLD () {
|
||||
return 9;
|
||||
return 30 / paper.view.zoom;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,9 +94,7 @@ class Blobbiness {
|
|||
};
|
||||
|
||||
this.tool.onMouseDown = function (event) {
|
||||
blob.cursorPreview.remove();
|
||||
blob.cursorPreview = null;
|
||||
//blob.resizeCursorIfNeeded(event.point);
|
||||
blob.resizeCursorIfNeeded(event.point);
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
this.active = true;
|
||||
|
||||
|
@ -107,13 +105,11 @@ class Blobbiness {
|
|||
blob.brush = Blobbiness.SEGMENT;
|
||||
blob.segmentBrushHelper.onSegmentMouseDown(event, blob.tool, blob.options);
|
||||
}
|
||||
// blob.cursorPreview.bringToFront();
|
||||
// blob.cursorPreview.position = event.point;
|
||||
// paper.view.draw();
|
||||
blob.cursorPreview.bringToFront();
|
||||
blob.cursorPreview.position = event.point;
|
||||
};
|
||||
|
||||
this.tool.onMouseDrag = function (event) {
|
||||
//blob.resizeCursorIfNeeded(event.point);
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
if (blob.brush === Blobbiness.BROAD) {
|
||||
blob.broadBrushHelper.onBroadMouseDrag(event, blob.tool, blob.options);
|
||||
|
@ -123,9 +119,8 @@ class Blobbiness {
|
|||
log.warn(`Brush type does not exist: ${blob.brush}`);
|
||||
}
|
||||
|
||||
// blob.cursorPreview.bringToFront();
|
||||
// blob.cursorPreview.position = event.point;
|
||||
// paper.view.draw();
|
||||
blob.cursorPreview.bringToFront();
|
||||
blob.cursorPreview.position = event.point;
|
||||
};
|
||||
|
||||
this.tool.onMouseUp = function (event) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Broadbrush based on http://paperjs.org/tutorials/interaction/working-with-mouse-vectors/
|
||||
import paper from '@scratch/paper';
|
||||
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
|
||||
|
@ -20,11 +21,14 @@ class BroadBrushHelper {
|
|||
this.smoothed = 0;
|
||||
this.smoothingThreshold = 20;
|
||||
this.steps = 0;
|
||||
// End caps round out corners and are not merged into the path until the end.
|
||||
this.endCaps = [];
|
||||
}
|
||||
|
||||
onBroadMouseDown (event, tool, options) {
|
||||
this.steps = 0;
|
||||
this.smoothed = 0;
|
||||
tool.minDistance = Math.max(2, options.brushSize / 2);
|
||||
tool.minDistance = Math.min(5, Math.max(2, options.brushSize / 2));
|
||||
tool.maxDistance = options.brushSize;
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
|
||||
|
@ -39,11 +43,18 @@ class BroadBrushHelper {
|
|||
onBroadMouseDrag (event, tool, options) {
|
||||
this.steps++;
|
||||
const step = (event.delta).normalize(options.brushSize / 2);
|
||||
|
||||
// Add an end cap if the mouse has changed direction very quickly
|
||||
if (this.lastVec) {
|
||||
const angle = this.lastVec.getDirectedAngle(step);
|
||||
// If the angle is large, the broad brush tends to leave behind a flat edge.
|
||||
// This code fills in the flat edge with a rounded shape.
|
||||
if (Math.abs(angle) > 126) {
|
||||
// This will cause us to skip simplifying this sharp angle. Running simplify on
|
||||
// sharp angles causes the stroke to blob outwards.
|
||||
this.simplify(1);
|
||||
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(
|
||||
|
@ -58,7 +69,7 @@ class BroadBrushHelper {
|
|||
);
|
||||
rect2.fillColor = options.fillColor;
|
||||
rect2.rotate(step.angle - 90, event.point);
|
||||
this.union(circ, this.union(rect, rect2));
|
||||
this.endCaps.push(this.union(circ, this.union(rect, rect2)));
|
||||
}
|
||||
}
|
||||
this.lastVec = event.delta;
|
||||
|
@ -66,9 +77,17 @@ class BroadBrushHelper {
|
|||
|
||||
// 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.add(new paper.Segment(this.lastPoint.subtract(step)));
|
||||
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);
|
||||
|
@ -102,7 +121,7 @@ class BroadBrushHelper {
|
|||
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) - 1));
|
||||
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
|
||||
|
@ -147,20 +166,20 @@ class BroadBrushHelper {
|
|||
const temp = path1.unite(path2);
|
||||
path1.remove();
|
||||
path2.remove();
|
||||
console.log(temp);
|
||||
return temp;
|
||||
}
|
||||
|
||||
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
|
||||
// the second to last point to get the right direction vector for the end cap
|
||||
if (!event.point.equals(this.lastPoint)) {
|
||||
const step = (event.point.subtract(this.lastPoint)).normalize(options.brushSize / 2);
|
||||
const step = event.delta.normalize(options.brushSize / 2);
|
||||
step.angle += 90;
|
||||
|
||||
const top = event.point.add(step);
|
||||
|
@ -171,23 +190,48 @@ class BroadBrushHelper {
|
|||
|
||||
// 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();
|
||||
|
||||
// Add end cap
|
||||
const circ = new paper.Path.Circle(event.point, options.brushSize / 2);
|
||||
circ.fillColor = options.fillColor;
|
||||
this.finalPath = this.union(this.finalPath, circ);
|
||||
// Resolve self-crossings
|
||||
const newPath =
|
||||
this.finalPath
|
||||
.resolveCrossings()
|
||||
.reorient(true /* nonZero */, true /* clockwise */)
|
||||
.reduce({simplify: true});
|
||||
newPath.copyAttributes(this.finalPath);
|
||||
newPath.fillColor = this.finalPath.fillColor;
|
||||
this.finalPath.remove();
|
||||
this.finalPath = newPath;
|
||||
this.steps = 0;
|
||||
if (newPath !== this.finalPath) {
|
||||
newPath.copyAttributes(this.finalPath);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class SegmentBrushHelper {
|
|||
onSegmentMouseDown (event, tool, options) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
|
||||
tool.minDistance = 2 / paper.zoom;
|
||||
tool.minDistance = 2;
|
||||
tool.maxDistance = options.brushSize;
|
||||
|
||||
this.firstCircle = new paper.Path.Circle({
|
||||
|
|
Loading…
Reference in a new issue