kopia lustrzana https://github.com/backface/turtlestitch
2143 wiersze
63 KiB
JavaScript
2143 wiersze
63 KiB
JavaScript
/*
|
|
sketch.js
|
|
|
|
a vector paint editor for Snap!
|
|
inspired by the Snap bitmap paint editor and the Scratch vector editor.
|
|
|
|
written by Carles Paredes and Bernat Romagosa
|
|
Copyright (C) 2017 by Carles Paredes and Bernat Romagosa
|
|
|
|
This file is part of Snap!.
|
|
|
|
Snap! is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of
|
|
the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
prerequisites:
|
|
--------------
|
|
needs paint.js, blocks.js, gui.js, threads.js, objects.js and morphic.js
|
|
|
|
toc
|
|
---
|
|
the following list shows the order in which all constructors are
|
|
defined. Use this list to locate code in this document:
|
|
|
|
VectorShape
|
|
VectorRectangle
|
|
VectorLine
|
|
VectorEllipse
|
|
VectorPolygon
|
|
VectorSelection
|
|
VectorPaintEditorMorph
|
|
VectorPaintCanvasMorph
|
|
|
|
credits
|
|
-------
|
|
Carles Paredes wrote the first working prototype in 2015
|
|
Bernat Romagosa rewrote most of the code in 2017
|
|
|
|
revision history
|
|
-----------------
|
|
2018, June 5 (Jens):
|
|
- fixed initial rotation center for an existing costume
|
|
- fixed initial rendering, so costumes can be re-opened after saving
|
|
2018, June 20 (Jens):
|
|
- select primary color with right-click (in addition to shift-click)
|
|
2020, April 15 (Jens):
|
|
- migrated to new Morphic2 architecture
|
|
2021, March 17 (Jens):
|
|
- moved stage dimension handling to scenes
|
|
*/
|
|
|
|
/*global Point, Object, Rectangle, AlignmentMorph, Morph, XML_Element, localize,
|
|
PaintColorPickerMorph, Color, SliderMorph, InputFieldMorph, ToggleMorph, isNil,
|
|
TextMorph, Image, newCanvas, PaintEditorMorph, Costume, nop, PaintCanvasMorph,
|
|
StringMorph, detect, modules*/
|
|
|
|
/*jshint esversion: 6*/
|
|
|
|
modules.sketch = '2021-November-03';
|
|
|
|
// Declarations
|
|
|
|
var VectorShape;
|
|
var VectorRectangle;
|
|
var VectorLine;
|
|
var VectorEllipse;
|
|
var VectorPolygon;
|
|
var VectorSelection;
|
|
var VectorPaintEditorMorph;
|
|
var VectorPaintCanvasMorph;
|
|
|
|
// VectorShape
|
|
|
|
VectorShape.prototype = {};
|
|
VectorShape.prototype.constructor = VectorShape;
|
|
VectorShape.uber = Object.prototype;
|
|
|
|
function VectorShape (borderWidth, borderColor, fillColor) {
|
|
this.init(borderWidth, borderColor, fillColor);
|
|
}
|
|
|
|
VectorShape.prototype.init = function (borderWidth, borderColor, fillColor) {
|
|
this.borderWidth = (borderColor && borderColor.a) ? borderWidth : 0;
|
|
this.borderColor = borderColor || new Color(0,0,0,0);
|
|
this.fillColor = fillColor || new Color(0,0,0,0);
|
|
this.image = newCanvas();
|
|
this.isPolygon = false;
|
|
this.isSelection = false;
|
|
this.isCrosshair = false;
|
|
this.origin = new Point();
|
|
this.destination = new Point();
|
|
};
|
|
|
|
VectorShape.prototype.toString = function () {
|
|
return 'a ' +
|
|
(this.constructor.name ||
|
|
this.constructor.toString().split(' ')[1].split('(')[0]);
|
|
};
|
|
|
|
VectorShape.prototype.asSVG = function (tagName) {
|
|
var svg = new XML_Element(tagName);
|
|
|
|
if (this.borderColor && this.borderColor.a) {
|
|
// if border is not transparent
|
|
svg.attributes.stroke = this.borderColor.toRGBstring();
|
|
svg.attributes['stroke-linejoin'] = 'miter';
|
|
svg.attributes['stroke-width'] = this.borderWidth;
|
|
} else {
|
|
svg.attributes.stroke = 'none';
|
|
}
|
|
|
|
if (this.fillColor && this.fillColor.a) {
|
|
// if fill color is not transparent
|
|
svg.attributes.fill = this.fillColor.toRGBstring();
|
|
} else {
|
|
svg.attributes.fill = 'none';
|
|
}
|
|
|
|
svg.attributes.prototype = this.constructor.name;
|
|
|
|
return svg;
|
|
};
|
|
|
|
VectorShape.prototype.imageURL = function () {
|
|
var svg = new XML_Element('svg'),
|
|
bounds = this.bounds();
|
|
|
|
svg.attributes.xmlns = 'http://www.w3.org/2000/svg';
|
|
svg.attributes.version = '1.1';
|
|
svg.attributes.preserveAspectRatio = 'xMinYMin meet';
|
|
svg.attributes.viewBox =
|
|
bounds.left() + ' ' + bounds.top() + ' ' +
|
|
(bounds.right() - bounds.left()) + ' ' +
|
|
(bounds.bottom() - bounds.top());
|
|
svg.attributes.width = (bounds.right() - bounds.left());
|
|
svg.attributes.height = (bounds.bottom() - bounds.top());
|
|
|
|
svg.children = [ this.asSVG() ];
|
|
|
|
return 'data:image/svg+xml;base64,' + svg;
|
|
};
|
|
|
|
VectorShape.prototype.copy = function (newShape) {
|
|
var shape =
|
|
newShape ||
|
|
new VectorShape(
|
|
this.borderWidth,
|
|
this.borderColor,
|
|
this.fillColor
|
|
);
|
|
shape.image.width = this.image.width;
|
|
shape.image.height = this.image.height;
|
|
shape.image.getContext('2d').drawImage(this.image,0,0);
|
|
return shape;
|
|
};
|
|
|
|
VectorShape.prototype.bounds = function() {
|
|
return new Rectangle(
|
|
Math.min(this.origin.x, this.destination.x) - (this.borderWidth / 2),
|
|
Math.min(this.origin.y, this.destination.y) - (this.borderWidth / 2),
|
|
Math.max(this.origin.x, this.destination.x) + (this.borderWidth / 2),
|
|
Math.max(this.origin.y, this.destination.y) + (this.borderWidth / 2)
|
|
);
|
|
};
|
|
|
|
VectorShape.prototype.containsPoint = function (aPoint) {
|
|
return this.bounds().containsPoint(aPoint);
|
|
};
|
|
|
|
VectorShape.prototype.update = function (newPoint, constrain) {
|
|
this.destination = constrain ? this.constraintPoint(newPoint) : newPoint;
|
|
};
|
|
|
|
VectorShape.prototype.constraintPoint = function (aPoint) {
|
|
var newPoint = aPoint,
|
|
delta = newPoint.subtract(this.origin),
|
|
constraintPos = new Point(
|
|
Math.max(
|
|
Math.abs(delta.x),
|
|
Math.abs(delta.y)) * (delta.x / Math.abs(delta.x)
|
|
),
|
|
Math.max(
|
|
Math.abs(delta.x),
|
|
Math.abs(delta.y)) * (delta.y / Math.abs(delta.y)
|
|
)
|
|
);
|
|
|
|
newPoint = this.origin.add(constraintPos);
|
|
return newPoint;
|
|
};
|
|
|
|
VectorShape.prototype.setColor = function (color, isSecondary) {
|
|
if (isSecondary) {
|
|
this.borderColor = color;
|
|
} else {
|
|
this.fillColor = color;
|
|
}
|
|
};
|
|
|
|
VectorShape.prototype.setBorderWidth = function (width) {
|
|
if (this.borderColor && this.borderColor.a) {
|
|
this.borderWidth = width;
|
|
}
|
|
};
|
|
|
|
VectorShape.prototype.moveBy = function (delta) {
|
|
this.origin = this.origin.add(delta);
|
|
this.destination = this.destination.add(delta);
|
|
};
|
|
|
|
VectorShape.prototype.resizeBy = function (delta, origin) {
|
|
this.origin = this.origin.subtract(origin).multiplyBy(delta).add(origin);
|
|
this.destination = this.destination.subtract(origin).multiplyBy(delta).add(
|
|
origin
|
|
);
|
|
};
|
|
|
|
// Generic drawOn method that stamps the shape SVG into its image canvas
|
|
// with position relative to aCanvasMorph's and asks it to redraw itself
|
|
VectorShape.prototype.drawOn = function (aCanvasMorph) {
|
|
var myself = this,
|
|
origin = this.bounds().origin.subtract(aCanvasMorph.position()),
|
|
img = new Image();
|
|
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
|
|
img.onload = function () {
|
|
myself.image.getContext('2d').drawImage(img, origin.x, origin.y);
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
img.src = this.imageURL();
|
|
};
|
|
|
|
// VectorRectangle
|
|
|
|
VectorRectangle.prototype = new VectorShape();
|
|
VectorRectangle.prototype.constructor = VectorRectangle;
|
|
VectorRectangle.uber = VectorShape.prototype;
|
|
|
|
function VectorRectangle (
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin,
|
|
destination
|
|
) {
|
|
VectorRectangle.uber.init.call(this, borderWidth, borderColor, fillColor);
|
|
this.init(origin, destination);
|
|
}
|
|
|
|
VectorRectangle.prototype.init = function (origin, destination) {
|
|
this.origin = origin;
|
|
this.destination = destination;
|
|
};
|
|
|
|
VectorRectangle.fromSVG = function (svg) {
|
|
var attributes = svg.attributes;
|
|
|
|
return new VectorRectangle(
|
|
parseInt(attributes['stroke-width']), // borderWidth
|
|
attributes.stroke === 'none' ? null :
|
|
Color.fromString(attributes.stroke), // borderColor
|
|
attributes.fill === 'none' ? null :
|
|
Color.fromString(attributes.fill), // fillColor
|
|
new Point( // origin
|
|
parseInt(attributes.x), parseInt(attributes.y)
|
|
),
|
|
new Point( // destination
|
|
parseInt(attributes.x) + parseInt(attributes.width),
|
|
parseInt(attributes.y) + parseInt(attributes.height)
|
|
)
|
|
);
|
|
};
|
|
|
|
VectorRectangle.prototype.copy = function () {
|
|
var newRectangle = new VectorRectangle(
|
|
this.borderWidth,
|
|
this.borderColor,
|
|
this.fillColor,
|
|
this.origin.copy(),
|
|
this.destination.copy()
|
|
);
|
|
return VectorRectangle.uber.copy.call(this, newRectangle);
|
|
};
|
|
|
|
VectorRectangle.prototype.toString = function () {
|
|
return VectorRectangle.uber.toString.call(this) + this.bounds().toString();
|
|
};
|
|
|
|
VectorRectangle.prototype.width = function () {
|
|
return Math.abs(this.origin.x - this.destination.x);
|
|
};
|
|
|
|
VectorRectangle.prototype.height = function () {
|
|
return Math.abs(this.origin.y - this.destination.y);
|
|
};
|
|
|
|
VectorRectangle.prototype.x = function () {
|
|
return Math.min(this.origin.x, this.destination.x);
|
|
};
|
|
|
|
VectorRectangle.prototype.y = function () {
|
|
return Math.min(this.origin.y, this.destination.y);
|
|
};
|
|
|
|
VectorRectangle.prototype.asSVG = function () {
|
|
var svg = VectorRectangle.uber.asSVG.call(this, 'rect');
|
|
svg.attributes.width = this.width();
|
|
svg.attributes.height = this.height();
|
|
svg.attributes.x = this.x();
|
|
svg.attributes.y = this.y();
|
|
return svg;
|
|
};
|
|
|
|
VectorRectangle.prototype.drawOn = function (aCanvasMorph) {
|
|
var context,
|
|
canvasPosition = aCanvasMorph.position();
|
|
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
|
|
context = this.image.getContext('2d');
|
|
|
|
context.beginPath();
|
|
context.rect(
|
|
this.x() - canvasPosition.x,
|
|
this.y() - canvasPosition.y,
|
|
this.width(),
|
|
this.height()
|
|
);
|
|
|
|
if (this.fillColor.a > 0) {
|
|
context.fillStyle = this.fillColor.toRGBstring();
|
|
context.fill();
|
|
}
|
|
|
|
if (this.borderColor.a > 0) {
|
|
context.lineWidth = this.borderWidth;
|
|
context.strokeStyle = this.borderColor.toRGBstring();
|
|
context.stroke();
|
|
}
|
|
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
// VectorLine
|
|
|
|
VectorLine.prototype = new VectorShape();
|
|
VectorLine.prototype.constructor = VectorLine;
|
|
VectorLine.uber = VectorShape.prototype;
|
|
|
|
function VectorLine (borderWidth, borderColor, origin, destination) {
|
|
VectorLine.uber.init.call(this, borderWidth, borderColor);
|
|
this.init(origin, destination);
|
|
}
|
|
|
|
VectorLine.prototype.init = function(origin, destination) {
|
|
this.origin = origin;
|
|
this.destination = destination;
|
|
};
|
|
|
|
VectorLine.fromSVG = function (svg) {
|
|
var attributes = svg.attributes;
|
|
return new VectorLine(
|
|
parseInt(attributes['stroke-width']), // borderWidth
|
|
Color.fromString(attributes.stroke), // borderColor
|
|
new Point(parseInt(attributes.x1), parseInt(attributes.y1)), // origin
|
|
new Point(parseInt(attributes.x2), parseInt(attributes.y2)) // dest.
|
|
);
|
|
};
|
|
|
|
VectorLine.prototype.copy = function () {
|
|
var newLine = new VectorLine(
|
|
this.borderWidth,
|
|
this.borderColor.copy(),
|
|
this.origin.copy(),
|
|
this.destination.copy()
|
|
);
|
|
return VectorLine.uber.copy.call(this, newLine);
|
|
};
|
|
|
|
VectorLine.prototype.containsPoint = function (aPoint) {
|
|
var lineLength = this.origin.distanceTo(this.destination),
|
|
distancesSum = aPoint.distanceTo(this.origin) +
|
|
aPoint.distanceTo(this.destination);
|
|
|
|
return Math.abs(lineLength - distancesSum) <=
|
|
Math.sqrt(this.borderWidth / 2) / 2;
|
|
};
|
|
|
|
VectorLine.prototype.constraintPoint = function (aPoint) {
|
|
var angle,
|
|
newPoint = aPoint;
|
|
|
|
angle = newPoint.subtract(this.origin).abs().degrees();
|
|
if (angle < 22.5) {
|
|
// horizontal line
|
|
newPoint.y = this.origin.y;
|
|
} else if (angle > 67.5) {
|
|
// vertical line
|
|
newPoint.x = this.origin.x;
|
|
} else {
|
|
// line at 45º
|
|
newPoint = VectorLine.uber.constraintPoint.call(this, aPoint);
|
|
}
|
|
|
|
return newPoint;
|
|
};
|
|
|
|
VectorLine.prototype.setColor = function (color, isSecondary) {
|
|
VectorLine.uber.setColor.call(this, color, !isSecondary);
|
|
};
|
|
|
|
VectorLine.prototype.toString = function () {
|
|
return VectorLine.uber.toString.call(this) + this.bounds().toString();
|
|
};
|
|
|
|
VectorLine.prototype.asSVG = function() {
|
|
var svg = VectorLine.uber.asSVG.call(this, 'line');
|
|
svg.attributes.x1 = this.origin.x;
|
|
svg.attributes.y1 = this.origin.y;
|
|
svg.attributes.x2 = this.destination.x;
|
|
svg.attributes.y2 = this.destination.y;
|
|
return svg;
|
|
};
|
|
|
|
VectorLine.prototype.drawOn = function (aCanvasMorph) {
|
|
var context,
|
|
origin = this.origin.subtract(aCanvasMorph.position()),
|
|
destination = this.destination.subtract(aCanvasMorph.position());
|
|
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
|
|
context = this.image.getContext('2d');
|
|
|
|
context.beginPath();
|
|
|
|
if (this.borderColor.a > 0) {
|
|
context.lineWidth = this.borderWidth;
|
|
context.strokeStyle = this.borderColor.toRGBstring();
|
|
context.moveTo(origin.x, origin.y);
|
|
context.lineTo(destination.x, destination.y);
|
|
context.stroke();
|
|
}
|
|
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
// VectorEllipse
|
|
|
|
VectorEllipse.prototype = new VectorShape();
|
|
VectorEllipse.prototype.constructor = VectorEllipse;
|
|
VectorEllipse.uber = VectorShape.prototype;
|
|
|
|
function VectorEllipse (
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin,
|
|
destination)
|
|
{
|
|
VectorEllipse.uber.init.call(this, borderWidth, borderColor, fillColor);
|
|
this.init(origin, destination);
|
|
}
|
|
|
|
VectorEllipse.prototype.init = function (origin, destination) {
|
|
this.origin = origin;
|
|
this.destination = destination;
|
|
};
|
|
|
|
VectorEllipse.fromSVG = function (svg) {
|
|
var attributes = svg.attributes;
|
|
|
|
return new VectorEllipse(
|
|
parseInt(attributes['stroke-width']), // borderWidth
|
|
attributes.stroke === 'none' ? null :
|
|
Color.fromString(attributes.stroke), // borderColor
|
|
attributes.fill === 'none' ? null :
|
|
Color.fromString(attributes.fill), // fillColor
|
|
new Point(parseInt(attributes.cx), parseInt(attributes.cy)), // origin
|
|
new Point(
|
|
parseInt(attributes.cx) + parseInt(attributes.rx),
|
|
parseInt(attributes.cy) + parseInt(attributes.ry)) // destination
|
|
);
|
|
};
|
|
|
|
VectorEllipse.prototype.copy = function () {
|
|
var newEllipse = new VectorEllipse(
|
|
this.borderWidth,
|
|
this.borderColor,
|
|
this.fillColor,
|
|
this.origin.copy(),
|
|
this.destination.copy()
|
|
);
|
|
return VectorEllipse.uber.copy.call(this, newEllipse);
|
|
};
|
|
|
|
VectorEllipse.prototype.hRadius = function () {
|
|
return Math.abs(this.destination.x - this.origin.x);
|
|
};
|
|
|
|
VectorEllipse.prototype.vRadius = function () {
|
|
return Math.abs(this.destination.y - this.origin.y);
|
|
};
|
|
|
|
VectorEllipse.prototype.toString = function () {
|
|
return VectorEllipse.uber.toString.call(this) +
|
|
' center: ' + this.origin.toString() +
|
|
' radii: ' + this.hRadius().toString() + ',' +
|
|
this.vRadius().toString();
|
|
};
|
|
|
|
VectorEllipse.prototype.bounds = function () {
|
|
var hRadius = this.hRadius(),
|
|
vRadius = this.vRadius();
|
|
|
|
return new Rectangle(
|
|
this.origin.x - hRadius - (this.borderWidth / 2),
|
|
this.origin.y - vRadius - (this.borderWidth / 2),
|
|
this.origin.x + hRadius + (this.borderWidth / 2),
|
|
this.origin.y + vRadius + (this.borderWidth / 2)
|
|
);
|
|
};
|
|
|
|
VectorEllipse.prototype.containsPoint = function (aPoint) {
|
|
return (
|
|
Math.pow(aPoint.x - this.origin.x, 2) /
|
|
Math.pow(this.hRadius() + this.borderWidth / 2, 2)
|
|
+
|
|
Math.pow(aPoint.y - this.origin.y, 2) /
|
|
Math.pow(this.vRadius() + this.borderWidth / 2, 2)
|
|
) < 1;
|
|
};
|
|
|
|
VectorEllipse.prototype.asSVG = function () {
|
|
var svg = VectorEllipse.uber.asSVG.call(this, 'ellipse');
|
|
svg.attributes.cx = this.origin.x;
|
|
svg.attributes.cy = this.origin.y;
|
|
svg.attributes.rx = this.hRadius();
|
|
svg.attributes.ry = this.vRadius();
|
|
return svg;
|
|
};
|
|
|
|
VectorEllipse.prototype.drawOn = function (aCanvasMorph) {
|
|
var context,
|
|
canvasPosition = aCanvasMorph.position();
|
|
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
context = this.image.getContext('2d');
|
|
context.beginPath();
|
|
context.ellipse(
|
|
this.origin.x - canvasPosition.x,
|
|
this.origin.y - canvasPosition.y,
|
|
this.hRadius(),
|
|
this.vRadius(),
|
|
0,
|
|
0,
|
|
2 * Math.PI,
|
|
true);
|
|
|
|
if (this.fillColor && this.fillColor.a > 0) {
|
|
context.fillStyle = this.fillColor.toRGBstring();
|
|
context.fill();
|
|
}
|
|
|
|
if (this.borderColor.a > 0) {
|
|
context.lineWidth = this.borderWidth;
|
|
context.strokeStyle = this.borderColor.toRGBstring();
|
|
context.stroke();
|
|
}
|
|
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
|
|
// VectorPolygon
|
|
|
|
VectorPolygon.prototype = new VectorShape();
|
|
VectorPolygon.prototype.constructor = VectorPolygon;
|
|
VectorPolygon.uber = VectorShape.prototype;
|
|
|
|
function VectorPolygon (
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
points,
|
|
isClosed,
|
|
isFreeHand
|
|
) {
|
|
VectorPolygon.uber.init.call(this, borderWidth, borderColor, fillColor);
|
|
this.init(points, isClosed, isFreeHand);
|
|
}
|
|
|
|
VectorPolygon.prototype.init = function (points, isClosed, isFreeHand) {
|
|
this.points = points || [ ];
|
|
this.isClosed = isClosed;
|
|
this.isFreeHand = isFreeHand;
|
|
this.isPolygon = true;
|
|
};
|
|
|
|
VectorPolygon.fromSVG = function (svg) {
|
|
var attributes = svg.attributes,
|
|
points = attributes.d.slice(1).split(/L */).map(
|
|
function (pointString) {
|
|
var pointArray = pointString.split(' ');
|
|
return new Point(
|
|
parseInt(pointArray[0]),
|
|
parseInt(pointArray[1]));
|
|
}
|
|
);
|
|
|
|
return new VectorPolygon(
|
|
parseInt(attributes['stroke-width']), // borderWidth
|
|
attributes.stroke === 'none' ? null :
|
|
Color.fromString(attributes.stroke), // borderColor
|
|
attributes.fill === 'none' ? null :
|
|
Color.fromString(attributes.fill), // fillColor
|
|
points, // points
|
|
points[0].eq(points[points.length - 1]), // isClosed
|
|
false // isFreeHand, does only matter when drawing it
|
|
);
|
|
};
|
|
|
|
VectorPolygon.prototype.copy = function () {
|
|
var newPolygon = new VectorPolygon(
|
|
this.borderWidth,
|
|
this.borderColor,
|
|
this.fillColor,
|
|
this.points.map(function (point) { return point.copy(); }),
|
|
this.isClosed,
|
|
this.isFreeHand
|
|
);
|
|
return VectorPolygon.uber.copy.call(this, newPolygon);
|
|
};
|
|
|
|
VectorPolygon.prototype.toString = function () {
|
|
return VectorPolygon.uber.toString.call(this) + this.points;
|
|
};
|
|
|
|
VectorPolygon.prototype.bounds = function () {
|
|
var left = this.points[0].x,
|
|
top = this.points[0].y,
|
|
right = this.points[this.points.length - 1].x,
|
|
bottom = this.points[this.points.length - 1].y;
|
|
|
|
this.points.forEach(function (point) {
|
|
left = Math.min(left, point.x);
|
|
top = Math.min(top, point.y);
|
|
right = Math.max(right, point.x);
|
|
bottom = Math.max(bottom, point.y);
|
|
});
|
|
|
|
return new Rectangle(
|
|
left - (this.borderWidth / 2),
|
|
top - (this.borderWidth / 2),
|
|
right + (this.borderWidth / 2),
|
|
bottom + (this.borderWidth / 2)
|
|
);
|
|
};
|
|
|
|
VectorPolygon.prototype.containsPoint = function (aPoint) {
|
|
var myself = this,
|
|
pointCount = this.points.length,
|
|
inside = false,
|
|
i, j;
|
|
|
|
for (i = 1; i < pointCount; i += 1) {
|
|
if (pointIsBetween(this.points[i - 1], this.points[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (this.isClosed) {
|
|
for (i = 0, j = pointCount - 1; i < pointCount; i += 1) {
|
|
if (
|
|
(this.points[i].y > aPoint.y) !==
|
|
(this.points[j].y > aPoint.y) &&
|
|
aPoint.x <
|
|
(this.points[j].x - this.points[i].x) *
|
|
(aPoint.y - this.points[i].y) /
|
|
(this.points[j].y - this.points[i].y) + this.points[i].x
|
|
) {
|
|
inside = !inside;
|
|
}
|
|
j = i;
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
function pointIsBetween (a, b) {
|
|
return Math.abs(a.distanceTo(b) -
|
|
(aPoint.distanceTo(a) + aPoint.distanceTo(b))) <=
|
|
Math.sqrt(myself.borderWidth / 2) / 2;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
VectorPolygon.prototype.update = function (newPoint, constrain) {
|
|
if (this.isFreeHand || this.points.length === 1) {
|
|
this.points.push(newPoint);
|
|
} else if (!this.isFreeHand) {
|
|
if (constrain) {
|
|
// we reuse origin to store the previous point and perform the
|
|
// constraint calculations as if we were drawing a single line
|
|
this.origin = this.points[this.points.length - 2];
|
|
newPoint = VectorLine.prototype.constraintPoint.call(
|
|
this,
|
|
newPoint
|
|
);
|
|
}
|
|
this.points[this.points.length - 1] = newPoint;
|
|
}
|
|
};
|
|
|
|
VectorPolygon.prototype.setColor = function (color, isSecondary) {
|
|
VectorPolygon.uber.setColor.call(
|
|
this,
|
|
color,
|
|
!this.isClosed || isSecondary
|
|
);
|
|
};
|
|
|
|
VectorPolygon.prototype.moveBy = function (delta) {
|
|
this.points.forEach(function (eachPoint) {
|
|
eachPoint.x += delta.x;
|
|
eachPoint.y += delta.y;
|
|
});
|
|
};
|
|
|
|
VectorPolygon.prototype.resizeBy = function (delta, origin) {
|
|
this.points = this.points.map(function (point) {
|
|
return point.subtract(origin).multiplyBy(delta).add(origin);
|
|
});
|
|
};
|
|
|
|
VectorPolygon.prototype.close = function () {
|
|
if (this.isClosed) {
|
|
this.points.push(this.points[0].copy());
|
|
}
|
|
};
|
|
|
|
VectorPolygon.prototype.asSVG = function () {
|
|
var svg = VectorPolygon.uber.asSVG.call(this, 'path');
|
|
|
|
svg.attributes['stroke-linejoin'] = 'round';
|
|
svg.attributes['stroke-linecap'] = 'round';
|
|
|
|
// M stands for MoveTo and defines the starting point
|
|
svg.attributes.d = 'M' + this.points[0].x + ' ' + this.points[0].y;
|
|
|
|
// L stands for LineTo and defines the rest of the points
|
|
this.points.slice(1).forEach(function (point) {
|
|
svg.attributes.d += ' L ' + point.x + ' ' + point.y;
|
|
});
|
|
|
|
return svg;
|
|
};
|
|
|
|
VectorPolygon.prototype.drawOn = function (aCanvasMorph) {
|
|
var context,
|
|
points =
|
|
this.points.map(
|
|
function (eachPoint) {
|
|
return eachPoint.subtract(aCanvasMorph.position());
|
|
}
|
|
);
|
|
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
context = this.image.getContext('2d');
|
|
|
|
context.lineCap = 'round';
|
|
context.lineJoin = 'round';
|
|
|
|
context.beginPath();
|
|
context.moveTo(points[0].x, points[0].y);
|
|
|
|
points.slice(1).forEach(function (point) {
|
|
context.lineTo(point.x, point.y);
|
|
});
|
|
|
|
if (this.fillColor && this.fillColor.a > 0) {
|
|
context.fillStyle = this.fillColor.toRGBstring();
|
|
context.fill();
|
|
}
|
|
|
|
if (this.borderColor.a > 0) {
|
|
context.lineWidth = this.borderWidth;
|
|
context.strokeStyle = this.borderColor.toRGBstring();
|
|
context.stroke();
|
|
} else if (this.points.length === 2) {
|
|
// This is a polygon in construction, we should at least draw
|
|
// a thin line between its first two points
|
|
context.lineWidth = 1;
|
|
context.strokeStyle = this.fillColor.toRGBstring();
|
|
context.stroke();
|
|
}
|
|
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
|
|
// VectorSelection
|
|
|
|
VectorSelection.prototype = new VectorRectangle();
|
|
VectorSelection.prototype.constructor = VectorSelection;
|
|
VectorSelection.uber = VectorRectangle.prototype;
|
|
|
|
function VectorSelection (origin, destination) {
|
|
VectorRectangle.uber.init.call(
|
|
this,
|
|
1, // borderWidth
|
|
new Color(0, 0, 0, 255), // borderColor
|
|
null // fillColor
|
|
);
|
|
this.init(origin, destination);
|
|
}
|
|
|
|
VectorSelection.prototype.init = function (origin, destination) {
|
|
VectorSelection.uber.init.call(this, origin, destination);
|
|
this.isSelection = true;
|
|
this.threshold = 5;
|
|
};
|
|
|
|
VectorSelection.prototype.corners = function () {
|
|
var bounds = this.bounds();
|
|
return [
|
|
bounds.topLeft(),
|
|
bounds.topRight(),
|
|
bounds.bottomLeft(),
|
|
bounds.bottomRight()
|
|
];
|
|
};
|
|
|
|
VectorSelection.prototype.cornerAt = function (aPoint) {
|
|
var threshold = this.threshold;
|
|
|
|
return this.corners().find(function(corner) {
|
|
return aPoint.distanceTo(corner) <= threshold;
|
|
});
|
|
};
|
|
|
|
VectorSelection.prototype.cornerOppositeTo = function (aPoint) {
|
|
return this.corners().reduce(function(a, b) {
|
|
return (aPoint.distanceTo(a) > aPoint.distanceTo(b)) ? a : b;
|
|
});
|
|
};
|
|
|
|
VectorSelection.prototype.drawOn = function (aCanvasMorph) {
|
|
var context,
|
|
bounds = this.bounds(),
|
|
canvasPosition = aCanvasMorph.position(),
|
|
origin = bounds.origin.subtract(canvasPosition),
|
|
circleRadius = this.threshold;
|
|
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
|
|
context = this.image.getContext('2d');
|
|
|
|
context.rect(origin.x, origin.y, this.width(), this.height());
|
|
context.setLineDash([5]);
|
|
context.stroke();
|
|
|
|
context.setLineDash([]);
|
|
|
|
function drawCircle (x, y) {
|
|
context.beginPath();
|
|
context.arc(
|
|
x - canvasPosition.x,
|
|
y - canvasPosition.y,
|
|
circleRadius,
|
|
0,
|
|
2 * Math.PI
|
|
);
|
|
context.stroke();
|
|
}
|
|
|
|
drawCircle(bounds.left(), bounds.top());
|
|
drawCircle(bounds.left(), bounds.bottom());
|
|
drawCircle(bounds.right(), bounds.top());
|
|
drawCircle(bounds.right(), bounds.bottom());
|
|
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
// Crosshair
|
|
// For convenience, we'll inherit from VectorShape
|
|
|
|
Crosshair.prototype = VectorShape;
|
|
Crosshair.prototype.constructor = Crosshair;
|
|
Crosshair.uber = VectorShape.prototype;
|
|
|
|
function Crosshair (center, paper) {
|
|
this.init(center, paper);
|
|
}
|
|
|
|
Crosshair.prototype.init = function (center, paper) {
|
|
this.center = center;
|
|
this.paper = paper;
|
|
this.image = newCanvas();
|
|
this.isCrosshair = true;
|
|
};
|
|
|
|
Crosshair.prototype.update = function (newPosition) {
|
|
this.center = newPosition.subtract(this.paper.position());
|
|
};
|
|
|
|
Crosshair.prototype.moveBy = function (delta) {
|
|
this.center = this.center.add(delta);
|
|
};
|
|
|
|
Crosshair.prototype.drawOn = function (aCanvasMorph) {
|
|
this.image = newCanvas(aCanvasMorph.extent());
|
|
aCanvasMorph.rotationCenter = this.center.copy();
|
|
aCanvasMorph.drawcrosshair(this.image.getContext('2d'));
|
|
aCanvasMorph.redraw = true;
|
|
};
|
|
|
|
/////////// VectorPaintEditorMorph //////////////////////////
|
|
|
|
VectorPaintEditorMorph.prototype = new PaintEditorMorph();
|
|
VectorPaintEditorMorph.prototype.constructor = VectorPaintEditorMorph;
|
|
VectorPaintEditorMorph.uber = PaintEditorMorph.prototype;
|
|
|
|
function VectorPaintEditorMorph() {
|
|
this.init();
|
|
}
|
|
|
|
VectorPaintEditorMorph.prototype.init = function () {
|
|
// additional properties:
|
|
this.paper = null; // paint canvas
|
|
this.shapes = [];
|
|
this.selection = []; // currently selected objects
|
|
this.selecting = false;
|
|
this.originalSelection = null; // see VectorPaintEditorMorph >> dragSelection
|
|
this.moving = false;
|
|
this.resizing = false;
|
|
this.lastDragPosition = null;
|
|
this.history = []; // shapes history, for undo purposes
|
|
this.clipboard = []; // copied objects ready to be pasted
|
|
this.currentShape = null; // object being currently edited
|
|
|
|
VectorPaintEditorMorph.uber.init.call(this);
|
|
|
|
this.labelString = 'Vector Paint Editor';
|
|
this.createLabel();
|
|
this.fixLayout();
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.buildEdits = function () {
|
|
var myself = this;
|
|
|
|
this.edits.add(
|
|
this.pushButton(
|
|
'undo',
|
|
function () {
|
|
myself.undo();
|
|
}
|
|
)
|
|
);
|
|
|
|
this.edits.add(
|
|
this.pushButton(
|
|
'clear',
|
|
function () {
|
|
myself.paper.clearCanvas();
|
|
}
|
|
)
|
|
);
|
|
|
|
this.edits.add(
|
|
this.pushButton(
|
|
'Bitmap',
|
|
function () {
|
|
if (myself.shapes.length > 0) {
|
|
myself.ide.confirm(
|
|
'This will convert your vector objects into\n' +
|
|
'bitmaps, and you will not be able to convert\n' +
|
|
'them back into vector drawings.\n' +
|
|
'Are you sure you want to continue?',
|
|
'Convert to bitmap?',
|
|
() => {
|
|
setTimeout(() => {myself.convertToBitmap(); });
|
|
}
|
|
);
|
|
} else {
|
|
myself.convertToBitmap();
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
this.edits.fixLayout();
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.convertToBitmap = function () {
|
|
var canvas = newCanvas(this.ide.stage.dimensions),
|
|
myself = this;
|
|
|
|
this.object = new Costume();
|
|
|
|
this.shapes.forEach(function(each) {
|
|
canvas.getContext('2d').drawImage(each.image, 0, 0);
|
|
});
|
|
|
|
this.object.rotationCenter = this.paper.rotationCenter.copy();
|
|
this.object.contents = canvas;
|
|
this.object.edit(
|
|
this.world(),
|
|
this.ide,
|
|
false,
|
|
null,
|
|
() => {
|
|
myself.ide.currentSprite.shadowAttribute('costumes');
|
|
myself.ide.currentSprite.addCostume(myself.object);
|
|
myself.ide.spriteEditor.updateList();
|
|
if (myself.ide) {
|
|
myself.ide.currentSprite.wearCostume(myself.object);
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.buildScaleBox = function () {
|
|
var myself = this;
|
|
['Top', 'Bottom', 'Up', 'Down'].forEach(function (label) {
|
|
myself.scaleBox.add(
|
|
myself.pushButton(
|
|
label,
|
|
function () {
|
|
myself.changeSelectionLayer(label.toLowerCase());
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
this.scaleBox.fixLayout();
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.openIn = function (
|
|
world,
|
|
oldim,
|
|
oldrc,
|
|
callback,
|
|
anIDE,
|
|
shapes
|
|
) {
|
|
var myself = this,
|
|
isEmpty = isNil(shapes) || shapes.length === 0;
|
|
|
|
VectorPaintEditorMorph.uber.openIn.call(
|
|
this,
|
|
world,
|
|
null,
|
|
oldrc,
|
|
callback,
|
|
anIDE
|
|
);
|
|
this.ide = anIDE;
|
|
this.paper.drawNew();
|
|
this.paper.changed();
|
|
|
|
// make sure shapes are initialized and can be rendered
|
|
shapes.forEach(function (shape) {
|
|
shape.drawOn(myself.paper);
|
|
});
|
|
// copy the shapes for editing and re-render the copies
|
|
this.shapes = shapes.map(function (eachShape) {
|
|
return eachShape.copy();
|
|
});
|
|
this.shapes.forEach(function (shape) {
|
|
shape.drawOn(myself.paper);
|
|
});
|
|
// init the rotation center, if any
|
|
if (oldrc && !isEmpty) {
|
|
this.paper.automaticCrosshairs = false;
|
|
this.paper.rotationCenter = this.getBounds(this.shapes).origin.subtract(
|
|
this.paper.bounds.origin
|
|
).add(oldrc);
|
|
} else {
|
|
this.paper.automaticCrosshairs = true;
|
|
}
|
|
|
|
this.updateHistory();
|
|
|
|
this.processKeyUp = function () {
|
|
myself.shift = false;
|
|
myself.ctrl = false;
|
|
myself.propertiesControls.constrain.refresh();
|
|
};
|
|
|
|
this.processKeyDown = function (event) {
|
|
|
|
var pos;
|
|
|
|
myself.shift = myself.world().currentKey === 16;
|
|
myself.ctrl = event.ctrlKey;
|
|
|
|
switch (myself.world().currentKey) {
|
|
/* Del and backspace keys */
|
|
case 46:
|
|
case 8:
|
|
myself.sortSelection();
|
|
myself.selection.slice().reverse().forEach(function (shape) {
|
|
myself.shapes.splice(myself.shapes.indexOf(shape), 1);
|
|
});
|
|
myself.clearSelection();
|
|
myself.updateHistory();
|
|
break;
|
|
/* Enter key */
|
|
case 13:
|
|
if (myself.currentShape && myself.currentShape.isPolygon) {
|
|
myself.currentShape.close();
|
|
myself.currentShape.drawOn(myself.paper);
|
|
myself.shapes.push(myself.currentShape);
|
|
myself.currentShape = null;
|
|
myself.updateHistory();
|
|
}
|
|
break;
|
|
/* Page Up key */
|
|
case 33:
|
|
myself.changeSelectionLayer('up');
|
|
break;
|
|
/* Page Down key */
|
|
case 34:
|
|
myself.changeSelectionLayer('down');
|
|
break;
|
|
/* End key */
|
|
case 35:
|
|
myself.changeSelectionLayer('bottom');
|
|
break;
|
|
|
|
/* Home key */
|
|
case 36:
|
|
myself.changeSelectionLayer('top');
|
|
break;
|
|
case 90:
|
|
/* Ctrl + Z */
|
|
if (myself.ctrl) {
|
|
myself.undo();
|
|
}
|
|
break;
|
|
case 67:
|
|
/* Ctrl + C */
|
|
if (myself.ctrl && myself.selection.length) {
|
|
myself.clipboard =
|
|
myself.selection.map(function (each) {
|
|
return each.copy();
|
|
}
|
|
);
|
|
}
|
|
break;
|
|
case 86:
|
|
/* Ctrl + V */
|
|
pos = myself.world().hand.position();
|
|
if (myself.ctrl && myself.paper.bounds.containsPoint(pos)) {
|
|
myself.paper.pasteAt(pos);
|
|
myself.updateHistory();
|
|
}
|
|
break;
|
|
case 65:
|
|
/* Ctrl + A */
|
|
if (myself.ctrl) {
|
|
myself.paper.currentTool = 'selection';
|
|
myself.paper.toolChanged('selection');
|
|
myself.refreshToolButtons();
|
|
myself.paper.selectShapes(myself.shapes);
|
|
}
|
|
break;
|
|
case 27:
|
|
/* Escape key */
|
|
myself.clearSelection();
|
|
break;
|
|
case 37:
|
|
/* Left arrow */
|
|
myself.moveSelectionBy(new Point(-1, 0));
|
|
myself.updateHistory();
|
|
break;
|
|
case 38:
|
|
/* Up arrow */
|
|
myself.moveSelectionBy(new Point(0, -1));
|
|
myself.updateHistory();
|
|
break;
|
|
case 39:
|
|
/* Right arrow */
|
|
myself.moveSelectionBy(new Point(1, 0));
|
|
myself.updateHistory();
|
|
break;
|
|
case 40:
|
|
/* Down arrow */
|
|
myself.moveSelectionBy(new Point(0, 1));
|
|
myself.updateHistory();
|
|
break;
|
|
default:
|
|
nop();
|
|
}
|
|
myself.propertiesControls.constrain.refresh();
|
|
};
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.buildContents = function() {
|
|
var myself = this;
|
|
|
|
VectorPaintEditorMorph.uber.buildContents.call(this);
|
|
|
|
this.paper.destroy();
|
|
this.paper = new VectorPaintCanvasMorph(myself.shift);
|
|
this.paper.setExtent(this.ide.stage.dimensions);
|
|
this.body.add(this.paper);
|
|
|
|
this.refreshToolButtons();
|
|
this.fixLayout();
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.buildToolbox = function () {
|
|
var tools = {
|
|
brush:
|
|
'Paintbrush tool\n(free draw)',
|
|
rectangle:
|
|
'Rectangle\n(shift: square)',
|
|
ellipse:
|
|
'Ellipse\n(shift: circle)',
|
|
selection:
|
|
'Selection tool',
|
|
crosshairs:
|
|
'Set the rotation center',
|
|
line:
|
|
'Line tool\n(shift: constrain to 45º)',
|
|
closedBrush:
|
|
'Closed brush\n(free draw)',
|
|
polygon:
|
|
'Polygon',
|
|
paintbucket:
|
|
'Paint a shape\n(shift: edge color)',
|
|
pipette:
|
|
'Pipette tool\n(pick a color from anywhere\nshift: fill color)'
|
|
},
|
|
myself = this,
|
|
left = this.toolbox.left(),
|
|
top = this.toolbox.top(),
|
|
padding = 2,
|
|
inset = 5,
|
|
x = 0,
|
|
y = 0;
|
|
|
|
Object.keys(tools).forEach(function (toolName) {
|
|
var button = myself.toolButton(toolName, tools[toolName]);
|
|
button.setPosition(new Point(
|
|
left + x,
|
|
top + y
|
|
));
|
|
x += button.width() + padding;
|
|
if (toolName === 'crosshairs') { /* this tool marks the newline */
|
|
x = 0;
|
|
y += button.height() + padding;
|
|
myself.paper.drawcrosshair();
|
|
}
|
|
myself.toolbox[toolName] = button;
|
|
myself.toolbox.add(button);
|
|
});
|
|
|
|
this.toolbox.bounds = this.toolbox.fullBounds().expandBy(inset * 2);
|
|
};
|
|
|
|
// TODO :'(
|
|
VectorPaintEditorMorph.prototype.populatePropertiesMenu = function () {
|
|
var c = this.controls,
|
|
myself = this,
|
|
pc = this.propertiesControls,
|
|
alpen = new AlignmentMorph("row", this.padding),
|
|
alignColor = new AlignmentMorph("row", this.padding),
|
|
alignNames = new AlignmentMorph("row", this.padding),
|
|
brushControl = new AlignmentMorph("column", 3);
|
|
|
|
brushControl.alignment = "left";
|
|
|
|
pc.primaryColorViewer = new Morph();
|
|
pc.primaryColorViewer.color = new Color(0, 0, 0);
|
|
pc.primaryColorViewer.setExtent(new Point(85, 15)); // 40 = height primary & brush size
|
|
|
|
pc.primaryColorViewer.render = function (ctx) {
|
|
myself.renderColorSelection(ctx, myself.paper.settings.primaryColor);
|
|
};
|
|
|
|
pc.secondaryColorViewer = new Morph();
|
|
pc.secondaryColorViewer.color = new Color(0, 0, 0);
|
|
pc.secondaryColorViewer.setExtent(new Point(85, 15)); // 20 = height secondaryColor box
|
|
|
|
pc.secondaryColorViewer.render = function (ctx) {
|
|
myself.renderColorSelection(ctx, myself.paper.settings.secondaryColor);
|
|
};
|
|
|
|
pc.colorpicker = new PaintColorPickerMorph(
|
|
new Point(180, 100),
|
|
function (color, isSecondary) {
|
|
myself.selectColor(color, !isSecondary);
|
|
}
|
|
);
|
|
|
|
// allow right-click on the color picker to select the fill color
|
|
pc.colorpicker.mouseDownRight = function (pos) {
|
|
if ((pos.subtract(this.position()).x > this.width() * 2 / 3) &&
|
|
(pos.subtract(this.position()).y > this.height() - 10)) {
|
|
this.action("transparent", true);
|
|
} else {
|
|
this.action(this.getPixelColor(pos), true);
|
|
}
|
|
};
|
|
|
|
// also allow selecting the fill color via touch-hold
|
|
pc.colorpicker.mouseClickRight = pc.colorpicker.mouseDownRight;
|
|
|
|
pc.colorpicker.action(new Color(0, 0, 0)); // secondary color
|
|
pc.colorpicker.action('transparent', true);
|
|
|
|
pc.penSizeSlider = new SliderMorph(0, 20, 5, 5);
|
|
pc.penSizeSlider.orientation = "horizontal";
|
|
pc.penSizeSlider.setHeight(15);
|
|
pc.penSizeSlider.setWidth(150);
|
|
pc.penSizeSlider.action = function (num) {
|
|
if (pc.penSizeField) {
|
|
pc.penSizeField.setContents(num);
|
|
}
|
|
myself.paper.settings.lineWidth = num;
|
|
myself.selection.forEach(function (shape) {
|
|
shape.setBorderWidth(num);
|
|
shape.drawOn(myself.paper);
|
|
myself.paper.updateSelection();
|
|
});
|
|
myself.updateHistory();
|
|
};
|
|
pc.penSizeField = new InputFieldMorph("3", true, null, false);
|
|
pc.penSizeField.contents().minWidth = 20;
|
|
pc.penSizeField.setWidth(25);
|
|
pc.penSizeField.accept = function (num) {
|
|
var val = parseFloat(pc.penSizeField.getValue());
|
|
pc.penSizeSlider.value = val;
|
|
pc.penSizeSlider.updateValue();
|
|
this.setContents(val);
|
|
myself.paper.settings.lineWidth = val;
|
|
this.world().keyboardFocus = myself;
|
|
myself.selection.forEach(function (shape) {
|
|
shape.setBorderWidth(num);
|
|
shape.drawOn(myself.paper);
|
|
myself.paper.updateSelection();
|
|
});
|
|
myself.updateHistory();
|
|
};
|
|
alpen.add(pc.penSizeSlider);
|
|
alpen.add(pc.penSizeField);
|
|
alpen.color = myself.color;
|
|
alpen.fixLayout();
|
|
|
|
pc.constrain = new ToggleMorph(
|
|
"checkbox",
|
|
this,
|
|
function () { myself.shift = !myself.shift; },
|
|
"Constrain proportions of shapes?\n(you can also hold shift)",
|
|
function () { return myself.shift; }
|
|
);
|
|
|
|
pc.constrain.label.isBold = false;
|
|
alignColor.add(pc.secondaryColorViewer);
|
|
alignColor.add(pc.primaryColorViewer);
|
|
alignColor.fixLayout();
|
|
|
|
alignNames.add(new TextMorph(localize('Edge color\n(left click)'),
|
|
10, null, null, null,
|
|
'center', 85));
|
|
alignNames.add(new TextMorph(localize('Fill color\n(right click)'),
|
|
10, null, null, null,
|
|
'center', 85));
|
|
alignNames.fixLayout();
|
|
c.add(pc.colorpicker);
|
|
c.add(alignNames);
|
|
c.add(alignColor);
|
|
brushControl.add(
|
|
new StringMorph(localize("Brush size") + ":", 10, null, true)
|
|
);
|
|
brushControl.add(alpen);
|
|
brushControl.add(pc.constrain);
|
|
brushControl.fixLayout();
|
|
c.add(brushControl);
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.selectColor = function (color, secondary) {
|
|
var myself = this,
|
|
isSecondary = this.paper.isShiftPressed() ? false : secondary,
|
|
propertyName = (isSecondary ? 'secondary' : 'primary') + 'Color';
|
|
|
|
this.paper.settings[(propertyName)] = color;
|
|
|
|
if (this.selection.length) {
|
|
this.selection.forEach(function (shape) {
|
|
shape.setColor(color, isSecondary);
|
|
shape.drawOn(myself.paper);
|
|
});
|
|
this.updateHistory();
|
|
}
|
|
|
|
this.propertiesControls[propertyName + 'Viewer'].rerender();
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.renderColorSelection = function (
|
|
ctx,
|
|
color = 'transparent'
|
|
) {
|
|
var i, j;
|
|
|
|
if (color === 'transparent' || color.a === 0) {
|
|
for (i = 0; i < 180; i += 5) {
|
|
for (j = 0; j < 15; j += 5) {
|
|
ctx.fillStyle =
|
|
((j + i) / 5) % 2 === 0 ?
|
|
'rgba(0, 0, 0, 0.2)'
|
|
:'rgba(0, 0, 0, 0.5)';
|
|
ctx.fillRect(i, j, 5, 5);
|
|
}
|
|
}
|
|
} else {
|
|
ctx.fillStyle = color.toString();
|
|
ctx.fillRect(0, 0, 180, 15);
|
|
}
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.changeSelectionLayer = function (destination) {
|
|
// I move the selected shapes across the z axis
|
|
var myself = this;
|
|
|
|
this.sortSelection();
|
|
|
|
switch (destination) {
|
|
case 'top':
|
|
this.selection.forEach(function (shape) {
|
|
myself.shapes.splice(myself.shapes.indexOf(shape), 1);
|
|
myself.shapes.push(shape);
|
|
});
|
|
break;
|
|
case 'bottom':
|
|
this.selection.slice().reverse().forEach(function (shape) {
|
|
myself.shapes.splice(myself.shapes.indexOf(shape), 1);
|
|
myself.shapes.splice(0, 0, shape);
|
|
});
|
|
break;
|
|
case 'up':
|
|
this.selection.forEach(function (shape) {
|
|
var index = myself.shapes.indexOf(shape);
|
|
myself.shapes.splice(index, 1);
|
|
myself.shapes.splice(index + myself.selection.length, 0, shape);
|
|
});
|
|
break;
|
|
case 'down':
|
|
if (this.shapes[0] !== this.selection[0]) {
|
|
this.selection.forEach(function (shape) {
|
|
var index = myself.shapes.indexOf(shape);
|
|
myself.shapes.splice(index, 1);
|
|
myself.shapes.splice(index - 1, 0, shape);
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.updateHistory();
|
|
this.paper.redraw = true;
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.dragSelection = function (pos) {
|
|
var origin,
|
|
ratio,
|
|
delta;
|
|
|
|
if (this.lastDragPosition) {
|
|
if (this.moving) {
|
|
delta = pos.subtract(this.lastDragPosition);
|
|
this.moveSelectionBy(delta);
|
|
} else if (this.resizing) {
|
|
if (this.shift) {
|
|
// constrain delta if shift is pressed
|
|
origin = this.originalSelection.origin;
|
|
ratio = Math.max(
|
|
(pos.x - origin.x) /
|
|
(this.originalSelection.destination.x - origin.x),
|
|
(pos.y - origin.y) /
|
|
(this.originalSelection.destination.y - origin.y)
|
|
);
|
|
pos = this.originalSelection.destination.subtract(
|
|
origin
|
|
).multiplyBy(ratio).add(origin);
|
|
}
|
|
// this.currentShape holds the selection shape
|
|
delta = (pos.subtract(this.currentShape.origin)).divideBy(
|
|
this.lastDragPosition.subtract(this.currentShape.origin));
|
|
this.resizeSelectionBy(delta);
|
|
}
|
|
} else if (this.resizing) {
|
|
// we save the selection as it was before we started resizing so that
|
|
// we can use it to constrain its proportions later
|
|
this.originalSelection = this.currentShape.copy();
|
|
}
|
|
|
|
this.lastDragPosition = pos;
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.moveSelectionBy = function (delta) {
|
|
var paper = this.paper;
|
|
|
|
this.selection.forEach(function (shape) {
|
|
shape.moveBy(delta);
|
|
shape.drawOn(paper);
|
|
});
|
|
|
|
if (this.currentShape && this.currentShape.isSelection) {
|
|
this.currentShape.moveBy(delta);
|
|
this.currentShape.drawOn(paper);
|
|
}
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.resizeSelectionBy = function (delta) {
|
|
var paper = this.paper,
|
|
selectionShape;
|
|
|
|
if (this.currentShape && this.currentShape.isSelection) {
|
|
selectionShape = this.currentShape;
|
|
|
|
this.selection.forEach(function (shape) {
|
|
shape.resizeBy(delta, selectionShape.origin);
|
|
shape.drawOn(paper);
|
|
});
|
|
|
|
selectionShape.resizeBy(delta, selectionShape.origin);
|
|
selectionShape.drawOn(paper);
|
|
}
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.sortSelection = function () {
|
|
var myself = this;
|
|
this.selection.sort(function (a, b) {
|
|
return myself.shapes.indexOf(a) > myself.shapes.indexOf(b);
|
|
});
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.clearSelection = function () {
|
|
this.currentShape = null;
|
|
this.selection = [];
|
|
this.paper.redraw = true;
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.getSVG = function () {
|
|
var svg = new XML_Element('svg'),
|
|
bounds = this.getBounds(this.shapes);
|
|
|
|
svg.attributes.xmlns = 'http://www.w3.org/2000/svg';
|
|
svg.attributes.snap = 'http://snap.berkeley.edu/run';
|
|
svg.attributes.version = '1.1';
|
|
svg.attributes.preserveAspectRatio = 'none meet';
|
|
svg.attributes.viewBox =
|
|
bounds.left() + ' ' + bounds.top() + ' ' +
|
|
(bounds.right() - bounds.left()) + ' ' +
|
|
(bounds.bottom() - bounds.top());
|
|
svg.attributes.width = (bounds.right() - bounds.left());
|
|
svg.attributes.height = (bounds.bottom() - bounds.top());
|
|
|
|
svg.children = this.shapes.map(function (shape) { return shape.asSVG(); });
|
|
|
|
return window.btoa(svg);
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.getBounds = function (shapeCollection) {
|
|
var shapeBounds = shapeCollection.map(function(each) {
|
|
return each.bounds();
|
|
});
|
|
|
|
if (shapeBounds.length === 0) {return null; }
|
|
|
|
return shapeBounds.reduce(
|
|
function(previous, current) {
|
|
return new Rectangle(
|
|
Math.min(previous.left(), current.left()),
|
|
Math.min(current.top(), previous.top()),
|
|
Math.max(previous.right(), current.right()),
|
|
Math.max(previous.bottom(), current.bottom())
|
|
);
|
|
}
|
|
);
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.silentMoveBy = function (delta) {
|
|
VectorPaintEditorMorph.uber.silentMoveBy.call(this, delta);
|
|
if (this.currentShape) {
|
|
this.currentShape.moveBy(delta);
|
|
}
|
|
this.shapes.forEach(function (shape) {
|
|
shape.moveBy(delta);
|
|
});
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.ok = function () {
|
|
var myself = this,
|
|
img = new Image(),
|
|
shapeOrigin,
|
|
originDelta;
|
|
|
|
if (this.shapes.length === 0) {
|
|
this.cancel();
|
|
return;
|
|
}
|
|
|
|
shapeOrigin = this.getBounds(this.shapes).origin;
|
|
originDelta = shapeOrigin.subtract(this.paper.bounds.origin);
|
|
|
|
this.paper.updateAutomaticCenter();
|
|
|
|
img.src = 'data:image/svg+xml;base64,' + this.getSVG().toString();
|
|
|
|
img.onload = function() {
|
|
myself.callback(
|
|
img,
|
|
myself.paper.rotationCenter.subtract(originDelta),
|
|
myself.shapes
|
|
);
|
|
};
|
|
|
|
this.destroy();
|
|
};
|
|
|
|
// Undo support
|
|
|
|
VectorPaintEditorMorph.prototype.updateHistory = function () {
|
|
this.history.push(this.shapes.map(function (shape) {
|
|
return shape.copy();
|
|
}));
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.undo = function () {
|
|
var paper = this.paper,
|
|
oldSum = this.checksum(),
|
|
newSum = oldSum;
|
|
|
|
function draw(shape) {
|
|
shape.drawOn(paper);
|
|
}
|
|
|
|
while (this.shapes.length && oldSum == newSum) {
|
|
this.shapes = this.history.pop() || [];
|
|
this.shapes.forEach(draw);
|
|
newSum = this.checksum();
|
|
}
|
|
|
|
this.clearSelection();
|
|
};
|
|
|
|
VectorPaintEditorMorph.prototype.checksum = function () {
|
|
return JSON.stringify(this.shapes).split('').reduce(
|
|
function (previousSum, currentChar) {
|
|
return previousSum + currentChar.charCodeAt(0);
|
|
},
|
|
0);
|
|
};
|
|
|
|
// VectorPaintCanvasMorph //////////////////////////
|
|
|
|
VectorPaintCanvasMorph.prototype = new PaintCanvasMorph();
|
|
VectorPaintCanvasMorph.prototype.constructor = VectorPaintCanvasMorph;
|
|
VectorPaintCanvasMorph.uber = PaintCanvasMorph.prototype;
|
|
|
|
function VectorPaintCanvasMorph (shift) {
|
|
this.init(shift);
|
|
}
|
|
|
|
VectorPaintCanvasMorph.prototype.init = function (shift) {
|
|
VectorPaintCanvasMorph.uber.init.call(this, shift);
|
|
this.isCachingImage = true;
|
|
this.pointBuffer = [];
|
|
this.currentTool = 'brush';
|
|
this.settings = {
|
|
primaryColor: new Color(0, 0, 0, 0),
|
|
secondaryColor: new Color(0, 0, 0, 255),
|
|
lineWidth: 3
|
|
};
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.calculateCanvasCenter = function () {
|
|
var canvasBounds = this.bounds;
|
|
|
|
// Can't use canvasBounds.center(), it rounds down.
|
|
return new Point(
|
|
(canvasBounds.width()) / 2,
|
|
(canvasBounds.height()) / 2);
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.updateAutomaticCenter = function () {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
shapeBounds = editor.getBounds(editor.shapes),
|
|
relativePosition;
|
|
|
|
if (this.automaticCrosshairs && shapeBounds) {
|
|
relativePosition = shapeBounds.origin.subtract(this.bounds.origin);
|
|
this.rotationCenter =
|
|
(new Point(
|
|
(shapeBounds.width()) / 2,
|
|
(shapeBounds.height()) / 2)).add(relativePosition);
|
|
} else if (this.automaticCrosshairs) {
|
|
this.calculateCanvasCenter();
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.clearCanvas = function () {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
editor.updateHistory();
|
|
editor.shapes = [];
|
|
editor.clearSelection();
|
|
this.mask.getContext('2d').clearRect(
|
|
0,
|
|
0,
|
|
this.bounds.width(),
|
|
this.bounds.height()
|
|
);
|
|
this.redraw = true;
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.toolChanged = function (tool) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
VectorPaintCanvasMorph.uber.toolChanged.call(this, tool);
|
|
|
|
if (editor.currentShape && editor.currentShape.isPolygon) {
|
|
editor.currentShape.close();
|
|
editor.currentShape.drawOn(this);
|
|
editor.shapes.push(editor.currentShape);
|
|
}
|
|
|
|
if (tool === 'crosshairs') {
|
|
editor.clearSelection();
|
|
editor.currentShape = new Crosshair(this.rotationCenter, this);
|
|
editor.currentShape.drawOn(this);
|
|
this.automaticCrosshairs = false;
|
|
} else if (tool === 'pipette' && editor.selection) {
|
|
return;
|
|
} else {
|
|
editor.clearSelection();
|
|
editor.currentShape = null;
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.drawNew = function () {
|
|
var myself = this,
|
|
editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
canvas = newCanvas(this.extent(), false, this.cachedImage);
|
|
|
|
this.merge(this.background, canvas);
|
|
this.merge(this.paper, canvas);
|
|
|
|
if (editor) {
|
|
editor.shapes.forEach(function(each) {
|
|
myself.merge(each.image, canvas);
|
|
});
|
|
|
|
if (editor.currentShape) {
|
|
this.merge(editor.currentShape.image, canvas);
|
|
}
|
|
}
|
|
|
|
this.cachedImage = canvas;
|
|
this.drawFrame();
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.render =
|
|
VectorPaintCanvasMorph.prototype.drawNew;
|
|
|
|
VectorPaintCanvasMorph.prototype.step = function () {
|
|
if (this.redraw) {
|
|
this.drawNew();
|
|
this.changed();
|
|
this.redraw = false;
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.mouseMove = function (pos) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
primaryColor = this.settings.primaryColor,
|
|
secondaryColor = this.settings.secondaryColor,
|
|
borderWidth = this.settings.lineWidth,
|
|
selectionCorner,
|
|
oppositeCorner;
|
|
|
|
if (this.currentTool === 'paintbucket') {
|
|
return;
|
|
|
|
} else if (editor.currentShape && editor.currentShape.isSelection
|
|
&& !editor.selecting) {
|
|
|
|
selectionCorner = editor.currentShape.cornerAt(pos);
|
|
|
|
if (editor.resizing || editor.moving) {
|
|
editor.dragSelection(pos);
|
|
} else if (selectionCorner) {
|
|
oppositeCorner = editor.currentShape.cornerOppositeTo(
|
|
selectionCorner
|
|
);
|
|
editor.currentShape = new VectorSelection(
|
|
oppositeCorner,
|
|
selectionCorner
|
|
);
|
|
editor.currentShape.drawOn(this);
|
|
editor.resizing = true;
|
|
document.body.style.cursor = 'move';
|
|
} else if (editor.currentShape.containsPoint(pos)) {
|
|
editor.moving = true;
|
|
document.body.style.cursor = 'move';
|
|
}
|
|
|
|
} else if (!editor.currentShape || editor.currentShape.isSelection
|
|
&& !editor.selecting) {
|
|
this.beginShape(borderWidth, primaryColor, secondaryColor, pos);
|
|
editor.currentShape.drawOn(this);
|
|
} else {
|
|
editor.currentShape.update(pos, editor.shift);
|
|
editor.currentShape.drawOn(this);
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.mouseEnter = function () {
|
|
if (this.currentTool === 'selection') {
|
|
document.body.style.cursor = 'crosshair';
|
|
} else {
|
|
document.body.style.cursor = 'default';
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.mouseLeave = function () {
|
|
document.body.style.cursor = 'default';
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.mouseClickLeft = function (pos) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
shape = editor.currentShape;
|
|
|
|
if (shape) {
|
|
if (shape.isPolygon && !shape.isFreeHand) {
|
|
shape.points.push(shape.points[shape.points.length - 1].copy());
|
|
} else if (shape.isPolygon) {
|
|
shape.close();
|
|
shape.drawOn(this);
|
|
editor.shapes.push(shape);
|
|
editor.currentShape = null;
|
|
} else if (shape.isSelection) {
|
|
if (editor.selecting) {
|
|
shape.destination = pos;
|
|
this.selectInside(shape);
|
|
editor.selecting = false;
|
|
} else if (editor.moving || editor.resizing) {
|
|
editor.moving = false;
|
|
editor.resizing = false;
|
|
this.updateSelection();
|
|
} else {
|
|
this.selectAtPoint(pos);
|
|
}
|
|
} else if (shape.isCrosshair) {
|
|
this.rotationCenter = pos.subtract(this.bounds.origin);
|
|
} else {
|
|
shape.update(pos, editor.shift);
|
|
editor.shapes.push(shape);
|
|
editor.currentShape = null;
|
|
}
|
|
} else if (this.currentTool === 'selection') {
|
|
this.selectAtPoint(pos);
|
|
}
|
|
|
|
editor.lastDragPosition = null;
|
|
this.mouseEnter();
|
|
editor.updateHistory();
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.mouseDoubleClick = function (pos) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
shape = editor.currentShape;
|
|
|
|
if (shape && shape.isPolygon) {
|
|
shape.close(); // if it applies
|
|
shape.drawOn(this);
|
|
editor.shapes.push(shape);
|
|
editor.currentShape = null;
|
|
editor.updateHistory();
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.beginShape = function (
|
|
borderWidth,
|
|
primaryColor,
|
|
secondaryColor,
|
|
pos
|
|
) {
|
|
switch (this.currentTool) {
|
|
case 'brush':
|
|
this.beginPolygon( // unclosed, freehanded
|
|
borderWidth,
|
|
secondaryColor,
|
|
null,
|
|
pos,
|
|
false,
|
|
true
|
|
);
|
|
break;
|
|
case 'line':
|
|
this.beginLine(borderWidth, secondaryColor, pos);
|
|
break;
|
|
case 'rectangle':
|
|
this.beginRectangle(borderWidth, secondaryColor, primaryColor, pos);
|
|
break;
|
|
case 'ellipse':
|
|
this.beginEllipse(borderWidth, secondaryColor, primaryColor, pos);
|
|
break;
|
|
case 'polygon':
|
|
this.beginPolygon( // closed, point-based
|
|
borderWidth,
|
|
secondaryColor,
|
|
primaryColor,
|
|
pos,
|
|
true,
|
|
false
|
|
);
|
|
break;
|
|
case 'closedBrush':
|
|
this.beginPolygon( // closed, freehanded
|
|
borderWidth,
|
|
secondaryColor,
|
|
primaryColor,
|
|
pos,
|
|
true,
|
|
true
|
|
);
|
|
break;
|
|
case 'selection':
|
|
this.beginSelection(pos);
|
|
break;
|
|
// pipette is defined in PaintCanvasMorph >> toolButton
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.beginPolygon = function (
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin,
|
|
isClosed,
|
|
isFreeHand
|
|
) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
editor.currentShape = new VectorPolygon(
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
[origin],
|
|
isClosed,
|
|
isFreeHand
|
|
);
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.beginLine = function (
|
|
borderWidth,
|
|
borderColor,
|
|
origin
|
|
) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
editor.currentShape = new VectorLine(
|
|
borderWidth,
|
|
borderColor,
|
|
origin,
|
|
origin
|
|
);
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.beginRectangle = function (
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin
|
|
) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
editor.currentShape = new VectorRectangle(
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin,
|
|
origin
|
|
);
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.beginEllipse = function (
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin
|
|
) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
editor.currentShape = new VectorEllipse(
|
|
borderWidth,
|
|
borderColor,
|
|
fillColor,
|
|
origin,
|
|
origin
|
|
);
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.beginSelection = function (origin) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
editor.currentShape = new VectorSelection(origin, origin);
|
|
editor.selecting = true;
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.selectInside = function (selectionShape) {
|
|
// I find and select all shapes contained inside
|
|
// the bounds of selectionShape
|
|
var selectionBounds = selectionShape.bounds(),
|
|
editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
|
|
editor.selection = editor.shapes.filter(function (eachShape) {
|
|
return selectionBounds.containsRectangle(eachShape.bounds());
|
|
});
|
|
|
|
if (editor.selection.length > 0) {
|
|
selectionBounds = editor.getBounds(editor.selection);
|
|
selectionShape.origin = selectionBounds.topLeft();
|
|
selectionShape.destination = selectionBounds.bottomRight();
|
|
selectionShape.drawOn(this);
|
|
} else {
|
|
editor.currentShape = null;
|
|
this.redraw = true;
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.selectAtPoint = function (position) {
|
|
// I find and select the topmost shape at position
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
shape = this.shapeAt(position),
|
|
bounds,
|
|
index;
|
|
|
|
if (shape) {
|
|
if (editor.shift) {
|
|
index = editor.selection.indexOf(shape);
|
|
if (index > -1) {
|
|
editor.selection.splice(index, 1);
|
|
} else {
|
|
editor.selection.push(shape);
|
|
}
|
|
} else {
|
|
editor.selection = [ shape ];
|
|
}
|
|
bounds = editor.getBounds(editor.selection);
|
|
}
|
|
|
|
if (bounds) {
|
|
editor.currentShape = new VectorSelection(
|
|
bounds.topLeft(),
|
|
bounds.bottomRight()
|
|
);
|
|
editor.currentShape.drawOn(this);
|
|
} else {
|
|
editor.clearSelection();
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.selectShapes = function (shapes) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
bounds;
|
|
|
|
if (shapes.length > 0) {
|
|
bounds = editor.getBounds(shapes);
|
|
editor.selection = shapes;
|
|
editor.currentShape = new VectorSelection(
|
|
bounds.topLeft(),
|
|
bounds.bottomRight()
|
|
);
|
|
editor.currentShape.drawOn(this);
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.updateSelection = function () {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
this.selectShapes(editor.selection);
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.shapeAt = function (position) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph);
|
|
return detect(
|
|
editor.shapes.slice().reverse(),
|
|
function (shape) {
|
|
return shape.containsPoint(position);
|
|
});
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.pasteAt = function (position) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
myself = this,
|
|
clipboard = editor.clipboard,
|
|
delta,
|
|
copies = [];
|
|
|
|
if (clipboard.length > 0) {
|
|
// Each shape is positioned according to the difference between
|
|
// the first shape's original position and the paste position
|
|
delta = position.subtract(clipboard[0].bounds().origin);
|
|
}
|
|
|
|
clipboard.forEach(function (shape) {
|
|
var copy = shape.copy();
|
|
copy.moveBy(delta);
|
|
editor.selection.push(copy);
|
|
editor.shapes.push(copy);
|
|
copy.drawOn(myself);
|
|
copies.push(copy);
|
|
});
|
|
|
|
if (copies.length > 0) {
|
|
this.selectShapes(copies);
|
|
editor.updateHistory();
|
|
}
|
|
};
|
|
|
|
VectorPaintCanvasMorph.prototype.floodfill = function (sourcepoint) {
|
|
var editor = this.parentThatIsA(VectorPaintEditorMorph),
|
|
shape = this.shapeAt(sourcepoint.add(this.position()));
|
|
|
|
if (shape) {
|
|
shape.setColor(
|
|
editor.shift ?
|
|
this.settings.secondaryColor
|
|
: this.settings.primaryColor, editor.shift
|
|
);
|
|
shape.drawOn(this);
|
|
}
|
|
};
|
|
|