Fabricjs - rounded corners just in one side - javascript

I'm working with fabricjs and I need to create few Rects that attached each other..
The first and the last rect need to have a rounded corner only on the side facing outwards..
I know that I can create custom class on fabricjs but I really new in all the canvas stuff..
Is someone can give me a guidance?

Yes creating a custom class is the best and less hackish thing you can do.
You have to extend rect and override the render function:
fabric.RectAsymmetric = fabric.util.createClass(fabric.Rect, /** #lends fabric.Rect.prototype */ {
/**
* Side of rounding corners
* #type String
* #default
*/
side: 'left',
_render: function(ctx, noTransform) {
if (this.width === 1 && this.height === 1) {
ctx.fillRect(0, 0, 1, 1);
return;
}
var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0,
ry = this.ry ? Math.min(this.ry, this.height / 2) : 0,
w = this.width,
h = this.height,
x = noTransform ? this.left : -this.width / 2,
y = noTransform ? this.top : -this.height / 2,
isRoundedLeft = (rx !== 0 || ry !== 0) && side === 'left',
isRoundedRight = (rx !== 0 || ry !== 0) && side === 'right',
k = 1 - 0.5522847498
ctx.beginPath();
ctx.moveTo(x + (isRoundedLeft ? rx : 0), y);
ctx.lineTo(x + w - (isRoundedRight ? rx : 0), y);
isRoundedRight && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry);
ctx.lineTo(x + w, y + h - (isRoundedRight ? ry : 0));
isRoundedRight && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h);
ctx.lineTo(x + (isRoundedLeft ? rx : 0), y + h);
isRoundedLeft && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry);
ctx.lineTo(x, y + (isRoundedLeft ? ry : 0));
isRoundedLeft && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y);
ctx.closePath();
this._renderFill(ctx);
this._renderStroke(ctx);
},
}
Please consider a draft of the class, but should get you the idea.
Then just do var rect = fabric.rectAsymmetric({width:200, height:100, side:'left'}); and try. Change the name of the class to something less ugly.

Related

Handle mouse hovering image inside of canvas isometric grid

