kopia lustrzana https://github.com/backface/turtlestitch
1008 wiersze
30 KiB
JavaScript
1008 wiersze
30 KiB
JavaScript
/*
|
|
paint.js
|
|
|
|
a paint editor for Snap!
|
|
inspired by the Scratch paint editor.
|
|
|
|
written by Kartik Chandra
|
|
Copyright (C) 2015 by Kartik Chandra
|
|
|
|
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/>.
|
|
|
|
|
|
toc
|
|
---
|
|
the following list shows the order in which all constructors are
|
|
defined. Use this list to locate code in this document:
|
|
|
|
PaintEditorMorph
|
|
PaintColorPickerMorph
|
|
PaintCanvasMorph
|
|
|
|
|
|
credits
|
|
-------
|
|
Nathan Dinsmore contributed a fully working prototype,
|
|
Nathan's brilliant flood-fill tool has been more or less
|
|
directly imported into this paint implementation.
|
|
|
|
Jens Mönig has contributed icons and bugfixes and says he has probably
|
|
introduced many other bugs in that process. :-)
|
|
|
|
|
|
revision history
|
|
----------------
|
|
May 10 - first full release (Kartik)
|
|
May 14 - bugfixes, Snap integration (Jens)
|
|
May 16 - flat design adjustments (Jens)
|
|
July 12 - pipette tool, code formatting adjustments (Jens)
|
|
Sept 16 - flood fill freeze fix (Kartik)
|
|
Jan 08 - mouse leave dragging fix (Kartik)
|
|
Feb 11 - dynamically adjust to stage dimensions (Jens)
|
|
Apr 30 - localizations (Manuel)
|
|
June 3 - transformations (Kartik)
|
|
June 4 - tweaks (Jens)
|
|
Aug 24 - floodfill alpha-integer issue (Kartik)
|
|
Sep 29 - tweaks (Jens)
|
|
Sep 28 [of the following year :)] - Try to prevent antialiasing (Kartik)
|
|
Oct 02 - revert disable smoothing (Jens)
|
|
*/
|
|
|
|
/*global Point, Rectangle, DialogBoxMorph, fontHeight, AlignmentMorph,
|
|
FrameMorph, PushButtonMorph, Color, SymbolMorph, newCanvas, Morph, TextMorph,
|
|
CostumeIconMorph, IDE_Morph, Costume, SpriteMorph, nop, Image, WardrobeMorph,
|
|
TurtleIconMorph, localize, MenuMorph, InputFieldMorph, SliderMorph,
|
|
ToggleMorph, ToggleButtonMorph, BoxMorph, modules, radians,
|
|
MorphicPreferences, getDocumentPositionOf, StageMorph
|
|
*/
|
|
|
|
// Global stuff ////////////////////////////////////////////////////////
|
|
|
|
modules.paint = '2015-October-02';
|
|
|
|
// Declarations
|
|
|
|
var PaintEditorMorph;
|
|
var PaintCanvasMorph;
|
|
var PaintColorPickerMorph;
|
|
|
|
// PaintEditorMorph //////////////////////////
|
|
|
|
// A complete paint editor
|
|
|
|
PaintEditorMorph.prototype = new DialogBoxMorph();
|
|
PaintEditorMorph.prototype.constructor = PaintEditorMorph;
|
|
PaintEditorMorph.uber = DialogBoxMorph.prototype;
|
|
|
|
PaintEditorMorph.prototype.padding = 10;
|
|
|
|
function PaintEditorMorph() {
|
|
this.init();
|
|
}
|
|
|
|
PaintEditorMorph.prototype.init = function () {
|
|
// additional properties:
|
|
this.paper = null; // paint canvas
|
|
this.oncancel = null;
|
|
|
|
// initialize inherited properties:
|
|
PaintEditorMorph.uber.init.call(this);
|
|
|
|
// override inherited properties:
|
|
this.labelString = "Paint Editor";
|
|
this.createLabel();
|
|
|
|
// build contents:
|
|
this.buildContents();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.buildContents = function () {
|
|
var myself = this;
|
|
|
|
this.paper = new PaintCanvasMorph(function () {return myself.shift; });
|
|
this.paper.setExtent(StageMorph.prototype.dimensions);
|
|
|
|
this.addBody(new AlignmentMorph('row', this.padding));
|
|
this.controls = new AlignmentMorph('column', this.padding / 2);
|
|
this.controls.alignment = 'left';
|
|
|
|
this.edits = new AlignmentMorph('row', this.padding / 2);
|
|
this.buildEdits();
|
|
this.controls.add(this.edits);
|
|
|
|
this.body.color = this.color;
|
|
|
|
this.body.add(this.controls);
|
|
this.body.add(this.paper);
|
|
|
|
this.toolbox = new BoxMorph();
|
|
this.toolbox.color = SpriteMorph.prototype.paletteColor.lighter(8);
|
|
this.toolbox.borderColor = this.toolbox.color.lighter(40);
|
|
if (MorphicPreferences.isFlat) {
|
|
this.toolbox.edge = 0;
|
|
}
|
|
|
|
this.buildToolbox();
|
|
this.controls.add(this.toolbox);
|
|
|
|
this.scaleBox = new AlignmentMorph('row', this.padding / 2);
|
|
this.buildScaleBox();
|
|
this.controls.add(this.scaleBox);
|
|
|
|
this.propertiesControls = {
|
|
colorpicker: null,
|
|
penSizeSlider: null,
|
|
penSizeField: null,
|
|
primaryColorButton: null,
|
|
primaryColorViewer: null,
|
|
constrain: null
|
|
};
|
|
this.populatePropertiesMenu();
|
|
|
|
this.addButton("ok", "OK");
|
|
this.addButton("cancel", "Cancel");
|
|
|
|
this.refreshToolButtons();
|
|
this.fixLayout();
|
|
this.drawNew();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.buildToolbox = function () {
|
|
var tools = {
|
|
brush:
|
|
"Paintbrush tool\n(free draw)",
|
|
rectangle:
|
|
"Stroked Rectangle\n(shift: square)",
|
|
circle:
|
|
"Stroked Ellipse\n(shift: circle)",
|
|
eraser:
|
|
"Eraser tool",
|
|
crosshairs:
|
|
"Set the rotation center",
|
|
|
|
line:
|
|
"Line tool\n(shift: vertical/horizontal)",
|
|
rectangleSolid:
|
|
"Filled Rectangle\n(shift: square)",
|
|
circleSolid:
|
|
"Filled Ellipse\n(shift: circle)",
|
|
paintbucket:
|
|
"Fill a region",
|
|
pipette:
|
|
"Pipette tool\n(pick a color anywhere)"
|
|
},
|
|
myself = this,
|
|
left = this.toolbox.left(),
|
|
top = this.toolbox.top(),
|
|
padding = 2,
|
|
inset = 5,
|
|
x = 0,
|
|
y = 0;
|
|
|
|
Object.keys(tools).forEach(function (tool) {
|
|
var btn = myself.toolButton(tool, tools[tool]);
|
|
btn.setPosition(new Point(
|
|
left + x,
|
|
top + y
|
|
));
|
|
x += btn.width() + padding;
|
|
if (tool === "crosshairs") {
|
|
x = 0;
|
|
y += btn.height() + padding;
|
|
myself.paper.drawcrosshair();
|
|
}
|
|
myself.toolbox[tool] = btn;
|
|
myself.toolbox.add(btn);
|
|
});
|
|
|
|
this.toolbox.bounds = this.toolbox.fullBounds().expandBy(inset * 2);
|
|
this.toolbox.drawNew();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.buildEdits = function () {
|
|
var paper = this.paper;
|
|
|
|
this.edits.add(this.pushButton(
|
|
"undo",
|
|
function () {paper.undo(); }
|
|
));
|
|
|
|
this.edits.add(this.pushButton(
|
|
"clear",
|
|
function () {paper.clearCanvas(); }
|
|
));
|
|
this.edits.fixLayout();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.buildScaleBox = function () {
|
|
var paper = this.paper;
|
|
this.scaleBox.add(this.pushButton(
|
|
"grow",
|
|
function () {paper.scale(0.05, 0.05); }
|
|
));
|
|
this.scaleBox.add(this.pushButton(
|
|
"shrink",
|
|
function () {paper.scale(-0.05, -0.05); }
|
|
));
|
|
this.scaleBox.add(this.pushButton(
|
|
"flip ↔",
|
|
function () {paper.scale(-2, 0); }
|
|
));
|
|
this.scaleBox.add(this.pushButton(
|
|
"flip ↕",
|
|
function () {paper.scale(0, -2); }
|
|
));
|
|
this.scaleBox.fixLayout();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.openIn = function (world, oldim, oldrc, callback) {
|
|
// Open the editor in a world with an optional image to edit
|
|
this.oldim = oldim;
|
|
this.oldrc = oldrc.copy();
|
|
this.callback = callback || nop;
|
|
|
|
this.processKeyUp = function () {
|
|
this.shift = false;
|
|
this.propertiesControls.constrain.refresh();
|
|
};
|
|
|
|
this.processKeyDown = function () {
|
|
this.shift = this.world().currentKey === 16;
|
|
this.propertiesControls.constrain.refresh();
|
|
};
|
|
|
|
//merge oldim:
|
|
if (this.oldim) {
|
|
this.paper.centermerge(this.oldim, this.paper.paper);
|
|
this.paper.rotationCenter =
|
|
this.oldrc.add(
|
|
new Point(
|
|
(this.paper.paper.width - this.oldim.width) / 2,
|
|
(this.paper.paper.height - this.oldim.height) / 2
|
|
)
|
|
);
|
|
this.paper.drawNew();
|
|
}
|
|
|
|
this.key = 'paint';
|
|
this.popUp(world);
|
|
};
|
|
|
|
PaintEditorMorph.prototype.fixLayout = function () {
|
|
var oldFlag = Morph.prototype.trackChanges;
|
|
|
|
this.changed();
|
|
oldFlag = Morph.prototype.trackChanges;
|
|
Morph.prototype.trackChanges = false;
|
|
|
|
if (this.paper) {
|
|
this.paper.buildContents();
|
|
this.paper.drawNew();
|
|
}
|
|
if (this.controls) {this.controls.fixLayout(); }
|
|
if (this.body) {this.body.fixLayout(); }
|
|
PaintEditorMorph.uber.fixLayout.call(this);
|
|
|
|
Morph.prototype.trackChanges = oldFlag;
|
|
this.changed();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.refreshToolButtons = function () {
|
|
this.toolbox.children.forEach(function (toggle) {
|
|
toggle.refresh();
|
|
});
|
|
};
|
|
|
|
PaintEditorMorph.prototype.ok = function () {
|
|
this.callback(
|
|
this.paper.paper,
|
|
this.paper.rotationCenter
|
|
);
|
|
this.destroy();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.cancel = function () {
|
|
if (this.oncancel) {this.oncancel(); }
|
|
this.destroy();
|
|
};
|
|
|
|
PaintEditorMorph.prototype.populatePropertiesMenu = function () {
|
|
var c = this.controls,
|
|
myself = this,
|
|
pc = this.propertiesControls,
|
|
alpen = new AlignmentMorph("row", this.padding);
|
|
|
|
pc.primaryColorViewer = new Morph();
|
|
pc.primaryColorViewer.setExtent(new Point(180, 50));
|
|
pc.primaryColorViewer.color = new Color(0, 0, 0);
|
|
pc.colorpicker = new PaintColorPickerMorph(
|
|
new Point(180, 100),
|
|
function (color) {
|
|
var ni = newCanvas(pc.primaryColorViewer.extent()),
|
|
ctx = ni.getContext("2d"),
|
|
i,
|
|
j;
|
|
myself.paper.settings.primarycolor = color;
|
|
if (color === "transparent") {
|
|
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);
|
|
}
|
|
ctx.strokeStyle = "black";
|
|
ctx.lineWidth = Math.min(myself.paper.settings.linewidth, 20);
|
|
ctx.beginPath();
|
|
ctx.lineCap = "round";
|
|
ctx.moveTo(20, 30);
|
|
ctx.lineTo(160, 30);
|
|
ctx.stroke();
|
|
pc.primaryColorViewer.image = ni;
|
|
pc.primaryColorViewer.changed();
|
|
}
|
|
);
|
|
pc.colorpicker.action(new Color(0, 0, 0));
|
|
|
|
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;
|
|
pc.colorpicker.action(myself.paper.settings.primarycolor);
|
|
};
|
|
pc.penSizeField = new InputFieldMorph("5", true, null, false);
|
|
pc.penSizeField.contents().minWidth = 20;
|
|
pc.penSizeField.setWidth(25);
|
|
pc.penSizeField.accept = function () {
|
|
var val = parseFloat(pc.penSizeField.getValue());
|
|
pc.penSizeSlider.value = val;
|
|
pc.penSizeSlider.drawNew();
|
|
pc.penSizeSlider.updateValue();
|
|
this.setContents(val);
|
|
myself.paper.settings.linewidth = val;
|
|
this.world().keyboardReceiver = myself;
|
|
pc.colorpicker.action(myself.paper.settings.primarycolor);
|
|
};
|
|
alpen.add(pc.penSizeSlider);
|
|
alpen.add(pc.penSizeField);
|
|
alpen.color = myself.color;
|
|
alpen.fixLayout();
|
|
pc.penSizeField.drawNew();
|
|
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; }
|
|
);
|
|
c.add(pc.colorpicker);
|
|
//c.add(pc.primaryColorButton);
|
|
c.add(pc.primaryColorViewer);
|
|
c.add(new TextMorph(localize("Brush size")));
|
|
c.add(alpen);
|
|
c.add(pc.constrain);
|
|
};
|
|
|
|
PaintEditorMorph.prototype.toolButton = function (icon, hint) {
|
|
var button, myself = this;
|
|
|
|
button = new ToggleButtonMorph(
|
|
null,
|
|
this,
|
|
function () { // action
|
|
myself.paper.currentTool = icon;
|
|
myself.paper.toolChanged(icon);
|
|
myself.refreshToolButtons();
|
|
if (icon === 'pipette') {
|
|
myself.getUserColor();
|
|
}
|
|
},
|
|
new SymbolMorph(icon, 18),
|
|
function () {return myself.paper.currentTool === icon; }
|
|
);
|
|
|
|
button.hint = hint;
|
|
button.drawNew();
|
|
button.fixLayout();
|
|
return button;
|
|
};
|
|
|
|
PaintEditorMorph.prototype.pushButton = function (title, action, hint) {
|
|
return new PushButtonMorph(
|
|
this,
|
|
action,
|
|
title,
|
|
null,
|
|
hint
|
|
);
|
|
};
|
|
|
|
PaintEditorMorph.prototype.getUserColor = function () {
|
|
var myself = this,
|
|
world = this.world(),
|
|
hand = world.hand,
|
|
posInDocument = getDocumentPositionOf(world.worldCanvas),
|
|
mouseMoveBak = hand.processMouseMove,
|
|
mouseDownBak = hand.processMouseDown,
|
|
mouseUpBak = hand.processMouseUp;
|
|
|
|
hand.processMouseMove = function (event) {
|
|
var color;
|
|
hand.setPosition(new Point(
|
|
event.pageX - posInDocument.x,
|
|
event.pageY - posInDocument.y
|
|
));
|
|
color = world.getGlobalPixelColor(hand.position());
|
|
color.a = 255;
|
|
myself.propertiesControls.colorpicker.action(color);
|
|
};
|
|
|
|
hand.processMouseDown = nop;
|
|
|
|
hand.processMouseUp = function () {
|
|
myself.paper.currentTool = 'brush';
|
|
myself.paper.toolChanged('brush');
|
|
myself.refreshToolButtons();
|
|
hand.processMouseMove = mouseMoveBak;
|
|
hand.processMouseDown = mouseDownBak;
|
|
hand.processMouseUp = mouseUpBak;
|
|
};
|
|
};
|
|
|
|
// AdvancedColorPickerMorph //////////////////
|
|
|
|
// A large hsl color picker
|
|
|
|
PaintColorPickerMorph.prototype = new Morph();
|
|
PaintColorPickerMorph.prototype.constructor = PaintColorPickerMorph;
|
|
PaintColorPickerMorph.uber = Morph.prototype;
|
|
|
|
function PaintColorPickerMorph(extent, action) {
|
|
this.init(extent, action);
|
|
}
|
|
|
|
PaintColorPickerMorph.prototype.init = function (extent, action) {
|
|
this.setExtent(extent || new Point(200, 100));
|
|
this.action = action || nop;
|
|
this.drawNew();
|
|
};
|
|
|
|
PaintColorPickerMorph.prototype.drawNew = function () {
|
|
var x = 0,
|
|
y = 0,
|
|
can = newCanvas(this.extent()),
|
|
ctx = can.getContext("2d"),
|
|
colorselection,
|
|
r;
|
|
for (x = 0; x < this.width(); x += 1) {
|
|
for (y = 0; y < this.height() - 20; y += 1) {
|
|
ctx.fillStyle = "hsl(" +
|
|
(360 * x / this.width()) +
|
|
"," +
|
|
"100%," +
|
|
(y * 100 / (this.height() - 20)) +
|
|
"%)";
|
|
ctx.fillRect(x, y, 1, 1);
|
|
}
|
|
}
|
|
for (x = 0; x < this.width(); x += 1) {
|
|
r = Math.floor(255 * x / this.width());
|
|
ctx.fillStyle = "rgb(" + r + ", " + r + ", " + r + ")";
|
|
ctx.fillRect(x, this.height() - 20, 1, 10);
|
|
}
|
|
colorselection = ["black", "white", "gray"];
|
|
for (x = 0; x < colorselection.length; x += 1) {
|
|
ctx.fillStyle = colorselection[x];
|
|
ctx.fillRect(
|
|
x * this.width() / colorselection.length,
|
|
this.height() - 10,
|
|
this.width() / colorselection.length,
|
|
10
|
|
);
|
|
}
|
|
for (x = this.width() * 2 / 3; x < this.width(); x += 2) {
|
|
for (y = this.height() - 10; y < this.height(); y += 2) {
|
|
if ((x + y) / 2 % 2 === 0) {
|
|
ctx.fillStyle = "#DDD";
|
|
ctx.fillRect(x, y, 2, 2);
|
|
}
|
|
}
|
|
}
|
|
this.image = can;
|
|
};
|
|
|
|
PaintColorPickerMorph.prototype.mouseDownLeft = function (pos) {
|
|
if ((pos.subtract(this.position()).x > this.width() * 2 / 3) &&
|
|
(pos.subtract(this.position()).y > this.height() - 10)) {
|
|
this.action("transparent");
|
|
} else {
|
|
this.action(this.getPixelColor(pos));
|
|
}
|
|
};
|
|
|
|
PaintColorPickerMorph.prototype.mouseMove =
|
|
PaintColorPickerMorph.prototype.mouseDownLeft;
|
|
|
|
// PaintCanvasMorph ///////////////////////////
|
|
/*
|
|
A canvas which reacts to drag events to
|
|
modify its image, based on a 'tool' property.
|
|
*/
|
|
|
|
PaintCanvasMorph.prototype = new Morph();
|
|
PaintCanvasMorph.prototype.constructor = PaintCanvasMorph;
|
|
PaintCanvasMorph.uber = Morph.prototype;
|
|
|
|
function PaintCanvasMorph(shift) {
|
|
this.init(shift);
|
|
}
|
|
|
|
PaintCanvasMorph.prototype.init = function (shift) {
|
|
this.rotationCenter = new Point(240, 180);
|
|
this.dragRect = null;
|
|
this.previousDragPoint = null;
|
|
this.currentTool = "brush";
|
|
this.dragRect = new Rectangle();
|
|
// rectangle with origin being the starting drag position and
|
|
// corner being the current drag position
|
|
this.mask = newCanvas(this.extent()); // Temporary canvas
|
|
this.paper = newCanvas(this.extent()); // Actual canvas
|
|
this.erasermask = newCanvas(this.extent()); // eraser memory
|
|
this.background = newCanvas(this.extent()); // checkers
|
|
this.settings = {
|
|
"primarycolor": new Color(0, 0, 0, 255), // usually fill color
|
|
"secondarycolor": new Color(0, 0, 0, 255), // (unused)
|
|
"linewidth": 3 // stroke width
|
|
};
|
|
this.brushBuffer = [];
|
|
this.undoBuffer = [];
|
|
this.isShiftPressed = shift || function () {
|
|
var key = this.world().currentKey;
|
|
return (key === 16);
|
|
};
|
|
this.buildContents();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.scale = function (x, y) {
|
|
this.mask = newCanvas(this.extent());
|
|
var c = newCanvas(this.extent());
|
|
c.getContext("2d").save();
|
|
c.getContext("2d").translate(
|
|
this.rotationCenter.x,
|
|
this.rotationCenter.y
|
|
);
|
|
c.getContext("2d").scale(1 + x, 1 + y);
|
|
c.getContext("2d").drawImage(
|
|
this.paper,
|
|
-this.rotationCenter.x,
|
|
-this.rotationCenter.y
|
|
);
|
|
c.getContext("2d").restore();
|
|
this.paper = c;
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.cacheUndo = function () {
|
|
var cachecan = newCanvas(this.extent());
|
|
this.merge(this.paper, cachecan);
|
|
this.undoBuffer.push(cachecan);
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.undo = function () {
|
|
if (this.undoBuffer.length > 0) {
|
|
this.paper = newCanvas(this.extent());
|
|
this.mask.width = this.mask.width + 1 - 1;
|
|
this.merge(this.undoBuffer.pop(), this.paper);
|
|
this.drawNew();
|
|
this.changed();
|
|
}
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.merge = function (a, b) {
|
|
b.getContext("2d").drawImage(a, 0, 0);
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.centermerge = function (a, b) {
|
|
b.getContext("2d").drawImage(
|
|
a,
|
|
(b.width - a.width) / 2,
|
|
(b.height - a.height) / 2
|
|
);
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.clearCanvas = function () {
|
|
this.buildContents();
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.toolChanged = function (tool) {
|
|
this.mask = newCanvas(this.extent());
|
|
if (tool === "crosshairs") {
|
|
this.drawcrosshair();
|
|
}
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.drawcrosshair = function (context) {
|
|
var ctx = context || this.mask.getContext("2d"),
|
|
rp = this.rotationCenter;
|
|
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = 'black';
|
|
ctx.clearRect(0, 0, this.mask.width, this.mask.height);
|
|
|
|
// draw crosshairs:
|
|
ctx.globalAlpha = 0.5;
|
|
|
|
// circle around center:
|
|
ctx.fillStyle = 'white';
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
rp.x,
|
|
rp.y,
|
|
20,
|
|
radians(0),
|
|
radians(360),
|
|
false
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
rp.x,
|
|
rp.y,
|
|
10,
|
|
radians(0),
|
|
radians(360),
|
|
false
|
|
);
|
|
ctx.stroke();
|
|
|
|
// horizontal line:
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, rp.y);
|
|
ctx.lineTo(this.mask.width, rp.y);
|
|
ctx.stroke();
|
|
|
|
// vertical line:
|
|
ctx.beginPath();
|
|
ctx.moveTo(rp.x, 0);
|
|
ctx.lineTo(rp.x, this.mask.height);
|
|
ctx.stroke();
|
|
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.floodfill = function (sourcepoint) {
|
|
var width = this.paper.width,
|
|
height = this.paper.height,
|
|
ctx = this.paper.getContext("2d"),
|
|
img = ctx.getImageData(0, 0, width, height),
|
|
data = img.data,
|
|
stack = [Math.round(sourcepoint.y) * width + sourcepoint.x],
|
|
currentpoint,
|
|
read,
|
|
sourcecolor,
|
|
checkpoint;
|
|
read = function (p) {
|
|
var d = p * 4;
|
|
return [data[d], data[d + 1], data[d + 2], data[d + 3]];
|
|
};
|
|
sourcecolor = read(stack[0]);
|
|
checkpoint = function (p) {
|
|
return p[0] === sourcecolor[0] &&
|
|
p[1] === sourcecolor[1] &&
|
|
p[2] === sourcecolor[2] &&
|
|
p[3] === sourcecolor[3];
|
|
};
|
|
|
|
// if already filled, abort
|
|
if (sourcecolor[3] === 0 &&
|
|
this.settings.primarycolor === "transparent") {
|
|
return;
|
|
}
|
|
if (sourcecolor[0] === this.settings.primarycolor.r &&
|
|
sourcecolor[1] === this.settings.primarycolor.g &&
|
|
sourcecolor[2] === this.settings.primarycolor.b &&
|
|
sourcecolor[3] === this.settings.primarycolor.a) {
|
|
return;
|
|
}
|
|
if (sourcecolor[3] === 0 && this.settings.primarycolor.a === 0) {
|
|
return;
|
|
}
|
|
|
|
while (stack.length > 0) {
|
|
currentpoint = stack.pop();
|
|
if (checkpoint(read(currentpoint))) {
|
|
if (currentpoint % width > 1) {
|
|
stack.push(currentpoint + 1);
|
|
stack.push(currentpoint - 1);
|
|
}
|
|
if (currentpoint > 0 && currentpoint < height * width) {
|
|
stack.push(currentpoint + width);
|
|
stack.push(currentpoint - width);
|
|
}
|
|
}
|
|
if (this.settings.primarycolor === "transparent") {
|
|
data[currentpoint * 4 + 3] = 0;
|
|
} else {
|
|
data[currentpoint * 4] = this.settings.primarycolor.r;
|
|
data[currentpoint * 4 + 1] = this.settings.primarycolor.g;
|
|
data[currentpoint * 4 + 2] = this.settings.primarycolor.b;
|
|
data[currentpoint * 4 + 3] = this.settings.primarycolor.a * 255;
|
|
}
|
|
}
|
|
ctx.putImageData(img, 0, 0);
|
|
this.drawNew();
|
|
this.changed();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.mouseDownLeft = function (pos) {
|
|
this.cacheUndo();
|
|
this.dragRect.origin = pos.subtract(this.bounds.origin);
|
|
this.dragRect.corner = pos.subtract(this.bounds.origin);
|
|
this.previousDragPoint = this.dragRect.corner.copy();
|
|
if (this.currentTool === 'crosshairs') {
|
|
this.rotationCenter = pos.subtract(this.bounds.origin);
|
|
this.drawcrosshair();
|
|
return;
|
|
}
|
|
if (this.currentTool === "paintbucket") {
|
|
return this.floodfill(pos.subtract(this.bounds.origin));
|
|
}
|
|
if (this.settings.primarycolor === "transparent" &&
|
|
this.currentTool !== "crosshairs") {
|
|
this.erasermask = newCanvas(this.extent());
|
|
this.merge(this.paper, this.erasermask);
|
|
}
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.mouseMove = function (pos) {
|
|
if (this.currentTool === "paintbucket") {
|
|
return;
|
|
}
|
|
|
|
var relpos = pos.subtract(this.bounds.origin),
|
|
mctx = this.mask.getContext("2d"),
|
|
pctx = this.paper.getContext("2d"),
|
|
x = this.dragRect.origin.x, // original drag X
|
|
y = this.dragRect.origin.y, // original drag y
|
|
p = relpos.x, // current drag x
|
|
q = relpos.y, // current drag y
|
|
w = (p - x) / 2, // half the rect width
|
|
h = (q - y) / 2, // half the rect height
|
|
i, // iterator number
|
|
width = this.paper.width;
|
|
|
|
mctx.save();
|
|
function newW() {
|
|
return Math.max(Math.abs(w), Math.abs(h)) * (w / Math.abs(w));
|
|
}
|
|
function newH() {
|
|
return Math.max(Math.abs(w), Math.abs(h)) * (h / Math.abs(h));
|
|
}
|
|
this.brushBuffer.push([p, q]);
|
|
mctx.lineWidth = this.settings.linewidth;
|
|
mctx.clearRect(0, 0, this.bounds.width(), this.bounds.height()); // mask
|
|
|
|
this.dragRect.corner = relpos.subtract(this.dragRect.origin); // reset crn
|
|
|
|
if (this.settings.primarycolor === "transparent" &&
|
|
this.currentTool !== "crosshairs") {
|
|
this.merge(this.erasermask, this.mask);
|
|
pctx.clearRect(0, 0, this.bounds.width(), this.bounds.height());
|
|
mctx.globalCompositeOperation = "destination-out";
|
|
} else {
|
|
mctx.fillStyle = this.settings.primarycolor.toString();
|
|
mctx.strokeStyle = this.settings.primarycolor.toString();
|
|
}
|
|
switch (this.currentTool) {
|
|
case "rectangle":
|
|
if (this.isShiftPressed()) {
|
|
mctx.strokeRect(x, y, newW() * 2, newH() * 2);
|
|
} else {
|
|
mctx.strokeRect(x, y, w * 2, h * 2);
|
|
}
|
|
break;
|
|
case "rectangleSolid":
|
|
if (this.isShiftPressed()) {
|
|
mctx.fillRect(x, y, newW() * 2, newH() * 2);
|
|
} else {
|
|
mctx.fillRect(x, y, w * 2, h * 2);
|
|
}
|
|
break;
|
|
case "brush":
|
|
mctx.lineCap = "round";
|
|
mctx.lineJoin = "round";
|
|
mctx.beginPath();
|
|
mctx.moveTo(this.brushBuffer[0][0], this.brushBuffer[0][1]);
|
|
for (i = 0; i < this.brushBuffer.length; i += 1) {
|
|
mctx.lineTo(this.brushBuffer[i][0], this.brushBuffer[i][1]);
|
|
}
|
|
mctx.stroke();
|
|
break;
|
|
case "line":
|
|
mctx.beginPath();
|
|
mctx.moveTo(x, y);
|
|
if (this.isShiftPressed()) {
|
|
if (Math.abs(h) > Math.abs(w)) {
|
|
mctx.lineTo(x, q);
|
|
} else {
|
|
mctx.lineTo(p, y);
|
|
}
|
|
} else {
|
|
mctx.lineTo(p, q);
|
|
}
|
|
mctx.stroke();
|
|
break;
|
|
case "circle":
|
|
case "circleSolid":
|
|
mctx.beginPath();
|
|
if (this.isShiftPressed()) {
|
|
mctx.arc(
|
|
x,
|
|
y,
|
|
new Point(x, y).distanceTo(new Point(p, q)),
|
|
0,
|
|
Math.PI * 2,
|
|
false
|
|
);
|
|
} else {
|
|
for (i = 0; i < width; i += 1) {
|
|
mctx.lineTo(
|
|
i,
|
|
(2 * h) * Math.sqrt(2 - Math.pow(
|
|
(i - x) / (2 * w),
|
|
2
|
|
)) + y
|
|
);
|
|
}
|
|
for (i = width; i > 0; i -= 1) {
|
|
mctx.lineTo(
|
|
i,
|
|
-1 * (2 * h) * Math.sqrt(2 - Math.pow(
|
|
(i - x) / (2 * w),
|
|
2
|
|
)) + y
|
|
);
|
|
}
|
|
}
|
|
mctx.closePath();
|
|
if (this.currentTool === "circleSolid") {
|
|
mctx.fill();
|
|
} else {
|
|
if (this.currentTool === "circle") {
|
|
mctx.stroke();
|
|
}
|
|
}
|
|
break;
|
|
case "crosshairs":
|
|
this.rotationCenter = relpos.copy();
|
|
this.drawcrosshair(mctx);
|
|
break;
|
|
case "eraser":
|
|
this.merge(this.paper, this.mask);
|
|
mctx.save();
|
|
mctx.globalCompositeOperation = "destination-out";
|
|
mctx.beginPath();
|
|
mctx.moveTo(this.brushBuffer[0][0], this.brushBuffer[0][1]);
|
|
for (i = 0; i < this.brushBuffer.length; i += 1) {
|
|
mctx.lineTo(this.brushBuffer[i][0], this.brushBuffer[i][1]);
|
|
}
|
|
mctx.stroke();
|
|
mctx.restore();
|
|
this.paper = newCanvas(this.extent());
|
|
this.merge(this.mask, this.paper);
|
|
break;
|
|
default:
|
|
nop();
|
|
}
|
|
this.previousDragPoint = relpos;
|
|
this.drawNew();
|
|
this.changed();
|
|
mctx.restore();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.mouseClickLeft = function () {
|
|
if (this.currentTool !== "crosshairs") {
|
|
this.merge(this.mask, this.paper);
|
|
}
|
|
this.brushBuffer = [];
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.mouseLeaveDragging
|
|
= PaintCanvasMorph.prototype.mouseClickLeft;
|
|
|
|
PaintCanvasMorph.prototype.buildContents = function () {
|
|
this.background = newCanvas(this.extent());
|
|
this.paper = newCanvas(this.extent());
|
|
this.mask = newCanvas(this.extent());
|
|
this.erasermask = newCanvas(this.extent());
|
|
var i, j, bkctx = this.background.getContext("2d");
|
|
for (i = 0; i < this.background.width; i += 5) {
|
|
for (j = 0; j < this.background.height; j += 5) {
|
|
if ((i + j) / 5 % 2 === 1) {
|
|
bkctx.fillStyle = "rgba(255, 255, 255, 1)";
|
|
} else {
|
|
bkctx.fillStyle = "rgba(255, 255, 255, 0.3)";
|
|
}
|
|
bkctx.fillRect(i, j, 5, 5);
|
|
}
|
|
}
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.drawNew = function () {
|
|
var can = newCanvas(this.extent());
|
|
this.merge(this.background, can);
|
|
this.merge(this.paper, can);
|
|
this.merge(this.mask, can);
|
|
this.image = can;
|
|
this.drawFrame();
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.drawFrame = function () {
|
|
var context, borderColor;
|
|
|
|
context = this.image.getContext('2d');
|
|
if (this.parent) {
|
|
this.color = this.parent.color.lighter(this.contrast * 0.75);
|
|
borderColor = this.parent.color;
|
|
} else {
|
|
borderColor = new Color(120, 120, 120);
|
|
}
|
|
context.fillStyle = this.color.toString();
|
|
|
|
// cache my border colors
|
|
this.cachedClr = borderColor.toString();
|
|
this.cachedClrBright = borderColor.lighter(this.contrast)
|
|
.toString();
|
|
this.cachedClrDark = borderColor.darker(this.contrast).toString();
|
|
this.drawRectBorder(context);
|
|
};
|
|
|
|
PaintCanvasMorph.prototype.drawRectBorder
|
|
= InputFieldMorph.prototype.drawRectBorder;
|
|
|
|
PaintCanvasMorph.prototype.edge
|
|
= InputFieldMorph.prototype.edge;
|
|
|
|
PaintCanvasMorph.prototype.fontSize
|
|
= InputFieldMorph.prototype.fontSize;
|
|
|
|
PaintCanvasMorph.prototype.typeInPadding
|
|
= InputFieldMorph.prototype.typeInPadding;
|
|
|
|
PaintCanvasMorph.prototype.contrast
|
|
= InputFieldMorph.prototype.contrast;
|