Skip to content

Commit

Permalink
Issue viliusle#266 - Draw border by drawing lines through the points
Browse files Browse the repository at this point in the history
- Constract all those points which are alinged on the edges of the image
- Constract separate shapes by using the points above
- Draw each shape through Canvas line drawing APIs
  • Loading branch information
kmanaseryan committed Nov 19, 2021
1 parent ee6cc19 commit 8277c89
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 43 deletions.
5 changes: 4 additions & 1 deletion src/js/core/base-layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
163 changes: 142 additions & 21 deletions src/js/libs/image-border-effect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
37 changes: 16 additions & 21 deletions src/js/modules/effects/borders.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();
}
Expand Down Expand Up @@ -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();
}

Expand Down

2 comments on commit 8277c89

@JordanMagnuson
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @kmanaseryan this is what I'm seeing using latest method vs. Photoshop: https://i.imgur.com/McovnEk.png

@JordanMagnuson
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is Photoshop on left, Photopea.com on right (outside stroke 10px): https://i.imgur.com/Lz20IS7.png

You can seat that Photopea actually does have a little bit of stair-step on diagonal line (Photoshop does not), so maybe they are using some version of the algorythm you are using here?

Please sign in to comment.