I got a isometric grid in html canvas.
I am trying to handle the mouse hover the buildings.
Some buildings will have different heights.
As you can see in the image below I am hovering a tile, the mouse pointer is inside the blueish tile.
The problem is when the mouse pointer is off the ground tile, or in the middle of the building image, the highlighted tile goes off.
Need a way to click on each individual building, how can this be resolved?
Main basic functions:
let applied_map = ref([]); // tileMap
let tile_images = ref([]); // this will contain loaded IMAGES for canvas to consume from
let tile_height = ref(50);
let tile_width = ref(100);
const renderTiles = (x, y) => {
let tileWidth = tile_width.value;
let tileHeight = tile_height.value;
let tile_half_width = tileWidth / 2;
let tile_half_height = tileHeight / 2;
for (let tileX = 0; tileX < gridSize.value; ++tileX) {
for (let tileY = 0; tileY < gridSize.value; ++tileY) {
let renderX = x + (tileX - tileY) * tile_half_width;
let renderY = y + (tileX + tileY) * tile_half_height;
let tile = applied_map.value[tileY * gridSize.value + tileX];
renderTileBackground(renderX, renderY + 50, tileWidth, tileHeight);
if (tile !== -1) {
if (tile_images.value.length) {
renderTexturedTile(
tile_images.value[tile].img,
renderX,
renderY + 40,
tileHeight
);
}
}
}
}
if (
hoverTileX.value >= 0 &&
hoverTileY.value >= 0 &&
hoverTileX.value < gridSize.value &&
hoverTileY.value < gridSize.value
) {
let renderX = x + (hoverTileX.value - hoverTileY.value) * tile_half_width;
let renderY = y + (hoverTileX.value + hoverTileY.value) * tile_half_height;
renderTileHover(renderX, renderY + 50, tileWidth, tileHeight);
}
};
const renderTileBackground = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([5, 5]);
ctx.value.strokeStyle = "black";
ctx.value.fillStyle = "rgba(25,34, 44,0.2)";
ctx.value.lineWidth = 1;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
const renderTexturedTile = (imgSrc, x, y, tileHeight) => {
let offsetY = tileHeight - imgSrc.height;
ctx.value.drawImage(imgSrc, x, y + offsetY);
};
const renderTileHover = (x, y, width, height) => {
ctx.value.beginPath();
ctx.value.setLineDash([]);
ctx.value.strokeStyle = "rgba(161, 153, 255, 0.8)";
ctx.value.fillStyle = "rgba(161, 153, 255, 0.4)";
ctx.value.lineWidth = 2;
ctx.value.moveTo(x, y);
ctx.value.lineTo(x + width / 2, y - height / 2);
ctx.value.lineTo(x + width, y);
ctx.value.lineTo(x + width / 2, y + height / 2);
ctx.value.lineTo(x, y);
ctx.value.stroke();
ctx.value.fill();
};
Updates after answer below
Based on Helder Sepulveda answer I created a function drawCube.
And added to my click function and to the renderTiles. So on click and frame update it creates a cube with 3 faces,and its placed on same position as the building and stores the Path on a global variable, the cube follows the isometric position.
In the drawCube, there is a condition where i need to hide the right face from the cube. Hide if there's a building on the next tile. So if you hover the building it wont trigger the last building on.
//...some code click function
//...
if (tile_images.value[tileIndex] !== undefined) {
drawCube(
hoverTileX.value + tile_height.value,
hoverTileY.value +
Number(tile_images.value[tileIndex].img.height / 2) -
10,
tile_height.value, // grow X pos to left
tile_height.value, // grow X pos to right,
Number(tile_images.value[tileIndex].img.height / 2), // height,
ctx.value,
{
tile_index: tileIndex - 1 < 0 ? 0 : tileIndex - 1,
}
);
}
This is the drawCube
const drawCube = (x, y, wx, wy, h, the_ctx, options = {}) => {
// https://codepen.io/AshKyd/pen/JYXEpL
let path = new Path2D();
let hide_options = {
left_face: false,
right_face: false,
top_face: false,
};
if (options.hasOwnProperty("hide")) {
hide_options = Object.assign(hide_options, options.hide);
}
// left face
if (!hide_options.left_face) {
path.moveTo(x, y);
path.lineTo(x - wx, y - wx * 0.5);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x, y - h * 1);
}
// right;
if (
!hide_options.right_face &&
!coliders.value[options.tile_index].hide_right_face
) {
path.moveTo(x, y);
path.lineTo(x + wy, y - wy * 0.5);
path.lineTo(x + wy, y - h - wy * 0.5);
path.lineTo(x, y - h * 1);
}
//top
if (!hide_options.right_face) {
path.moveTo(x, y - h);
path.lineTo(x - wx, y - h - wx * 0.5);
path.lineTo(x - wx + wy, y - h - (wx * 0.5 + wy * 0.5));
path.lineTo(x + wy, y - h - wy * 0.5);
}
// the_ctx.beginPath();
let isONHover = the_ctx.isPointInPath(
path,
mousePosition.x - 10,
mousePosition.y - 10
);
the_ctx.fillStyle = null;
if (isONHover) {
// let indx = options.tile_pos.y * gridSize.value + options.tile_pos.x;
//this is the click on object event
if (isMouseDown.value) {
//Trigger
if (buildozer.value === true) {
coliders.value[options.tile_index] = -1;
applied_map.value[options.tile_index] = -1;
}
isMouseDown.value = false;
}
the_ctx.fillStyle = "green";
}
the_ctx.fill(path);
if (
coliders.value[options.tile_index] == -1 &&
applied_map.value[options.tile_index]
) {
coliders.value[options.tile_index] = path;
}
};
In a nutshell you need to be able to detect mouseover on more complex shapes ...
I recommend you to use Path2d:
https://developer.mozilla.org/en-US/docs/Web/API/Path2D
That way you can build any shape you like and then we have access to isPointInPath to detect if the mouse is over our shape.
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
Here is a small example:
class Shape {
constructor(x, y, width, height) {
this.path = new Path2D()
this.path.arc(x, y, 12, 0, 2 * Math.PI)
this.path.arc(x, y - 9, 8, 0, 1.5 * Math.PI)
this.path.lineTo(x + width / 2, y)
this.path.lineTo(x, y + height / 2)
this.path.lineTo(x - width / 2, y)
this.path.lineTo(x, y - height / 2)
this.path.lineTo(x + width / 2, y)
}
draw(ctx, pos) {
ctx.beginPath()
ctx.fillStyle = ctx.isPointInPath(this.path, pos.x, pos.y) ? "red" : "green"
ctx.fill(this.path)
}
}
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect()
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
shapes = []
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
shapes.push(new Shape(50 + i * 40, 40 + j * 40, 40, 20))
}
}
canvas.addEventListener("mousemove", function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
var mousePos = getMousePos(canvas, evt)
shapes.forEach((s) => {s.draw(ctx, mousePos)})
},
false
)
shapes.forEach((s) => {
s.draw(ctx, {x: 0, y: 0})
})
<canvas id="canvas" width="200" height="200"></canvas>
This example draws a "complex" shape (two arcs and a few lines) and the shape changes color to red when the mouse is hovering the shape

Html5 canvas - Translate function behaving weirdly

