diff --git a/history.txt b/history.txt
index 816334a9..e3a39342 100755
--- a/history.txt
+++ b/history.txt
@@ -1700,3 +1700,7 @@ ______
130510
------
* Reset Password via e-mailed link (frontend only)
+
+140514
+------
+* paint.js: Paint editor, first version, contributed by Kartik Chandra, Yay!!
diff --git a/paint.js b/paint.js
new file mode 100644
index 00000000..adb53f48
--- /dev/null
+++ b/paint.js
@@ -0,0 +1,1097 @@
+/*
+ paint.js
+ Paint editor for Snap!
+ Inspired by the Scratch paint editor.
+
+ written by Kartik Chandra
+
+ Latest revision: May 10 (Kartik)
+
+ This file is part of Snap!.
+
+ --current changes
+ Shrinkwrap
+ Draw crosshairs immediately
+ TRANSPARENT PAINT
+ Line width viewer
+
+ --To-Do list (in rough order of priority):
+ Eraser tool
+
+ After release:
+ --
+ rgba sliders
+ Import image
+ Zoom/pan/Selection tools
+ Pick color from canvas
+ */
+
+/*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
+ */
+
+// Global definitions
+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
+
+ // 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.addBody(new AlignmentMorph('row', this.padding));
+ this.controls = new AlignmentMorph('column', this.padding);
+ this.controls.alignment = 'left';
+
+ this.edits = new AlignmentMorph('row', this.padding);
+ 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);
+
+ this.buildToolbox();
+ this.controls.add(this.toolbox);
+
+ 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 (free draw)",
+ rectangle:
+ "Stroked Rectangle (shift: square)",
+ circle:
+ "Stroked Ellipse (shift: circle)",
+ eraser:
+ "Eraser tool",
+ crosshairs:
+ "Set the rotation center",
+
+ line:
+ "Line tool (shift: vertical/horizontal)",
+ rectangleSolid:
+ "Filled Rectangle (shift: square)",
+ circleSolid:
+ "Filled Ellipse (shift: circle)",
+ paintbucket:
+ "Fill a region"
+ },
+ 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(); },
+ "Undo last action"
+ ));
+
+ this.edits.add(this.pushButton(
+ "clear",
+ function () {paper.clearCanvas(); },
+ "Clear the paper"
+ ));
+ this.edits.fixLayout();
+};
+
+
+// Open the editor in a world with an optional image to edit
+PaintEditorMorph.prototype.openIn = function(world, oldim, oldrc, callback) {
+ this.setCenter(world.center());
+ this.oldim = oldim;
+ this.oldrc = oldrc.copy();
+ this.callback = callback || nop;
+ world.add(this);
+ this.world().keyboardReceiver = this;
+ this.processKeyUp = function() {
+ this.shift = false;
+ this.propertiesControls.constrain.refresh();
+ };
+ this.processKeyDown = function() {
+ this.shift = this.world().currentKey === 16;
+ this.propertiesControls.constrain.refresh();
+ };
+ this.fixLayout(); // merge oldim - factor out into separate function
+ this.changed();
+};
+
+PaintEditorMorph.prototype.fixLayout = function() {
+ if (this.paper) {
+ this.paper.setExtent(new Point(480, 360));
+ this.paper.fixLayout();
+ 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();
+ }
+ if (this.controls) {this.controls.fixLayout(); }
+ if (this.body) {this.body.fixLayout(); }
+ PaintEditorMorph.uber.fixLayout.call(this);
+};
+
+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.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("Pen 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();
+ },
+ 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
+ );
+};
+
+// 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.fixLayout();
+ 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);
+ };
+};
+
+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.fixLayout();
+ 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() {
+ var mctx = this.mask.getContext("2d"),
+ pos = this.rotationCenter;
+
+ mctx.strokeStyle = "rgba(0,0,0,0.6)";
+ mctx.lineWidth = 5;
+ mctx.beginPath();
+ mctx.moveTo(pos.x, 0);
+ mctx.lineTo(pos.x, this.extent().y);
+ mctx.moveTo(0, pos.y);
+ mctx.lineTo(this.extent().x, pos.y);
+ mctx.stroke();
+
+ mctx.strokeStyle = "rgba(255,255,255,0.6)";
+ mctx.lineWidth = 1;
+ mctx.beginPath();
+ mctx.moveTo(pos.x, 0);
+ mctx.lineTo(pos.x, this.extent().y);
+ mctx.moveTo(0, pos.y);
+ mctx.lineTo(this.extent().x, pos.y);
+ mctx.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];
+ };
+ while (stack.length > 0) {
+ currentpoint = stack.pop();
+ if (checkpoint(read(currentpoint))) {
+ if (currentpoint % 480 > 1) {
+ stack.push(currentpoint + 1);
+ stack.push(currentpoint - 1);
+ }
+ if (currentpoint > 0 && currentpoint < 360 * 480) {
+ 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;
+ }
+ }
+ ctx.putImageData(img, 0, 0);
+};
+
+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 === "paintbucket") {
+ 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) {
+ 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
+ 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 < 480; i += 1) {
+ mctx.lineTo(
+ i,
+ (2 * h) * Math.sqrt(2 - Math.pow(
+ (i - x) / (2 * w),
+ 2
+ )) + y
+ );
+ }
+ for (i = 480; 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":
+ mctx.save();
+ mctx.globalCompositeOperation = "source-over";
+ mctx.strokeStyle = "rgba(0,0,0,0.6)";
+ mctx.lineWidth = 5;
+ mctx.beginPath();
+ mctx.moveTo(p, 0);
+ mctx.lineTo(p, this.extent().y);
+ mctx.moveTo(0, q);
+ mctx.lineTo(this.extent().x, q);
+ mctx.stroke();
+ mctx.strokeStyle = "rgba(255,255,255,0.6)";
+ mctx.lineWidth = 1;
+ mctx.beginPath();
+ mctx.moveTo(p, 0);
+ mctx.lineTo(p, this.extent().y);
+ mctx.moveTo(0, q);
+ mctx.lineTo(this.extent().x, q);
+ mctx.stroke();
+ this.rotationCenter = relpos.copy();
+ mctx.restore();
+ 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.fixLayout = 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;
+
+// Changes to gui.js and object.js (temporarily here) //////////////////
+// These will be incorporated into the respective files later
+// They add costume editing functionality to Snap!.
+////////////////////////////////////////////////////////////////////////
+CostumeIconMorph.prototype.editCostume = function () {
+ var ide = this.parentThatIsA(IDE_Morph);
+ this.object.edit(this.world(), ide);
+};
+
+Costume.prototype.edit = function (aWorld, anIDE, isnew, oncancel, onsubmit) {
+ var myself = this,
+ editor = new PaintEditorMorph();
+ editor.oncancel = oncancel || nop;
+ editor.openIn(
+ aWorld,
+ isnew ?
+ newCanvas(new Point(480, 360)) :
+ this.contents,
+ isnew ?
+ new Point(240, 180) :
+ this.rotationCenter,
+ function (img, rc) {
+ myself.contents = img;
+ myself.rotationCenter = rc;
+ myself.shrinkWrap();
+ myself.version = Date.now();
+ aWorld.changed();
+ if (anIDE) {
+ anIDE.currentSprite.wearCostume(myself);
+ anIDE.hasChangedMedia = true;
+ }
+ (onsubmit || nop)();
+ }
+ );
+};
+
+
+
+
+IDE_Morph.prototype.createCorralBar = function () {
+ // assumes the stage has already been created
+ var padding = 5,
+ newbutton,
+ paintbutton,
+ colors = [
+ this.groupColor,
+ this.frameColor.darker(50),
+ this.frameColor.darker(50)
+ ];
+ if (this.corralBar) {
+ this.corralBar.destroy();
+ }
+ this.corralBar = new Morph();
+ this.corralBar.color = this.frameColor;
+ this.corralBar.setHeight(this.logo.height()); // height is fixed
+ this.add(this.corralBar);
+ // new sprite button
+ newbutton = new PushButtonMorph(
+ this,
+ "addNewSprite",
+ new SymbolMorph("turtle", 14)
+ );
+ newbutton.corner = 12;
+ newbutton.color = colors[0];
+ newbutton.highlightColor = colors[1];
+ newbutton.pressColor = colors[2];
+ newbutton.labelMinExtent = new Point(36, 18);
+ newbutton.padding = 0;
+ newbutton.labelShadowOffset = new Point(-1, -1);
+ newbutton.labelShadowColor = colors[1];
+ newbutton.labelColor = new Color(255, 255, 255);
+ newbutton.contrast = this.buttonContrast;
+ newbutton.drawNew();
+ newbutton.hint = "add a new Turtle sprite";
+ newbutton.fixLayout();
+ newbutton.setCenter(this.corralBar.center());
+ newbutton.setLeft(this.corralBar.left() + padding);
+ this.corralBar.add(newbutton);
+ paintbutton = new PushButtonMorph(
+ this,
+ "paintNewSprite",
+ new SymbolMorph("brush", 15)
+ );
+ paintbutton.corner = 12;
+ paintbutton.color = colors[0];
+ paintbutton.highlightColor = colors[1];
+ paintbutton.pressColor = colors[2];
+ paintbutton.labelMinExtent = new Point(36, 18);
+ paintbutton.padding = 0;
+ paintbutton.labelShadowOffset = new Point(-1, -1);
+ paintbutton.labelShadowColor = colors[1];
+ paintbutton.labelColor = new Color(255, 255, 255);
+ paintbutton.contrast = this.buttonContrast;
+ paintbutton.drawNew();
+ paintbutton.hint = "paint a new sprite";
+ paintbutton.fixLayout();
+ paintbutton.setCenter(this.corralBar.center());
+ paintbutton.setLeft(
+ this.corralBar.left() + padding + newbutton.width() + padding
+ );
+ this.corralBar.add(paintbutton);
+};
+
+IDE_Morph.prototype.paintNewSprite = function() {
+ var sprite = new SpriteMorph(this.globalVariables),
+ cos = new Costume();
+ sprite.name = sprite.name +
+ (this.corral.frame.contents.children.length + 1);
+ sprite.setCenter(this.stage.center());
+ this.stage.add(sprite);
+ this.sprites.add(sprite);
+ this.corral.addSprite(sprite);
+ this.selectSprite(sprite);
+ cos.edit(this.world(), this, true, function() {sprite.remove(); });
+ sprite.addCostume(cos);
+};
+
+WardrobeMorph.prototype.updateList = function () {
+ var myself = this,
+ x = this.left() + 5,
+ y = this.top() + 5,
+ padding = 4,
+ oldFlag = Morph.prototype.trackChanges,
+ oldPos = this.contents.position(),
+ icon,
+ template,
+ txt,
+ paintbutton,
+ colors = [
+ new Color(50, 50, 50, 1),
+ new Color(60, 60, 60, 1),
+ new Color(70, 70, 70, 1)
+ ];
+ this.changed();
+ oldFlag = Morph.prototype.trackChanges;
+ Morph.prototype.trackChanges = false;
+ this.contents.destroy();
+ this.contents = new FrameMorph(this);
+ this.contents.acceptsDrops = false;
+ this.contents.reactToDropOf = function (icon) {
+ myself.reactToDropOf(icon);
+ };
+ this.addBack(this.contents);
+ icon = new TurtleIconMorph(this.sprite);
+ icon.setPosition(new Point(x, y));
+ myself.addContents(icon);
+ y = icon.bottom() + padding;
+
+ paintbutton = new PushButtonMorph(
+ this,
+ "paintNew",
+ new SymbolMorph("brush", 15)
+ );
+ paintbutton.padding = 5;
+ paintbutton.corner = 12;
+ paintbutton.color = colors[0];
+ paintbutton.highlightColor = colors[1];
+ paintbutton.pressColor = colors[2];
+ paintbutton.labelMinExtent = new Point(36, 18);
+ paintbutton.labelShadowOffset = new Point(-1, -1);
+ paintbutton.labelShadowColor = colors[1];
+ paintbutton.labelColor = new Color(255, 255, 255);
+ paintbutton.contrast = this.buttonContrast;
+ paintbutton.drawNew();
+ paintbutton.hint = "Paint a new costume";
+ paintbutton.setPosition(new Point(x, y));
+ paintbutton.fixLayout();
+
+ this.addContents(paintbutton);
+ y = paintbutton.bottom() + padding;
+ txt = new TextMorph(localize(
+ "costumes tab help" // look up long string in translator
+ ));
+ txt.fontSize = 9;
+ txt.setColor(new Color(230, 230, 230));
+ txt.setPosition(new Point(x, y));
+ this.addContents(txt);
+ y = txt.bottom() + padding;
+ this.sprite.costumes.asArray().forEach(function (costume) {
+ template = icon = new CostumeIconMorph(costume, template);
+ icon.setPosition(new Point(x, y));
+ myself.addContents(icon);
+ y = icon.bottom() + padding;
+ });
+ this.costumesVersion = this.sprite.costumes.lastChanged;
+ this.contents.setPosition(oldPos);
+ this.adjustScrollBars();
+ Morph.prototype.trackChanges = oldFlag;
+ this.changed();
+ this.updateSelection();
+};
+
+WardrobeMorph.prototype.paintNew = function() {
+ var cos = new Costume(newCanvas(), "Untitled"),
+ myself = this;
+ cos.edit(this.world(), null, true, null, function() {
+ myself.sprite.addCostume(cos);
+ myself.updateList();
+ if (myself.parentThatIsA(IDE_Morph)) {
+ myself.parentThatIsA(IDE_Morph).currentSprite.wearCostume(cos);
+ }
+ });
+};
+
+CostumeIconMorph.prototype.userMenu = function () {
+ var menu = new MenuMorph(this);
+ if (!(this.object instanceof Costume)) {return null; }
+ menu.addItem("edit", "editCostume");
+ menu.addItem("rename", "renameCostume");
+ menu.addLine();
+ menu.addItem("duplicate", "duplicateCostume");
+ menu.addItem("delete", "removeCostume");
+ menu.addLine();
+ menu.addItem("export", "exportCostume");
+ return menu;
+};
+
+CostumeIconMorph.prototype.duplicateCostume = function() {
+ var wardrobe = this.parentThatIsA(WardrobeMorph),
+ ide = this.parentThatIsA(IDE_Morph),
+ newcos = this.object.copy(),
+ split = newcos.name.split(" ");
+ if (split[split.length - 1] === "copy") {
+ newcos.name += " 2";
+ } else if (isNaN(split[split.length - 1])) {
+ newcos.name = newcos.name + " copy";
+ } else {
+ split[split.length - 1] = Number(split[split.length - 1]) + 1;
+ newcos.name = split.join(" ");
+ }
+ wardrobe.sprite.addCostume(newcos);
+ wardrobe.updateList();
+ if (ide) {
+ ide.currentSprite.wearCostume(newcos);
+ }
+};
+
+
+// I had to change this because adding a "paint new" button changed the offset
+// of the costume (so the 5th child would be the 3rd costume, not the 4th as
+// it was before with only the text morph child).
+CostumeIconMorph.prototype.removeCostume = function () {
+ var wardrobe = this.parentThatIsA(WardrobeMorph),
+ idx = this.parent.children.indexOf(this),
+ ide = this.parentThatIsA(IDE_Morph);
+ wardrobe.removeCostumeAt(idx - 2);
+ if (ide.currentSprite.costume === this.object) {
+ ide.currentSprite.wearCostume(null);
+ }
+};
+
+
+Costume.prototype.shrinkWrap = function () {
+ // adjust my contents' bounds to my visible bounding box
+ var bb = this.boundingBox(),
+ ext = bb.extent(),
+ pic = newCanvas(ext),
+ ctx = pic.getContext('2d');
+
+ ctx.drawImage(
+ this.contents,
+ bb.origin.x,
+ bb.origin.y,
+ ext.x,
+ ext.y,
+ 0,
+ 0,
+ ext.x,
+ ext.y
+ );
+ this.rotationCenter = this.rotationCenter.subtract(bb.origin);
+ this.contents = pic;
+ this.version = Date.now();
+};
\ No newline at end of file
diff --git a/snap.html b/snap.html
index 2a0a175a..c14b3441 100755
--- a/snap.html
+++ b/snap.html
@@ -10,6 +10,7 @@
+