diff --git a/area.js b/area.js index e800b95..c33de80 100644 --- a/area.js +++ b/area.js @@ -471,7 +471,7 @@ var DrawingArea = new Lang.Class({ if (duplicate) { // deep cloning - let copy = new Elements.DrawingElement(JSON.parse(JSON.stringify(this.grabbedElement))); + let copy = new this.grabbedElement.constructor(JSON.parse(JSON.stringify(this.grabbedElement))); this.elements.push(copy); this.grabbedElement = copy; } @@ -554,28 +554,32 @@ var DrawingArea = new Lang.Class({ this._stopDrawing(); }); - this.currentElement = new Elements.DrawingElement ({ - shape: this.currentTool, - color: this.currentColor.to_string(), - line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, - dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, - fill: this.fill, - fillRule: this.currentFillRule, - eraser: eraser, - transform: { active: false, center: [0, 0], angle: 0, startAngle: 0, ratio: 1 }, - points: [] - }); - if (this.currentTool == Shapes.TEXT) { - this.currentElement.fill = false; - this.currentElement.font = { - family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), - weight: this.currentFontWeight, - style: this.currentFontStyle, - stretch: this.currentFontStretch, - variant: this.currentFontVariant }; - this.currentElement.text = _("Text"); - this.currentElement.textRightAligned = this.currentTextRightAligned; + this.currentElement = new Elements.DrawingElement({ + shape: this.currentTool, + color: this.currentColor.to_string(), + eraser: eraser, + font: { + family: (this.currentFontGeneric == 0 ? this.currentThemeFontFamily : FontGenericNames[this.currentFontGeneric]), + weight: this.currentFontWeight, + style: this.currentFontStyle, + stretch: this.currentFontStretch, + variant: this.currentFontVariant }, + text: _("Text"), + textRightAligned: this.currentTextRightAligned, + points: [] + }); + } else { + this.currentElement = new Elements.DrawingElement({ + shape: this.currentTool, + color: this.currentColor.to_string(), + eraser: eraser, + fill: this.fill, + fillRule: this.currentFillRule, + line: { lineWidth: this.currentLineWidth, lineJoin: this.currentLineJoin, lineCap: this.currentLineCap }, + dash: { active: this.dashedLine, array: this.dashedLine ? [this.dashArray[0] || this.currentLineWidth, this.dashArray[1] || this.currentLineWidth * 3] : [0, 0] , offset: this.dashOffset }, + points: [] + }); } this.currentElement.startDrawing(startX, startY); diff --git a/elements.js b/elements.js index 9b4f7bc..6eb40ee 100644 --- a/elements.js +++ b/elements.js @@ -57,10 +57,14 @@ const MIN_TRANSLATION_DISTANCE = 1; // px const MIN_ROTATION_ANGLE = Math.PI / 1000; // rad const MIN_DRAWING_SIZE = 3; // px +var DrawingElement = function(params) { + return params.shape == Shapes.TEXT ? new TextElement(params) : new _DrawingElement(params); +}; + // DrawingElement represents a "brushstroke". // It can be converted into a cairo path as well as a svg element. // See DrawingArea._startDrawing() to know its params. -var DrawingElement = new Lang.Class({ +const _DrawingElement = new Lang.Class({ Name: 'DrawOnYourScreenDrawingElement', _init: function(params) { @@ -69,16 +73,12 @@ var DrawingElement = new Lang.Class({ // compatibility with json generated by old extension versions - if (params.fillRule === undefined) - this.fillRule = Cairo.FillRule.WINDING; if (params.transformations === undefined) this.transformations = []; - if (params.shape == Shapes.TEXT) { - if (params.font && params.font.weight === 0) - this.font.weight = 400; - if (params.font && params.font.weight === 1) - this.font.weight = 700; - } + if (params.font && params.font.weight === 0) + this.font.weight = 400; + if (params.font && params.font.weight === 1) + this.font.weight = 700; if (params.transform && params.transform.center) { let angle = (params.transform.angle || 0) + (params.transform.startAngle || 0); @@ -104,10 +104,6 @@ var DrawingElement = new Lang.Class({ fillRule: this.fillRule, eraser: this.eraser, transformations: this.transformations, - text: this.text, - lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, - textRightAligned: this.textRightAligned, - font: this.font, points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) }; }, @@ -129,9 +125,12 @@ var DrawingElement = new Lang.Class({ cr.stroke(); } - cr.setLineCap(this.line.lineCap); - cr.setLineJoin(this.line.lineJoin); - cr.setLineWidth(this.line.lineWidth); + if (this.line) { + cr.setLineCap(this.line.lineCap); + cr.setLineJoin(this.line.lineJoin); + cr.setLineWidth(this.line.lineWidth); + } + if (this.fillRule) cr.setFillRule(this.fillRule); @@ -175,6 +174,12 @@ var DrawingElement = new Lang.Class({ } }); + this._drawCairo(cr, params); + + cr.identityMatrix(); + }, + + _drawCairo: function(cr, params) { let [points, shape] = [this.points, this.shape]; if (shape == Shapes.LINE && points.length == 3) { @@ -218,51 +223,10 @@ var DrawingElement = new Lang.Class({ if (shape == Shapes.POLYGON) cr.closePath(); - } else if (shape == Shapes.TEXT && points.length == 2) { - let layout = PangoCairo.create_layout(cr); - let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; - let fontDescription = new Pango.FontDescription(); - fontDescription.set_absolute_size(fontSize); - ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { - if (this.font[attribute] !== undefined) - try { - fontDescription[`set_${attribute}`](this.font[attribute]); - } catch(e) {} - }); - layout.set_font_description(fontDescription); - layout.set_text(this.text, -1); - this.textWidth = layout.get_pixel_size()[0]; - cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); - layout.set_text(this.text, -1); - PangoCairo.show_layout(cr, layout); - - if (params.showTextCursor) { - let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; - layout.set_text(this.text.slice(0, cursorPosition), -1); - let width = layout.get_pixel_size()[0]; - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), - Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); - cr.fill(); - } - - if (params.showTextRectangle || params.drawTextRectangle) { - cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), - this.textWidth, - Math.abs(points[1][1] - points[0][1])); - if (params.showTextRectangle) - setDummyStroke(cr); - else - // Only draw the rectangle to find the element, not to show it. - cr.setLineWidth(0); - } } - - cr.identityMatrix(); }, getContainsPoint: function(cr, x, y) { - if (this.shape == Shapes.TEXT) - return cr.inFill(x, y); - cr.save(); cr.setLineWidth(Math.max(this.line.lineWidth, 25)); cr.setDash([], 0); @@ -274,33 +238,6 @@ var DrawingElement = new Lang.Class({ }, buildSVG: function(bgColor) { - 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; - let fill = this.fill && !this.isStraightLine; - let attributes = ''; - - if (fill) { - attributes = `fill="${color}"`; - if (this.fillRule) - attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; - } else { - attributes = `fill="none"`; - } - - if (this.line && this.line.lineWidth) { - attributes += ` stroke="${color}"` + - ` stroke-width="${this.line.lineWidth}"`; - if (this.line.lineCap) - attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; - if (this.line.lineJoin && !this.isStraightLine) - attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; - if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) - attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; - } else { - attributes += ` stroke="none"`; - } - let transAttribute = ''; this.transformations.slice(0).reverse().forEach(transformation => { transAttribute += transAttribute ? ' ' : ' transform="'; @@ -328,6 +265,37 @@ var DrawingElement = new Lang.Class({ }); transAttribute += transAttribute ? '"' : ''; + return this._drawSvg(transAttribute); + }, + + _drawSvg: function(transAttribute) { + 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; + let fill = this.fill && !this.isStraightLine; + let attributes = ''; + + if (fill) { + attributes = `fill="${color}"`; + if (this.fillRule) + attributes += ` fill-rule="${FillRuleNames[this.fillRule].toLowerCase()}"`; + } else { + attributes = `fill="none"`; + } + + if (this.line && this.line.lineWidth) { + attributes += ` stroke="${color}"` + + ` stroke-width="${this.line.lineWidth}"`; + if (this.line.lineCap) + attributes += ` stroke-linecap="${LineCapNames[this.line.lineCap].toLowerCase()}"`; + if (this.line.lineJoin && !this.isStraightLine) + attributes += ` stroke-linejoin="${LineJoinNames[this.line.lineJoin].toLowerCase()}"`; + if (this.dash && this.dash.active && this.dash.array && this.dash.array[0] && this.dash.array[1]) + attributes += ` stroke-dasharray="${this.dash.array[0]} ${this.dash.array[1]}" stroke-dashoffset="${this.dash.offset}"`; + } else { + attributes += ` stroke="none"`; + } + if (this.shape == Shapes.LINE && points.length == 4) { row += ``; - } else if (this.shape == Shapes.TEXT && points.length == 2) { - attributes = `fill="${color}" ` + - `stroke="transparent" ` + - `stroke-opacity="0" ` + - `font-size="${Math.abs(points[1][1] - points[0][1])}"`; - - if (this.font.family) - attributes += ` font-family="${this.font.family}"`; - if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) - attributes += ` font-weight="${this.font.weight}"`; - if (this.font.style && FontStyleNames[this.font.style]) - attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; - if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) - attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; - if (this.font.variant && FontVariantNames[this.font.variant]) - attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; - - // this.textWidth is computed during Cairo building. - row += `${this.text}`; } return row; @@ -469,14 +417,6 @@ var DrawingElement = new Lang.Class({ } else if (this.shape == Shapes.POLYGON || this.shape == Shapes.POLYLINE) { points[points.length - 1] = [x, y]; - } else if (this.shape == Shapes.TEXT && transform) { - if (points.length < 2) - return; - - let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; - points[0] = [points[0][0] + slideX, points[0][1] + slideY]; - points[1] = [x, y]; - } else { points[1] = [x, y]; @@ -590,11 +530,6 @@ var DrawingElement = new Lang.Class({ } }, - // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. - _getLineOffset: function() { - return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); - }, - // The figure rotation center before transformations (original). // this.textWidth is computed during Cairo building. _getOriginalCenter: function() { @@ -603,7 +538,6 @@ var DrawingElement = new Lang.Class({ this._originalCenter = this.shape == Shapes.ELLIPSE ? [points[0][0], points[0][1]] : this.shape == Shapes.LINE && points.length == 4 ? getCurveCenter(points[0], points[1], points[2], points[3]) : this.shape == Shapes.LINE && points.length == 3 ? getCurveCenter(points[0], points[0], points[1], points[2]) : - this.shape == Shapes.TEXT && this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : points.length >= 3 ? getCentroid(points) : getNaiveCenter(points); } @@ -648,6 +582,139 @@ var DrawingElement = new Lang.Class({ } }); +const TextElement = new Lang.Class({ + Name: 'DrawOnYourScreenTextElement', + Extends: _DrawingElement, + + toJSON: function() { + return { + shape: this.shape, + color: this.color, + eraser: this.eraser, + transformations: this.transformations, + text: this.text, + lineIndex: this.lineIndex !== undefined ? this.lineIndex : undefined, + textRightAligned: this.textRightAligned, + font: this.font, + points: this.points.map((point) => [Math.round(point[0]*100)/100, Math.round(point[1]*100)/100]) + }; + }, + + _drawCairo: function(cr, params) { + let points = this.points; + + if (points.length == 2) { + let layout = PangoCairo.create_layout(cr); + let fontSize = Math.abs(points[1][1] - points[0][1]) * Pango.SCALE; + let fontDescription = new Pango.FontDescription(); + fontDescription.set_absolute_size(fontSize); + ['family', 'weight', 'style', 'stretch', 'variant'].forEach(attribute => { + if (this.font[attribute] !== undefined) + try { + fontDescription[`set_${attribute}`](this.font[attribute]); + } catch(e) {} + }); + layout.set_font_description(fontDescription); + layout.set_text(this.text, -1); + this.textWidth = layout.get_pixel_size()[0]; + cr.moveTo(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1],points[1][1]) - layout.get_baseline() / Pango.SCALE); + layout.set_text(this.text, -1); + PangoCairo.show_layout(cr, layout); + + if (params.showTextCursor) { + let cursorPosition = this.cursorPosition == -1 ? this.text.length : this.cursorPosition; + layout.set_text(this.text.slice(0, cursorPosition), -1); + let width = layout.get_pixel_size()[0]; + cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0) + width, Math.max(points[0][1],points[1][1]), + Math.abs(points[1][1] - points[0][1]) / 25, - Math.abs(points[1][1] - points[0][1])); + cr.fill(); + } + + if (params.showTextRectangle || params.drawTextRectangle) { + cr.rectangle(points[1][0] - (this.textRightAligned ? this.textWidth : 0), Math.max(points[0][1], points[1][1]), + this.textWidth, - Math.abs(points[1][1] - points[0][1])); + if (params.showTextRectangle) + setDummyStroke(cr); + else + // Only draw the rectangle to find the element, not to show it. + cr.setLineWidth(0); + } + } + }, + + getContainsPoint: function(cr, x, y) { + return cr.inFill(x, y); + }, + + _drawSvg: function(transAttribute) { + 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; + let attributes = ''; + + if (points.length == 2) { + attributes = `fill="${color}" ` + + `stroke="transparent" ` + + `stroke-opacity="0" ` + + `font-size="${Math.abs(points[1][1] - points[0][1])}"`; + + if (this.font.family) + attributes += ` font-family="${this.font.family}"`; + if (this.font.weight && this.font.weight != Pango.Weight.NORMAL) + attributes += ` font-weight="${this.font.weight}"`; + if (this.font.style && FontStyleNames[this.font.style]) + attributes += ` font-style="${FontStyleNames[this.font.style].toLowerCase()}"`; + if (FontStretchNames[this.font.stretch] && this.font.stretch != Pango.Stretch.NORMAL) + attributes += ` font-stretch="${FontStretchNames[this.font.stretch].toLowerCase()}"`; + if (this.font.variant && FontVariantNames[this.font.variant]) + attributes += ` font-variant="${FontVariantNames[this.font.variant].toLowerCase()}"`; + + // this.textWidth is computed during Cairo building. + row += `${this.text}`; + } + + return row; + }, + + updateDrawing: function(x, y, transform) { + let points = this.points; + if (x == points[points.length - 1][0] && y == points[points.length - 1][1]) + return; + + transform = transform || this.transformations.length >= 1; + + if (transform) { + if (points.length < 2) + return; + + let [slideX, slideY] = [x - points[1][0], y - points[1][1]]; + points[0] = [points[0][0] + slideX, points[0][1] + slideY]; + points[1] = [x, y]; + + } else { + points[1] = [x, y]; + + } + }, + + // When rotating grouped lines, lineOffset is used to retrieve the rotation center of the first line. + _getLineOffset: function() { + return (this.lineIndex || 0) * Math.abs(this.points[1][1] - this.points[0][1]); + }, + + _getOriginalCenter: function() { + if (!this._originalCenter) { + let points = this.points; + this._originalCenter = this.textWidth ? [points[1][0], Math.max(points[0][1], points[1][1]) - this._getLineOffset()] : + points.length >= 3 ? getCentroid(points) : + getNaiveCenter(points); + } + + return this._originalCenter; + }, +}); + const setDummyStroke = function(cr) { cr.setLineWidth(2); cr.setLineCap(0); @@ -655,10 +722,6 @@ const setDummyStroke = function(cr) { cr.setDash([1, 2], 0); }; -/* - Some geometric utils -*/ - const getNearness = function(pointA, pointB, distance) { return Math.hypot(pointB[0] - pointA[0], pointB[1] - pointA[1]) < distance; };