Im trying to use the translate function when drawing a circle, but when i try to do it it doesnt behave properly. Instead of drawing the circle it draws this:
if the image doesnt show up: click here
This is my code for the drawing of the circle (inside a circle class):
ctx.strokeStyle = "white"
ctx.translate(this.x, this.y)
ctx.beginPath()
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// tried with and without translating back, inside and outside of this function
ctx.translate(0, 0)
This is the rest of my code:
let canvas
let ctx
let circle
function init() {
canvas = document.querySelector("#canvas")
ctx = canvas.getContext("2d")
// x, y, radius
circle = new Circle(canvas.width/5, canvas.height/2, 175)
requestAnimationFrame(loop)
}
function loop() {
// Background
ctx.fillStyle = "black"
ctx.fillRect(0, 0, canvas.width, canvas.height)
// The function with the drawing of the circle
circle.draw()
requestAnimationFrame(loop)
}
Btw: When i dont use the translate function it draws the circle normally.
Edit:
I answered my own question below as i found that the translate functions a little bit differently in javascript than how i thought it would.
Answer
Your function
ctx.strokeStyle = "white"
ctx.translate(this.x, this.y)
ctx.beginPath()
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// tried with and without translating back, inside and outside of this function
ctx.translate(0, 0)
Can be improved as follows
ctx.strokeStyle = "white"
ctx.setTransform(1, 0, 0, 1, this.x, this.y); //BM67 This call is faster than ctx.translate
ctx.beginPath()
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
// ctx.closePath() //BM67 This line does nothing and is not related to beginPath.
// tried with and without translating back, inside and outside of this function
//ctx.translate(0, 0) //BM67 You don't need to reset the transform
// The call to ctx.setTransfrom replaces
// the current transform before you draw the circle
and would look like
ctx.strokeStyle = "white"
ctx.setTransform(1, 0, 0, 1, this.x, this.y);
ctx.beginPath()
ctx.arc(0, 0, this.r, 0, 2 * Math.PI)
ctx.stroke()
Why this is better will need you to understand how 2D transformations work and why some 2D API calls should not be used, and that 99% of all transformation needs can be done faster and with less mind f with ctx.setTransform than the poorly named ctx.translate, ctx.scale, or ctx.rotate
Read on if interested.
Understanding the 2D transformation
When you render to the canvas all coordinates are transformed via the transformation matrix.
The matrix consists of 6 values as set by setTransform(a,b,c,d,e,f). The values a,b,c,d,e,f are rather obscure and the literature does not help explaining them.
The best way to think of them is by what they do. I will rename them as setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY) they represent the direction and size of the x axis, y axis and the origin.
xAxisX, xAxisY are X Axis X, X Axis Y
yAxisX, yAxisY are Y Axis X, Y Axis Y
originX, originY are the canvas real pixel coordinates of the origin
The default transform is setTransform(1, 0, 0, 1, 0, 0) meaning that the X Axis moves across 1 down 0, the Y Axis moves across 0 and down 1 and the origin is at 0, 0
You can manually apply the transform to a 2D point as follows
function transformPoint(x, y) {
return {
// Move x dist along X part of X Axis
// Move y dist along X part of Y Axis
// Move to the X origin
x : x * xAxisX + y * yAxisX + originX,
// Move x dist along Y part of X Axis
// Move y dist along Y part of Y Axis
// Move to the Y origin
y : x * xAxisY + y * yAxisY + originY,
};
}
If we substitute the default matrix setTransform(1, 0, 0, 1, 0, 0) we get
{
x : x * 1 + y * 0 + 0,
y : x * 0 + y * 1 + 0,
}
// 0 * n is 0 so removing the * 0
{
x : x * 1,
y : y * 1,
}
// 1 time n is n so remove the * 1
{
x : x,
y : y,
}
As you can see the default transform does nothing to the point
Translation
If we set the translation ox, oy to setTransform(1, 0, 0, 1, 100, 200) the transform is
{
x : x * 1 + y * 0 + 100,
y : x * 0 + y * 1 + 200,
}
// or simplified as
{
x : x + 100,
y : y + 200,
}
Scale
If we set the scale of the X Axis and Y Axis to setTransform(2, 0, 0, 2, 100, 200) the transform is
{
x : x * 2 + y * 0 + 100,
y : x * 0 + y * 2 + 200,
}
// or simplified as
{
x : x * 2 + 100,
y : y * 2 + 200,
}
Rotation
Rotation is a little more complex and requires some trig. You can use cos and sin to get a unit vector in a direction angle (NOTE all angles are in radians PI * 2 is 360deg, PI is 180deg, PI / 2 is 90deg)
Thus the unit vector for 0 radians is
xAxisX = Math.cos(0);
yAxisY = Math.sin(0);
So for angles 0, PI * (1 / 2), PI, PI * (3 / 2), PI * 2
angle = 0;
xAxisX = Math.cos(angle); // 1
yAxisY = Math.sin(angle); // 0
angle = Math.PI * (1 / 2); // 90deg (points down screen)
xAxisX = Math.cos(angle); // 0
yAxisY = Math.sin(angle); // 1
angle = Math.PI; // 180deg (points to left screen)
xAxisX = Math.cos(angle); // -1
yAxisY = Math.sin(angle); // 0
angle = Math.PI * (3 / 2); // 270deg (points to up screen)
xAxisX = Math.cos(angle); // 0
yAxisY = Math.sin(angle); // -1
Uniform transformation
In 90% of cases when you transform points you want the points to remain square, that is the Y axis remains at PI / 2 (90deg) clockwise of the X axis and the Scale of the Y axis is the same as the scale of the X axis.
You can rotate a vector 90 deg by swapping the x and y and negating the new x
x = 1; // X axis points from left to right
y = 0; // No downward part
// Rotate 90deg clockwise
x90 = -y; // 0 no horizontal part
y90 = x; // Points down the screen
We can take advantage of this simple 90 rotation to create a uniform rotation by only defining the angle of the X Axis
xAxisX = Math.cos(angle);
xAxisY = Math.sin(angle);
// create a matrix as setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, 0, 0)
// to transform the point
{
x : x * xAxisX + y * (-xAxisY) + 0,
y : x * xAxisY + y * xAxisX + 0,
}
// to simplify
{
x : x * xAxisX - y * xAxisY,
y : x * xAxisY + y * xAxisX,
}
Rotate, scale, and translate
Using the above info you can now manually create a uniform matrix using only 4 values, The origin x,y the scale, and the rotate
function transformPoint(x, y, originX, originY, scale, rotate) {
// get the direction of the X Axis
var xAxisX = Math.cos(rotate);
var xAxisY = Math.sin(rotate);
// Scale the x Axis
xAxisX *= Math.cos(rotate);
xAxisY *= Math.sin(rotate);
// Get the Y Axis as X Axis rotated 90 deg
const yAxisX = -xAxisY;
const yAxisY = xAxisX;
// we have the 6 values for the transform
// [xAxisX, xAxisY, yAxisX, yAxisY, originX, originY]
// Transform the point
return {
x : x * xAxisX + y * yAxisX + originX,
y : x * xAxisY + y * yAxisY + originY,
}
}
// we can simplify the above down to
function transformPoint(x, y, originX, originY, scale, rotate) {
// get the direction and scale of the X Axis
const xAxisX = Math.cos(rotate) * scale;
const xAxisY = Math.sin(rotate) * scale;
// Transform the point
return {
x : x * xAxisX - y * xAxisY + originX,
// note the ^ negative
y : x * xAxisY + y * xAxisX + originY,
}
}
Or we can create the matrix using ctx.setTransform using the above and let the GPU hardware do the transform
function createTransform(originX, originY, scale, rotate) {
const xAxisX = Math.cos(rotate) * scale;
const xAxisY = Math.sin(rotate) * scale;
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, originX, originY);
}
Setting or Multiplying the transform.
I will rename this section to
WHY YOU SHOULD AVOID ctx.translate, ctx.scale, or ctx.rotate
The 2D API has some bad naming which is the reason for 90% of the transform question that appear in html5-canvas tag.
If we rename the API calls you will get a better understanding of what they do
ctx.translate(x, y); // should be ctx.multiplyCurrentMatirxWithTranslateMatrix
// or shorten ctx.matrixMutliplyTranslate(x, y)
The function ctx.translate does not actually translate a point, but rather it translates the current matrix. It does this by first creating a matrix and then multiplying that matrix with the current matrix
Multiplying one matrix by another, means that the 6 values or 3 vectors for X Axis, Y Axis, and Origin are transform by the other matrix.
If written as code
const current = [1,0,0,1,0,0]; // Default matrix
function translate(x, y) { // Translate current matrix
const translationMatrix = [1,0,0,1,x,y];
const c = current
const m = translationMatrix
const r = []; // the resulting matrix
r[0] = c[0] * m[0] + c[1] * m[2]; // rotate current X Axis with new transform
r[1] = c[0] * m[1] + c[1] * m[3];
r[2] = c[2] * m[0] + c[3] * m[2]; // rotate current Y Axis with new transform
r[3] = c[2] * m[1] + c[3] * m[3];
r[4] = c[4] + m[4]; // Translate current origine with transform
r[5] = c[5] + m[5];
c.length = 0;
c.push(...r);
}
That is the simple version. Under the hood you can not multiply the two matrix as they have different dimensions. The actual matrix is stored as 9 values and requires 27 multiplications and 18 additions
// The real 2D default matrix
const current = [1,0,0,0,1,0,0,0,1];
// The real Translation matrix
const translation = [1,0,0,0,1,0,x,y,1];
//The actual transformation calculation
const c = current
const m = translationMatrix
const r = []; // the resulting matrix
r[0] = c[0] * m[0] + c[1] * m[3] + c[2] * m[6];
r[1] = c[0] * m[1] + c[1] * m[4] + c[2] * m[7];
r[2] = c[0] * m[2] + c[1] * m[5] + c[2] * m[8];
r[3] = c[3] * m[0] + c[4] * m[3] + c[5] * m[6];
r[4] = c[3] * m[1] + c[4] * m[4] + c[5] * m[7];
r[5] = c[3] * m[2] + c[4] * m[5] + c[5] * m[8];
r[6] = c[6] * m[0] + c[7] * m[3] + c[8] * m[6];
r[7] = c[6] * m[1] + c[7] * m[4] + c[8] * m[7];
r[8] = c[6] * m[2] + c[7] * m[5] + c[8] * m[8];
That's a bucket load of math that is always done under the hood when you use ctx.translate and NOTE that this math is not done on the GPU, it is done on the CPU and the resulting matrix is moved to the GPU.
If we continue the renaming
ctx.translate(x, y); // should be ctx.matrixMutliplyTranslate(
ctx.scale(scaleY, scaleX); // should be ctx.matrixMutliplyScale(
ctx.rotate(angle); // should be ctx.matrixMutliplyRotate(
ctx.transform(a,b,c,d,e,f) // should be ctx.matrixMutliplyTransform(
It is common for JS scripts to use the above function to scale translate and rotates, usually with reverse rotations and translations because their objects are not defined around there local origins.
Thus when you do the following
ctx.rotate(angle);
ctx.scale(sx, sy);
ctx.translate(x, y);
The under the hood math must do all of the following
// create rotation matrix
rr = [Math.cos(rot), Math.sin(rot), 0, -Math.sin(rot), Math.cos(rot), 0, 0, 0, 1];
// Transform the current matix with the rotation matrix
r[0] = c[0] * rr[0] + c[1] * rr[3] + c[2] * rr[6];
r[1] = c[0] * rr[1] + c[1] * rr[4] + c[2] * rr[7];
r[2] = c[0] * rr[2] + c[1] * rr[5] + c[2] * rr[8];
r[3] = c[3] * rr[0] + c[4] * rr[3] + c[5] * rr[6];
r[4] = c[3] * rr[1] + c[4] * rr[4] + c[5] * rr[7];
r[5] = c[3] * rr[2] + c[4] * rr[5] + c[5] * rr[8];
r[6] = c[6] * rr[0] + c[7] * rr[3] + c[8] * rr[6];
r[7] = c[6] * rr[1] + c[7] * rr[4] + c[8] * rr[7];
r[8] = c[6] * rr[2] + c[7] * rr[5] + c[8] * rr[8];
// STOP the GPU and send the resulting matrix over the bus to set new state
c = [...r]; // set the current matrix
// create the scale matrix
ss = [scaleX, 0, 0, 0, scaleY, 0, 0, 0, 1];
// scale the current matrix
r[0] = c[0] * ss[0] + c[1] * ss[3] + c[2] * ss[6];
r[1] = c[0] * ss[1] + c[1] * ss[4] + c[2] * ss[7];
r[2] = c[0] * ss[2] + c[1] * ss[5] + c[2] * ss[8];
r[3] = c[3] * ss[0] + c[4] * ss[3] + c[5] * ss[6];
r[4] = c[3] * ss[1] + c[4] * ss[4] + c[5] * ss[7];
r[5] = c[3] * ss[2] + c[4] * ss[5] + c[5] * ss[8];
r[6] = c[6] * ss[0] + c[7] * ss[3] + c[8] * ss[6];
r[7] = c[6] * ss[1] + c[7] * ss[4] + c[8] * ss[7];
r[8] = c[6] * ss[2] + c[7] * ss[5] + c[8] * ss[8];
// STOP the GPU and send the resulting matrix over the bus to set new state
c = [...r]; // set the current matrix
// create the translate matrix
tt = [1, 0, 0, 0, 1, 0, x, y, 1];
// translate the current matrix
r[0] = c[0] * tt[0] + c[1] * tt[3] + c[2] * tt[6];
r[1] = c[0] * tt[1] + c[1] * tt[4] + c[2] * tt[7];
r[2] = c[0] * tt[2] + c[1] * tt[5] + c[2] * tt[8];
r[3] = c[3] * tt[0] + c[4] * tt[3] + c[5] * tt[6];
r[4] = c[3] * tt[1] + c[4] * tt[4] + c[5] * tt[7];
r[5] = c[3] * tt[2] + c[4] * tt[5] + c[5] * tt[8];
r[6] = c[6] * tt[0] + c[7] * tt[3] + c[8] * tt[6];
r[7] = c[6] * tt[1] + c[7] * tt[4] + c[8] * tt[7];
r[8] = c[6] * tt[2] + c[7] * tt[5] + c[8] * tt[8];
// STOP the GPU and send the resulting matrix over the bus to set new state
c = [...r]; // set the current matrix
So that is a total of 3 GPU state changes, 81 floating point multiplications, 54 floating point additions, 4 high level math calls and about 0.25K RAM allocated and dumped for GC to clean up.
Easy and Fast
The function setTransform does not multiply matrices. It converts the 6 arguments to a 3 by 3 matrix by directly putting the values into the current transform and the moving it to the GPU
// ct is the current transform 9 value under hood version
// The 6 arguments of the ctx.setTransform call
ct[0] = a;
ct[1] = b;
ct[2] = 0;
ct[3] = c;
ct[4] = d;
ct[5] = 0;
ct[6] = e;
ct[7] = f;
ct[8] = 1;
// STOP the GPU and send the resulting matrix over the bus to set new state
So if you use the JS function
function createTransform(originX, originY, scale, rotate) {
const xAxisX = Math.cos(rotate) * scale;
const xAxisY = Math.sin(rotate) * scale;
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, originX, originY);
}
You reduce the complexity under the hood to 2 floating point multiplications, 2 high level math function calls, 1 floating point addition (negating the -xAxisY), one GPU state change, and using only 64 bytes of RAM from the heap.
And because the ctx.setTransform does not depend on the current state of the 2D transform you don't need to use ctx.resetTransform, or ctx.save and restore
When animating many items the performance benefit is noticeable. When struggling with the complexity of transformed matrices the simplicity of setTransform can save you hours of time better spend creating good content.
The problem is that after each translation in Circle.draw(), the context is not restored to its original state. Future translate(this.x, this.y); calls keep moving the context right and downward relative to the previous transformation endlessly.
Use ctx.save() and ctx.restore() at the beginning and end of your draw() function to move the context back to its original location after drawing.
class Circle {
constructor(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
}
draw() {
ctx.save();
ctx.strokeStyle = "white";
ctx.translate(this.x, this.y);
ctx.beginPath();
ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
let canvas;
let ctx;
let circle;
(function init() {
canvas = document.querySelector("canvas");
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
circle = new Circle(canvas.width / 2, canvas.height / 2, 30);
loop();
})();
function loop() {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
circle.draw();
requestAnimationFrame(loop);
}
body {
margin: 0;
height: 100vh;
}
<canvas></canvas>
Alternately, you can just write:
ctx.strokeStyle = "white";
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
and skip the translation step entirely.
I just found the answer. As #mpen commented ctx.translate(0, 0) doesnt reset the translation, but this does: ctx.setTransform(1, 0, 0, 1, 0, 0);. The ctx.translate function translates related to the previous translation.
In your code, the ctx.translate(0, 0) does absolutely nothing, because that function sets transformation relative to current transformation. You are telling the context "move 0 pixels right and 0 pixels down". You could fix that by changing the line to ctx.translate(-this.x, -this.y) so you do the opposite transformation.
However, usually, this is done by saving the context state with CanvasRenderingContext2D.save before making transformations and then restoring it with CanvasRenderingContext2D.restore. In your example, it would look like this:
ctx.save(); // here, we are saving state of the context
ctx.strokeStyle = "white";
ctx.translate(this.x, this.y);
ctx.beginPath();
// Draws the circle
ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
ctx.restore(); // after this, context will have the state it had when we called save()
This way is good in cases when you want to return the context to its original state after the operation, rather than the default state (which you usually do when making more complex operations), and when you do multiple transformations which would be complicated to revert.

FabricJs - Radial gradient for pie pieces

As you see here: http://jsfiddle.net/Da7SP/60/ I have 64 pie pieces that I want to have a radial gradient from the middle out to the edge. (Live each piece will have different colors) but I don't succeed to create that effect.
Here is the code:
var canvas = window._canvas = new fabric.Canvas('c');
var x=300
, y=300
, totalGates=64
, start=0
, radius=200
, val = 360 / totalGates;
for (var i = 0; i < totalGates; i++) {
createPath(x, y, radius, val*i, (val*i)+val);
}
function createPath (x, y, radius, startAngle, endAngle) {
var flag = (endAngle - startAngle) > 180;
startAngle = (startAngle % 360) * Math.PI / 180;
endAngle = (endAngle % 360) * Math.PI / 180;
var path = 'M '+x+' '+y+' l ' + radius * Math.cos(startAngle) + ' ' + radius * Math.sin(startAngle) +
' A ' + radius + ' ' + radius + ' 0 ' + (+flag) + ' 1 ' + (x + radius * Math.cos(endAngle))+ ' ' + (y + radius * Math.sin(endAngle)) + ' z';
var piePiece = new fabric.Path(path);
piePiece.set({
strokeWidth:0
});
piePiece.setGradient('fill', {
type:'radial',
x1: x,
y1: y,
//x2: x + radius * Math.cos(endAngle),
//y2: y + radius * Math.sin(endAngle),
r1: radius,
r2: 0,
colorStops: {
0: '#000',
1: '#fff',
}
});
canvas.add(piePiece);
}
I thought that it would be enough to set x1 to x and y1 to y to define the coordinates for the middle and then the radius, (before when I used PathGroup that did the trick). but now when I add the Path to a Group or directly to the Canvas the gradient looks completely different.
So, how do I use the setGradient with a radial gradient so it is displayed as if the complete pie was a circle, it would go from the center to the edge
Update:
I noticed that if I set x=0 and y=0 then the gradient get centered.
Update2:
If both x1 and y2 is set to 0 the gradient is drawn from the top left corner, this makes the gradient looks good in the 90 degree bottom - right corner: http://jsfiddle.net/Da7SP/61/
Update3:
I solved it! Here http://jsfiddle.net/Da7SP/64/ is the Fiddle for you who has the same problem and below you see the result and the code.
This made the trick:
x1: x > Math.round(piePiece.left) ? x - piePiece.left : 0,
y1: y > Math.round(piePiece.top) ? y - piePiece.top : 0,
x2: x > Math.round(piePiece.left) ? x - piePiece.left : 0,
y2: y > Math.round(piePiece.top) ? y - piePiece.top : 0,
Here is the expected result:
Here is the code
var canvas = window._canvas = new fabric.Canvas('c');
var x=300
, y=300
, totalGates=64
, start=0
, radius=200
, val = 360 / totalGates;
/* Loops through each gate and prints selected options */
for (var i = 0; i < totalGates; i++) {
createPath(x, y, radius, val*i, (val*i)+val, i);
}
function createPath (x, y, radius, startAngle, endAngle,i ) {
var flag = (endAngle - startAngle) > 180;
startAngle = (startAngle % 360) * Math.PI / 180;
endAngle = (endAngle % 360) * Math.PI / 180;
var path = 'M '+x+' '+y+' l ' + radius * Math.cos(startAngle) + ' ' + radius * Math.sin(startAngle) +
' A ' + radius + ' ' + radius + ' 0 ' + (+flag) + ' 1 ' + (x + radius * Math.cos(endAngle))+ ' ' + (y + radius * Math.sin(endAngle)) + ' z';
var piePiece = new fabric.Path(path);
piePiece.set({
strokeWidth:0
});
piePiece.setGradient('fill', {
type:'radial',
x1: x > Math.round(piePiece.left) ? x - piePiece.left : 0,
y1: y > Math.round(piePiece.top) ? y - piePiece.top : 0,
x2: x > Math.round(piePiece.left) ? x - piePiece.left : 0,
y2: y > Math.round(piePiece.top) ? y - piePiece.top : 0,
r1: radius,
r2: 0,
colorStops: {
0: '#000',
1: '#f0f',
}
});
canvas.add(piePiece);
}
Here is the code that was missing:
piePiece.setGradient('fill', {
type:'radial',
x1: x > Math.round(piePiece.left) ? x - piePiece.left : 0,
y1: y > Math.round(piePiece.top) ? y - piePiece.top : 0,
x2: x > Math.round(piePiece.left) ? x - piePiece.left : 0,
y2: y > Math.round(piePiece.top) ? y - piePiece.top : 0,
r1: radius,
r2: 0,
colorStops: {
0: '#000',
1: '#f0f',
}
});

how to clear old points on polygon

I have the code that draw a point inside a polygon. Each time I change value on textbox for x & y, it will draw a new point and still keep old points on my polygon, so I want to ask how can I clear all old points ?
I already try to remove old tag and create new each time draw a new point but it's not ok. If you know about this, pls help. Thanks
Canvas = function(){ //v1.0
var o = this;
(o.penPos = {x: 0, y: 0}, o.pixelSize = 10, o.pen = {style: "solid",
size: 1, color: "#000"}, o.brush = {style: "solid", color: "#000"});
};
with({p: Canvas.prototype}){
p.pixel = function(x, y, color) {
var o = this, s = document.body.appendChild(document.createElement("div")).style;
//alert ("top"+(y * o.pixelSize) + "px");
return (s.position = "absolute", s.width = (o.pen.size * o.pixelSize) + "px",
s.height = (o.pen.size * o.pixelSize) + "px", s.fontSize = "1px",
s.left = (x * o.pixelSize) + "px", s.top = (y * o.pixelSize) + "px",
s.backgroundColor = color || o.pen.color, o);
};
p.line = function(x1, y1, x2, y2){
if(Math.abs(x1 - x2) < Math.abs(y1 - y2))
for(y = Math.min(y1, y2) - 1, x = Math.max(y1, y2); ++y <= x;
this.pixel((y * (x1 - x2) - x1 * y2 + y1 * x2) / (y1 - y2), y));
else
for(x = Math.min(x1, x2) - 1, y = Math.max(x1, x2); ++x <= y;
this.pixel(x, (x * (y1 - y2) - y1 * x2 + x1 * y2) / (x1 - x2)));
return this;
};
p.arc = function(x, y, raio, startAngle, degrees) {
for(degrees += startAngle; degrees --> startAngle;
this.pixel(Math.cos(degrees * Math.PI / 180) * raio + x,
Math.sin(degrees * Math.PI / 180) * raio + y)); return this;
};
p.rectangle = function(x, y, width, height, rotation){
return this.moveTo(x, y).lineBy(0, height).lineBy(width, 0).lineBy(0, -height).lineBy(-width, 0);
};
p.moveTo = function(x, y){var o = this; return (o.penPos.x = x, o.penPos.y = y, o);};
p.moveBy = function(x, y){var o = this; return o.moveTo(o.penPos.x + x, o.penPos.y + y);};
p.lineTo = function(x, y){var o = this; return o.line(o.penPos.x, o.penPos.y, x, y).moveTo(x, y);};
p.lineBy = function(x, y){var o = this; return o.lineTo(o.penPos.x + x, o.penPos.y + y);};
p.curveTo = function(cx, cy, x, y){};
p.polyBezier = function(points){};
p.path = function(points){};
}
function isPointInPoly(poly, pt){
for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
((poly[i].y <= pt.y && pt.y < poly[j].y) || (poly[j].y <= pt.y && pt.y < poly[i].y))
&& (pt.x < (poly[j].x - poly[i].x) * (pt.y - poly[i].y) / (poly[j].y - poly[i].y) + poly[i].x)
&& (c = !c);
return c;
}
var length = 50,
points = [
{x: 35, y:10422},
{x: 36, y:32752},
{x: 40, y:35752},
{x: 55, y:27216},
{x: 59, y:29319},
{x: 58, y:10411}
];
var canvas = new Canvas;
canvas.pen.color = "#f00";
canvas.pixelSize = 1;
canvas.moveTo(getx(points[points.length-1].x) , gety(points[points.length-1].y));
for(var i = points.length; i--; canvas.lineTo(getx( points[i].x), gety(points[i].y)));
function draw(){
var x=38; var y=10433;
canvas.pixel(getx(x),gety(y));
alert(isPointInPoly(points, {x: x,y: y}) ? "In" : "Out");
}
function getx(x){
return Math.round(x*10);
}
function gety(y){
return Math.round(400-y/250);
}
//]]>
You can clear a rectangular section of the canvas by using context.clearRect(x,y,width,height). For your application, I would imagine every time you draw a polygon, you want to clear our your canvas and start over. To do this, simply, call context.clearRect(0,0,canvas.width,canvas.height).
There is also a trick to clearing any polygon shaped region outlined in this SO question.

Calculate if line crosses circle, weird behavior at certain angles

Okay, I want to be able to calculate whether a line crosses a circle(at least a part of the line inside the circle). I found several answers to this, but I thought they were too complicated so I came up with this. I'm no math guy, so I'm kinda stuck now. When the line is aligned vertically the "radius >= Math.sqrt(len * len + len * len - o);" becomes true( with 45° angles it becomes 0). I have no clue why this happens. Thanks :)
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
cx -= sx; x -= sx; //sx is the first point's x position
cy -= sy; y -= sy;//sy is the first point's y position
len = Math.sqrt((cy * cy) + (cx * cx))//hypotenuse of circle (cy, cx) to (0, 0) (with offset)
atanx = Math.atan(y / x); //angle of (0, 0) to (x, y) in radians
atany = atanx - Math.atan(cy / cx); //to center
var o = 2 * len * len * Math.cos(atany);
var o = o < 0 ? -o:o//Had to do this, at some point the value can become inverted
return radius >= Math.sqrt(len * len + len * len - o);
}
Edit:
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
cx -= sx; x -= sx; //sx is the first point's x position
cy -= sy; y -= sy;//sy is the first point's y position
ctp = Math.sin(Math.atan(y / x) - Math.atan(cy / cx)) * Math.sqrt((cy * cy) + (cx * cx));
return radius >= ctp && ctp >= -radius;
}
Works pretty much the same but is faster. The problem is that it calculates an infinite line. How would I fix that?
Edit 2:
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
cx -= sx; x -= sx;
cy -= sy; y -= sy;
var h = Math.sqrt(cy * cy + cx * cx)
ctp = Math.sin(Math.atan(y / x) - Math.atan(cy / cx)) * h;
sideb = Math.sqrt(h * h - ctp * ctp);
line = Math.sqrt(x * x + y * y)
if (sideb - radius > line) {return false}
return radius >= ctp && ctp >= -radius;
}
Partial fix, doesn't go on to infinity for one direction from the line(line end)
Edit 3:
A bit longer but more than twice as fast, back to square one
function lineInCircle2(sx, sy, x, y, cx, cy, radius) {
var ysy = y - sy
var xsx = x - sx
var k = ((y-sy) * (cx-sx) - (x-sx) * (cy-sy)) / (ysy * ysy + xsx * xsx)
var ncx = cx - k * (y-sy)
var ncy = cy + k * (x-sx)
ncx -= cx
ncy -= cy
var ctp = Math.sqrt(ncx * ncx + ncy * ncy)
return radius >= ctp && ctp >= -radius;
}
Edit 4:
Success!
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
if (sx > cx + radius && x > cx + radius || x < cx - radius && sx < cx - radius) {return false;}
if (sy > cy + radius && y > cy + radius || y < cy - radius && sy < cy - radius) {return false;}
var k = ((y - sy) * (cx - sx) - (x - sx) * (cy - sy)) / ((y - sy) * (y - sy) + (x - sx) * (x - sx))
var ncx = k * (y - sy)
var ncy = k * (x - sx)
return radius >= Math.sqrt(ncx * ncx + ncy * ncy);
}
Does exactly what I want, I optimized it down to 4.5 - 4.6 seconds for 100000000 iterations compared for 10+ secs for the first version and still is much more accurate(meaning no more weird behavior in certain angles). I'm satisfied :D
Too much work. Find the normal that passes through the center, and see if the intersection is closer than the radius.
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
if (sx > cx + radius && x > cx + radius || x < cx - radius && sx < cx - radius) {return false;}
if (sy > cy + radius && y > cy + radius || y < cy - radius && sy < cy - radius) {return false;}
var k = ((y - sy) * (cx - sx) - (x - sx) * (cy - sy)) / ((y - sy) * (y - sy) + (x - sx) * (x - sx))
var ncx = k * (y - sy)
var ncy = k * (x - sx)
return radius >= Math.sqrt(ncx * ncx + ncy * ncy);
}
Takes about 4.5 - 4.6 seconds for 100000000 iterations to finish on my machine.

Categories

Resources