Skew issue when maintaining constant image stroke - fabricjs

I'm working on a project where images can be added to a FabricJS canvas at a variety of resolutions. Because of how Fabric handles stroke widths, this variance in image sizes causes strokes to be inconsistently rendered.
Borrowing from byoungb's solution for maintaining a constant stroke width, I've managed to get my image borders to remain both visually consistent, and have correct control/bounding positions by overriding the _renderStroke and _getTransformedDimensions methods of the Image class. This breaks down however in cases where the image is skewed and I can't seem to nail down the correct place to modify the object dimension calculation in cases where skewing is present (I expect I need to modify the _calcDimensionsTransformMatrix method for Image but have had no success so far).
http://jsfiddle.net/melchiar/bt4ckmea/ (bounding rectangle shown in black)
//override stroke rendering for constant stroke width independent of scaling
fabric.Image.prototype._renderStroke = function(ctx) {
if (!this.stroke || this.strokeWidth === 0) {
return;
}
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
ctx.save();
ctx.scale(1 / this.scaleX, 1 / this.scaleY);
this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke);
this._applyPatternGradientTransform(ctx, this.stroke);
ctx.stroke();
ctx.restore();
};
//modify method for calculating control and bounding box positions
fabric.Image.prototype._getTransformedDimensions = function(skewX, skewY) {
if (typeof skewX === 'undefined') {
skewX = this.skewX;
}
if (typeof skewY === 'undefined') {
skewY = this.skewY;
}
var dimensions = this._getNonTransformedDimensions();
if (skewX === 0 && skewY === 0) {
return {
x: dimensions.x * this.scaleX + (this.strokeWidth * (1 - this.scaleX)),
y: dimensions.y * this.scaleY + (this.strokeWidth * (1 - this.scaleY))
};
}
var dimX = dimensions.x / 2,
dimY = dimensions.y / 2,
points = [{
x: -dimX,
y: -dimY
},
{
x: dimX,
y: -dimY
},
{
x: -dimX,
y: dimY
},
{
x: dimX,
y: dimY
}
],
i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY, false),
bbox;
for (i = 0; i < points.length; i++) {
points[i] = fabric.util.transformPoint(points[i], transformMatrix);
}
bbox = fabric.util.makeBoundingBoxFromPoints(points);
return {
x: bbox.width,
y: bbox.height
};
};

I ended up finding the solution. The bounding box size of skewed objects with modified stroke can be fixed by modifying the bbox values being returned at the end of the _getTransformedDimensions method.
http://jsfiddle.net/melchiar/6Ljguz7k/
//override stroke rendering for constant stroke width independent of scaling
fabric.Image.prototype._renderStroke = function(ctx) {
if (!this.stroke || this.strokeWidth === 0) {
return;
}
if (this.shadow && !this.shadow.affectStroke) {
this._removeShadow(ctx);
}
ctx.save();
ctx.scale(1 / this.scaleX, 1 / this.scaleY);
this._setLineDash(ctx, this.strokeDashArray, this._renderDashedStroke);
this._applyPatternGradientTransform(ctx, this.stroke);
ctx.stroke();
ctx.restore();
};
//modify method for calculating control and bounding box positions
fabric.Image.prototype._getTransformedDimensions = function(skewX, skewY) {
if (typeof skewX === 'undefined') {
skewX = this.skewX;
}
if (typeof skewY === 'undefined') {
skewY = this.skewY;
}
var dimensions = this._getNonTransformedDimensions();
if (skewX === 0 && skewY === 0) {
return {
x: dimensions.x * this.scaleX + (this.strokeWidth * (1 - this.scaleX)),
y: dimensions.y * this.scaleY + (this.strokeWidth * (1 - this.scaleY))
};
}
var dimX = dimensions.x / 2,
dimY = dimensions.y / 2,
points = [{
x: -dimX,
y: -dimY
},
{
x: dimX,
y: -dimY
},
{
x: -dimX,
y: dimY
},
{
x: dimX,
y: dimY
}
],
i, transformMatrix = this._calcDimensionsTransformMatrix(skewX, skewY, false),
bbox;
for (i = 0; i < points.length; i++) {
points[i] = fabric.util.transformPoint(points[i], transformMatrix);
}
bbox = fabric.util.makeBoundingBoxFromPoints(points);
return {
x: bbox.width + (this.strokeWidth * (1 - this.scaleX)),
y: bbox.height + (this.strokeWidth * (1 - this.scaleY))
};
};

Related

Dashed border seems like its multiplying

