diff --git a/src/js/core/base-layers.js b/src/js/core/base-layers.js index d54b92c3..22c4e9a4 100644 --- a/src/js/core/base-layers.js +++ b/src/js/core/base-layers.js @@ -49,7 +49,6 @@ class Base_layers_class { return instance; } instance = this; - this.Base_gui = new Base_gui_class(); this.Helper = new Helper_class(); this.Image_trim = new Image_trim_class(); @@ -95,6 +94,10 @@ class Base_layers_class { this.render(true); } + getZoomView() { + return zoomView; + } + init_zoom_lib() { zoomView.setBounds(0, 0, config.WIDTH, config.HEIGHT); zoomView.setContext(this.ctx); diff --git a/src/js/libs/image-border-effect.js b/src/js/libs/image-border-effect.js index c7f277b2..9cf20977 100644 --- a/src/js/libs/image-border-effect.js +++ b/src/js/libs/image-border-effect.js @@ -10,21 +10,114 @@ function hexToRgb(hex) { : [0, 0, 0]; } -const drawBorder = (ctx, hexColor) => { - const width = ctx.canvas.width, - height = ctx.canvas.height; - const imageData = ctx.getImageData(0, 0, width, height); +/** + * Detects the shapes from the list of points then creates + * separate list of points for each shape. In every shapes + * the points are ordered so that it's possible to draw the shape + * just iterating one by one + * @param {number[][]} points + * @returns {number[][]} + */ +function constructShapes(points) { + const shapes = []; + + points.map((point) => { + let shapeIndex = -1; + let closestPointIndex = -1; + + shapes.map((shape, index) => { + let { index: _closestPointIndex, ...rest } = getClosestPoint( + shape, + point, + 2 + ); + if (_closestPointIndex >= 0) { + shapeIndex = index; + closestPointIndex = _closestPointIndex; + } + }); + + if (shapeIndex === -1) { + shapes.push([]); + shapeIndex = shapes.length - 1; + closestPointIndex = 0; + } + let nextPoint = shapes[shapeIndex][closestPointIndex + 1]; + if ( + !nextPoint || + nextPoint[0] != point[0] || + nextPoint[1] != point[1] + ) { + shapes[shapeIndex].splice(closestPointIndex + 1, 0, point); + } + }); + return shapes; +} + +/** + * + * @param {number[][]} points + * @param {number[]} point + * @param {number} distance + * @returns + */ +function getClosestPoint(points, point, distance) { + let dist = distance + 1; + let i = -1; + points.map((nextPoint, index) => { + const d = getDistance(point, nextPoint); + if (d !== 0 && d < dist) { + i = index; + dist = d; + } + }); + + if (dist > distance || i === -1) return { closestPoint: null }; - const dataCopy = new Uint8ClampedArray(imageData.data); + return { closestPoint: points[i], index: i }; +} + +/** + * Calculates the distance between to coordinates + * @param {number[]} point1 + * @param {number[]} point2 + * @returns + */ +function getDistance(point1, point2) { + const xDif = Math.pow(point2[0] - point1[0], 2); + const yDif = Math.pow(point2[1] - point1[1], 2); + const d = Math.pow(xDif + yDif, 0.5); + return d; +} + +/** + * + * @param {canvas.context} ctx + * @param {string} hexColor + * @param {number} borderWidth + */ +const drawBorder = (ctx, hexColor, borderWidth) => { + const points = []; + + const width = ctx.canvas.width + 150, + height = ctx.canvas.height + 150; + + const imageData = ctx.getImageData(0, 0, width, height); const length = imageData.data.length; - const changeColor = (position) => { - if (imageData.data[position + 3] === 0) { - dataCopy.set([...hexToRgb(hexColor), 255], position); + const usePoint = (position, row, col) => { + if (imageData.data[position + 3] <= 1) { + points.push([col, row]); } }; + let row = -1; for (let i = 0; i < length; i += 4) { + if (!(i % (width * 4))) { + row++; + } + const col = (i - row * width * 4) / 4; + const top = i - width * 4; const bottom = i + width * 4; const left = i - 4; @@ -43,22 +136,50 @@ const drawBorder = (ctx, hexColor) => { continue; } - // Change left, top, right and bottom neighbors colors if they are transparent - changeColor(left); - changeColor(top); - changeColor(right); - changeColor(bottom); - - // Changes corner neighbors colors if they are transparent - changeColor(topLeftCorner); - changeColor(topRightCorner); - changeColor(bottomLeftCorner); - changeColor(bottomRightCorner); + // Use left, top, right and bottom neighbors if they are transparent + usePoint(left, row, col); + usePoint(top, row, col); + usePoint(right, row, col); + usePoint(bottom, row, col); + + // Use corner neighbors if they are transparent + usePoint(topLeftCorner, row, col); + usePoint(topRightCorner, row, col); + usePoint(bottomLeftCorner, row, col); + usePoint(bottomRightCorner, row, col); } - imageData.data.set(dataCopy); + const shapes = constructShapes(points); + + ctx.lineWidth = borderWidth; + ctx.strokeStyle = hexColor; + shapes.map((shape) => { + ctx.beginPath(); + ctx.moveTo(...shape[0]); + shape.map((point, index) => { + if (index > 1) { + // Smooth lines + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + // If it's further than 2 pixels then move to the next point + if (getDistance(shape[index - 1], point) > 2) { + ctx.moveTo(...point); + } else { + const prevPoint = shape[index - 1]; + const controlPointX = (prevPoint[0] + point[0]) / 2; + const controlPointY = (prevPoint[1] + point[1]) / 2; - return imageData; + ctx.quadraticCurveTo( + ...shape[index - 1], + controlPointX, + controlPointY + ); + } + } + }); + ctx.stroke(); + }); }; export default drawBorder; diff --git a/src/js/modules/effects/borders.js b/src/js/modules/effects/borders.js index a6560168..79a60712 100644 --- a/src/js/modules/effects/borders.js +++ b/src/js/modules/effects/borders.js @@ -1,6 +1,7 @@ import app from "./../../app.js"; import config from "./../../config.js"; import Base_layers_class from "./../../core/base-layers.js"; +import Base_gui_class from "./../../core/base-gui.js"; import Dialog_class from "./../../libs/popup.js"; import alertify from "./../../../../node_modules/alertifyjs/build/alertify.min.js"; import Effects_browser_class from "./browser"; @@ -9,6 +10,7 @@ import drawBorder from "../../libs/image-border-effect.js"; class Effects_borders_class { constructor() { this.POP = new Dialog_class(); + this.Base_gui = new Base_gui_class(); this.Base_layers = new Base_layers_class(); this.Effects_browser = new Effects_browser_class(); } @@ -69,33 +71,26 @@ class Effects_borders_class { render_pre(ctx, data) {} render_post(ctx, data, layer) { + const zoomPos = this.Base_layers.getZoomView().getPosition(); + const { w, h } = this.Base_gui.GUI_preview.PREVIEW_SIZE; const size = Math.max(0, data.params.size); - const x = layer.x; - const y = layer.y; - const width = parseInt(layer.width); - const height = parseInt(layer.height); - - //legacy check - if (x == null) x = 0; - if (y == null) y = 0; - if (!width) width = config.WIDTH; - if (!height) height = config.HEIGHT; ctx.save(); + let borderWidth = size * config.ZOOM; - // We need to get aspect ratio for preview canvas and for the main canvas when it gets zoom < 1 - const aspectRatio = ctx.canvas.height / config.HEIGHT; - - let borderWidth = size * aspectRatio; - // This is the case when it's zoomed more than the canvas size - if (aspectRatio >= 1 && aspectRatio <= config.ZOOM) { - borderWidth = size * config.ZOOM; - } - // Draw border multiple times to get the necessary with in pixels - for (let i = 0; i < borderWidth; i++) { - ctx.putImageData(drawBorder(ctx, data.params.color), 0, 0); + // If this is the preview canvas + if ( + ctx.canvas.id === this.Base_gui.GUI_preview.canvas_preview.canvas.id + ) { + ctx.scale(config.WIDTH / w, config.HEIGHT / h); + borderWidth = (size * w) / config.WIDTH; + } else { + ctx.scale(1 / config.ZOOM, 1 / config.ZOOM); + ctx.translate(-zoomPos.x, -zoomPos.y); } + drawBorder(ctx, data.params.color, borderWidth); + ctx.restore(); }