From 8718e219a27804ccb3d920e84bfa6acb5a85f10e Mon Sep 17 00:00:00 2001 From: abakkk Date: Thu, 7 Mar 2019 14:28:35 +0100 Subject: [PATCH] Add transformations by pressing Ctrl key rectangle and text: rotate circle: extend (ellipse) and rotate line: cubic Bezier curve --- draw.js | 143 ++++++++++++++++++++++++++++++--- locale/draw-on-your-screen.pot | 27 ++++++- prefs.js | 20 +++-- stylesheet.css | 2 +- 4 files changed, 171 insertions(+), 21 deletions(-) diff --git a/draw.js b/draw.js index 72f34e8..e4ac7a9 100644 --- a/draw.js +++ b/draw.js @@ -38,7 +38,7 @@ const Prefs = Extension.imports.prefs; const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext; var Shapes = { NONE: 0, LINE: 1, ELLIPSE: 2, RECTANGLE: 3, TEXT: 4 }; -var ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Circle", 3: "Rectangle", 4: "Text" }; +var ShapeNames = { 0: "Free drawing", 1: "Line", 2: "Ellipse", 3: "Rectangle", 4: "Text" }; var LineCapNames = { 0: 'Butt', 1: 'Round', 2: 'Square' }; var LineJoinNames = { 0: 'Miter', 1: 'Round', 2: 'Bevel' }; var FontWeightNames = { 0: 'Normal', 1: 'Bold' }; @@ -119,7 +119,9 @@ var DrawingArea = new Lang.Class({ for (let i = 0; i < this.elements.length; i++) { this.elements[i].buildCairo(cr, false); - if (this.elements[i].fill && this.elements[i].shape != Shapes.LINE) + let isStraightLine = this.elements[i].shape == Shapes.LINE && + (this.elements[i].points.length < 3 || this.elements[i].points[2] == this.elements[i].points[1] || this.elements[i].points[2] == this.elements[i].points[0]); + if (this.elements[i].fill && !isStraightLine) cr.fill(); else cr.stroke(); @@ -227,6 +229,7 @@ var DrawingArea = new Lang.Class({ fill: fill, eraser: eraser, shape: this.currentShape == Shapes.TEXT ? Shapes.RECTANGLE : this.currentShape, + transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, text: '', font: { family: (this.currentFontFamilyId == 0 ? this.fontFamily : FontFamilyNames[this.currentFontFamilyId]), weight: this.currentFontWeight, style: this.currentFontStyle }, points: [[startX, startY]] @@ -237,7 +240,8 @@ var DrawingArea = new Lang.Class({ let [s, x, y] = this.transform_stage_point(coords[0], coords[1]); if (!s) return; - this._updateDrawing(x, y); + let controlPressed = event.has_control_modifier(); + this._updateDrawing(x, y, controlPressed); }); }, @@ -268,11 +272,17 @@ var DrawingArea = new Lang.Class({ this._redisplay(); }, - _updateDrawing: function(x, y) { + _updateDrawing: function(x, y, controlPressed) { if (!this.currentElement) return; if (this.currentElement.shape == Shapes.NONE) this.currentElement.addPoint(x, y, this.smoothedStroke); + else if (this.currentElement.shape == Shapes.RECTANGLE && (controlPressed || this.currentElement.transform.active)) + this.currentElement.transformRectangle(x, y); + else if (this.currentElement.shape == Shapes.ELLIPSE && (controlPressed || this.currentElement.transform.active)) + this.currentElement.transformEllipse(x, y); + else if (this.currentElement.shape == Shapes.LINE && (controlPressed || this.currentElement.transform.active)) + this.currentElement.transformLine(x, y); else this.currentElement.points[1] = [x, y]; this._redisplay(); @@ -549,24 +559,37 @@ var DrawingElement = new Lang.Class({ Clutter.cairo_set_source_color(cr, this.color); - let [points, shape] = [this.points, this.shape]; + let [points, shape, trans] = [this.points, this.shape, this.transform]; - if (shape == Shapes.NONE || shape == Shapes.LINE) { + if (shape == Shapes.LINE && points.length == 3) { + cr.moveTo(points[0][0], points[0][1]); + cr.curveTo(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1]); + } else if (shape == Shapes.NONE || shape == Shapes.LINE) { cr.moveTo(points[0][0], points[0][1]); for (let j = 1; j < points.length; j++) { cr.lineTo(points[j][0], points[j][1]); } + } else if (shape == Shapes.ELLIPSE && points.length == 2) { + this.rotate(cr, trans.angle + trans.startAngle, trans.center[0], trans.center[1]); + this.scale(cr, trans.ratio, trans.center[0], trans.center[1]); let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); cr.arc(points[0][0], points[0][1], r, 0, 2 * Math.PI); + this.scale(cr, 1 / trans.ratio, trans.center[0], trans.center[1]); + this.rotate(cr, - (trans.angle + trans.startAngle), trans.center[0], trans.center[1]); } else if (shape == Shapes.RECTANGLE && points.length == 2) { + this.rotate(cr, trans.angle, trans.center[0], trans.center[1]); cr.rectangle(points[0][0], points[0][1], points[1][0] - points[0][0], points[1][1] - points[0][1]); + this.rotate(cr, - trans.angle, trans.center[0], trans.center[1]); + } else if (shape == Shapes.TEXT && points.length == 2) { + this.rotate(cr, trans.angle, trans.center[0], trans.center[1]); cr.selectFontFace(this.font.family, this.font.style, this.font.weight); cr.setFontSize(Math.abs(points[1][1] - points[0][1])); cr.moveTo(Math.min(points[0][0], points[1][0]), Math.max(points[0][1], points[1][1])); cr.showText((showTextCursor) ? (this.text + "_") : this.text); + this.rotate(cr, - trans.angle, trans.center[0], trans.center[1]); } }, @@ -574,9 +597,11 @@ var DrawingElement = new Lang.Class({ let row = "\n "; let points = this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]); let color = this.eraser ? bgColor : this.color.to_string(); - let attributes = `fill="${this.fill ? color : 'transparent'}" ` + - `stroke="${this.fill ? 'transparent' : color}" ` + - `${this.fill ? 'stroke-opacity="0"' : 'fill-opacity="0"'} ` + + let isStraightLine = this.shape == Shapes.LINE && (points.length < 3 || points[2] == points[1] || points[2] == points[0]); + let fill = this.fill && !isStraightLine; + let attributes = `fill="${fill ? color : 'transparent'}" ` + + `stroke="${fill ? 'transparent' : color}" ` + + `${fill ? 'stroke-opacity="0"' : 'fill-opacity="0"'} ` + `stroke-width="${this.line.lineWidth}" ` + `stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}" ` + `stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; @@ -584,21 +609,43 @@ var DrawingElement = new Lang.Class({ if (this.dash.array[0] > 0 && this.dash.array[1] > 0) attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - if (this.shape == Shapes.NONE || this.shape == Shapes.LINE) { + if (this.shape == Shapes.LINE && points.length == 3) { row += ``; + } else if (this.shape == Shapes.NONE || this.shape == Shapes.LINE) { + row += ``; + + } else if (this.shape == Shapes.ELLIPSE && points.length == 2 && this.transform.ratio != 1) { + let ry = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); + let rx = ry * this.transform.ratio; + let angle = (this.transform.angle + this.transform.startAngle) * 180 / Math.PI; + row += ``; - row += `${this.fill ? 'z' : ''}"/>`; } else if (this.shape == Shapes.ELLIPSE && points.length == 2) { let r = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); row += ``; + } else if (this.shape == Shapes.RECTANGLE && points.length == 2) { + let transAttribute = ""; + if (this.transform.angle != 0) { + let angle = this.transform.angle * 180 / Math.PI; + transAttribute = ` transform="rotate(${angle}, ${this.transform.center[0]}, ${this.transform.center[1]})"`; + } row += ``; + `width="${Math.abs(points[1][0] - points[0][0])}" height="${Math.abs(points[1][1] - points[0][1])}"${transAttribute}/>`; + } else if (this.shape == Shapes.TEXT && points.length == 2) { + let transAttribute = ""; + if (this.transform.angle != 0) { + let angle = this.transform.angle * 180 / Math.PI; + transAttribute = ` transform="rotate(${angle}, ${this.transform.center[0]}, ${this.transform.center[1]})"`; + } attributes = `fill="${color}" ` + `stroke="transparent" ` + `stroke-opacity="0" ` + @@ -607,7 +654,7 @@ var DrawingElement = new Lang.Class({ `font-weight="${FontWeightNames[this.font.weight].toLowerCase()}" ` + `font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; - row += `${this.text}`; + row += `${this.text}`; } return row; @@ -630,8 +677,78 @@ var DrawingElement = new Lang.Class({ this.smooth(i); } }, + + rotate: function(cr, angle, x, y) { + if (angle == 0) + return; + cr.translate(x, y); + cr.rotate(angle); + cr.translate(-x, -y); + }, + + scale: function(cr, ratio, x, y) { + if (ratio == 1) + return; + cr.translate(x, y); + cr.scale(ratio, 1); + cr.translate(-x, -y); + }, + + transformRectangle: function(x, y) { + let points = this.points; + if (points.length < 2 || points[0][0] == points[1][0] || points[0][1] == points[1][1]) + return; + + this.transform.center = [points[0][0] + (points[1][0] - points[0][0]) / 2, points[0][1] + (points[1][1] - points[0][1]) / 2]; + + this.transform.angle = getAngle(this.transform.center[0], this.transform.center[1], points[1][0], points[1][1], x, y); + this.transform.active = true; + }, + + transformEllipse: function(x, y) { + let points = this.points; + if (points.length < 2 || points[0][0] == points[1][0] || points[0][1] == points[1][1]) + return; + + this.transform.center = [points[0][0], points[0][1]]; + + let r1 = Math.hypot(points[1][0] - points[0][0], points[1][1] - points[0][1]); + let r2 = Math.hypot(x - points[0][0], y - points[0][1]); + this.transform.ratio = r2 / r1; + + this.transform.angle = getAngle(this.transform.center[0], this.transform.center[1], points[1][0], points[1][1], x, y); + if (!this.transform.startAngle) + // that is the angle between the direction when starting ellipticalizing, and the x-axis + this.transform.startAngle = getAngle(points[0][0], points[0][1], points[0][0] + 1, points[0][1], points[1][0], points[1][1]); + this.transform.active = true; + }, + + transformLine: function(x, y) { + if (this.points.length < 2) + return; + if (this.points.length == 2) + this.points[2] = this.points[1]; + this.points[1] = [x, y]; + this.transform.active = true; + }, }); +function getAngle(xO, yO, xA, yA, xB, yB) { + // calculate angle of rotation in absolute value + // cos(AOB) = (OA.OB)/(||OA||*||OB||) where OA.OB = (xA-xO)*(xB-xO) + (yA-yO)*(yB-yO) + let angle = Math.acos( ((xA - xO)*(xB - xO) + (yA - yO)*(yB - yO)) / (Math.hypot(xA - xO, yA - yO) * Math.hypot(xB - xO, yB - yO)) ); + + // determine the sign of the angle + // equation of OA: y = ax + b + let a = (yA - yO) / (xA - xO); + let b = yA - a*xA; + if (yB < a*xB + b) + angle = - angle; + if (xA < xO) + angle = - angle; + return angle; +} + var HELPER_ANIMATION_TIME = 0.25; // DrawingHelper provides the "help osd" (Ctrl + F1) diff --git a/locale/draw-on-your-screen.pot b/locale/draw-on-your-screen.pot index 1bf09b6..c3194bb 100644 --- a/locale/draw-on-your-screen.pot +++ b/locale/draw-on-your-screen.pot @@ -34,7 +34,7 @@ msgstr "" msgid "Line" msgstr "" -msgid "Circle" +msgid "Ellipse" msgstr "" msgid "Rectangle" @@ -55,12 +55,17 @@ msgid "" msgstr "" #: prefs.js + +# GLOBAL_KEYBINDINGS + msgid "Enter/leave drawing mode" msgstr "" msgid "Erase all drawings" msgstr "" +# INTERNAL_KEYBINDINGS + msgid "Undo last brushstroke" msgstr "" @@ -91,13 +96,14 @@ msgstr "" msgid "Change linecap" msgstr "" +# already in draw.js #msgid "Dashed line" #msgstr "" msgid "Select line" msgstr "" -msgid "Select circle" +msgid "Select ellipse" msgstr "" msgid "Select rectangle" @@ -133,6 +139,8 @@ msgstr "" msgid "Show help" msgstr "" +# OTHER_SHORTCUTS + msgid "Draw" msgstr "" @@ -151,6 +159,12 @@ msgstr "" msgid "Center click" msgstr "" +msgid "Transform shape (when drawing)" +msgstr "" + +msgid "Ctrl key" +msgstr "" + msgid "Increment/decrement line width" msgstr "" @@ -175,6 +189,8 @@ msgstr "" msgid "Escape key" msgstr "" +# + msgid "" "Start drawing with Super+Alt+D\n" "Then save your beautiful work by taking a screenshot" @@ -189,6 +205,13 @@ msgstr "" msgid "(in drawing mode)" msgstr "" +msgid "" +"By pressing Ctrl key during the drawing process, you can:\n" +" . rotate a rectangle or a text area\n" +" . extend and rotate an ellipse\n" +" . curve a line (cubic Bezier curve)" +msgstr "" + msgid "Smooth stroke during the drawing process" msgstr "" diff --git a/prefs.js b/prefs.js index e308a0e..2f9737b 100644 --- a/prefs.js +++ b/prefs.js @@ -52,7 +52,7 @@ var INTERNAL_KEYBINDINGS = { 'toggle-dash': "Dashed line", '-separator-2': '', 'select-line-shape': "Select line", - 'select-ellipse-shape': "Select circle", + 'select-ellipse-shape': "Select ellipse", 'select-rectangle-shape': "Select rectangle", 'select-text-shape': "Select text", 'select-none-shape': "Unselect shape (free drawing)", @@ -73,6 +73,7 @@ var OTHER_SHORTCUTS = [ { desc: "Draw", shortcut: "Left click" }, { desc: "Draw by filling in", shortcut: "Right click" }, { desc: "Toggle shape", shortcut: "Center click" }, + { desc: "Transform shape (when drawing)", shortcut: "Ctrl key" }, { desc: "Increment/decrement line width", shortcut: "Scroll" }, { desc: "Select color", shortcut: "Ctrl+1...9" }, { desc: "Select eraser", shortcut: "Shift key held" }, @@ -104,7 +105,7 @@ const PrefsPage = new GObject.Class({ let textBox1 = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, margin: MARGIN }); let text1 = new Gtk.Label({ wrap: true, justify: 2, use_markup: true, - label: _("Start drawing with Super+Alt+D\nThen save your beautiful work by taking a screenshot") }); + label: " " + _("Start drawing with Super+Alt+D\nThen save your beautiful work by taking a screenshot") + "" }); textBox1.pack_start(text1, false, false, 0); box.add(textBox1); @@ -149,6 +150,16 @@ const PrefsPage = new GObject.Class({ listBox.add(new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN })); + let controlBox = new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN, margin_bottom:MARGIN }); + let controlLabel = new Gtk.Label({ + use_markup: true, + label: _("By pressing Ctrl key during the drawing process, you can:\n . rotate a rectangle or a text area\n . extend and rotate an ellipse\n . curve a line (cubic Bezier curve)") + }); + controlLabel.set_halign(1); + controlLabel.get_style_context().add_class("dim-label"); + controlBox.pack_start(controlLabel, true, true, 4); + listBox.add(controlBox); + let smoothBox = new Gtk.Box({ margin: MARGIN }); let smoothLabelBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); let smoothLabel1 = new Gtk.Label({label: _("Smooth stroke during the drawing process")}); @@ -178,12 +189,11 @@ const PrefsPage = new GObject.Class({ let noteBox = new Gtk.Box({ margin_top: MARGIN, margin_left: MARGIN, margin_right: MARGIN, margin_bottom:MARGIN }); let noteLabel = new Gtk.Label({ use_markup: true, - label: _("Note: When you save elements made with eraser in a SVG file,\nthey are colored with background color, transparent if it is disabled.\n(See \"Add a drawing background\" or edit the SVG file afterwards)") + label: _("Note: When you save elements made with eraser in a SVG file,\nthey are colored with background color, transparent if it is disabled.\n(See \"Add a drawing background\" or edit the SVG file afterwards)") }); noteLabel.set_halign(1); - //let noteLabel2 = new Gtk.Label({ label: _("See notesheet.css") }); + noteLabel.get_style_context().add_class("dim-label"); noteBox.pack_start(noteLabel, true, true, 4); - //noteBox.pack_start(noteLabel2, false, false, 4); listBox.add(noteBox); this.addSeparator(listBox); diff --git a/stylesheet.css b/stylesheet.css index 15bb5af..9261fd1 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -58,7 +58,7 @@ } .draw-on-your-screen-helper StBoxLayout { - spacing: 5em; + spacing: 7em; } .draw-on-your-screen-helper StBoxLayout StLabel {