From 3f92313ae488d515f190652deed0945d76cb5de2 Mon Sep 17 00:00:00 2001 From: jmoenig Date: Tue, 14 May 2013 13:07:08 +0200 Subject: [PATCH] Paint editor fixes --- paint.js | 443 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 270 insertions(+), 173 deletions(-) diff --git a/paint.js b/paint.js index adb53f48..e6de9a26 100644 --- a/paint.js +++ b/paint.js @@ -1,46 +1,75 @@ /* - 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 + paint.js - After release: - -- - rgba sliders - Import image - Zoom/pan/Selection tools - Pick color from canvas + 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 + ToggleMorph, ToggleButtonMorph, BoxMorph, modules, radians, SVG_Costume */ -// Global definitions +// 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; @@ -54,7 +83,8 @@ function PaintEditorMorph() { PaintEditorMorph.prototype.init = function() { // additional properties: - this.paper = null; // paint canvas + this.paper = null; // paint canvas + this.oncancel = null; // initialize inherited properties: PaintEditorMorph.uber.init.call(this); @@ -70,7 +100,8 @@ PaintEditorMorph.prototype.init = function() { PaintEditorMorph.prototype.buildContents = function () { var myself = this; - this.paper = new PaintCanvasMorph(function() {return myself.shift; }); + 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); @@ -113,22 +144,22 @@ PaintEditorMorph.prototype.buildContents = function () { PaintEditorMorph.prototype.buildToolbox = function() { var tools = { brush: - "Paintbrush tool (free draw)", + "Paintbrush tool\n(free draw)", rectangle: - "Stroked Rectangle (shift: square)", + "Stroked Rectangle\n(shift: square)", circle: - "Stroked Ellipse (shift: circle)", + "Stroked Ellipse\n(shift: circle)", eraser: "Eraser tool", crosshairs: "Set the rotation center", line: - "Line tool (shift: vertical/horizontal)", + "Line tool\n(shift: vertical/horizontal)", rectangleSolid: - "Filled Rectangle (shift: square)", + "Filled Rectangle\n(shift: square)", circleSolid: - "Filled Ellipse (shift: circle)", + "Filled Ellipse\n(shift: circle)", paintbucket: "Fill a region" }, @@ -165,21 +196,18 @@ PaintEditorMorph.prototype.buildEdits = function() { this.edits.add(this.pushButton( "undo", - function () {paper.undo(); }, - "Undo last action" + function () {paper.undo(); } )); this.edits.add(this.pushButton( "clear", - function () {paper.clearCanvas(); }, - "Clear the paper" + function () {paper.clearCanvas(); } )); this.edits.fixLayout(); }; - -// Open the editor in a world with an optional image to edit 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(); @@ -194,29 +222,40 @@ PaintEditorMorph.prototype.openIn = function(world, oldim, oldrc, callback) { this.shift = this.world().currentKey === 16; this.propertiesControls.constrain.refresh(); }; - this.fixLayout(); // merge oldim - factor out into separate function - this.changed(); + + //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() { - 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 - ) - ); - } +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() { @@ -232,6 +271,11 @@ PaintEditorMorph.prototype.ok = function() { ); this.destroy(); }; + +PaintEditorMorph.prototype.cancel = function () { + if (this.oncancel) {this.oncancel(); } + this.destroy(); +}; PaintEditorMorph.prototype.populatePropertiesMenu = function() { var c = this.controls, @@ -317,7 +361,7 @@ PaintEditorMorph.prototype.populatePropertiesMenu = function() { c.add(pc.colorpicker); //c.add(pc.primaryColorButton); c.add(pc.primaryColorViewer); - c.add(new TextMorph("Pen size")); + c.add(new TextMorph("Brush size")); c.add(alpen); c.add(pc.constrain); }; @@ -354,8 +398,8 @@ PaintEditorMorph.prototype.pushButton = function(title, action, hint) { }; // AdvancedColorPickerMorph ////////////////// + // A large hsl color picker -////////////////////////////////////////////// PaintColorPickerMorph.prototype = new Morph(); PaintColorPickerMorph.prototype.constructor = PaintColorPickerMorph; @@ -425,11 +469,13 @@ PaintColorPickerMorph.prototype.mouseDownLeft = function(pos) { }; PaintColorPickerMorph.prototype.mouseMove = - PaintColorPickerMorph.prototype.mouseDownLeft; -// PaintCanvasMorph /////////////////////////// -// A canvas which reacts to drag events to -// modify its image, based on a 'tool' property. -/////////////////////////////////////////////// + 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; @@ -440,14 +486,13 @@ function PaintCanvasMorph(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 + // 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 @@ -462,7 +507,8 @@ PaintCanvasMorph.prototype.init = function(shift) { this.isShiftPressed = shift || function() { var key = this.world().currentKey; return (key === 16); - }; + }; + this.buildContents(); }; PaintCanvasMorph.prototype.cacheUndo = function() { @@ -483,7 +529,8 @@ PaintCanvasMorph.prototype.undo = function() { PaintCanvasMorph.prototype.merge = function(a, b) { b.getContext("2d").drawImage(a, 0, 0); -}; +}; + PaintCanvasMorph.prototype.centermerge = function(a, b) { b.getContext("2d").drawImage( a, @@ -507,33 +554,60 @@ PaintCanvasMorph.prototype.toolChanged = function(tool) { 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(); +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) { +PaintCanvasMorph.prototype.floodfill = function(sourcepoint) { var width = this.paper.width, height = this.paper.height, ctx = this.paper.getContext("2d"), @@ -555,7 +629,7 @@ PaintCanvasMorph.prototype.floodfill = function(sourcepoint) { p[2] === sourcecolor[2] && p[3] === sourcecolor[3]; }; - while (stack.length > 0) { + while (stack.length > 0) { currentpoint = stack.pop(); if (checkpoint(read(currentpoint))) { if (currentpoint % 480 > 1) { @@ -576,7 +650,9 @@ PaintCanvasMorph.prototype.floodfill = function(sourcepoint) { data[currentpoint * 4 + 3] = this.settings.primarycolor.a; } } - ctx.putImageData(img, 0, 0); + ctx.putImageData(img, 0, 0); + this.drawNew(); + this.changed(); }; PaintCanvasMorph.prototype.mouseDownLeft = function(pos) { @@ -584,8 +660,13 @@ PaintCanvasMorph.prototype.mouseDownLeft = function(pos) { 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") { - this.floodfill(pos.subtract(this.bounds.origin)); + return this.floodfill(pos.subtract(this.bounds.origin)); } if (this.settings.primarycolor === "transparent" && this.currentTool !== "crosshairs") { @@ -594,7 +675,11 @@ PaintCanvasMorph.prototype.mouseDownLeft = function(pos) { } }; -PaintCanvasMorph.prototype.mouseMove = function(pos) { +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"), @@ -707,27 +792,9 @@ PaintCanvasMorph.prototype.mouseMove = function(pos) { } } 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(); + case "crosshairs": + this.rotationCenter = relpos.copy(); + this.drawcrosshair(mctx); break; case "eraser": this.merge(this.paper, this.mask); @@ -752,30 +819,30 @@ PaintCanvasMorph.prototype.mouseMove = function(pos) { mctx.restore(); }; -PaintCanvasMorph.prototype.mouseClickLeft = function() { +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.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()); @@ -825,9 +892,16 @@ PaintCanvasMorph.prototype.contrast // 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); + +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) { @@ -857,9 +931,6 @@ Costume.prototype.edit = function (aWorld, anIDE, isnew, oncancel, onsubmit) { ); }; - - - IDE_Morph.prototype.createCorralBar = function () { // assumes the stage has already been created var padding = 5, @@ -869,14 +940,17 @@ IDE_Morph.prototype.createCorralBar = function () { 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, @@ -899,6 +973,7 @@ IDE_Morph.prototype.createCorralBar = function () { newbutton.setCenter(this.corralBar.center()); newbutton.setLeft(this.corralBar.left() + padding); this.corralBar.add(newbutton); + paintbutton = new PushButtonMorph( this, "paintNewSprite", @@ -922,21 +997,31 @@ IDE_Morph.prototype.createCorralBar = function () { 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); -}; +}; + +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, @@ -948,15 +1033,12 @@ WardrobeMorph.prototype.updateList = function () { icon, template, txt, - paintbutton, - colors = [ - new Color(50, 50, 50, 1), - new Color(60, 60, 60, 1), - new Color(70, 70, 70, 1) - ]; + paintbutton; + this.changed(); oldFlag = Morph.prototype.trackChanges; Morph.prototype.trackChanges = false; + this.contents.destroy(); this.contents = new FrameMorph(this); this.contents.acceptsDrops = false; @@ -964,6 +1046,7 @@ WardrobeMorph.prototype.updateList = function () { myself.reactToDropOf(icon); }; this.addBack(this.contents); + icon = new TurtleIconMorph(this.sprite); icon.setPosition(new Point(x, y)); myself.addContents(icon); @@ -974,31 +1057,37 @@ WardrobeMorph.prototype.updateList = function () { "paintNew", new SymbolMorph("brush", 15) ); - paintbutton.padding = 5; + paintbutton.padding = 0; paintbutton.corner = 12; - paintbutton.color = colors[0]; - paintbutton.highlightColor = colors[1]; - paintbutton.pressColor = colors[2]; + 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 = colors[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.fixLayout(); + paintbutton.setCenter(icon.center()); + paintbutton.setLeft(icon.right() + padding * 4); + + 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; + y = txt.bottom() + padding; + + this.sprite.costumes.asArray().forEach(function (costume) { template = icon = new CostumeIconMorph(costume, template); icon.setPosition(new Point(x, y)); @@ -1006,10 +1095,12 @@ WardrobeMorph.prototype.updateList = function () { y = icon.bottom() + padding; }); this.costumesVersion = this.sprite.costumes.lastChanged; + this.contents.setPosition(oldPos); this.adjustScrollBars(); Morph.prototype.trackChanges = oldFlag; this.changed(); + this.updateSelection(); }; @@ -1029,6 +1120,14 @@ 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"); @@ -1058,7 +1157,6 @@ CostumeIconMorph.prototype.duplicateCostume = function() { } }; - // 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). @@ -1072,7 +1170,6 @@ CostumeIconMorph.prototype.removeCostume = function () { } }; - Costume.prototype.shrinkWrap = function () { // adjust my contents' bounds to my visible bounding box var bb = this.boundingBox(),