I'm makking a Node.js app with Jimp. I have a function for selecting part of an image. It draws a rectangle in the area, writes the number of the selection in the middle and then draws a dashed border around the outside of the rectangle selection. The dashed border seems to be multiplying and I have no idea why. Here's the full code of the file:
imageManipulationUtil.js:
const Jimp = require("jimp");
module.exports = async (readPath, writePath, comments, callback) => {
const originalImage = await Jimp.read(readPath);
const font = await Jimp.loadFont(Jimp.FONT_SANS_32_BLACK);
// Please ignore this part. Focus on the other part (the functions I mentioned above).
const addedWidth = 500;
const commentsHeight = comments.reduce((commentsHeight, { comment }, i) => {
comments[i].comment = `${i + 1}. ${comment}`;
const textWidth = Jimp.measureText(font, comment);
const textHeight = Jimp.measureTextHeight(font, comment);
const lines = Math.ceil(textWidth / addedWidth);
const height = textHeight * lines;
return commentsHeight + height;
}, 0);
const imageHeight = commentsHeight + 10;
if (imageHeight > originalImage.getHeight())
originalImage.resize(Jimp.AUTO, imageHeight);
const newImage = new Jimp(
originalImage.getWidth() + addedWidth,
originalImage.getHeight(),
0xffffffff
);
// Some other code for another purpose
// !!! Important code START !!!
drawSelectionRects(comments, font, newImage);
async function drawSelectionRects(comments, font, image) {
comments.forEach(({ dimensions }) => {
image.scanQuiet(
dimensions.x,
dimensions.y,
dimensions.width,
dimensions.height,
(x, y, idx) => {
const color = {
r: image.bitmap.data[idx + 0],
g: image.bitmap.data[idx + 1],
b: image.bitmap.data[idx + 2],
a: image.bitmap.data[idx + 3] / 255,
};
const selectionColor = {
r: 187,
g: 187,
b: 187,
a: 187,
};
const newColor = blendColors(color, selectionColor);
const hexColor = Jimp.rgbaToInt(
newColor.r,
newColor.g,
newColor.b,
255
);
image.setPixelColor(hexColor, x, y);
}
);
dashedBorder(
image,
{ lineDash: [20, 5], lineWidth: 3, color: 0x1a53ffbb },
dimensions
);
});
comments.forEach(({ dimensions }, i) => {
const text = `${i + 1}`;
let textX =
dimensions.x + (dimensions.width - Jimp.measureText(font, text)) / 2;
let textY =
dimensions.y +
(dimensions.height - Jimp.measureTextHeight(font, text)) / 2;
image.print(font, textX, textY, text);
});
}
function blendColors(c1, c2) {
const stepPoint = c2.a / 255;
const r = c1.r + stepPoint * (c2.r - c1.r);
const g = c1.g + stepPoint * (c2.g - c1.g);
const b = c1.b + stepPoint * (c2.b - c1.b);
return { r, g, b };
}
function dashedBorder(
image,
{ lineDash, lineWidth, color },
{ x, y, width, height }
) {
let drawing = true,
passed = 0;
color = Jimp.intToRGBA(color);
// Top border
for (let i = x; i < x + width; i++) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(x, y));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
i,
y - k
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
drawing = true;
// Right border
for (let j = y; j < y + height; j++) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(x + width, y));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
x + width + k,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
drawing = true;
// Bottom border
for (let i = x + width; i > x; i--) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(i, y + height));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
i,
y + height + k
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
drawing = true;
// Left border
for (let j = y + height; j > y + lineWidth; j--) {
if (drawing) {
const pixelColor = Jimp.intToRGBA(image.getPixelColor(x, j));
const newColor = blendColors(pixelColor, color);
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(newColor.r, newColor.g, newColor.b, 255),
x - k,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
}
newImage.write(writePath);
callback();
};
// !!! Important code END !!!
The code exports a function that takes some parameters. The most important parameter is the comments parameter (specifically the dimensions of the comment). The comments param is an array of objects. The objects have a dimensions key which is the important one in this question.
The image uploaded:
Image recieved:
The problem that makes me think the image is multiplying is that in some places the border is more transparent than in other parts. Originally I thought that was just because of the image I used but then I switched it and noticed it was a real problem.
New full code:
const Jimp = require("jimp");
function dashedBorder(
image,
{ lineDash, lineWidth, color },
{ x, y, width, height }
) {
let drawing = true,
passed = 0,
outsideWidth = lineWidth - 1;
color = Jimp.intToRGBA(color);
// Top border
for (let i = x - outsideWidth; i < x + width + outsideWidth; i++) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
i,
y - k
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
// Right border
for (
let j = y + lineWidth - outsideWidth;
j < y + height - (lineWidth - outsideWidth);
j++
) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
x + width + k - 1,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
// Bottom border
for (let i = x + width + lineWidth - outsideWidth; i > x - lineWidth; i--) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
i,
y + height + k - 1
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
// Left border
for (let j = y + height - outsideWidth; j > y; j--) {
if (drawing) {
for (let k = 0; k < lineWidth; k++) {
image.setPixelColor(
Jimp.rgbaToInt(color.r, color.g, color.b, 255),
x - k,
j
);
}
}
passed++;
if (
(passed >= lineDash[0] && drawing) ||
(passed >= lineDash[1] && !drawing)
) {
drawing = !drawing;
passed = 0;
}
}
}
(async () => {
let image = await Jimp.read("./test.png");
dashedBorder(
image,
{ lineWidth: 3, lineDash: [20, 5], color: 0x1a53ffbb },
{ x: 0, y: 0, width: image.bitmap.width, height: image.bitmap.height }
);
image.write("./test-border.png");
})();
Image:

Fabric custom object

