/* paint.js a paint editor for Snap! inspired by the Scratch paint editor. written by Kartik Chandra Copyright (C) 2013 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 . 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 (bugfixes, Snap integration (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, SVG_Costume */ // Global stuff //////////////////////////////////////////////////////// modules.paint = '2013-May-14'; // 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(new Point(480, 360)); 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\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" }, 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.openIn = function(world, oldim, oldrc, callback) { // Open the editor in a world with an optional image to edit 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(); }; //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.fullChanged(); }; 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("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(); }, 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.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.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(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]; }; 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); 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 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": 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.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; // 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 () { if (this.object instanceof SVG_Costume) { this.object.editRotationPointOnly(this.world()); } else { this.object.edit( this.world(), this.parentThatIsA(IDE_Morph) ); } }; 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(), myself = this; 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() {myself.removeSprite(sprite); }, function () { sprite.addCostume(cos); sprite.wearCostume(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; 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 = 0; paintbutton.corner = 12; paintbutton.color = IDE_Morph.prototype.groupColor; paintbutton.highlightColor = IDE_Morph.prototype.frameColor.darker(50); paintbutton.pressColor = paintbutton.highlightColor; paintbutton.labelMinExtent = new Point(36, 18); paintbutton.labelShadowOffset = new Point(-1, -1); paintbutton.labelShadowColor = paintbutton.highlightColor; 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(); paintbutton.setCenter(icon.center()); paintbutton.setLeft(icon.right() + padding * 4); this.addContents(paintbutton); 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"); if (this.world().currentKey === 16) { // shift clicked menu.addItem( 'edit rotation point only...', 'editRotationPointOnly', null, new Color(100, 0, 0) ); } 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(); };