I have a code in canvas which help me in making gas triangles(duval triangles).
I need to convert the code from canvas to svg.
One of the reason why I moving from canvas to svg, is because I can't add event handlers in canvas(which act like bitmap) but in svg I can do it.
1.Is it possible?
2.Can I do the same things in canvas also in svg?
3.Should I use libraries to help me in writing svg, any recommendations for specific svg library?
My code is based on the following post:
how to create Duval Triangle in canvas
$(function() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// https://www.researchgate.net/publication/4345236_A_Software_Implementation_of_the_Duval_Triangle_Method
var v0 = {
x: 58,
y: 845
};
var v1 = {
x: 984,
y: 845
};
var v2 = {
x: 521,
y: 41
};
var triangle = [v0, v1, v2];
// Define all your segments here
var segments = [{
points: [{
x: 58,
y: 845
}, {
x: 272,
y: 845
}, {
x: 567,
y: 333
}, {
x: 461,
y: 150
}],
fill: 'rgb(172,236,222)',
label: {
text: 'D1',
cx: 300,
cy: 645,
withLine: false,
endX: null,
endY: null
},
}, {
points: [{
x: 272,
y: 845
}, {
x: 567,
y: 333
}, {
x: 646,
y: 468
}, {
x: 572,
y: 597
}, {
x: 716,
y: 845
}],
fill: 'deepskyblue',
label: {
text: 'D2',
cx: 490,
cy: 645,
withLine: false,
endX: null,
endY: null
},
}, {
points: [{
x: 716,
y: 845
}, {
x: 845,
y: 845
}, {
x: 683,
y: 565
}, {
x: 734,
y: 476
}, {
x: 503,
y: 76
}, {
x: 461,
y: 150
}, {
x: 646,
y: 468
}, {
x: 572,
y: 595
}],
fill: 'lightCyan',
label: {
text: 'DT',
cx: 656,
cy: 645,
withLine: false,
endX: 366,
endY: 120
},
}, { //here - I am in hell.-s5
points: [{
x: 530,
y: 59
}, {
x: 512,
y: 59
}, {
x: 521,
y: 41
}],
fill: 'black',
label: {
text: 'PD',
cx: 600,
cy: 52,
withLine: true,
endX: 520,
endY: 70
},
}, {
points: [{
x: 595,
y: 235
}, {
x: 614,
y: 203
}, {
x: 530,
y: 59
}, {
x: 512,
y: 59
}, {
x: 503,
y: 76
}],
fill: 'navajoWhite',
label: {
text: 'T1',
cx: 670,
cy: 140,
withLine: true,
endX: 574,
endY: 105
},
}, {
points: [{
x: 753,
y: 446
}, {
x: 735,
y: 476
}, {
x: 595,
y: 235
}, {
x: 614,
y: 203
}],
fill: 'tan',
label: {
text: 'T2',
cx: 800,
cy: 290,
withLine: true,
endX: 662,
endY: 120
},
}, {
points: [{
x: 845,
y: 845
}, {
x: 683,
y: 565
}, {
x: 753,
y: 446
}, {
x: 984,
y: 845
}],
fill: 'peru',
label: {
text: 'T3',
cx: 800,
cy: 645,
withLine: false,
endX: null,
endY: null
},
}, ];
// label styles
var labelfontsize = 12;
var labelfontface = 'verdana';
var labelpadding = 3;
// pre-create a canvas-image of the arrowhead
var arrowheadLength = 10;
var arrowheadWidth = 8;
var arrowhead = document.createElement('canvas');
premakeArrowhead();
var legendTexts = ['PD = Partial Discharge',
'DT = Discharges and Thermal',
'T1 = Thermal fault T < 300 ℃',
'T2 = Thermal fault 300 ℃ < T < 700 ℃',
'T3 = Thermal fault T > 700 ℃',
'D1 = Discharges of low energy',
'D2 = Discharges of high energy'
];
// start drawing
/////////////////////
// draw colored segments inside triangle
for (var i = 0; i < segments.length; i++) {
drawSegment(segments[i]);
}
// draw ticklines
ticklines(v0, v1, 9, Math.PI * 1.2, 20);
ticklines(v1, v2, 9, Math.PI * 3 / 4, 20);
ticklines(v2, v0, 9, Math.PI * 2, 20);
// molecules
moleculeLabel(v0, v1, 100, Math.PI / 2, '% CH4');
moleculeLabel(v1, v2, 100, 0, '% C2H4');
moleculeLabel(v2, v0, 100, Math.PI, '% C2H2');
// draw outer triangle
drawTriangle(triangle);
// draw legend
drawLegend(legendTexts, 10, 10, 12.86);
drawCircle(canvas.width / 3, canvas.height / 2, 2.5, 'red');
// end drawing
/////////////////////
function drawCircle(point1, point2, radius, color) {
ctx.beginPath();
ctx.arc(point1, point2, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
}
function drawSegment(s) {
// draw and fill the segment path
ctx.beginPath();
ctx.moveTo(s.points[0].x, s.points[0].y);
for (var i = 1; i < s.points.length; i++) {
ctx.lineTo(s.points[i].x, s.points[i].y);
}
ctx.closePath();
ctx.fillStyle = s.fill;
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = 'black';
ctx.stroke();
// draw segment's box label
if (s.label.withLine) {
lineBoxedLabel(s, labelfontsize, labelfontface, labelpadding);
} else {
boxedLabel(s, labelfontsize, labelfontface, labelpadding);
}
}
function moleculeLabel(start, end, offsetLength, angle, text) {
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'
ctx.font = '14px verdana';
var dx = end.x - start.x;
var dy = end.y - start.y;
var x0 = parseInt(start.x + dx * 0.50);
var y0 = parseInt(start.y + dy * 0.50);
var x1 = parseInt(x0 + offsetLength * Math.cos(angle));
var y1 = parseInt(y0 + offsetLength * Math.sin(angle));
ctx.fillStyle = 'black';
ctx.fillText(text, x1, y1);
// arrow
var x0 = parseInt(start.x + dx * 0.35);
var y0 = parseInt(start.y + dy * 0.35);
var x1 = parseInt(x0 + 50 * Math.cos(angle));
var y1 = parseInt(y0 + 50 * Math.sin(angle));
var x2 = parseInt(start.x + dx * 0.65);
var y2 = parseInt(start.y + dy * 0.65);
var x3 = parseInt(x2 + 50 * Math.cos(angle));
var y3 = parseInt(y2 + 50 * Math.sin(angle));
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x3, y3);
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.stroke();
var angle = Math.atan2(dy, dx);
ctx.save(); // save
ctx.translate(x3, y3);
ctx.rotate(angle);
ctx.drawImage(arrowhead, -arrowheadLength, -arrowheadWidth / 2);
ctx.restore()
}
function boxedLabel(s, fontsize, fontface, padding) {
var centerX = s.label.cx;
var centerY = s.label.cy;
var text = s.label.text;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'
ctx.font = fontsize + 'px ' + fontface
var textwidth = ctx.measureText(text).width;
var textheight = fontsize * 1.286;
var leftX = centerX - textwidth / 2 - padding;
var topY = centerY - textheight / 2 - padding;
ctx.fillStyle = 'white';
ctx.fillRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.lineWidth = 1;
ctx.strokeRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.fillStyle = 'black';
ctx.fillText(text, centerX, centerY);
}
function lineBoxedLabel(s, fontsize, fontface, padding) {
var centerX = s.label.cx;
var centerY = s.label.cy;
var text = s.label.text;
var lineToX = s.label.endX;
var lineToY = s.label.endY;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'
ctx.font = fontsize + 'px ' + fontface
var textwidth = ctx.measureText(text).width;
var textheight = fontsize * 1.286;
var leftX = centerX - textwidth / 2 - padding;
var topY = centerY - textheight / 2 - padding;
// the line
ctx.beginPath();
ctx.moveTo(leftX, topY + textheight / 2);
ctx.lineTo(lineToX, topY + textheight / 2);
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.stroke();
// the boxed text
ctx.fillStyle = 'white';
ctx.fillRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.strokeRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.fillStyle = 'black';
ctx.fillText(text, centerX, centerY);
}
function ticklines(start, end, count, angle, length) {
var dx = end.x - start.x;
var dy = end.y - start.y;
ctx.lineWidth = 1;
for (var i = 1; i < count; i++) {
var x0 = parseInt(start.x + dx * i / count);
var y0 = parseInt(start.y + dy * i / count);
var x1 = parseInt(x0 + length * Math.cos(angle));
var y1 = parseInt(y0 + length * Math.sin(angle));
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
if (i == 2 || i == 4 || i == 6 || i == 8) {
var labelOffset = length * 3 / 4;
var x1 = parseInt(x0 - labelOffset * Math.cos(angle));
var y1 = parseInt(y0 - labelOffset * Math.sin(angle));
ctx.fillStyle = 'black';
ctx.fillText(parseInt(i * 10), x1, y1);
}
}
}
function premakeArrowhead() {
var actx = arrowhead.getContext('2d');
arrowhead.width = arrowheadLength;
arrowhead.height = arrowheadWidth;
actx.beginPath();
actx.moveTo(0, 0);
actx.lineTo(arrowheadLength, arrowheadWidth / 2);
actx.lineTo(0, arrowheadWidth);
actx.closePath();
actx.fillStyle = 'black';
actx.fill();
}
function drawTriangle(t) {
ctx.beginPath();
ctx.moveTo(t[0].x, t[0].y);
ctx.lineTo(t[1].x, t[1].y);
ctx.lineTo(t[2].x, t[2].y);
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
}
function drawLegend(texts, x, y, lineheight) {
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillStyle = 'black';
ctx.font = '12px arial';
for (var i = 0; i < texts.length; i++) {
ctx.fillText(texts[i], x, y + i * lineheight);
}
}
})
body {
background-color: ivory;
padding: 10px;
}
#canvas {
border: 1px solid red;
margin: 0 auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width=1024 height=1020></canvas>
Yes it is possible.
You can do more things on svg than on canvas.
You can try SVG.JS library. It is lightweight and easy to use.
Try fabric JS to convert canvas to svg. JsFiddle
HTML
<canvas id="canvas" width=1024 height=1020></canvas>
<button id="canvas2svg">Canvas 2 SVG</button>
JS
var canvas = new fabric.Canvas('canvas', { isDrawingMode: true });
//var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
$("#canvas2svg").click(function(){
canvas.isDrawingMode = false;
alert(canvas.toSVG());
});
JsFiddle
Also refer this example fiddle
Related
I'm trying to identify the following parameters from the example for oval triangle but when i modify the line:
drawCircle(canvas.width / 3,canvas.height / 2,2.5,'red');
//want to replace with specific values but its not working
drawCircle(32 / 3,33 / 2,2.5,'red');
I want to identify the correct parameter so the demo can change the red point into other space inside the triangle
CH4= 20
C2H4= 70
C2H2= 20
Demo:
https://codepen.io/Barak/pen/WwdPxQ
I read the post from stackoverflow community and cannot see values
how to create Duval Triangle in canvas
In the post you've mentioned, markE did a great job replicating the look of a Duval triangle. The only problem is that the codepen including the drawCircle() function is just a dummy and does nothing more than placing a dot at an arbitrary position, given by it's x and y parameters.
To make this function show the correct position of actual data on the triangle - e.g. CH4=20 | C2H4=70 | C2H2=20 - there's a lot more involved.
Let's have a more in-depth look, if we were to solve this graphically on paper.
(The following is based on this paper)
The Duval triangle is basically an equilateral triangle like this, where side b=%CH4,
side a=%C2H4 and side c=%C2H2.
If we consider the following example data %CH4=25 | %C2H4=35 | %C2H2=40
we would have to go from point A to point C, 25% the length of side b and draw a line parallel to side c:
then from point C to point B, 35% the length of side a and draw a line parallel to side b:
and finally from point B to point A, 40% the length of side c and draw a line parallel to side a:
So where those three lines intersect - so the paper says - we have our target position indicating the status. This can be done programatically using plain trigonometry. Well, I'm not too sure why we need all three lines though. As th percentage for CH4 is always parallel to the triangle's base and C2H4 is always parallel to side b, we have an intersection yet and the line for C2H2 is given automatically.
Basically we just need a function which calculates the intersection between the CH4 and the C2H4 line.
I've taken markE's existing code and enhanced it by a function plotResult(), which takes three parameters for the CH4, C2H2 and C2H4 ppm values:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// https://www.researchgate.net/publication/4345236_A_Software_Implementation_of_the_Duval_Triangle_Method
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
var v0 = {
x: 114,
y: 366
};
var v1 = {
x: 306,
y: 30
};
var v2 = {
x: 498,
y: 366
};
var triangle = [v0, v1, v2];
// Define all your segments here
var segments = [{
points: [{
x: 114,
y: 366
}, {
x: 281,
y: 76
}, {
x: 324,
y: 150
}, {
x: 201,
y: 366
}],
fill: 'rgb(172,236,222)',
label: {
text: 'D1',
cx: 200,
cy: 290,
withLine: false,
endX: null,
endY: null
},
},
{
points: [{
x: 385,
y: 366
}, {
x: 201,
y: 366
}, {
x: 324,
y: 150
}, {
x: 356,
y: 204
}, {
x: 321,
y: 256
}],
fill: 'deepskyblue',
label: {
text: 'D2',
cx: 290,
cy: 290,
withLine: false,
endX: null,
endY: null
},
},
{
points: [{
x: 297,
y: 46
}, {
x: 392,
y: 214
}, {
x: 372,
y: 248
}, {
x: 441,
y: 366
}, {
x: 385,
y: 366
}, {
x: 321,
y: 256
}, {
x: 356,
y: 204
}, {
x: 281,
y: 76
}],
fill: 'lightCyan',
label: {
text: 'DT',
cx: 370,
cy: 290,
withLine: false,
endX: 366,
endY: 120
},
},
{
points: [{
x: 306,
y: 30
}, {
x: 312,
y: 40
}, {
x: 300,
y: 40
}],
fill: 'black',
label: {
text: 'PD',
cx: 356,
cy: 40,
withLine: true,
endX: 321,
endY: 40
},
},
{
points: [{
x: 312,
y: 40
}, {
x: 348,
y: 103
}, {
x: 337,
y: 115
}, {
x: 297,
y: 46
}, {
x: 300,
y: 40
}],
fill: 'navajoWhite',
label: {
text: 'T1',
cx: 375,
cy: 70,
withLine: true,
endX: 340,
endY: 75
},
},
{
points: [{
x: 348,
y: 103
}, {
x: 402,
y: 199
}, {
x: 392,
y: 214
}, {
x: 337,
y: 115
}],
fill: 'tan',
label: {
text: 'T2',
cx: 400,
cy: 125,
withLine: true,
endX: 366,
endY: 120
},
},
{
points: [{
x: 402,
y: 199
}, {
x: 498,
y: 366
}, {
x: 441,
y: 366
}, {
x: 372,
y: 248
}],
fill: 'peru',
label: {
text: 'T3',
cx: 425,
cy: 290,
withLine: false,
endX: null,
endY: null
},
},
];
// label styles
var labelfontsize = 12;
var labelfontface = 'verdana';
var labelpadding = 3;
// pre-create a canvas-image of the arrowhead
var arrowheadLength = 10;
var arrowheadWidth = 8;
var arrowhead = document.createElement('canvas');
premakeArrowhead();
var legendTexts = ['PD = Partial Discharge', 'T1 = Thermal fault < 300 celcius', '...'];
// start drawing
/////////////////////
// draw colored segments inside triangle
for (var i = 0; i < segments.length; i++) {
drawSegment(segments[i]);
}
// draw ticklines
ticklines(v0, v1, 9, 0, 20);
ticklines(v1, v2, 9, Math.PI * 3 / 4, 20);
ticklines(v2, v0, 9, Math.PI * 5 / 4, 20);
// molecules
moleculeLabel(v0, v1, 100, Math.PI, '% CH4');
moleculeLabel(v1, v2, 100, 0, '% C2H4');
moleculeLabel(v2, v0, 75, Math.PI / 2, '% C2H2');
// draw outer triangle
drawTriangle(triangle);
// draw legend
drawLegend(legendTexts, 10, 10, 12.86);
plotResult(25, 40, 35);
// end drawing
/////////////////////
function drawSegment(s) {
// draw and fill the segment path
ctx.beginPath();
ctx.moveTo(s.points[0].x, s.points[0].y);
for (var i = 1; i < s.points.length; i++) {
ctx.lineTo(s.points[i].x, s.points[i].y);
}
ctx.closePath();
ctx.fillStyle = s.fill;
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = 'black';
ctx.stroke();
// draw segment's box label
if (s.label.withLine) {
lineBoxedLabel(s, labelfontsize, labelfontface, labelpadding);
} else {
boxedLabel(s, labelfontsize, labelfontface, labelpadding);
}
}
function moleculeLabel(start, end, offsetLength, angle, text) {
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'
ctx.font = '14px verdana';
var dx = end.x - start.x;
var dy = end.y - start.y;
var x0 = parseInt(start.x + dx * 0.50);
var y0 = parseInt(start.y + dy * 0.50);
var x1 = parseInt(x0 + offsetLength * Math.cos(angle));
var y1 = parseInt(y0 + offsetLength * Math.sin(angle));
ctx.fillStyle = 'black';
ctx.fillText(text, x1, y1);
// arrow
var x0 = parseInt(start.x + dx * 0.35);
var y0 = parseInt(start.y + dy * 0.35);
var x1 = parseInt(x0 + 50 * Math.cos(angle));
var y1 = parseInt(y0 + 50 * Math.sin(angle));
var x2 = parseInt(start.x + dx * 0.65);
var y2 = parseInt(start.y + dy * 0.65);
var x3 = parseInt(x2 + 50 * Math.cos(angle));
var y3 = parseInt(y2 + 50 * Math.sin(angle));
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x3, y3);
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.stroke();
var angle = Math.atan2(dy, dx);
ctx.translate(x3, y3);
ctx.rotate(angle);
ctx.drawImage(arrowhead, -arrowheadLength, -arrowheadWidth / 2);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
function boxedLabel(s, fontsize, fontface, padding) {
var centerX = s.label.cx;
var centerY = s.label.cy;
var text = s.label.text;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'
ctx.font = fontsize + 'px ' + fontface
var textwidth = ctx.measureText(text).width;
var textheight = fontsize * 1.286;
var leftX = centerX - textwidth / 2 - padding;
var topY = centerY - textheight / 2 - padding;
ctx.fillStyle = 'white';
ctx.fillRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.lineWidth = 1;
ctx.strokeRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.fillStyle = 'black';
ctx.fillText(text, centerX, centerY);
}
function lineBoxedLabel(s, fontsize, fontface, padding) {
var centerX = s.label.cx;
var centerY = s.label.cy;
var text = s.label.text;
var lineToX = s.label.endX;
var lineToY = s.label.endY;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'
ctx.font = fontsize + 'px ' + fontface
var textwidth = ctx.measureText(text).width;
var textheight = fontsize * 1.286;
var leftX = centerX - textwidth / 2 - padding;
var topY = centerY - textheight / 2 - padding;
// the line
ctx.beginPath();
ctx.moveTo(leftX, topY + textheight / 2);
ctx.lineTo(lineToX, topY + textheight / 2);
ctx.strokeStyle = 'black';
ctx.lineWidth = 1;
ctx.stroke();
// the boxed text
ctx.fillStyle = 'white';
ctx.fillRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.strokeRect(leftX, topY, textwidth + padding * 2, textheight + padding * 2);
ctx.fillStyle = 'black';
ctx.fillText(text, centerX, centerY);
}
function ticklines(start, end, count, angle, length) {
var dx = end.x - start.x;
var dy = end.y - start.y;
ctx.lineWidth = 1;
for (var i = 1; i < count; i++) {
var x0 = parseInt(start.x + dx * i / count);
var y0 = parseInt(start.y + dy * i / count);
var x1 = parseInt(x0 + length * Math.cos(angle));
var y1 = parseInt(y0 + length * Math.sin(angle));
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
if (i == 2 || i == 4 || i == 6 || i == 8) {
var labelOffset = length * 3 / 4;
var x1 = parseInt(x0 - labelOffset * Math.cos(angle));
var y1 = parseInt(y0 - labelOffset * Math.sin(angle));
ctx.fillStyle = 'black';
ctx.fillText(parseInt(i * 10), x1, y1);
}
}
}
function premakeArrowhead() {
var actx = arrowhead.getContext('2d');
arrowhead.width = arrowheadLength;
arrowhead.height = arrowheadWidth;
actx.beginPath();
actx.moveTo(0, 0);
actx.lineTo(arrowheadLength, arrowheadWidth / 2);
actx.lineTo(0, arrowheadWidth);
actx.closePath();
actx.fillStyle = 'black';
actx.fill();
}
function drawTriangle(t) {
ctx.beginPath();
ctx.moveTo(t[0].x, t[0].y);
ctx.lineTo(t[1].x, t[1].y);
ctx.lineTo(t[2].x, t[2].y);
ctx.closePath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
}
function drawLegend(texts, x, y, lineheight) {
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillStyle = 'black';
ctx.font = '12px arial';
for (var i = 0; i < texts.length; i++) {
ctx.fillText(texts[i], x, y + i * lineheight);
}
}
function plotResult(val1, val2, val3) {
let deltaX, length;
let sum = val1 + val2 + val3;
const cos60 = Math.cos(Math.PI / 3);
const sin60 = Math.sin(Math.PI / 3);
let ch4 = val1 / sum;
let c2h2 = val2 / sum;
let c2h4 = val3 / sum;
length = Math.sqrt(Math.pow((v1.x - v0.x), 2) + Math.pow((v1.y - v0.y), 2));
let ch4PointA = new Point(v0.x + (length * ch4) * cos60, v0.y - (length * ch4) * sin60);
length = Math.sqrt(Math.pow((v2.x - v1.x), 2) + Math.pow((v2.y - v1.y), 2));
let ch4PointB = new Point(v2.x - (length * ch4) * cos60, v2.y - (length * ch4) * sin60);
length = Math.sqrt(Math.pow((v1.x - v2.x), 2) + Math.pow((v1.y - v2.y), 2));
let c2h4PointA = new Point(v1.x + (length * c2h4) * cos60, v1.y + (length * c2h4) * sin60);
deltaX = (v2.x - v0.x) * c2h4;
let c2h4PointB = new Point(v0.x + deltaX, v0.y);
let point = getIntersection(ch4PointA, ch4PointB, c2h4PointA, c2h4PointB);
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI, false);
ctx.fillStyle = "red";
ctx.fill();
}
function getIntersection(pointA, pointB, pointC, pointD) {
let denominator, a, b, numeratorA, numeratorB;
denominator = ((pointD.y - pointC.y) * (pointB.x - pointA.x)) - ((pointD.x - pointC.x) * (pointB.y - pointA.y));
a = pointA.y - pointC.y;
b = pointA.x - pointC.x;
numeratorA = ((pointD.x - pointC.x) * a) - ((pointD.y - pointC.y) * b);
numeratorB = ((pointB.x - pointA.x) * a) - ((pointB.y - pointA.y) * b);
a = numeratorA / denominator;
b = numeratorB / denominator;
return new Point(pointA.x + (a * (pointB.x - pointA.x)), pointA.y + (a * (pointB.y - pointA.y)));
}
body {
background-color: ivory;
padding: 10px;
}
#canvas {
border: 1px solid red;
margin: 0 auto;
}
<canvas id="canvas" width=650 height=500></canvas>
I have a 2 points projection sketch based on two lines intersection algorithms.
For example, if there's a 3D point { x: -4, y: 2, z: -2 } and axes origin is pre-defined, I could find a = {x: -4, y: 2, z: 0} and b = {x: 0, y: 2, z: -2} points and find one possible intersection of line {vanishingPointX, a) and another line of {vanishingPointZ, b) and as you can see the code works fine.
And the task is to add another 3-rd point, so the output should like this:
it's a rough illustration, some lines are distorted.
I have tried to project Y values along X-axis, but anyway neither there's no algorithm of three lines intersection nor these three lines don't intersect together at one point.
And the last but not least, I am aware that this problem could be solved with matrices, however, I'm trying to do it calculus-free.
const scale = 64.0;
const far = 6.0;
const cube = [
{ x: 1.0, y: 1.0, z: 1.0 },
{ x: 1.0, y: -1.0, z: 1.0 },
{ x: -1.0, y: -1.0, z: 1.0 },
{ x: -1.0, y: 1.0, z: 1.0 },
{ x: 1.0, y: 1.0, z: -1.0 },
{ x: 1.0, y: -1.0, z: -1.0 },
{ x: -1.0, y: -1.0, z: -1.0 },
{ x: -1.0, y: 1.0, z: -1.0 },
];
const sides = [0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7];
let vanishingPointZ = { x: -3.5, y: 2.0 };
let vanishingPointX = { x: 5.0, y: 2.0 };
let vanishingPointY = { x: 0.0, y: -6.0 };
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let zero = { x: canvas.width / 2, y: canvas.height / 2 };
draw();
function draw(){
ctx.font = '32px serif';
ctx.fillStyle = "#0F0"
ctx.fillText('X', zero.x + vanishingPointX.x * scale + 16, zero.y + vanishingPointX.y * scale + 16);
ctx.fillStyle = "#F0F";
ctx.fillText('Y', zero.x + vanishingPointY.x * scale + 32, zero.y + vanishingPointY.y * scale + 16);
ctx.fillStyle = "#00F";
ctx.fillText('Z', zero.x + vanishingPointZ.x * scale - 32, zero.y + vanishingPointZ.y * scale + 16);
cube.forEach((p_, i_) =>{
project(p_);
let pos = { x: zero.x + p_.dx * scale, y: zero.y + p_.dy * scale };
//to x
ctx.beginPath();
ctx.moveTo(zero.x + vanishingPointX.x * scale, zero.y + vanishingPointX.y * scale);
ctx.lineTo(pos.x, pos.y);
ctx.closePath();
ctx.strokeStyle = "rgba(0, 255, 0, 0.33)";
ctx.stroke();
//to z
ctx.beginPath();
ctx.moveTo(zero.x + vanishingPointZ.x * scale, zero.y + vanishingPointZ.y * scale);
ctx.lineTo(pos.x, pos.y);
ctx.closePath();
ctx.strokeStyle = "rgba(0, 0, 255, 0.33)";
ctx.stroke();
//to upper y
//to x
ctx.beginPath();
ctx.moveTo(zero.x + vanishingPointY.x * scale, zero.y + vanishingPointY.y * scale);
ctx.lineTo(pos.x, pos.y);
ctx.closePath();
ctx.strokeStyle = "rgba(255, 0, 255, 0.33)";
ctx.stroke();
ctx.beginPath();
ctx.arc(pos.x, pos.y, 8, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = "#DEDEDE";
ctx.fill();
})
for(let i = 0; i < sides.length; i += 2){
ctx.beginPath();
ctx.moveTo(zero.x + cube[sides[i]].dx * scale, zero.y + cube[sides[i]].dy * scale);
ctx.lineTo(zero.x + cube[sides[i + 1]].dx * scale, zero.y + cube[sides[i + 1]].dy * scale);
ctx.closePath();
ctx.strokeStyle = "#000";
ctx.stroke();
}
}
function project(p_){
let distX = Math.sqrt(Math.pow(vanishingPointX.x, 2), Math.pow(vanishingPointX.y - p_.y, 2));
let vx = { x: vanishingPointX.x, y: vanishingPointX.y - p_.y };
let nx = Math.exp( p_.x / far );
vx.x *= nx;
vx.y *= nx;
let x = { x: vanishingPointX.x - vx.x, y: vanishingPointX.y - vx.y };
let distZ = Math.sqrt(Math.pow(vanishingPointZ.x, 2), Math.pow(vanishingPointZ.y - p_.y, 2));
let vz = { x: vanishingPointZ.x, y: vanishingPointZ.y - p_.y };
let nz = Math.exp( p_.z / far );
vz.x *= nz;
vz.y *= nz;
let z = { x: vanishingPointZ.x - vz.x, y: vanishingPointZ.y - vz.y };
let out = twoLinesIntersection(vanishingPointZ, z, vanishingPointX, x);
//trying to calculate y projection and it seems that as standalone it work fine
let distY = Math.sqrt(Math.pow(vanishingPointY.x, 2), Math.pow(vanishingPointX.y - p_.x, 2));
let vy = { x: vanishingPointY.y, y: vanishingPointY.y - p_.x };
let ny = Math.exp( p_.y / far );
vy.x *= ny;
vy.y *= ny;
let y = { x: vanishingPointY.x - vy.x, y: vanishingPointY.y - vy.y };
p_.dx = out.x;
p_.dy = out.y;
}
function twoLinesIntersection(p1_, p4_, p3_, p2_){
let d1 = (p1_.x - p2_.x) * (p3_.y - p4_.y);
let d2 = (p1_.y - p2_.y) * (p3_.x - p4_.x);
let d = (d1) - (d2);
let u1 = (p1_.x * p2_.y - p1_.y * p2_.x);
let u4 = (p3_.x * p4_.y - p3_.y * p4_.x);
let u2x = p3_.x - p4_.x;
let u3x = p1_.x - p2_.x;
let u2y = p3_.y - p4_.y;
let u3y = p1_.y - p2_.y;
return { x: (u1 * u2x - u3x * u4) / d, y: (u1 * u2y - u3y * u4) / d };
}
body { margin: 0; }
<canvas id='canvas' width='800' height='800'></canvas>
We can make this easier to compute by, rather than adding vector compounds to a start coordinate until we're at the "real" coordinate, treating the problem as one of line intersections. For example, if we want the point XYZ = (1,2,2) we can find it using line intersections by following this visual "recipe":
For any 3D point we can first compute the points on the YZ and XY planes, after which we can find the true point with one more line intersection. This does rely on knowing where on the screen our origin can be found (we could pick a point in the middle of the triangle Z-Y-X, but whatever screen point we pick, let's call that C, after which the code looks like this:
function get(x, y, z) {
if (x===0 && y===0 && z===0) return C;
// Get the points at the distances along our Z and X axes:
const px = lerp(C, X, perspectiveMap(x));
const pz = lerp(C, Z, perspectiveMap(z));
// If our 3D coordinate lies on the XZ plane, then this
// is just a matter of finding the line/line intersection:
if (y==0) return twoLinesIntersection(X, pz, Z, px);
// If it's not, we construct the two points on the YZ and XY planes:
const py = lerp(C, Y, perspectiveMap(y));
const YZ = twoLinesIntersection(Y, pz, Z, py);
const XY = twoLinesIntersection(Y, px, X, py);
// And then the 3D coordinate is a line/line intersection with those.
return twoLinesIntersection(XY, Z, X, YZ);
}
function lerp(v1, v2, r) {
const mr = 1 - r;
return {
x: v1.x * mr + v2.x * r,
y: v1.y * mr + v2.y * r,
z: v1.z * mr + v2.z * r,
};
}
With the perspective mapping function turns a value on the axis into a distance ratio between the origin and the axis' vanishing point. What that function looks like is effectively up to you, but if we want straight lines to stay straight, we'll want a rational perspective function:
const DEFAULT_PERSPECTIVE_STRENGTH = 0.3;
function perspectiveMap(value, strength=DEFAULT_PERSPECTIVE_STRENGTH) {
return 1 - 1/(1 + value * strength);
}
(we computer "1 minus..." because as a ratio between the origin and a vanishing point, we want this value to be 0 at x/y/z=0, so that lerp(origin, vanishingPoint, value) yields the origin, and we want it to become 1 as x/y/z approach infinity. Just on its own, 1/(1+v*s) does the exact opposite of that, so subtracting it from one "flips" it around)
Implemented as a snippet:
const w = 600, h = 300;
perspective.width = w;
perspective.height = h;
const DEFAULT_PERSPECTIVE_STRENGTH = 0.2;
const ctx = perspective.getContext("2d");
// our vanishing points and origin
let Z = { x: w*0.05, y: h*0.85 };
let X = { x: w*0.92, y: h*0.95 };
let Y = { x: w*0.65, y: h*0.1 };
let C = { x: w*0.65, y: h*0.50 };
// draw our "grid" and labels
line(X,C);
line(Y,C);
line(Z,C);
ctx.strokeStyle = `#00000020`;
drawGrid(10);
ctx.fillStyle = `#400`;
text("X", X, 10, 0);
text("Z", Z, -10, 0);
text("Y", Y, 0, -5);
// draw a 2x2x2 cube
ctx.strokeStyle = `#000000`;
drawCube(2);
// ------------ functions for content ------------
function drawGrid(e) {
for(let i=0; i<e; i++) {
line(X, get(0,i,0));
line(X, get(0,0,i));
line(Y, get(i,0,0));
line(Y, get(0,0,i));
line(Z, get(i,0,0));
line(Z, get(0,i,0));
}
}
function drawCube(n) {
const cube = getCube(n);
const [p1,p2,p3,p4,p5,p6,p7,p8] = cube.map(p => get(p));
quad(p1,p2,p3,p4);
quad(p5,p6,p7,p8);
line(p1,p5);
line(p2,p6);
line(p3,p7);
line(p4,p8);
}
function getCube(n) {
return [
{x: n, y: n, z: n},
{x: 0, y: n, z: n},
{x: 0, y: n, z: 0},
{x: n, y: n, z: 0},
{x: n, y: 0, z: n},
{x: 0, y: 0, z: n},
{x: 0, y: 0, z: 0},
{x: n, y: 0, z: 0},
];
}
// ------------ some draw util functions ------------
function line(p1, p2) {
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
}
function quad(p1, p2, p3, p4) {
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineTo(p3.x, p3.y);
ctx.lineTo(p4.x, p4.y);
ctx.closePath();
ctx.stroke();
}
function text(str, point, ox=0, oy=0) {
const { x, y } = point;
ctx.fillText(str, x+ox, y+oy);
}
// ------------ and: our coordinate computer ------------
function get(x,y,z) {
if (y === undefined) {
z = x.z;
y = x.y;
x = x.x
}
if (x===0 && y===0 && z===0) return C;
const px = lerp(C, X, map(x));
const pz = lerp(C, Z, map(z));
if (y==0) return lli(X, pz, Z, px);
const py = lerp(C, Y, map(y));
const YZ = lli(Y, pz, Z, py);
const XY = lli(Y, px, X, py);
return lli(XY, Z, X, YZ);
}
function lerp(v1, v2, r) {
const mr = 1 - r;
return {
x: v1.x * mr + v2.x * r,
y: v1.y * mr + v2.y * r,
z: v1.z * mr + v2.z * r
}
}
function lli(p1,p2,p3,p4) {
return lli8(p1.x,p1.y,p2.x,p2.y,p3.x,p3.y,p4.x,p4.y);
}
function lli8(x1,y1,x2,y2,x3,y3,x4,y4) {
const d = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4);
if (d === 0) return undefined;
const f12 = (x1*y2 - y1*x2);
const f34 = (x3*y4 - y3*x4);
const nx = f12*(x3-x4) - (x1-x2)*f34;
const ny = f12*(y3-y4) - (y1-y2)*f34;
return { x: nx/d, y: ny/d };
}
function map(value, strength=DEFAULT_PERSPECTIVE_STRENGTH) {
return 1 - 1/(1 + value*strength);
}
canvas { border: 1px solid black; }
<canvas id="perspective">
OK, if I have the following shapes that are rotated and then selected you will see their bounding boxes:
I am trying to write some code to align objects with respect to each other. So I would like to get each object's "containing box".
I am aware of getBoundingRect but, for the above shapes this gives me the following:
As such, these boxes are not that useful to me. Is there a standard method of getting what I would call the "containing boxes" for all shapes? For example, I would like to be able to have the following boxes returned to me:
So, for any given shape I would like to be able get the red bounding rectangle (with no rotation).
Obviously, I could write a routine for each possible shape within fabricJS but I would prefer not to reinvent the wheel! Any ideas?
Edit Here's an interactive snippet that shows the current bounding boxes (in red):
$(function ()
{
canvas = new fabric.Canvas('c');
canvas.add(new fabric.Triangle({
left: 50,
top: 50,
fill: '#FF0000',
width: 50,
height: 50,
angle : 30
}));
canvas.add(new fabric.Circle({
left: 250,
top: 50,
fill: '#00ff00',
radius: 50,
angle : 30
}));
canvas.add(new fabric.Polygon([
{x: 185, y: 0},
{x: 250, y: 100},
{x: 385, y: 170},
{x: 0, y: 245} ], {
left: 450,
top: 50,
fill: '#0000ff',
angle : 30
}));
canvas.on("after:render", function(opt)
{
canvas.contextContainer.strokeStyle = '#FF0000';
canvas.forEachObject(function(obj)
{
var bound = obj.getBoundingRect();
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
});
});
canvas.renderAll();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>
So the getBoundingBox is a method of the Object class of fabricjs.
Nothing stops you from rewriting this method for each shape of which you can think of.
I ll start with circle and triangle, i'll let you imagine polygon. It gets harder and harder when shapes are paths or when circle is scaled as an ellipse.
Circle is the hardest.
I sampled the circle at 30, 60, 90 degrees for all the quadrants. still is not perfect. You may need to increase sampling or find a better formula (maybe sample every 15 degrees will make the trick ).
Triangle is the easier since it has 3 points of interest.
Polygon is derived from triangle, nothing difficult here.
fabric.Circle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Triangle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Polygon.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = this.points;
var offsetX = this.pathOffset.x;
var offsetY = this.pathOffset.y;
points = points.map(function(p) {
return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
offsetY}, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
$(function ()
{
fabric.util.makeBoundingBoxFromPoints = function(points) {
var minX = fabric.util.array.min(points, 'x'),
maxX = fabric.util.array.max(points, 'x'),
width = Math.abs(minX - maxX),
minY = fabric.util.array.min(points, 'y'),
maxY = fabric.util.array.max(points, 'y'),
height = Math.abs(minY - maxY);
return {
left: minX,
top: minY,
width: width,
height: height
};
};
fabric.Circle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:0}, {x:this.width/2, y:0}, {x:0, y: -this.height/2}, {x: 0, y: this.height/2}, {x: 0, y: -this.height/2}, {x: 0.433 * this.width, y: this.height/4}, {x: -0.433 * this.width, y: this.height/4}, {y: 0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: this.width/4}, {y: -0.433 * this.height, x: -this.width/4}, {y: 0.433 * this.height, x: -this.width/4}, {x: 0.433 * this.width, y: -this.height/4}, {x: -0.433 * this.width, y: -this.height/4}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Triangle.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = [{x:-this.width/2, y:this.height/2}, {x:this.width/2, y:this.height/2}, {x:0, y: -this.height/2}, {x: 0, y: 0}];
points = points.map(function(p) {
return fabric.util.transformPoint(p, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
fabric.Polygon.prototype.getBoundingRect = function() {
var matrix = this.calcTransformMatrix();
var points = this.points;
var offsetX = this.pathOffset.x;
var offsetY = this.pathOffset.y;
points = points.map(function(p) {
return fabric.util.transformPoint({x: p.x - offsetX , y: p.y -
offsetY}, matrix);
});
return fabric.util.makeBoundingBoxFromPoints(points);
}
canvas = new fabric.Canvas('c');
canvas.add(new fabric.Triangle({
left: 50,
top: 50,
fill: '#FF0000',
width: 50,
height: 50,
angle : 30
}));
canvas.add(new fabric.Circle({
left: 250,
top: 50,
fill: '#00ff00',
radius: 50,
angle : 30
}));
canvas.add(new fabric.Polygon([
{x: 185, y: 0},
{x: 250, y: 100},
{x: 385, y: 170},
{x: 0, y: 245} ], {
left: 450,
top: 50,
fill: '#0000ff',
angle : 30
}));
canvas.on("after:render", function(opt)
{
canvas.contextContainer.strokeStyle = '#FF0000';
canvas.forEachObject(function(obj)
{
var bound = obj.getBoundingRect();
if(bound)
{
canvas.contextContainer.strokeRect(
bound.left + 0.5,
bound.top + 0.5,
bound.width,
bound.height
);
}
});
});
canvas.renderAll();
});
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.6/fabric.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="c" width="800" height="600"></canvas><br/>
I had the same issue and I found a workaround. I created a temporary SVG from the active object and placed it outside the viewport. Then I measured the real bounding box using the native getBBox() function.
UPDATE
Apparently, the solution above works only in Firefox (76), so I came up with a different solution. Since I couldn't find a properly working, native function to get the real bounding box of a shape, I decided to scan the pixels and retrieve the boundaries from there.
Fiddle: https://jsfiddle.net/divpusher/2m7c61gw/118/
How it works:
export the selected fabric object toDataURL()
place it in a hidden canvas and get the pixels with getImageData()
scan the pixels to retrieve x1, x2, y1, y2 edge coords of the shape
Demo below
// ---------------------------
// the magic
var tempCanv, ctx, w, h;
function getImageData(dataUrl) {
// we need to use a temp canvas to get imagedata
if (tempCanv == null) {
tempCanv = document.createElement('canvas');
tempCanv.style.border = '1px solid blue';
tempCanv.style.visibility = 'hidden';
ctx = tempCanv.getContext('2d');
document.body.appendChild(tempCanv);
}
return new Promise(function(resolve, reject) {
if (dataUrl == null) return reject();
var image = new Image();
image.addEventListener('load', function() {
w = image.width;
h = image.height;
tempCanv.width = w;
tempCanv.height = h;
ctx.drawImage(image, 0, 0, w, h);
var imageData = ctx.getImageData(0, 0, w, h).data.buffer;
resolve(imageData, false);
});
image.src = dataUrl;
});
}
function scanPixels(imageData) {
var data = new Uint32Array(imageData),
len = data.length,
x, y, y1, y2, x1 = w, x2 = 0;
// y1
for(y = 0; y < h; y++) {
for(x = 0; x < w; x++) {
if (data[y * w + x] & 0xff000000) {
y1 = y;
y = h;
break;
}
}
}
// y2
for(y = h - 1; y > y1; y--) {
for(x = 0; x < w; x++) {
if (data[y * w + x] & 0xff000000) {
y2 = y;
y = 0;
break;
}
}
}
// x1
for(y = y1; y < y2; y++) {
for(x = 0; x < w; x++) {
if (x < x1 && data[y * w + x] & 0xff000000) {
x1 = x;
break;
}
}
}
// x2
for(y = y1; y < y2; y++) {
for(x = w - 1; x > x1; x--) {
if (x > x2 && data[y * w + x] & 0xff000000) {
x2 = x;
break;
}
}
}
return {
x1: x1,
x2: x2,
y1: y1,
y2: y2
}
}
// ---------------------------
// align buttons
function alignLeft(){
var obj = canvas.getActiveObject();
obj.set('left', 0);
obj.setCoords();
canvas.renderAll();
}
function alignLeftbyBoundRect(){
var obj = canvas.getActiveObject();
var bound = obj.getBoundingRect();
obj.set('left', (obj.left - bound.left));
obj.setCoords();
canvas.renderAll();
}
function alignRealLeft(){
var obj = canvas.getActiveObject();
getImageData(obj.toDataURL())
.then(function(data) {
var bound = obj.getBoundingRect();
var realBound = scanPixels(data);
obj.set('left', (obj.left - bound.left - realBound.x1));
obj.setCoords();
canvas.renderAll();
});
}
// ---------------------------
// set up canvas
var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 150 50 L 120 150 z');
path.set({
left: 170,
top: 30,
fill: 'rgba(0, 128, 0, 0.5)',
stroke: '#000',
strokeWidth: 4,
strokeLineCap: 'square',
angle: 65
});
canvas.add(path);
canvas.setActiveObject(path);
var circle = new fabric.Circle({
left: 370,
top: 30,
radius: 45,
fill: 'blue',
scaleX: 1.5,
angle: 30
});
canvas.add(circle);
canvas.forEachObject(function(obj) {
var setCoords = obj.setCoords.bind(obj);
obj.on({
moving: setCoords,
scaling: setCoords,
rotating: setCoords
});
});
canvas.on('after:render', function() {
canvas.contextContainer.strokeStyle = 'red';
canvas.forEachObject(function(obj) {
getImageData(obj.toDataURL())
.then(function(data) {
var boundRect = obj.getBoundingRect();
var realBound = scanPixels(data);
canvas.contextContainer.strokeRect(
boundRect.left + realBound.x1,
boundRect.top + realBound.y1,
realBound.x2 - realBound.x1,
realBound.y2 - realBound.y1
);
});
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<p> </p>
<button onclick="alignLeft()">align left (default)</button>
<button onclick="alignLeftbyBoundRect()">align left (by bounding rect)</button>
<button onclick="alignRealLeft()">align REAL left (by pixel)</button>
<p></p>
<canvas id="c" width="600" height="250" style="border: 1px solid rgb(204, 204, 204); touch-action: none; user-select: none;" class="lower-canvas"></canvas>
<p></p>
I am making a game in HTML5 Canvas.
I'm trying to get the bullets shoot relative to the players velX and velY (vx and vy), here's the code, the problem is that its speed should be constant.
Here's the part where the problem comes:
if (keys[32]) {
bullet.push({
x: player.x,
y: player.y,
vx: player.vx * 10,
vy: player.vy * 10,
})
}
Here's the main code for the game:
<style>
.canvas {
border: 0px;
background-color: #FFF;
width: 1000px;
height: 500px;
position: fixed;
left: 0;
top: 0;
}
</style>
<center>
<canvas id="canvas" class="canvas" width="1000" height="500"></canvas>
</center>
<script language="javascript">
var canvas = document.getElementById("canvas")
var ctx = canvas.getContext("2d")
canvas.width = canvas.style.width = window.innerWidth
canvas.height = canvas.style.height = window.innerHeight
var w = canvas.width
var h = canvas.height
var player = {
x: w / 2,
y: h / 2,
s: 5,
vx: 0,
vy: 0,
}
var enemy = {
x: 0,
y: 0,
s: 4,
vx: 0,
vy: 0,
}
var bullet = []
var keys = []
var friction = 0.9
setInterval(draw, 1000/60)
function draw() {
canvas.width = canvas.style.width = window.innerWidth
canvas.height = canvas.style.height = window.innerHeight
w = canvas.width
h = canvas.height
ctx.clearRect(0, 0, w, h)
if (keys[40]) {
if (player.vy < player.s) {
player.vy++;
}
}
if (keys[38]) {
if (player.vy > -player.s) {
player.vy--;
}
}
if (keys[39]) {
if (player.vx < player.s) {
player.vx++;
}
}
if (keys[37]) {
if (player.vx > -player.s) {
player.vx--;
}
}
if (keys[32]) {
bullet.push({
x: player.x,
y: player.y,
vx: player.vx * 10,
vy: player.vy * 10,
})
}
for (i = 0; i < bullet.length; i++) {
bullet[i].x += bullet[i].vx
bullet[i].y += bullet[i].vy
ctx.beginPath()
ctx.arc(bullet[i].x, bullet[i].y, 3, 0, Math.PI * 2, false)
ctx.fillStyle = "red"
ctx.fill()
ctx.closePath()
}
player.vx *= friction;
player.vy *= friction;
player.x += player.vx;
player.y += player.vy;
var dx = (enemy.x - player.x);
var dy = (enemy.y - player.y);
var mag = Math.sqrt(dx * dx + dy * dy);
enemy.vx = (dx / mag) * -enemy.s,
enemy.vy = (dy / mag) * -enemy.s,
enemy.x += enemy.vx
enemy.y += enemy.vy
ctx.beginPath()
ctx.arc(player.x, player.y, 8, 0, Math.PI * 2, false)
ctx.fillStyle = "black"
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.arc(enemy.x, enemy.y, 8, 0, Math.PI * 2, false)
ctx.fillStyle = "red"
ctx.fill()
ctx.closePath()
}
document.body.addEventListener("keydown", function(e) {
keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function(e) {
keys[e.keyCode] = false;
});
</script>
If you want the speed to head the same direction as the player is moving you'd need to keep the proportions between the X and Y the same. Best to do this is through trigo.
The direction angle would be the tangent, because velocity is a vector
var angle = Math.atan(Math.abs(vy/vx)) //Angle between Vx and the Velocity
then, you know the speed would be Vb,
so each axis would be calculated through trigo:
var vB = 1 //Speed of bullet
var vBx = vB*Math.cos(angle) //Speed of bullet on x-axis
if (vx<0) vBx*=-1; //Makes it the same direction
var vBy = vB*Math.sin(angle) //Speed of bullet on y-axis
if (vx<0) vBy*=-1; //Makes it the same direction
Here's a working example:
http://jsfiddle.net/WJKHD/
EDIT:
For anybody who is curious, here is the finished result.
http://jsfiddle.net/Javalsu/vxP5q/743/embedded/result/
I'm building off of the code I found in this link
http://thecodeplayer.com/walkthrough/html5-canvas-snow-effect
I want to make this more of a confetti falling effect than a snow effect, and I would need to make each element a different color. But it seems that the fill color is set for entire canvas at once.
Is there a way to specify a different fill color for each element or am I going about this the entirely wrong way?
Thanks
Update: Here is the finished product if anybody has a need for confetti
http://jsfiddle.net/mj3SM/6/
window.onload = function () {
//canvas init
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
//canvas dimensions
var W = window.innerWidth;
var H = window.innerHeight;
canvas.width = W;
canvas.height = H;
//snowflake particles
var mp = 200; //max particles
var particles = [];
for (var i = 0; i < mp; i++) {
particles.push({
x: Math.random() * W, //x-coordinate
y: Math.random() * H, //y-coordinate
r: Math.random() * 15 + 1, //radius
d: Math.random() * mp, //density
color: "rgba(" + Math.floor((Math.random() * 255)) + ", " + Math.floor((Math.random() * 255)) + ", " + Math.floor((Math.random() * 255)) + ", 0.8)",
tilt: Math.floor(Math.random() * 5) - 5
});
}
//Lets draw the flakes
function draw() {
ctx.clearRect(0, 0, W, H);
for (var i = 0; i < mp; i++) {
var p = particles[i];
ctx.beginPath();
ctx.lineWidth = p.r;
ctx.strokeStyle = p.color; // Green path
ctx.moveTo(p.x, p.y);
ctx.lineTo(p.x + p.tilt + p.r / 2, p.y + p.tilt);
ctx.stroke(); // Draw it
}
update();
}
//Function to move the snowflakes
//angle will be an ongoing incremental flag. Sin and Cos functions will be applied to it to create vertical and horizontal movements of the flakes
var angle = 0;
function update() {
angle += 0.01;
for (var i = 0; i < mp; i++) {
var p = particles[i];
//Updating X and Y coordinates
//We will add 1 to the cos function to prevent negative values which will lead flakes to move upwards
//Every particle has its own density which can be used to make the downward movement different for each flake
//Lets make it more random by adding in the radius
p.y += Math.cos(angle + p.d) + 1 + p.r / 2;
p.x += Math.sin(angle) * 2;
//Sending flakes back from the top when it exits
//Lets make it a bit more organic and let flakes enter from the left and right also.
if (p.x > W + 5 || p.x < -5 || p.y > H) {
if (i % 3 > 0) //66.67% of the flakes
{
particles[i] = {
x: Math.random() * W,
y: -10,
r: p.r,
d: p.d,
color: p.color,
tilt: p.tilt
};
} else {
//If the flake is exitting from the right
if (Math.sin(angle) > 0) {
//Enter from the left
particles[i] = {
x: -5,
y: Math.random() * H,
r: p.r,
d: p.d,
color: p.color,
tilt: p.tilt
};
} else {
//Enter from the right
particles[i] = {
x: W + 5,
y: Math.random() * H,
r: p.r,
d: p.d,
color: p.color,
tilt: p.tilt
};
}
}
}
}
}
//animation loop
setInterval(draw, 20);
}
Try it like this: http://jsfiddle.net/vxP5q/
The JS:
window.onload = function(){
//canvas init
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
//canvas dimensions
var W = window.innerWidth;
var H = window.innerHeight;
canvas.width = W;
canvas.height = H;
//snowflake particles
var mp = 25; //max particles
var particles = [];
for(var i = 0; i < mp; i++)
{
particles.push({
x: Math.random()*W, //x-coordinate
y: Math.random()*H, //y-coordinate
r: Math.random()*4+1, //radius
d: Math.random()*mp, //density
color: "rgba(" + Math.floor((Math.random() * 255)) +", " + Math.floor((Math.random() * 255)) +", " + Math.floor((Math.random() * 255)) + ", 0.8)"
})
}
//Lets draw the flakes
function draw()
{
ctx.clearRect(0, 0, W, H);
for(var i = 0; i < mp; i++)
{
var p = particles[i];
ctx.beginPath();
ctx.fillStyle = p.color;
ctx.moveTo(p.x, p.y);
ctx.arc(p.x, p.y, p.r, 0, Math.PI*2, true);
ctx.fill();
}
update();
}
//Function to move the snowflakes
//angle will be an ongoing incremental flag. Sin and Cos functions will be applied to it to create vertical and horizontal movements of the flakes
var angle = 0;
function update()
{
angle += 0.01;
for(var i = 0; i < mp; i++)
{
var p = particles[i];
//Updating X and Y coordinates
//We will add 1 to the cos function to prevent negative values which will lead flakes to move upwards
//Every particle has its own density which can be used to make the downward movement different for each flake
//Lets make it more random by adding in the radius
p.y += Math.cos(angle+p.d) + 1 + p.r/2;
p.x += Math.sin(angle) * 2;
//Sending flakes back from the top when it exits
//Lets make it a bit more organic and let flakes enter from the left and right also.
if(p.x > W+5 || p.x < -5 || p.y > H)
{
if(i%3 > 0) //66.67% of the flakes
{
particles[i] = {x: Math.random()*W, y: -10, r: p.r, d: p.d, color : p.color};
}
else
{
//If the flake is exitting from the right
if(Math.sin(angle) > 0)
{
//Enter from the left
particles[i] = {x: -5, y: Math.random()*H, r: p.r, d: p.d, color: p.color};
}
else
{
//Enter from the right
particles[i] = {x: W+5, y: Math.random()*H, r: p.r, d: p.d, color : p.color};
}
}
}
}
}
//animation loop
setInterval(draw, 33);
}
What I've done. Where the pixels are generated I've added an unique (random) color. Where the update is, I'm making sure the colors are changed and where its drawn I've changed it so that it will create an inuque path for each confetti item.
Great question. Consider the drawing loop for the sample:
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.beginPath();
for(var i = 0; i < mp; i++)
{
var p = particles[i];
ctx.moveTo(p.x, p.y);
ctx.arc(p.x, p.y, p.r, 0, Math.PI*2, true);
}
ctx.fill();
It is making one path, adding many arcs, and then filling it one time.
To change it you will need to fill it once per particle instead. You'll also want to give each particle a unique color:
for (var i = 0; i < mp; i++) {
var p = particles[i];
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2, true);
ctx.fill();
}
Note how beginPath() and fill() are now inside the loop. This is important, because each arc needs its own path and fill. This is much slower than making them all with one path, but is necessary if you want different colored particles.
That p.color:
particles.push({
x: Math.random() * W, //x-coordinate
y: Math.random() * H, //y-coordinate
r: Math.random() * 4 + 1, //radius
d: Math.random() * mp, //density
// I'm new!
color: "rgba(" + Math.floor(Math.random()*255) +
", " + Math.floor(Math.random()*255) + ", 255, 0.8)"
})
Here's a working example:
http://jsfiddle.net/j4NZK/1/
Here is a version based on the post by Niels, I wanted a reusable object that I can call and add to any page.
Usage:
confetti.Init(#IdofContainer(div)#, 50,25,100)
Code:
var confetti = {
angle: 0,
ctx: 0,
H: 0,
W: 0,
mp: 0,
particles: [],
endFunction: '',
Init: function (parent, maxParticles, iCount, speed, endFunct) {
confetti.stopped = false;
confetti.runner = null;
confetti.endFunction = endFunct;
var canvas = document.getElementById("confettiCanvasId");
if (canvas) {
canvas.parentNode.removeChild(canvas);
}
canvas = document.createElement('canvas');
canvas.className = 'confettiCanvas';
canvas.id = 'confettiCanvasId'
$id(parent).appendChild(canvas);
var ctx = canvas.getContext("2d");
var W = $id(parent).clientHeight;
var H = $id(parent).clientWidth;
canvas.width = W;
canvas.height = H;
confetti.particles = [];
for (var i = 0; i < maxParticles; i++) {
confetti.particles.push({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 4 + 1, //radius
d: Math.random() * maxParticles, //density
color: "rgba(" + Math.floor((Math.random() * 255)) + ", " + Math.floor((Math.random() * 255)) + ", " + Math.floor((Math.random() * 255)) + ", 0.8)"
});
}
myCounter = new confetti.Counter({
seconds: iCount,
speed: speed,
onUpdateStatus: function (sec) {
$l(Math.random() * 255)
ctx.clearRect(0, 0, W, H);
for (var i = 0; i < maxParticles; i++) {
var p = confetti.particles[i];
ctx.beginPath();
ctx.fillStyle = p.color;
ctx.moveTo(p.x, p.y);
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2, true);
ctx.fill();
}
confetti.angle += 0.01;
for (var i = 0; i < maxParticles; i++) {
var p = confetti.particles[i];
p.y += Math.cos(confetti.angle + p.d) + 1 + p.r / 2;
p.x += Math.sin(confetti.angle) * 2;
if (p.x > W + 5 || p.x < -5 || p.y > H) {
if (i % 3 > 0) //66.67% of the flakes
{
confetti.particles[i] = {x: Math.random() * W, y: -10, r: p.r, d: p.d, color: p.color};
}
else {
if (Math.sin(confetti.angle) > 0) {
confetti.particles[i] = {x: -5, y: Math.random() * H, r: p.r, d: p.d, color: p.color};
}
else {
confetti.particles[i] = {x: W + 5, y: Math.random() * H, r: p.r, d: p.d, color: p.color};
}
}
}
}
},
onCounterEnd: function () {
stopTimer();
myCounter.stop();
confetti.Stop();
}});
myCounter.start();
},
FadeOut:function fadeOut() {
var alpha = 1.0; // full opacity
for (var i = 0; i < confetti.particles.length; i++) {
var p = confetti.particles[i];
interval = setInterval(function () {
//confetti.canvas.width = confetti.canvas.width; // Clears the canvas
p.color = "rgba(255, 0, 0, " + alpha + ")";
alpha = alpha - 0.05; // decrease opacity (fade out)
if (alpha < 0) {
//confetti.canvas.width = confetti.canvas.width;
clearInterval(interval);
}
}, 50);
}
},
Counter: function Countdown(options) {
var timer,
instance = this,
seconds = options.seconds || 10,
updateStatus = options.onUpdateStatus || function () {
},
counterEnd = options.onCounterEnd || function () {
};
function decrementCounter() {
updateStatus(seconds);
if (seconds === 0) {
counterEnd();
instance.stop();
}
seconds--;
}
this.start = function () {
clearInterval(timer);
timer = 0;
seconds = options.seconds;
timer = setInterval(decrementCounter, options.speed);
};
this.stop = function () {
clearInterval(timer);
};
},
Stop: function stop() {
$('#confettiCanvasId').fadeOut();
setTimeout(function(){
var canvas = document.getElementById("confettiCanvasId");
if (canvas) {
canvas.parentNode.removeChild(canvas);
}
if (confetti.endFunction) {
confetti.endFunction();
}
},1000);
}
};
CSS:
.confettiCanvas{
overflow: hidden;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
I think canvas-confetti is more realistic than any others here, and it has more feature: confetti, snow, fireworks, etc. Click here for demo!