turtlestitch/src/sketch.js

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);
}
};