diff --git a/src/js/tools/drawing/BaseTool.js b/src/js/tools/drawing/BaseTool.js index 4b58408..9d1f1df 100644 --- a/src/js/tools/drawing/BaseTool.js +++ b/src/js/tools/drawing/BaseTool.js @@ -82,44 +82,4 @@ ns.BaseTool.prototype.releaseToolAt = function (col, row, frame, overlay, event) {}; - /** - * Bresenham line algorithm: Get an array of pixels from - * start and end coordinates. - * - * http://en.wikipedia.org/wiki/Bresenham's_line_algorithm - * http://stackoverflow.com/questions/4672279/bresenham-algorithm-in-javascript - * - * @private - */ - ns.BaseTool.prototype.getLinePixels_ = function (x0, x1, y0, y1) { - x1 = pskl.utils.normalize(x1, 0); - y1 = pskl.utils.normalize(y1, 0); - - var pixels = []; - var dx = Math.abs(x1 - x0); - var dy = Math.abs(y1 - y0); - var sx = (x0 < x1) ? 1 : -1; - var sy = (y0 < y1) ? 1 : -1; - var err = dx - dy; - while (true) { - - // Do what you need to for this - pixels.push({'col': x0, 'row': y0}); - - if ((x0 == x1) && (y0 == y1)) { - break; - } - - var e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x0 += sx; - } - if (e2 < dx) { - err += dx; - y0 += sy; - } - } - return pixels; - }; })(); diff --git a/src/js/tools/drawing/SimplePen.js b/src/js/tools/drawing/SimplePen.js index f4d3fe8..591d21e 100644 --- a/src/js/tools/drawing/SimplePen.js +++ b/src/js/tools/drawing/SimplePen.js @@ -63,7 +63,7 @@ // The pen movement is too fast for the mousemove frequency, there is a gap between the // current point and the previously drawn one. // We fill the gap by calculating missing dots (simple linear interpolation) and draw them. - var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow); + var interpolatedPixels = pskl.PixelUtils.getLinePixels(col, this.previousCol, row, this.previousRow); for (var i = 0, l = interpolatedPixels.length ; i < l ; i++) { var coords = interpolatedPixels[i]; this.applyToolAt(coords.col, coords.row, frame, overlay, event); diff --git a/src/js/tools/drawing/Stroke.js b/src/js/tools/drawing/Stroke.js index 527d327..fd3a0f3 100644 --- a/src/js/tools/drawing/Stroke.js +++ b/src/js/tools/drawing/Stroke.js @@ -90,13 +90,13 @@ }; ns.Stroke.prototype.draw_ = function (col, row, color, targetFrame, penSize, isStraight) { + var linePixels; if (isStraight) { - var coords = this.getStraightLineCoordinates_(col, row); - col = coords.col; - row = coords.row; + linePixels = pskl.PixelUtils.getUniformLinePixels(this.startCol, col, this.startRow, row); + } else { + linePixels = pskl.PixelUtils.getLinePixels(col, this.startCol, row, this.startRow); } - var linePixels = this.getLinePixels_(this.startCol, col, this.startRow, row); pskl.PixelUtils.resizePixels(linePixels, penSize).forEach(function (point) { targetFrame.setPixel(point[0], point[1], color); }); @@ -108,50 +108,4 @@ this.draw_(replayData.col, replayData.row, replayData.color, frame, replayData.penSize, replayData.isStraight); }; - /** - * Convert col, row to be on the nearest straight line or 45 degrees diagonal. - */ - ns.Stroke.prototype.getStraightLineCoordinates_ = function(col, row) { - // coordinates translated using startCol, startRow as origin - var tCol = col - this.startCol; - var tRow = this.startRow - row; - - var dist = Math.sqrt(Math.pow(tCol, 2) + Math.pow(tRow, 2)); - - var axisDistance = Math.round(dist); - var diagDistance = Math.round(dist / Math.sqrt(2)); - - var PI8 = Math.PI / 8; - var angle = Math.atan2(tRow, tCol); - if (angle < PI8 && angle >= -PI8) { - row = this.startRow; - col = this.startCol + axisDistance; - } else if (angle >= PI8 && angle < 3 * PI8) { - row = this.startRow - diagDistance; - col = this.startCol + diagDistance; - } else if (angle >= 3 * PI8 && angle < 5 * PI8) { - row = this.startRow - axisDistance; - col = this.startCol; - } else if (angle >= 5 * PI8 && angle < 7 * PI8) { - row = this.startRow - diagDistance; - col = this.startCol - diagDistance; - } else if (angle >= 7 * PI8 || angle < -7 * PI8) { - row = this.startRow; - col = this.startCol - axisDistance; - } else if (angle >= -7 * PI8 && angle < -5 * PI8) { - row = this.startRow + diagDistance; - col = this.startCol - diagDistance; - } else if (angle >= -5 * PI8 && angle < -3 * PI8) { - row = this.startRow + axisDistance; - col = this.startCol; - } else if (angle >= -3 * PI8 && angle < -PI8) { - row = this.startRow + diagDistance; - col = this.startCol + diagDistance; - } - - return { - col : col, - row : row - }; - }; })(); diff --git a/src/js/tools/drawing/selection/LassoSelect.js b/src/js/tools/drawing/selection/LassoSelect.js index 7c9afa2..711c329 100644 --- a/src/js/tools/drawing/selection/LassoSelect.js +++ b/src/js/tools/drawing/selection/LassoSelect.js @@ -55,7 +55,7 @@ * @private */ ns.LassoSelect.prototype.getLassoPixels_ = function () { - var line = this.getLinePixels_(this.previousCol, this.startCol, this.previousRow, this.startRow); + var line = pskl.PixelUtils.getLinePixels(this.previousCol, this.startCol, this.previousRow, this.startRow); return this.pixels.concat(line); }; @@ -69,7 +69,7 @@ row = pskl.utils.Math.minmax(row, 0, frame.getHeight() - 1); // line interpolation needed in case mousemove was too fast - var interpolatedPixels = this.getLinePixels_(col, this.previousCol, row, this.previousRow); + var interpolatedPixels = pskl.PixelUtils.getLinePixels(col, this.previousCol, row, this.previousRow); this.pixels = this.pixels.concat(interpolatedPixels); // update state diff --git a/src/js/utils/Math.js b/src/js/utils/Math.js index 1a7ef93..f24aa81 100644 --- a/src/js/utils/Math.js +++ b/src/js/utils/Math.js @@ -4,6 +4,15 @@ ns.Math = { minmax : function (val, min, max) { return Math.max(Math.min(val, max), min); + }, + + /** + * Calculate the distance between {x0, y0} and {x1, y1} + */ + distance : function (x0, x1, y0, y1) { + var dx = Math.abs(x1 - x0); + var dy = Math.abs(y1 - y0); + return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); } }; })(); diff --git a/src/js/utils/PixelUtils.js b/src/js/utils/PixelUtils.js index d6d77fe..92d4232 100644 --- a/src/js/utils/PixelUtils.js +++ b/src/js/utils/PixelUtils.js @@ -234,6 +234,97 @@ */ calculateZoomForContainer : function (container, pictureHeight, pictureWidth) { return this.calculateZoom(container.height(), container.width(), pictureHeight, pictureWidth); + }, + + /** + * Bresenham line algorithm: Get an array of pixels from + * start and end coordinates. + * + * http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + * http://stackoverflow.com/questions/4672279/bresenham-algorithm-in-javascript + */ + getLinePixels : function (x0, x1, y0, y1) { + var pixels = []; + + x1 = pskl.utils.normalize(x1, 0); + y1 = pskl.utils.normalize(y1, 0); + + var dx = Math.abs(x1 - x0); + var dy = Math.abs(y1 - y0); + + var sx = (x0 < x1) ? 1 : -1; + var sy = (y0 < y1) ? 1 : -1; + + var err = dx - dy; + while (true) { + // Do what you need to for this + pixels.push({'col': x0, 'row': y0}); + + if ((x0 == x1) && (y0 == y1)) { + break; + } + + var e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + + return pixels; + }, + + /** + * Create a uniform line using the same number of pixel at each step, between the provided + * origin and target coordinates. + */ + getUniformLinePixels : function (x0, x1, y0, y1) { + var pixels = []; + + x1 = pskl.utils.normalize(x1, 0); + y1 = pskl.utils.normalize(y1, 0); + + var dx = Math.abs(x1 - x0) + 1; + var dy = Math.abs(y1 - y0) + 1; + + var sx = (x0 < x1) ? 1 : -1; + var sy = (y0 < y1) ? 1 : -1; + + var ratio = Math.max(dx, dy) / Math.min(dx, dy); + // in pixel art, lines should use uniform number of pixels for each step + var pixelStep = Math.round(ratio) || 0; + + if (pixelStep > Math.min(dx, dy)) { + pixelStep = Infinity; + } + + var maxDistance = pskl.utils.Math.distance(x0, x1, y0, y1); + + var x = x0; + var y = y0; + var i = 0; + while (true) { + i++; + + pixels.push({'col': x, 'row': y}); + if (pskl.utils.Math.distance(x0, x, y0, y) >= maxDistance) { + break; + } + + var isAtStep = i % pixelStep === 0; + if (dx >= dy || isAtStep) { + x += sx; + } + if (dy >= dx || isAtStep) { + y += sy; + } + } + + return pixels; } }; })(); diff --git a/test/js/utils/PixelUtilsTest.js b/test/js/utils/PixelUtilsTest.js new file mode 100644 index 0000000..1f5e81c --- /dev/null +++ b/test/js/utils/PixelUtilsTest.js @@ -0,0 +1,95 @@ +describe("PixelUtils test suite", function() { + + beforeEach(function() {}); + afterEach(function() {}); + + var checkPixel = function (pixel, col, row) { + expect(pixel.col).toBe(col); + expect(pixel.row).toBe(row); + }; + + var checkPixels = function (pixels /*, expectedPoints ... */) { + var expectedPoints = Array.prototype.slice.call(arguments, 1); + pixels.forEach(function (pixel, i) { + checkPixel(pixel, expectedPoints[i][0], expectedPoints[i][1]); + }); + }; + + var logPixels = function (pixels) { + var buffer = []; + pixels.forEach(function (p) { + buffer.push('[' + p.col + ',' + p.row + ']'); + }); + console.log(buffer.join(',')); + }; + + it("calculates line pixels", function() { + // single point + console.log('Check getLinePixels(0, 0, 0, 0)'); + var pixels = pskl.PixelUtils.getLinePixels(0, 0, 0, 0); + expect(pixels.length).toBe(1); + checkPixel(pixels[0], 0, 0); + + // 2-points line + console.log('Check getLinePixels(0, 1, 0, 1)'); + pixels = pskl.PixelUtils.getLinePixels(0, 1, 0, 1); + expect(pixels.length).toBe(2); + checkPixel(pixels[0], 0, 0); + checkPixel(pixels[1], 1, 1); + + // same line as before, reversed order + console.log('Check getLinePixels(1, 0, 1, 0)'); + pixels = pskl.PixelUtils.getLinePixels(1, 0, 1, 0); + expect(pixels.length).toBe(2); + // check pixels are returned in reverse order + checkPixel(pixels[0], 1, 1); + checkPixel(pixels[1], 0, 0); + + // non trivial line + console.log('Check getLinePixels(0, 2, 0, 7)'); + pixels = pskl.PixelUtils.getLinePixels(0, 2, 0, 7); + expect(pixels.length).toBe(8); + checkPixels(pixels, [0,0],[0,1],[1,2],[1,3],[1,4],[1,5],[2,6],[2,7]); + }); + + it("calculates uniform line pixels", function() { + // single point + console.log('Check getUniformLinePixels(0, 0, 0, 0)'); + var pixels = pskl.PixelUtils.getUniformLinePixels(0, 0, 0, 0); + expect(pixels.length).toBe(1); + checkPixel(pixels[0], 0, 0); + + // 2-points line + console.log('Check getUniformLinePixels(0, 1, 0, 1)'); + pixels = pskl.PixelUtils.getUniformLinePixels(0, 1, 0, 1); + expect(pixels.length).toBe(2); + checkPixel(pixels[0], 0, 0); + checkPixel(pixels[1], 1, 1); + + // same line as before, reversed order + console.log('Check getUniformLinePixels(1, 0, 1, 0)'); + pixels = pskl.PixelUtils.getUniformLinePixels(1, 0, 1, 0); + expect(pixels.length).toBe(2); + // check pixels are returned in reverse order + checkPixel(pixels[0], 1, 1); + checkPixel(pixels[1], 0, 0); + + // computed step should be 2, min dist is 3 (y1 -y0 + 1)-> step should be applied + console.log('Check getUniformLinePixels(0, 5, 0, 2)'); + pixels = pskl.PixelUtils.getUniformLinePixels(0, 5, 0, 2); + expect(pixels.length).toBe(6); + checkPixels(pixels, [0,0],[1,0],[2,1],[3,1],[4,2],[5,2]); + + // computed step should be 3, min dist is 2 (y1 -y0 + 1)-> step > minDist -> straight line + console.log('Check getUniformLinePixels(0, 5, 0, 1)'); + pixels = pskl.PixelUtils.getUniformLinePixels(0, 5, 0, 1); + expect(pixels.length).toBe(7); + checkPixels(pixels, [0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0]); + + // non trivial line + console.log('Check getUniformLinePixels(0, 2, 0, 7)'); + pixels = pskl.PixelUtils.getUniformLinePixels(0, 2, 0, 7); + expect(pixels.length).toBe(8); + checkPixels(pixels, [0,0],[0,1],[0,2],[1,3],[1,4],[1,5],[2,6],[2,7]); + }); +}); \ No newline at end of file