fabric.ThreePointArc = fabric.util.createClass(fabric.Circle, {
type: 'threePointArc',
points: [], //array of startPoint, intermediatePoint, endPoint
arcCenter: new fabric.Point(null, null),
arcBounds: null,
radius: null,
initialize: function (points, options) {
if (!points || points.length === 0) {
return;
}
this.points = points;
this.callSuper('initialize', options);
// supports only originX and originY as center
this.originX = this.originY = 'center';
},
_set: function(key, value) {
this.callSuper('_set', key, value);
if (key === 'points') {
this._calcArcCenter();
this._calcDimensions();
this.setCoords();
}
return this;
},
setRadius: function(value) {
this.radius = value;
return this;
},
_calcDimensions: function() {
this._calcArcAngles();
this.setRadius(cMath.getLength(this.arcCenter, this.points[0]));
this._calcArcBounds();
this.width = this.arcBounds.width;
this.height = this.arcBounds.height;
this.top = this.arcBounds.y + this.arcBounds.height / 2;
this.left = this.arcBounds.x + this.arcBounds.width / 2;
},
_calcArcCenter: function() {
var c1Mp = cMath.getMidPoint(this.points[0], this.points[1]), // chord 1 midpoint
c2Mp = cMath.getMidPoint(this.points[1], this.points[2]), // chord 2 midpoint
c1pm = -(1 / cMath.getSlope(this.points[0], this.points[1])), // chord 1 perpendicular bisector slope
c2pm = -(1 / cMath.getSlope(this.points[1], this.points[2])); // chord 2 perpendicular bisector slope
// chord perpendicular bisectors meet at the center
this.arcCenter.x = (c2Mp.y - (c2pm * c2Mp.x) + (c1pm * c1Mp.x) - c1Mp.y) / (c1pm - c2pm);
this.arcCenter.y = (c2pm * (this.arcCenter.x - c2Mp.x)) + c2Mp.y;
},
_calcArcBounds: function() {
var validPoints = this.buildValidPointsForArc(),
minX = fabric.util.array.min(validPoints, 'x'),
minY = fabric.util.array.min(validPoints, 'y'),
maxX = fabric.util.array.max(validPoints, 'x'),
maxY = fabric.util.array.max(validPoints, 'y'),
width = (maxX - minX) || 1,
height = (maxY - minY) || 1;
this.arcBounds = {
x: minX,
y: minY,
width: width,
height: height
}
},
buildValidPointsForArc: function() {
var direction = this.getRenderingDirection(),
possibleBoundingPoints = this.points.concat();
!this.arcAngles && this._calcArcAngles();
if (direction) {
for (var i = 1; i <= 4; i++) {
var randomAngle = i * (PI / 2);
if (this.arcAngles.startAngle < this.arcAngles.endAngle) {
!(this.arcAngles.startAngle <= randomAngle && randomAngle <= this.arcAngles.endAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
} else {
(this.arcAngles.endAngle <= randomAngle && randomAngle <= this.arcAngles.startAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
}
}
} else {
for (var i = 4; i >= 1; i--) {
var randomAngle = i * (PI / 2);
if (this.arcAngles.startAngle < this.arcAngles.endAngle) {
(this.arcAngles.startAngle <= randomAngle && randomAngle <= this.arcAngles.endAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
} else {
!(this.arcAngles.endAngle <= randomAngle && randomAngle <= this.arcAngles.startAngle) &&
possibleBoundingPoints.push(this.generateArcPoint(randomAngle));
}
}
}
return possibleBoundingPoints;
},
generateArcPoint: function(angle) {
return new fabric.Point(this.arcCenter.x + this.radius * Math.cos(angle), this.arcCenter.y + this.radius * Math.sin(angle));
},
_calcArcAngles: function() {
var angleKeyRepo = ["startAngle", "intermediateAngle", "endAngle"];
this.arcAngles = this.arcAngles || {};
this.points.forEach(function(point, index) {
var a = cMath.getAngle(this.arcCenter, point);
this.arcAngles[angleKeyRepo[index]] = a < 0 ? ((PI * 2) + a) :
a > 2 * PI ? ((PI * 2) - a) : a;
}, this);
},
getRenderingDirection: function() {
return (((this.points[1].x - this.points[0].x) * (this.points[2].y - this.points[0].y)) -
((this.points[1].y - this.points[0].y) * (this.points[2].x - this.points[0].x))) < 0;
},
_render: function(ctx, noTransform) {
if (!this.visible) {
return;
}
ctx.beginPath();
ctx.arc(
noTransform ? this.left : 0,
noTransform ? this.top : 0,
this.radius,
this.arcAngles.startAngle,
this.arcAngles.endAngle,
this.getRenderingDirection()
);
this._renderFill(ctx);
this._renderStroke(ctx);
},
toObject: function (propertiesToInclude) {
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
points: this.points
});
}
});
fabric.ThreePointArc.fromObject = function(object) {
return new fabric.ThreePointArc(object.points, object);
};
fabric.ThreePointArc.async = false;
fabric.util.cMath = {
getSlope : function (startPoint, endPoint) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
// hack to get around the indefinte slope problem
if (endPoint.x == startPoint.x) startPoint.x = startPoint.x + 0.01;
if (endPoint.y == startPoint.y) endPoint.y = endPoint.y + 0.01;
return (endPoint.y - startPoint.y) / (endPoint.x - startPoint.x);
},
getMidPoint: function (startPoint, endPoint) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
return { x: (startPoint.x + endPoint.x) / 2, y: (startPoint.y + endPoint.y) / 2 };
},
getAngle: function (startPoint, endPoint, isDegree) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
var radians = Math.atan2((endPoint.y - startPoint.y), (endPoint.x - startPoint.x)),
degrees = fabric.util.radiansToDegrees(radians);
return isDegree ? degrees < 0 ? 360 + degrees : degrees : radians;
},
getLength: function (startPoint, endPoint) {
if (!startPoint || !endPoint) {
console.error('startPoint and endPoint are required to evaluate slope');
return;
}
return Math.sqrt(Math.pow(endPoint.y - startPoint.y, 2) + Math.pow(endPoint.x - startPoint.x, 2));
}
}
var canvas = new fabric.Canvas('c');
var startPoint = new fabric.Point(47.25423728813553, 56.91525423728814),
intermediatePoint = new fabric.Point( 76.33898305084739,19.8983050847458 ),
endPoint = new fabric.Point( 105.42372881355931,86 );
var arc = new fabric.ThreePointArc([startPoint, intermediatePoint, endPoint] , {
fill: "#FF0000",
stroke: "#000",
strokeWidth: 10
});
canvas.add(arc);
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.10/fabric.js"></script>
<canvas id="c" width="800" height="800"></canvas>
I'm trying to create a three point arc on fabric js (ver: 1.4.10). What I want is exactly like this:- https://www.screencast.com/t/dMLvcOduEF
I have a points array from which I'm calcualting the startAngle, endAngle, arcCenter, radius, bounding boxc height and widht, top and left. When I draw it, the actual object is not drawing where my mouse points. It adds up the left and top value with the arcCenter.x and arcCenter.y respectively and draws there. Is there any workaround so that my arc can be drawn where my pointer is ?. So that I don't need to write any extra lines of code for moving, rotating and scaling.
What I'm getting is like this:- https://www.screencast.com/t/V9MUgB3pB
added the fiddle!!
Is there any workaround for this ??

