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 @@ +