=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java' --- src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java 2015-08-20 14:07:03 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java 2015-08-20 14:14:14 +0000 @@ -126,4 +126,7 @@ /** Restack the z-order of all windows. */ public static final byte MSG_RESTACK_WINDOWS = (byte) 0x95; + + /** Set the stroke style for a window. */ + public static final byte MSG_SET_STROKE_STYLE = (byte) 0x96; } === modified file 'src/com/goldencode/p2j/ui/client/driver/web/index.html' --- src/com/goldencode/p2j/ui/client/driver/web/index.html 2015-08-12 17:42:50 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/index.html 2015-08-20 12:40:07 +0000 @@ -9,6 +9,7 @@ + === modified file 'src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js' --- src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js 2015-08-20 14:07:03 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js 2015-08-20 14:15:40 +0000 @@ -24,7 +24,7 @@ ** requests. ** 010 GES 20150818 Added more GUI message support including fonts, cursors and z-order. ** Once the web socket has connected, notify the Java side, so that it can start -** sending requests. +** sending requests, added line strokes. */ "use strict"; @@ -534,6 +534,13 @@ } p2j.screen.restackZOrderEntries(winids); break; + case 0x96: + // set line stroke + var strokeStyle = me.readInt32BinaryMessage(message, 1); + var strokeWidth = me.readInt32BinaryMessage(message, 5); + var wid = me.readInt32BinaryMessage(message, 9); + p2j.screen.setLineStroke(strokeStyle, strokeWidth, wid); + break; }; } else === modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebEmulatedWindow.java' --- src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebEmulatedWindow.java 2015-08-18 19:50:34 +0000 +++ src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebEmulatedWindow.java 2015-08-20 10:02:03 +0000 @@ -17,7 +17,7 @@ ** font creation/selection. ** 005 GES 20150817 Removed isOnTop() since it is unnecessary. The saveFocusListeners(), ** restoreFocusListeners() and moveToTop() are only needed for Swing usage and -** have been removed from this class. +** have been removed from this class, added line strokes. */ package com.goldencode.p2j.ui.client.gui.driver.web; @@ -362,6 +362,7 @@ websock.setColor(ps.color.getRed(), ps.color.getGreen(), ps.color.getBlue()); break; case SET_LINE_STROKE: + websock.setLineStroke(ps.stroke.ordinal(), Math.round(ps.strokeWidth), getWindowId()); break; case SET_FONT: websock.setFont(ps.font.font); === modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java' --- src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java 2015-08-20 14:07:03 +0000 +++ src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java 2015-08-20 14:17:12 +0000 @@ -12,8 +12,8 @@ ** 001 GES 20150331 First version which is a placeholder using the common parent code as ChUI. ** 002 GES 20150701 Added messages for a first pass at GUI support, ** implemented an images drawing operation. -** 003 CA 20150810 Added operations for font, metrics and string drawing. Added z-order -** support. +** 003 CA 20150810 Added operations for font, metrics and string drawing. Added z-order, +** line strokes. ** 004 CA 20150820 Moved the message receiving APIs to parent class. */ @@ -1231,6 +1231,31 @@ } /** + * Sends MSG_SET_STROKE_STYLE to order the current window stroke to be changed + * on the client side. It sends a window its stroke style and line width. All possible stroke + * styles are listed by LineStroke enumeration. + * + * @param strokeStyle + * The known stroke style id. + * @param width + * The stroke line width. + * @param wid + * The window id. + */ + public void setLineStroke(int strokeStyle, int width, int wid) + { + byte[] message = new byte[13]; + + message[0] = WebClientMessageTypes.MSG_SET_STROKE_STYLE; + + writeMessageInt32(message, 1, strokeStyle); + writeMessageInt32(message, 5, width); + writeMessageInt32(message, 9, wid); + + sendBinaryMessage(message); + } + + /** * Draw a string at the specified location. * * @param text === modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.screen.js' --- src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.screen.js 2015-08-19 20:05:05 +0000 +++ src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.screen.js 2015-08-20 14:22:24 +0000 @@ -84,6 +84,8 @@ SET_TITLE : 29 }; + var strokesManager = new LineStrokes(); + /** * Constructor for a Window class that represents a Progress 4GL window instance. * @@ -138,12 +140,17 @@ // eliminate anti-aliasing of images this.ctx.imageSmoothingEnabled = false; + // the current window stroke style + this.strokeStyle = strokesManager.getDefaultStrokeStyle(); + // the current window stroke width + this.strokeWidth = 1; + // force all drawing to be inside pixel "cells" instead of "stradling" between two cells // which is the default; this eliminates much of the worst of the anti-aliasing and // positioning problems inherent in canvas operations; this will also save off the // current state of the graphics context so that it can be restored later this.translate(0.5, 0.5); - } + }; /** * Cleanup any resources, including any linkages to the window from an owning window. @@ -353,7 +360,7 @@ this.ctx.rect(x, y, width, height); this.ctx.clip(); } - } + } }; /** @@ -430,7 +437,7 @@ Window.prototype.resize = function(width, height) { // save off the previously drawn content - var oldPixels = ctx.getImageData(0, 0, canvas.width, canvas.height); + var oldPixels = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); // this is expensive, but we really do need to change the overall size of the canvas this.canvas.width = width; @@ -441,7 +448,7 @@ this.replay(); // re-blit the previously drawn content - ctx.putImageData(oldPixels, 0, 0); + this.ctx.putImageData(oldPixels, 0, 0); }; /** @@ -454,6 +461,42 @@ { this.canvas.style.cursor = style; }; + + /** + * Sets the stroke style. + * + * @param {Number} strokeStyle + * The predefined stroke style. + */ + Window.prototype.setStrokeStyle = function(strokeStyle) + { + this.strokeStyle = strokeStyle; + }; + + /** + * Sets the stroke line width. + * + * @param {Number} width + * The stroke line width. + */ + Window.prototype.setStrokeWidth = function(width) + { + this.strokeWidth = width; + }; + + /** + * Sets the line stroke. + * + * @param {Number} strokeStyle + * The predefined stroke style. + * @param {Number} width + * The stroke line width. + */ + Window.prototype.setLineStroke = function(strokeStyle, width) + { + this.setStrokeStyle(strokeStyle); + this.setStrokeWidth(width); + }; /** * Draw the given list of operations in the canvas. @@ -521,11 +564,11 @@ // in this case, the "y" is the height in which it needs to be vertically centered y = y / 2; - ctx.textBaseline = 'middle'; + this.ctx.textBaseline = 'middle'; } else { - ctx.textBaseline = 'top'; + this.ctx.textBaseline = 'top'; } this.ctx.fillText(text, x, y); @@ -554,16 +597,16 @@ var lheight = p2j.socket.readInt32BinaryMessage(message, idx + offset); offset = offset + 4; - var textWidth = p2j.fonts.getTextWidth(this.currentFont, text); - var textHeight = p2j.fonts.getTextHeight(this.currentFont, text); + var textWidth = p2j.fonts.getTextWidth(currentFont, text); + var textHeight = p2j.fonts.getTextHeight(currentFont, text); var widthScale = lwidth / textWidth; var heightScale = lheight / textHeight; // scale drawing context to computed width // this scales the desired structure.x as well so it needs adjusting - ctx.save(); - ctx.scale(widthScale, heightScale); + this.ctx.save(); + this.ctx.scale(widthScale, heightScale); var scaleBackWidth = 1 / widthScale; var scaleBackHeight = 1 / heightScale; @@ -573,15 +616,15 @@ // in this case, the "y" is the height in which it needs to be vertically centered y = y / 2; - ctx.textBaseline = 'middle'; + this.ctx.textBaseline = 'middle'; } else { - ctx.textBaseline = 'top'; + this.ctx.textBaseline = 'top'; } - ctx.fillText(text, x * scaleBackWidth, y * scaleBackHeight); - ctx.restore(); + this.ctx.fillText(text, x * scaleBackWidth, y * scaleBackHeight); + this.ctx.restore(); break; case ops.DRAW_PARAGRAPH: @@ -605,7 +648,7 @@ width = p2j.socket.readInt32BinaryMessage(message, idx + offset); offset = offset + 4; - ctx.textBaseline = 'top'; + this.ctx.textBaseline = 'top'; me.layoutParagraphWorker(text, fontId, x, y, width, true); @@ -615,7 +658,9 @@ var y1 = p2j.socket.readInt32BinaryMessage(message, idx + 5); var x2 = p2j.socket.readInt32BinaryMessage(message, idx + 9); var y2 = p2j.socket.readInt32BinaryMessage(message, idx + 13); - drawLine(this.ctx, x1, y1, x2, y2, this.rawColor); + strokesManager.applyStrokeToPath(this.ctx, + this.strokeStyle, this.strokeWidth, this.rawColor, + drawLine(this.ctx, x1, y1, x2, y2, this.rawColor)); this.ctx.stroke(); break; case ops.DRAW_RECT: @@ -623,7 +668,9 @@ y = p2j.socket.readInt32BinaryMessage(message, idx + 5); width = p2j.socket.readInt32BinaryMessage(message, idx + 9); height = p2j.socket.readInt32BinaryMessage(message, idx + 13); - drawRect(this.ctx, x, y, width, height, this.rawColor, false); + strokesManager.applyStrokeToPath(this.ctx, + this.strokeStyle, this.strokeWidth, this.rawColor, + drawRect(this.ctx, x, y, width, height, this.rawColor, false)); break; case ops.DRAW_ROUND_RECT: x = p2j.socket.readInt32BinaryMessage(message, idx + 1); @@ -658,7 +705,7 @@ imgDataOffset = idx + 22; loadedImages.set(key, message.subarray(imgDataOffset, imgDataOffset + pixelsInBytes)); } - drawImage(x, y, width, height, imgData, imgDataOffset); + drawImage(this.ctx, x, y, width, height, imgData, imgDataOffset); break; case ops.FILL_RECT: x = p2j.socket.readInt32BinaryMessage(message, idx + 1); @@ -673,7 +720,7 @@ width = p2j.socket.readInt32BinaryMessage(message, idx + 9); height = p2j.socket.readInt32BinaryMessage(message, idx + 13); diameter = p2j.socket.readInt32BinaryMessage(message, idx + 17); - drawRoundRect(ctx, x, y, width, height, diameter, this.rawColor, true); + drawRoundRect(this.ctx, x, y, width, height, diameter, this.rawColor, true); break; case ops.FILL_POLYGON: idx += 1; @@ -719,14 +766,14 @@ case ops.SET_FONT: fontId = p2j.socket.readInt32BinaryMessage(message, idx + 1); - this.setFont(fontId); + this.setFont(this.ctx, fontId); break; case ops.SET_FONT_STYLE: var style = p2j.socket.readInt32BinaryMessage(message, idx + 1); - if (p2j.fonts.setFontStyle(this.currentFont, style)) + if (p2j.fonts.setFontStyle(currentFont, style)) { - fname = p2j.fonts.getFontName(this.currentFont); + fname = p2j.fonts.getFontName(currentFont); this.ctx.font = fname; } @@ -792,7 +839,7 @@ function isValidWindowId(wid) { return (typeof wid === 'number') && (wid % 1 === 0) && wid > 0; - } + }; /** * Checks if the given value already exists as a window id. @@ -805,8 +852,8 @@ function isExistingWindowId(wid) { return winlist[wid] instanceof Window; - } - + }; + /** * Obtain the specified window. * @@ -819,8 +866,8 @@ function getWindow(wid) { return winlist[wid] instanceof Window ? winlist[wid] : null; - } - + }; + /** * Find the index of the given window in the z-order list. * @@ -855,8 +902,8 @@ } return found; - } - + }; + /** * Adds the given window to the end of the z-order list, setting the z-order value as needed. * @@ -875,7 +922,7 @@ // assign the z-order win.canvas.style.zIndex = zidx; - } + }; /** * Removes the given window from the z-order list and resets the z-order values of all @@ -898,7 +945,7 @@ } return removeZOrderEntryWorker(found); - } + }; /** * Removes the given element from the z-order list and resets the z-order values of all @@ -924,7 +971,7 @@ } return removed; - } + }; /** * Moves the given window to the top of the z-order list and resets the z-order values of all @@ -945,7 +992,7 @@ addZOrderEntry(entry.id, entry.win); } - } + }; /** * Directly draw the specified pixel in the given color. @@ -959,14 +1006,14 @@ * @param {Number[]} color * Array of 3 integer values between 0 and 255 inclusive, representing an RGB color. */ - function drawPixel(ctx, x, y, color) + Window.prototype.drawPixel = function(ctx, x, y, color) { - pixData[0] = color[0]; - pixData[1] = color[1]; - pixData[2] = color[2]; + this.pixData[0] = color[0]; + this.pixData[1] = color[1]; + this.pixData[2] = color[2]; - ctx.putImageData(pixel, x, y); - } + this.ctx.putImageData(pixel, x, y); + }; /** * Draw a line in the given color from (x1, y1) to (x2, y2) inclusive, using direct drawing. @@ -985,9 +1032,14 @@ * Y coordinate of the ending pixel to be drawn. * @param {Number[]} color * Array of 3 integer values between 0 and 255 inclusive, representing an RGB color. + * + * @return {Array} path + * The {x:.,y:.} point per a pixel array that represents the rectangle outerline. */ - function drawLine(ctx, x1, y1, x2, y2, color) + Window.prototype.drawLine = function(ctx, x1, y1, x2, y2, color) { + // holds the line pixels to draw + var path = []; var dx = Math.abs(x2 - x1); var dy = Math.abs(y2 - y1); @@ -1004,7 +1056,11 @@ while (true) { - drawPixel(ctx, x, y, color); + if (strokesManager.isDirectDrawingStrokeStyle(this.strokeStyle)) + { + drawPixel(ctx, x, y, color); + } + path.push({x : x, y : y}); if (x == x2 && y == y2) break; @@ -1023,7 +1079,9 @@ y += yIncr; } } - } + + return path; + }; /** * Fill or stroke the current path on the given context, first using the current path as a @@ -1052,8 +1110,8 @@ } ctx.restore(); - } - + }; + /** * Draw a rectangle in the given color and dimensions with the line drawing being overwritten * using direct drawing to eliminate the negative/unwanted effects of alni-aliasing. @@ -1073,8 +1131,11 @@ * This will be used for the border lines and when in fill mode, also the interior. * @param {Boolean} fill * true to fill the drawn rectangle with the given color. + * + * @return {Array} path + * The {x:.,y:.} point per a pixel array that represents the rectangle outerline. */ - function drawRect(ctx, x, y, width, height, color, fill) + Window.prototype.drawRect = function(ctx, x, y, width, height, color, fill) { // use vector operations for the interior of the rectangle if (fill) @@ -1085,15 +1146,20 @@ } // now overdraw the stroked portion to eliminate anit-aliasing - drawLine(ctx, x, y, x + width, y, color); - drawLine(ctx, x, y + 1, x, y + height, color); - drawLine(ctx, x + width, y + 1, x + width, y + height, color); - drawLine(ctx, x + 1, y + height, x + width - 1, y + height, color); - } + // to draw rectangle following a clockwise direction + var path = drawLine(ctx, x, y, x + width, y, color); + Array.prototype.push.apply(path, drawLine(ctx, x + width, y + 1, x + width, y + height, color)); + Array.prototype.push.apply(path, drawLine(ctx, x + width - 1, y + height, x + 1, y + height, color)); + Array.prototype.push.apply(path, drawLine(ctx, x, y + height, x, y + 1, color));// close the path + + return path; + }; /** * It draws an image on the canvas. * + * @param {CanvasRenderingContext2D} ctx + * The canvas 2D graphics context on which to draw. * @param {Number} x * X coordinate of the top left corner. * @param {Number} y @@ -1107,7 +1173,7 @@ * @param {Number} imgDataOffset * The offset of the encoded image data. */ - function drawImage(x, y, width, height, imgData, imgDataOffset) + Window.prototype.drawImage = function(ctx, x, y, width, height, imgData, imgDataOffset) { var img = ctx.createImageData(width, height); var data = img.data; @@ -1121,7 +1187,7 @@ data[i + 3] = imgData[j + 3]; } ctx.putImageData(img, x, y); - } + }; /** * Convert a standard fillStyle or strokeStyle color into an array of [ R, G, B ] values. @@ -1162,7 +1228,7 @@ } return [ r, g, b ]; - } + }; /** * Test if the given argument is an array of 3 integer values. @@ -1199,7 +1265,7 @@ } return true; - } + }; /** * Convert the given array of 3 integer values into an HTML color string. @@ -1219,7 +1285,7 @@ return "rgb(" + color[0].toString() + ", " + color[1].toString() + ", " + color[2].toString() + ")"; - } + }; /** * Convert the given array of RGB values into a lighter color in a manner that is compatible @@ -1254,7 +1320,7 @@ var b = Math.min(Math.floor(bIn / SCALE_FACTOR), 255); return [ r, g, b ]; - } + }; /** * Convert the given array of RGB values into a darker color in a manner that is compatible @@ -1277,7 +1343,7 @@ var b = Math.max(0, Math.floor(color[2] * SCALE_FACTOR)); return [ r, g, b ]; - } + }; /** * Draw a rounded rectangle with the given context, dimensions and fill. @@ -1302,7 +1368,7 @@ * true to fill the rectangle with the current color, otherwise just * stroke the rectangle. */ - function drawRoundRect(ctx, x, y, width, height, diameter, color, fill) + Window.prototype.drawRoundRect = function(ctx, x, y, width, height, diameter, color, fill) { var radius = diameter / 2; @@ -1352,7 +1418,7 @@ drawLine(ctx, rightUpX, rightUpY, rightDownX, rightDownY, color); drawLine(ctx, bottomLeftX, bottomLeftY, bottomRightX, bottomRightY, color); drawLine(ctx, leftUpX, leftUpY, leftDownX, leftDownY, color); - } + }; /** * Draw a slightly shaded "3D" rectangle with the given context, dimensions and fill. In @@ -1380,7 +1446,7 @@ * @param {Boolean} raise * true to draw in raised mode. */ - function draw3DRect(ctx, x, y, width, height, color, fill, raised) + Window.prototype.draw3DRect = function(ctx, x, y, width, height, color, fill, raised) { // save off our colors var oriFillColor = ctx.fillStyle; @@ -1422,8 +1488,8 @@ // restore our colors ctx.fillStyle = oriFillColor; ctx.strokeStyle = oriStrokeColor; - } - + }; + /** * Draw a closed polygon with the given context, vertices and fill. * @@ -1441,7 +1507,7 @@ * true to fill the rectangle with the current color, otherwise just * stroke the rectangle. */ - function drawPolygon(ctx, xPoints, yPoints, num, color, fill) + Window.prototype.drawPolygon = function(ctx, xPoints, yPoints, num, color, fill) { var i; @@ -1462,26 +1528,28 @@ { drawLine(ctx, xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1], color); } - } + }; /** * Set the font in the current context. If the current font does not get changed, then this * will be a no-op. * + * @param {CanvasRenderingContext2D} ctx + * The canvas 2D graphics context on which to draw. * @param {Number} fontId * The font identifier. */ - function setFont(fontId) + function setFont(ctx, fontId) { - if (fontId != this.currentFont) + if (fontId != currentFont) { var fname = p2j.fonts.getFontName(fontId); - this.ctx.font = fname; + ctx.font = fname; - this.currentFont = fontId; + currentFont = fontId; } - } + }; /** * Find the next split position, so that the sub-text fits the given width. @@ -1535,7 +1603,7 @@ while (end < txt.length); return end; - } + }; /** * Split the word starting on the given index so that it fits the specified width. @@ -1568,7 +1636,7 @@ } return end; - } + }; /** * Find the next occurrence of the character given by the specified regex, starting from the @@ -1587,7 +1655,7 @@ { var indexOf = txt.substring(start).search(regex); return (indexOf >= 0) ? (indexOf + start) : indexOf; - } + }; /** * Create a new top-level window (and the canvas that backs it). @@ -1770,13 +1838,16 @@ /** * Draw text on canvas. * - * @param {number} x x coordinate - * @param {number} y y coordinate - * @param {string} text text to draw - * @param {number} a attribute - * @param {object} v color pairs + * @param {CanvasRenderingContext2D} ctx + * The canvas 2D graphics context on which to draw. + * @param {Number} x + * The x coordinate. + * @param {Number} y + * The y coordinate. + * @param {String} text + * The text to draw */ - var drawText = function(ctx, x, y, text) + Window.prototype.drawText = function(ctx, x, y, text) { // TODO calc width and height var width; @@ -1902,6 +1973,25 @@ }; /** + * Sets the line stroke for a window with the given id. + * + * @param {Number} strokeStyle + * The predefined stroke style. + * @param {Number} width + * The stroke line width. + * @param {Number} wid + * Denotes the active window id. + */ + me.setLineStroke = function(strokeStyle, width, wid) + { + var win = getWindow(wid); + if (win) + { + win.setLineStroke(strokeStyle, width); + } + }; + + /** * Clear the screen. */ me.clear = function() @@ -1941,6 +2031,8 @@ * The method performs a layout operation on the supplied text and returns the resulting * paragraph height while maintaining the supplied maximum width. * + * @param {CanvasRenderingContext2D} ctx + * The canvas 2D graphics context on which to draw. * @param {String} txt * The paragraph text. * @param {Number} font @@ -1957,7 +2049,7 @@ * * @return {Number} The paragraph height. */ - me.layoutParagraphWorker = function(txt, font, x, y, width, draw) + me.layoutParagraphWorker = function(ctx, txt, font, x, y, width, draw) { var newLine = true; var i = 0; @@ -1965,9 +2057,9 @@ if (draw) { // unless we are drawing, there is no need in changing the context font - this.ctx.save(); + ctx.save(); - this.setFont(font); + this.setFont(ctx, font); } var height = 0; @@ -1992,7 +2084,7 @@ if (draw) { - this.ctx.restore(); + ctx.restore(); } return height; === added file 'src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.strokes.js' --- src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.strokes.js 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/ui/client/gui/driver/web/res/p2j.strokes.js 2015-08-20 09:36:41 +0000 @@ -0,0 +1,398 @@ +/* +** Module : p2j.strokes.js +** Abstract : GUI-specific screen management +** +** Copyright (c) 2014-2015, Golden Code Development Corporation. +** ALL RIGHTS RESERVED. Use is subject to license terms. +** +** Golden Code Development Corporation +** CONFIDENTIAL +** +** -#- -I- --Date-- ------------------------------Description---------------------------------- +** 001 SBI 20150819 The first version defines LineStrokes class as a strokes manager. +*/ + +"use strict"; + +/** + * Defines the known stroke styles and methods to draw styled outer lines. + */ +function LineStrokes() +{ + /** + * Joins path segments by extending their outside edges until + * they meet. + */ + var JOIN_MITER = 0; + + /** + * Joins path segments by rounding off the corner at a radius + * of half the line width. + */ + var JOIN_ROUND = 1; + + /** + * Joins path segments by connecting the outer corners of their + * wide outlines with a straight segment. + */ + var JOIN_BEVEL = 2; + + /** + * Ends unclosed subpaths and dash segments with no added + * decoration. + */ + var CAP_BUTT = 0; + + /** + * Ends unclosed subpaths and dash segments with a round + * decoration that has a radius equal to half of the width + * of the pen. + */ + var CAP_ROUND = 1; + + /** + * Ends unclosed subpaths and dash segments with a square + * projection that extends beyond the end of the segment + * to a distance equal to half of the line width. + */ + var CAP_SQUARE = 2; + + /** + * This is the list of all possible line stoke types. + */ + var LineStrokeEnum = + { + DEFAULT : 0, + SPECIFIED_WIDTH : 1, + DOTTED : 2, + DOTTED_SMALL : 3, + DOTTED_SMALL_THIN : 4 + }; + + var LineStroke = + { + /** Dotted line specific parameter. */ + DOT_STEP : 2.0, + + /** Dotted line specific parameter. */ + DOT_STEP_SMALL : 1.0, + + /** Dotted line width. */ + DOT_LINE_WIDTH : 1.0, + + /** Dotted thin line width. */ + DOT_LINE_THIN_WIDTH : 0.5 + }; + + var dotStroke = new BasicStroke(LineStroke.DOT_LINE_WIDTH, CAP_BUTT, JOIN_BEVEL, 0.0, + [LineStroke.DOT_STEP, LineStroke.DOT_STEP], LineStroke.DOT_STEP); + + var dotSmallStroke = new BasicStroke(LineStroke.DOT_LINE_WIDTH, CAP_BUTT, JOIN_BEVEL, 0.0, + [LineStroke.DOT_STEP_SMALL, LineStroke.DOT_STEP_SMALL], LineStroke.DOT_STEP_SMALL); + + var dotSmallThinStroke = new BasicStroke(LineStroke.DOT_LINE_THIN_WIDTH, CAP_BUTT, + JOIN_BEVEL, 0.0, [LineStroke.DOT_STEP_SMALL, LineStroke.DOT_STEP_SMALL], + LineStroke.DOT_STEP_SMALL); + + var defaultStroke = new BasicStroke(1.0, CAP_SQUARE, JOIN_MITER, 10.0, null, 0.0); + + /** Holds all java script styles of how the ends of lines are drawn. */ + var caps = [ "butt", "round", "square"]; + + /** + * Holds all java script styles of how two connecting segments with non-zero lengths + * in a shape are joined together. + */ + var joins = [ "miter", "round", "bevel"]; + + /** + * Holds properties for line drawing styles. + * + * @param width + * The width of lines to draw. + * @param cap + * The decoration of the line ends. + * @param join + * The decoration applied where path segments meet. + * @param miterlimit + * The limit to trim the miter join. + * @param dash + * The array representing the dashing pattern. + * @param dash_phase + * The offset to start the dashing pattern. + */ + function BasicStroke(width, cap, join, miterLimit, dash, dash_phase) + { + this.getWidth = function() { return width; }; + this.getCap = function() { return caps[cap]; }; + + this.getJoin = function() { return joins[join]; }; + this.getMiterLimit = function() { return miterLimit; }; + + this.getDash = function() { return dash; }; + this.getDashPhase = function() { return dash_phase; }; + }; + + /** + * Returns the stroke style object. + * + * @param {Number} strokeStyle + * The stroke style id according to the LineStroke enumeration. + * @param {Number} width + * The line width. + * + * @return The basic line stroke object. + */ + function getLineStroke(strokeStyle, width) + { + switch(strokeStyle) + { + case LineStrokeEnum.SPECIFIED_WIDTH: + return new BasicStroke(width, CAP_SQUARE, JOIN_MITER, 10.0, null, 0.0); + case LineStrokeEnum.DOTTED: + return dotStroke; + case LineStrokeEnum.DOTTED_SMALL: + return dotSmallStroke; + case LineStrokeEnum.DOTTED_SMALL_THIN: + return dotSmallThinStroke; + default: + return defaultStroke; + } + }; + + /** + * Returns the stroke path renderer. + * + * @param {CanvasRenderingContext2D} context + * The canvas renderer. + * @param {Number} strokeStyle + * The stroke style id according to the LineStroke enumeration. + * @param {Number} width + * The line width. + * @param {Number[]} strokeColor + * The array of 3 integer values between 0 and 255 inclusive, representing + * a stroke color. + * + * @return The stroke path renderer. + */ + function getStrokePathRenderer(context, strokeStyle, width, strokeColor) + { + var basicStroke = getLineStroke(strokeStyle, width); + switch(strokeStyle) + { + case LineStrokeEnum.SPECIFIED_WIDTH: + return new WidenPathRenderer(context, basicStroke, strokeColor); + case LineStrokeEnum.DOTTED: + case LineStrokeEnum.DOTTED_SMALL: + case LineStrokeEnum.DOTTED_SMALL_THIN: + return new DotsPathRenderer(context, basicStroke, strokeColor); + default: + return { applyStroke : function (path) {} }; + } + }; + + /** + * Apply the given stroke style to the JS native canvas renderer. + * + * @param {CanvasRenderingContext2D} context + * The canvas renderer. + * @param {Number} strokeStyle + * The stroke style id. + * @param {Number} width + * The line width. + */ + this.apply = function(context, strokeStyle, width) + { + if (context instanceof CanvasRenderingContext2D) + { + var basicStroke = getLineStroke(strokeStyle, width); + context.lineWidth = basicStroke.getWidth(); + + context.lineCap = basicStroke.getCap(); + + context.lineJoin = basicStroke.getJoin(); + + context.miterLimit = basicStroke.getMiterLimit(); + if (basicStroke.getDash()) + { + context.setLineDash(basicStroke.getDash()); + } + else + { + context.setLineDash([]); + } + context.lineDashOffset = basicStroke.getDashPhase(); + } + } + + /** + * Apply the given stroke style to the area outer bounds. + * + * @param {CanvasRenderingContext2D} context + * The canvas renderer. + * @param {Number} strokeStyle + * The stroke style id. + * @param {Number} width + * The line width. + * @param {Number[]} strokeColor + * The array of 3 integer values between 0 and 255 inclusive, representing + * a stroke color. + * @param {Array} path + * The array of points that forms the outline of the target area. + */ + this.applyStrokeToPath = function(context, strokeStyle, width, strokeColor, path) + { + if (context instanceof CanvasRenderingContext2D) + { + var renderer = getStrokePathRenderer(context, strokeStyle, width, strokeColor); + renderer.applyStroke(path); + } + } + + + this.isDirectDrawingStrokeStyle = function(strokeStyle) + { + return LineStrokeEnum.DEFAULT == strokeStyle; + } + + this.getDefaultStrokeStyle = function() + { + return LineStrokeEnum.DEFAULT; + } + + /** + * Used to draw dash patterns on lines of pixels. + * + * @param {CanvasRenderingContext2D} context + * The canvas renderer. + * @param {Object} basicStroke + * The stroke style object. + * @param {Number[]} strokeColor + * The array of 3 integer values between 0 and 255 inclusive, representing + * a stroke color. + */ + function DotsPathRenderer(ctx, basicStroke, strokeColor) + { + var pixel = ctx.createImageData(1, 1); + var pixelData = pixel.data; + pixelData[3] = 0xFF; + + /** + * Normalizes a dash offset distance according to the given dash pattern. + * A normalized dash offset distance is a distance that is cut the corresponding segment + * from the dash pattern that is extended periodically in order to cover the target dash + * offset. + * + * @param {Array} dash + * The dash array that specifies the number of units to be drawn followed + * by the number of units to be skipped. + * @param {Number} dashOffset + * The offset distance from the first pattern point to the point which becomes + * a start point of the dash pattern. + * + * @return The container {startPhase : phase, startDashOn : dashOn, startIdx : idx}, + * where the startPhase field holds a normalized dash offset, startDashOn defines + * to draw or to skip, the value of startIdx is the current dash pattern index. + */ + function normalize(dash, dashOffset) + { + var phase = dashOffset; + var idx = 0; + var dashOn = true; + var d; + while (phase >= (d = dash[idx])) + { + phase -= d; + idx = (idx + 1) % dash.length; + dashOn = !dashOn; + } + + return {startPhase : phase, startDashOn : dashOn, startIdx : idx}; + } + + /** + * Apply the given stroke to the line of pixels called a path. + * + * @param {Array} path + * The array of points that forms the outline of the target area. + */ + this.applyStroke = function (path) + { + var dash = basicStroke.getDash(); + if (!dash || dash.length == 0) + { + return; + } + var dashOffset = basicStroke.getDashPhase(); + var len = dash.length; + + var patternObj = normalize(dash, dashOffset); + + var dashOn = patternObj.startDashOn; + var phase = patternObj.startPhase; + var idx = patternObj.startIdx; + var rest = dash[idx] - phase; + for (var i = 0; i < path.length; i++) + { + var point = path[i]; + if (rest < 1) + { + idx = (idx + 1) % len; + rest = dash[idx]; + dashOn = !dashOn; + } + rest -= 1; + + if (dashOn) + { + pixelData[0] = strokeColor[0]; + pixelData[1] = strokeColor[1]; + pixelData[2] = strokeColor[2]; + ctx.putImageData(pixel, point.x, point.y); + } + } + + } + } + + /** + * Used to widen lines of pixels. + * + * @param {CanvasRenderingContext2D} context + * The canvas renderer. + * @param {Object} basicStroke + * The stroke style object. + * @param {Number[]} strokeColor + * The array of 3 integer values between 0 and 255 inclusive, representing + * a stroke color. + */ + function WidenPathRenderer(ctx, basicStroke, strokeColor) + { + var width = basicStroke.getWidth(); + var image = ctx.createImageData(width, width); + var imageData = image.data; + + for (var i = 0; i < 4 * width * width; i += 4) + { + imageData[i] = strokeColor[0]; + imageData[i + 1] = strokeColor[1]; + imageData[i + 2] = strokeColor[2]; + imageData[i + 3] = 0xFF; + } + + /** + * To widen the line of pixels called a path according to the stroke style. + * + * @param {Array} path + * The array of points that forms the outline of the target area. + */ + this.applyStroke = function (path) + { + for (var i = 0; i < path.length; i++) + { + var point = path[i]; + ctx.putImageData(image, point.x - (width >> 1), point.y - (width >> 1)); + } + } + } +} \ No newline at end of file