Finding the shortest path between two points in 2D Array

I have a simple game, I'm trying to get the shortest route between 2 points
The map consists of 2d array matrix: Node[][],
class Node{
index: {
x: number,
y: number
},
isAvailable: boolean
}
The algorithm should return the shortest path with respect to node availability.
e.g. Trees are marked as unavailable node.isAvailable = false
I'm stuck on implementing the algorithm for this matrix
I tried to use Dijkstras algorithm from here, but I couldn't figure out how to apply it, I did
const graph = new Dijkstra();
//convert the matrix (2d array) to graph
matrix.map((row) => {
row.map((node: Node) => {
let x = node.index.x;
let y = node.index.y;
graph.addVertex(x + ":" + y, {x: x, y: y});
});
});
console.log(graph.shortestPath('0:0', '5:5'));
//the output was ['0:0'] (definitly not the answer)
How can I apply the algorithm on this matrix?
P.S here is my full code
I used a method best described as spilling paint at the target:
You mark the target square with 0, then traverse the neighbours and mark them as 1, which represents distance to target, then traverse neighbours of neighbours, etc. Repeat the process until the paint reaches your troll. All that is left for troll to do is just start moving to the squares with lowest potential.
It becomes more fun once you have multiple characters that need to path around each other while everyone is moving.
I had to implement the A* algorithm
export class PathFinder {
grid: Tile[][];
gridHeight: number;
gridWidth: number;
startTile: Tile;
endTile: Tile;
/** Array of the already checked tiles. */
closedList: List<Tile> = new List<Tile>();
openList: List<Tile> = new List<Tile>();
constructor(grid: Tile[][], gridHeight: number, gridWidth: number) {
this.grid = grid;
this.gridHeight = gridHeight;
this.gridWidth = gridWidth;
}
searchPath(start: Tile, end: Tile): Tile[] {
this.startTile = start;
this.endTile = end;
/** Path validation */
if (!start.walkable) {
console.log('The start tile in not walkable, choose different tile than', start.index);
return [];
}
if (!end.walkable) {
console.log('The end tile in not walkable, choose different tile than', end.index);
return [];
}
/** Start A* Algorithm */
/** Add the starting tile to the openList */
this.openList.push(start);
let currentTile;
/** While openList is not empty */
while (this.openList.length) {
//current node = node for open list with the lowest cost.
currentTile = this.getTileWithLowestTotal(this.openList);
//if the currentTile is the endTile, then we can stop searching
if(JSON.stringify(currentTile.index) === JSON.stringify(end.index)){
this.startTile.setBackgroundColor("rgba(255, 45, 45, .8)");
this.endTile.setBackgroundColor("rgba(255, 45, 45, .8)");
return this.shortestPath();
}
else {
//move the current tile to the closed list and remove it from the open list.
this.openList.remove(currentTile);
this.closedList.push(currentTile);
// //Get all adjacent Tiles
let adjacentTiles = this.getAdjacentTiles(currentTile);
for (let adjacentTile of adjacentTiles) {
//Get tile is not in the open list
if (!this.openList.contains(adjacentTile)) {
//Get tile is not in the closed list
if (!this.closedList.contains(adjacentTile)) {
//move it to the open list and calculate cost
this.openList.push(adjacentTile);
//calculate the cost
adjacentTile.cost = currentTile.cost + 1;
//calculate the manhattan distance
adjacentTile.heuristic = this.manhattanDistance(adjacentTile);
// calculate the total amount
adjacentTile.total = adjacentTile.cost + adjacentTile.heuristic;
currentTile.setBackgroundColor('rgba(0, 181, 93, 0.8)');
}
}
}
}
}
}
getTileWithLowestTotal(openList: Tile[]): Tile {
let tileWithLowestTotal = new Tile();
let lowestTotal: number = 999999999;
/** Search open tiles and get the tile with the lowest total cost */
for (let openTile of openList) {
if (openTile.total <= lowestTotal) {
//clone lowestTotal
lowestTotal = openTile.total;
tileWithLowestTotal = openTile;
}
}
return tileWithLowestTotal;
}
getAdjacentTiles(current: Tile): Tile[] {
let adjacentTiles: Tile[] = [];
let adjacentTile: Tile;
//Tile to left
if (current.index.x - 1 >= 0) {
adjacentTile = this.grid[current.index.x - 1][current.index.y];
if (adjacentTile && adjacentTile.walkable) {
adjacentTiles.push(adjacentTile);
}
}
//Tile to right
if (current.index.x + 1 < this.gridWidth) {
adjacentTile = this.grid[current.index.x + 1][current.index.y];
if (adjacentTile && adjacentTile.walkable) {
adjacentTiles.push(adjacentTile);
}
}
//Tile to Under
if (current.index.y + 1 < this.gridHeight) {
adjacentTile = this.grid[current.index.x][current.index.y + 1];
if (adjacentTile && adjacentTile.walkable) {
adjacentTiles.push(adjacentTile);
}
}
//Tile to Above
if (current.index.y - 1 >= 0) {
adjacentTile = this.grid[current.index.x][current.index.y - 1];
if (adjacentTile && adjacentTile.walkable) {
adjacentTiles.push(adjacentTile);
}
}
/** TODO: Diagonal moves */
return adjacentTiles;
}
/** Calculate the manhattan distance */
manhattanDistance(adjacentTile: Tile): number {
return Math.abs((this.endTile.index.x - adjacentTile.index.x) +
(this.endTile.index.y - adjacentTile.index.y));
}
shortestPath() {
let startFound: boolean = false;
let currentTile = this.endTile;
let pathTiles = [];
//includes the end tile in the path
pathTiles.push(this.endTile);
this.endTile.ball = true;
while (!startFound) {
let adjacentTiles = this.getAdjacentTiles(currentTile);
//check to see what newest current tile.
for (let adjacentTile of adjacentTiles) {
//check if it is the start tile
if (JSON.stringify(adjacentTile.index) === JSON.stringify(this.startTile.index)){
return pathTiles;
}
//it has to be inside the closedList or openList
if (this.closedList.contains(adjacentTile) || this.openList.contains(adjacentTile)) {
if (adjacentTile.cost <= currentTile.cost && adjacentTile.cost > 0) {
//change the current tile.
currentTile = adjacentTile;
//Add this adjacentTile to the path list
pathTiles.push(adjacentTile);
//highlight way with yellow balls
adjacentTile.ball = true;
break;
}
}
}
}
}
}

How to detect collision on certain sides of a rectangle in canvas using socket.io

I am learning multiplayer development and I was wondering how I would detect collision and not allow the player to move into the collision but allow them to move away from it. I currently have a collision detection setup that is server side when the player tries to move. Though this collision detection works perfectly when the players collide they can not move out of the collision and are stuck forever. Here is the code that i'm using server-side to detect when a player moves & or collides:
socket.on('moveEvent', function(data) {
var player = players[find(connections, this.id)]
var r1 = {
left: player.position.x,
top: player.position.y,
bottom: player.position.y + 100,
right: player.position.x + 100
}
var intersects = false;
for (var i = 0; i < players.length; i++) {
if (connections[i] != this.id) {
var currPlayer = players[i]
var r2 = {
left: currPlayer.position.x,
top: currPlayer.position.y,
bottom: currPlayer.position.y + 100,
right: currPlayer.position.x + 100
}
intersects = intersectRect(r1, r2)
}
}
console.log(intersects)
if (data.direction == "x+" && intersects == false) {
player.position.x = player.position.x - speed
}
if (data.direction == "y+" && intersects == false) {
player.position.y = player.position.y - speed
}
if (data.direction == "x-" && intersects == false) {
player.position.x = player.position.x + speed
}
if (data.direction == "y-" && intersects == false) {
player.position.y = player.position.y + speed
}
});
function intersectRect(r1, r2) {
socket.emit('checkPlayers', {
p1: r1,
p2: r2
})
if (!(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top)) {
return true;
} else {
return false;
}
}
Here is the setInterval code that gets the new coordinates and draws them to the screen. I currently have an emit event in the intersectRect function that emits the two players it is checking the collision of. The server then takes that emit and prints out the two players it sends. This is strictly for bug fixing:
setInterval(function() {
if (w == true) {
socket.emit('moveEvent', {
direction: "y+",
id: sesID
})
}
if (a == true) {
socket.emit('moveEvent', {
direction: "x+",
id: sesID
})
}
if (s == true) {
socket.emit('moveEvent', {
direction: "y-",
id: sesID
})
}
if (d == true) {
socket.emit('moveEvent', {
direction: "x-",
id: sesID
})
}
gamectx.canvas.width = window.innerWidth;
gamectx.canvas.height = window.innerHeight;
gamectx.clearRect(0, 0, game.width, game.height);
socket.emit('request', {
request: "players",
id: sesID
});
for (var i = 0; i < players.length; i++) {
gamectx.fillStyle = "#ff0000"
gamectx.fillRect(((game.width / 2) + (players[i].position.x - player.position.x)) - 50, ((game.height / 2) + (players[i].position.y - player.position.y)) - 50, 100, 100)
}
gamectx.fillStyle = "#0000ff"
gamectx.fillRect((window.innerWidth / 2) - 50, (window.innerHeight / 2) - 50, 100, 100);
gamectx.font = "48px sans-serif";
gamectx.strokeText("x: " + GameX + ", y: " + GameY, 10, 50);
var playersList = ""
for (var i = 0; i < players.length; i++) {
playersList += "x: " + players[i].position.x + ", y: " + players[i].position.y + ", "
}
gamectx.font = "30px sans-serif";
gamectx.strokeText(playersList, 10, 100);
console.log(p1, p2)
}, 30);
So I think I may need to detect collisions on every side and not allow the player to moved in that direction when they collide OR move the player away from the collision when it happens. Thanks for your help in advance. Got no answers so I reposted this hoping for some. Thanks!

How to generate a "thick" bezier curve?

I'm looking for a way to generate a polygon programatically by "thickening" a Bezier curve. Something like this:
My initial idea was to find the normals in the line, and generate the polygon from them:
But the problem is that the normals can cross each other in steep curves, like this:
Are there any formulas or algorithms that generate a polygon from a bezier curve? I couldn't find any information on the internet, but perhaps I'm searching using the wrong words...
If you want a constant thickness, this is called an offset curve and your idea of using normals is correct.
This indeed raises two difficulties:
The offset curve is not exactly representable as a Bezier curve; you can use a polyline instead, or retrofit Beziers to the polyline;
There are indeed cusps appearing when the radius of curvature becomes smaller than the offset width. You will have to detect the self-intersections of the polyline.
As far as I know, there is no easy solution.
For a little more info, check 38. Curve offsetting.
Step-by-step process detailed here: How to Draw an Offset Curve
Solution based on the paper ‘Quadratic bezier offsetting with selective subdivision‘ by Gabriel Suchowolski. More from the author: MATH+CODE
Interactive example: CodePen
var canvas, ctx;
var drags;
var thickness = 30;
var drawControlPoints = true;
var useSplitCurve = true;
function init() {
canvas = document.createElement('canvas');
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
drags = [];
window.addEventListener('resize', resize);
window.addEventListener('mousedown', mousedown);
window.addEventListener('mouseup', mouseup);
window.addEventListener('mousemove', mousemove);
document.getElementById('btnControl').addEventListener('click', function(e) {
drawControlPoints = !drawControlPoints
});
document.getElementById('btnSplit').addEventListener('click', function(e) {
useSplitCurve = !useSplitCurve
});
resize();
draw();
var positions = [{
x: canvas.width * 0.3,
y: canvas.height * 0.4
}, {
x: canvas.width * 0.35,
y: canvas.height * 0.85
}, {
x: canvas.width * 0.7,
y: canvas.height * 0.25
}];
for (var i = 0; i < positions.length; i++) {
drags.push(new Drag(ctx, new Vec2D(positions[i].x, positions[i].y)));
}
}
function draw() {
requestAnimationFrame(draw);
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 1;
for (var i = 0; i < drags.length; i++) {
d = drags[i];
d.draw();
}
for (var i = 1; i < drags.length - 1; i++) {
/*
var d1 = (i == 0) ? drags[i].pos : drags[i - 1].pos;
var d2 = drags[i].pos;
var d3 = (i == drags.length - 1) ? drags[drags.length - 1].pos : drags[i + 1].pos;
var v1 = d2.sub(d1);
var v2 = d3.sub(d2);
var p1 = d2.sub(v1.scale(0.5));
var p2 = d3.sub(v2.scale(0.5));
var c = d2;
*/
var p1 = drags[i - 1].pos;
var p2 = drags[i + 1].pos;
var c = drags[i].pos;
var v1 = c.sub(p1);
var v2 = p2.sub(c);
var n1 = v1.normalizeTo(thickness).getPerpendicular();
var n2 = v2.normalizeTo(thickness).getPerpendicular();
var p1a = p1.add(n1);
var p1b = p1.sub(n1);
var p2a = p2.add(n2);
var p2b = p2.sub(n2);
var c1a = c.add(n1);
var c1b = c.sub(n1);
var c2a = c.add(n2);
var c2b = c.sub(n2);
var line1a = new Line2D(p1a, c1a);
var line1b = new Line2D(p1b, c1b);
var line2a = new Line2D(p2a, c2a);
var line2b = new Line2D(p2b, c2b);
var split = (useSplitCurve && v1.angleBetween(v2, true) > Math.PI / 2);
if (!split) {
var ca = line1a.intersectLine(line2a).pos;
var cb = line1b.intersectLine(line2b).pos;
} else {
var t = MathUtils.getNearestPoint(p1, c, p2);
var pt = MathUtils.getPointInQuadraticCurve(t, p1, c, p2);
var t1 = p1.scale(1 - t).add(c.scale(t));
var t2 = c.scale(1 - t).add(p2.scale(t));
var vt = t2.sub(t1).normalizeTo(thickness).getPerpendicular();
var qa = pt.add(vt);
var qb = pt.sub(vt);
var lineqa = new Line2D(qa, qa.add(vt.getPerpendicular()));
var lineqb = new Line2D(qb, qb.add(vt.getPerpendicular()));
var q1a = line1a.intersectLine(lineqa).pos;
var q2a = line2a.intersectLine(lineqa).pos;
var q1b = line1b.intersectLine(lineqb).pos;
var q2b = line2b.intersectLine(lineqb).pos;
}
if (drawControlPoints) {
// draw control points
var r = 2;
ctx.beginPath();
if (!split) {
ctx.rect(ca.x - r, ca.y - r, r * 2, r * 2);
ctx.rect(cb.x - r, cb.y - r, r * 2, r * 2);
} else {
// ctx.rect(pt.x - r, pt.y - r, r * 2, r * 2);
ctx.rect(p1a.x - r, p1a.y - r, r * 2, r * 2);
ctx.rect(q1a.x - r, q1a.y - r, r * 2, r * 2);
ctx.rect(p2a.x - r, p2a.y - r, r * 2, r * 2);
ctx.rect(q2a.x - r, q2a.y - r, r * 2, r * 2);
ctx.rect(qa.x - r, qa.y - r, r * 2, r * 2);
ctx.rect(p1b.x - r, p1b.y - r, r * 2, r * 2);
ctx.rect(q1b.x - r, q1b.y - r, r * 2, r * 2);
ctx.rect(p2b.x - r, p2b.y - r, r * 2, r * 2);
ctx.rect(q2b.x - r, q2b.y - r, r * 2, r * 2);
ctx.rect(qb.x - r, qb.y - r, r * 2, r * 2);
ctx.moveTo(qa.x, qa.y);
ctx.lineTo(qb.x, qb.y);
}
ctx.closePath();
ctx.strokeStyle = '#0072bc';
ctx.stroke();
ctx.fillStyle = '#0072bc';
ctx.fill();
// draw dashed lines
ctx.beginPath();
if (!split) {
ctx.moveTo(p1a.x, p1a.y);
ctx.lineTo(ca.x, ca.y);
ctx.lineTo(p2a.x, p2a.y);
ctx.moveTo(p1b.x, p1b.y);
ctx.lineTo(cb.x, cb.y);
ctx.lineTo(p2b.x, p2b.y);
} else {
ctx.moveTo(p1a.x, p1a.y);
ctx.lineTo(q1a.x, q1a.y);
ctx.lineTo(qa.x, qa.y);
ctx.lineTo(q2a.x, q2a.y);
ctx.lineTo(p2a.x, p2a.y);
ctx.moveTo(p1b.x, p1b.y);
ctx.lineTo(q1b.x, q1b.y);
ctx.lineTo(qb.x, qb.y);
ctx.lineTo(q2b.x, q2b.y);
ctx.lineTo(p2b.x, p2b.y);
}
ctx.setLineDash([2, 4]);
ctx.stroke();
ctx.closePath();
ctx.setLineDash([]);
}
// central line
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(c.x, c.y, p2.x, p2.y);
ctx.strokeStyle = '#959595';
ctx.stroke();
// offset curve a
ctx.beginPath();
ctx.moveTo(p1a.x, p1a.y);
if (!split) {
ctx.quadraticCurveTo(ca.x, ca.y, p2a.x, p2a.y);
} else {
ctx.quadraticCurveTo(q1a.x, q1a.y, qa.x, qa.y);
ctx.quadraticCurveTo(q2a.x, q2a.y, p2a.x, p2a.y);
}
ctx.strokeStyle = '#0072bc';
ctx.lineWidth = 2;
ctx.stroke();
// offset curve b
ctx.beginPath();
ctx.moveTo(p1b.x, p1b.y);
if (!split) {
ctx.quadraticCurveTo(cb.x, cb.y, p2b.x, p2b.y);
} else {
ctx.quadraticCurveTo(q1b.x, q1b.y, qb.x, qb.y);
ctx.quadraticCurveTo(q2b.x, q2b.y, p2b.x, p2b.y);
}
ctx.strokeStyle = '#0072bc';
ctx.stroke();
}
}
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function mousedown(e) {
e.preventDefault();
var m = new Vec2D(e.clientX, e.clientY);
for (var i = 0; i < drags.length; i++) {
var d = drags[i];
var dist = d.pos.distanceToSquared(m);
if (dist < d.hitRadiusSq) {
d.down = true;
break;
}
}
}
function mouseup() {
for (var i = 0; i < drags.length; i++) {
var d = drags[i];
d.down = false;
}
}
function mousemove(e) {
var m = new Vec2D(e.clientX, e.clientY);
for (var i = 0; i < drags.length; i++) {
var d = drags[i];
if (d.down) {
d.pos.x = m.x;
d.pos.y = m.y;
break;
}
}
}
function Drag(ctx, pos) {
this.ctx = ctx;
this.pos = pos;
this.radius = 6;
this.hitRadiusSq = 900;
this.down = false;
}
Drag.prototype = {
draw: function() {
this.ctx.beginPath();
this.ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
this.ctx.closePath();
this.ctx.strokeStyle = '#959595'
this.ctx.stroke();
}
}
// http://toxiclibs.org/docs/core/toxi/geom/Vec2D.html
function Vec2D(a, b) {
this.x = a;
this.y = b;
}
Vec2D.prototype = {
add: function(a) {
return new Vec2D(this.x + a.x, this.y + a.y);
},
angleBetween: function(v, faceNormalize) {
if (faceNormalize === undefined) {
var dot = this.dot(v);
return Math.acos(this.dot(v));
}
var theta = (faceNormalize) ? this.getNormalized().dot(v.getNormalized()) : this.dot(v);
return Math.acos(theta);
},
distanceToSquared: function(v) {
if (v !== undefined) {
var dx = this.x - v.x;
var dy = this.y - v.y;
return dx * dx + dy * dy;
} else {
return NaN;
}
},
dot: function(v) {
return this.x * v.x + this.y * v.y;
},
getNormalized: function() {
return new Vec2D(this.x, this.y).normalize();
},
getPerpendicular: function() {
return new Vec2D(this.x, this.y).perpendicular();
},
interpolateTo: function(v, f) {
return new Vec2D(this.x + (v.x - this.x) * f, this.y + (v.y - this.y) * f);
},
normalize: function() {
var mag = this.x * this.x + this.y * this.y;
if (mag > 0) {
mag = 1.0 / Math.sqrt(mag);
this.x *= mag;
this.y *= mag;
}
return this;
},
normalizeTo: function(len) {
var mag = Math.sqrt(this.x * this.x + this.y * this.y);
if (mag > 0) {
mag = len / mag;
this.x *= mag;
this.y *= mag;
}
return this;
},
perpendicular: function() {
var t = this.x;
this.x = -this.y;
this.y = t;
return this;
},
scale: function(a) {
return new Vec2D(this.x * a, this.y * a);
},
sub: function(a, b) {
return new Vec2D(this.x - a.x, this.y - a.y);
},
}
// http://toxiclibs.org/docs/core/toxi/geom/Line2D.html
function Line2D(a, b) {
this.a = a;
this.b = b;
}
Line2D.prototype = {
intersectLine: function(l) {
var isec,
denom = (l.b.y - l.a.y) * (this.b.x - this.a.x) - (l.b.x - l.a.x) * (this.b.y - this.a.y),
na = (l.b.x - l.a.x) * (this.a.y - l.a.y) - (l.b.y - l.a.y) * (this.a.x - l.a.x),
nb = (this.b.x - this.a.x) * (this.a.y - l.a.y) - (this.b.y - this.a.y) * (this.a.x - l.a.x);
if (denom !== 0) {
var ua = na / denom,
ub = nb / denom;
if (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.INTERSECTING, this.a.interpolateTo(this.b, ua));
} else {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.NON_INTERSECTING, this.a.interpolateTo(this.b, ua));
}
} else {
if (na === 0 && nb === 0) {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.COINCIDENT, undefined);
} else {
isec = new Line2D.LineIntersection(Line2D.LineIntersection.Type.COINCIDENT, undefined);
}
}
return isec;
}
}
Line2D.LineIntersection = function(type, pos) {
this.type = type;
this.pos = pos;
}
Line2D.LineIntersection.Type = {
COINCIDENT: 0,
PARALLEL: 1,
NON_INTERSECTING: 2,
INTERSECTING: 3
};
window.MathUtils = {
getPointInQuadraticCurve: function(t, p1, pc, p2) {
var x = (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * pc.x + t * t * p2.x;
var y = (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * pc.y + t * t * p2.y;
return new Vec2D(x, y);
},
// http://microbians.com/math/Gabriel_Suchowolski_Quadratic_bezier_offsetting_with_selective_subdivision.pdf
// http://www.math.vanderbilt.edu/~schectex/courses/cubic/
getNearestPoint: function(p1, pc, p2) {
var v0 = pc.sub(p1);
var v1 = p2.sub(pc);
var a = v1.sub(v0).dot(v1.sub(v0));
var b = 3 * (v1.dot(v0) - v0.dot(v0));
var c = 3 * v0.dot(v0) - v1.dot(v0);
var d = -1 * v0.dot(v0);
var p = -b / (3 * a);
var q = p * p * p + (b * c - 3 * a * d) / (6 * a * a);
var r = c / (3 * a);
var s = Math.sqrt(q * q + Math.pow(r - p * p, 3));
var t = MathUtils.cbrt(q + s) + MathUtils.cbrt(q - s) + p;
return t;
},
// http://stackoverflow.com/questions/12810765/calculating-cubic-root-for-negative-number
cbrt: function(x) {
var sign = x === 0 ? 0 : x > 0 ? 1 : -1;
return sign * Math.pow(Math.abs(x), 1 / 3);
}
}
init();
html,
body {
height: 100%;
margin: 0
}
canvas {
display: block
}
#btnControl {
position: absolute;
top: 10px;
left: 10px;
}
#btnSplit {
position: absolute;
top: 35px;
left: 10px;
}
<button type="button" id="btnControl">control points on/off</button>
<button type="button" id="btnSplit">split curve on/off</button>
This is a hard problem. There are reasonable approximations like Tiller-Hanson (see my answer to this question: How to get the outline of a stroke?) but the questioner specifically raises the difficulty that 'the normals can cross each other in steep curves'; another way of looking at it is that an envelope created using normals can produce an indefinitely large number of loops, depending on how closely spaced the normals are.
A perfect solution, without self-intersections, is the envelope of the Minkowski sum of a circle and the line. I think it's impractical to get such an envelope, though: you may have to accept the intersections.
Another interesting but daunting fact is that, as Richard Kinch notes in MetaFog: Converting METAFONT Shapes to Contours, "Algebra tells us
that stroking a 3rd degree polynomial curve (the ellipse
approximated by Bézier curves) along a 3rd
degree polynomial curve (the Bézier curve of the
stroked path) results in a 6th degree envelope curve.
We will have to approximate these 6th degree exact
envelope curves with 3rd degree (Bezier) curves".
Here I had the math papers about that theme.
The “Quadratic bezier offsetting with selective subdivision" covers a method to offset quadratic beziers using a criterion that set the parametric value on which the quadratic bezier is subdivided at the start in order to generate an offset approximation with other quadratic beziers segments. This method, obviously, may not be the most perfect approximation of a hypothetical “real” offset, but a fast algorithm for drawing strokes that can be performed on different quality levels by using a non recursive algorithm.
Here all the papers and examples https://microbians.com/mathcode
that imbrizi use for the codepen he put in the answers.
https://codepen.io/microbians/pen/OJPmBZg
code in the link

Resources