Html canvas draw smooth lines with bezier curve - javascript

I have a problem with my canvas lines is that whenever I try to draw lines they're not smooth, the lines look like a bunch of small lines connected with each other, I tried to find a solution using quadraticCurveTo and calculating a midpoint for the line
const draw = (e) => {
if (!isPainting) {
return;
}
const x = e.pageX - canvasOffsetX;
const y = e.pageY - canvasOffsetY;
points.push({ x: x, y: y });
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalAlpha = opacity;
ctx.imageSmoothingQuality = "high";
ctx.beginPath();
if (points.length === 1) {
ctx.moveTo(x, y);
} else {
for (let i = 1, len = points.length; i < len; i++) {
let xc = (points[i].x + points[i + 1].x) / 2;
let yc = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
ctx.stroke();
}
};
this method worked well but the lines appear after the mouse is up or after the calculation is done which isn't the best approach, I tried to draw the line before changing it after the mouse is up but it didn't work too
the only relative answer I was able to find was this codepen:
https://codepen.io/kangax/pen/kyYrdX
but the issue with it is that I have to clear the canvas before drawing new lines and I want all the drawings to be present

It might be too late, but here is the solution by editing the codepen you mentioned.
You need to take a snapshot of the canvas on mousedown event and
save it to a global variable
After you clear the canvas you need to paste that snapshot onto the
canvas
function midPointBtw(p1, p2) {
return {
x: p1.x + (p2.x - p1.x) / 2,
y: p1.y + (p2.y - p1.y) / 2
};
}
var el = document.getElementById('c');
var ctx = el.getContext('2d');
ctx.lineWidth = 10;
ctx.lineJoin = ctx.lineCap = 'round';
var isDrawing, points = [ ];
var snapshot;
el.onmousedown = function(e) {
snapshot = ctx.getImageData(0, 0, el.width, el.height);
isDrawing = true;
points.push({ x: e.clientX, y: e.clientY });
};
el.onmousemove = function(e) {
if (!isDrawing) return;
points.push({ x: e.clientX, y: e.clientY });
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.putImageData(snapshot, 0, 0);
var p1 = points[0];
var p2 = points[1];
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
console.log(points);
for (var i = 1, len = points.length; i < len; i++) {
// we pick the point between pi+1 & pi+2 as the
// end point and p1 as our control point
var midPoint = midPointBtw(p1, p2);
ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
p1 = points[i];
p2 = points[i+1];
}
// Draw last line as a straight line while
// we wait for the next point to be able to calculate
// the bezier control point
ctx.lineTo(p1.x, p1.y);
ctx.stroke();
};
el.onmouseup = function() {
isDrawing = false;
points.length = 0;
};
canvas { border: 1px solid #ccc }
<canvas id="c" width="500" height="300"></canvas>

Related

How to draw parallel rectangles with rubber-band in canvas

I am trying to draw a group of shapes using canvas.
I have referenced below SO threads:
Draw a parallel line
How to draw parallel line using three.js?
but not able to figure out how to calculate points for the rectangles parallel in as we stretch the line.
Any reference for stretching shapes with canvas is appreciated.
//Canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//Variables
var canvasx = $(canvas).offset().left;
var canvasy = $(canvas).offset().top;
var last_mousex = last_mousey = 0;
var mousex = mousey = 0;
var mousedown = false;
// grid parameters
var gridSpacing = 20; // pixels
var gridWidth = 1;
//var gridColor = "#f1f1f1";
var gridColor = "lightgray";
/** */
var originX = 0;
/** */
var originY = 0;
drawGrid();
//Mousedown
$(canvas).on('mousedown', function(e) {
last_mousex = parseInt(e.clientX-canvasx);
last_mousey = parseInt(e.clientY-canvasy);
mousedown = true;
});
//Mouseup
$(canvas).on('mouseup', function(e) {
mousedown = false;
});
//Mousemove
$(canvas).on('mousemove', function(e) {
mousex = parseInt(e.clientX-canvasx);
mousey = parseInt(e.clientY-canvasy);
if(mousedown) {
ctx.clearRect(0,0,canvas.width,canvas.height); //clear canvas
drawGrid();
ctx.setLineDash([5, 15]);
ctx.beginPath();
ctx.moveTo(last_mousex,last_mousey);
ctx.lineTo(mousex,mousey);
//ctx.lineTo(mousex,mousey);
ctx.strokeStyle = 'blue';
ctx.lineDashOffset = 2;
ctx.lineWidth = 5;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
startx = last_mousex;
starty = last_mousey;
drawPolygon([last_mousex, mousex, mousex, last_mousex, last_mousex],
[last_mousey-10, mousey-10, mousey-60, last_mousey-60],true, 'gray', false, 'black', 2);
drawPolygon([last_mousex, mousex, mousex, last_mousex, last_mousex],
[last_mousey+10, mousey+10, mousey+60, last_mousey+60],true, 'gray', false, 'black', 2);
}
//Output
$('#output').html('current: '+mousex+', '+mousey+'<br/>last: '+last_mousex+', '+last_mousey+'<br/>mousedown: '+mousedown);
});
/** */
function drawLine(startX, startY, endX, endY, width, color) {
// width is an integer
// color is a hex string, i.e. #ff0000
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.stroke();
}
function drawPolygon(xArr, yArr, fill, fillColor, stroke, strokeColor, strokeWidth) {
// fillColor is a hex string, i.e. #ff0000
fill = fill || false;
stroke = stroke || false;
ctx.beginPath();
ctx.moveTo(xArr[0], yArr[0]);
for (var i = 1; i < xArr.length; i++) {
ctx.lineTo(xArr[i], yArr[i]);
}
ctx.closePath();
if (fill) {
ctx.fillStyle = fillColor;
ctx.fill();
}
if (stroke) {
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
//console.log(xArr);
//console.log(yArr);
}
/** returns n where -gridSize/2 < n <= gridSize/2 */
function calculateGridOffset(n) {
if (n >= 0) {
return (n + gridSpacing / 2.0) % gridSpacing - gridSpacing / 2.0;
} else {
return (n - gridSpacing / 2.0) % gridSpacing + gridSpacing / 2.0;
}
}
/** */
function drawGrid() {
var offsetX = calculateGridOffset(-originX);
var offsetY = calculateGridOffset(-originY);
var width = canvas.width;
var height = canvas.height;
for (var x = 0; x <= (width / gridSpacing); x++) {
drawLine(gridSpacing * x + offsetX, 0, gridSpacing * x + offsetX, height, gridWidth, gridColor);
}
for (var y = 0; y <= (height / gridSpacing); y++) {
drawLine(0, gridSpacing * y + offsetY, width, gridSpacing * y + offsetY, gridWidth, gridColor);
}
}
canvas {
cursor: crosshair;
border: 1px solid #000000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="canvas" width="800" height="500"></canvas>
<div id="output"></div>
I'm guessing this is what you wanted.
Instead of trying to manually draw your line rotated, instead, move the origin of the canvas to the start of the line,
// save the canvas state
ctx.save();
// move origin to start of line
ctx.translate(last_mousex, last_mousey);
then rotate the origin so it points toward the end of the line in the positive X direction
// compute direction of line from start to end
const dx = mousex - last_mousex;
const dy = mousey - last_mousey;
const angle = Math.atan2(dy, dx);
// rotate to point to end of line
ctx.rotate(angle);
then compute the length of the line from the start to the end
// compute length of line
const length = Math.sqrt(dx * dx + dy * dy);
and just draw an arrow in the positive x direction of that length
ctx.setLineDash([5, 15]);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(length, 0);
ctx.strokeStyle = 'blue';
ctx.lineDashOffset = 2;
ctx.lineWidth = 5;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
drawPolygon([0, length, length, 0, 0],
[-10, -10, -60, -60],true, 'gray', false, 'black', 2);
drawPolygon([0, length, length, 0, 0],
[+10, +10, +60, +60],true, 'gray', false, 'black', 2);
// restore the canvas state
ctx.restore();
while we're at it your code for calculating the mouse position didn't work if the page is scrolled. This will get the mouse position relative to the pixels in the canvas.
const rect = canvas.getBoundingClientRect();
mousex = (e.clientX - rect.left) * canvas.width / canvas.clientWidth;
mousey = (e.clientY - rect.top ) * canvas.height / canvas.clientHeight;
//Canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//Variables
var last_mousex = last_mousey = 0;
var mousex = mousey = 0;
var mousedown = false;
// grid parameters
var gridSpacing = 20; // pixels
var gridWidth = 1;
//var gridColor = "#f1f1f1";
var gridColor = "lightgray";
/** */
var originX = 0;
/** */
var originY = 0;
drawGrid();
//Mousedown
$(canvas).on('mousedown', function(e) {
const rect = canvas.getBoundingClientRect();
last_mousex = (e.clientX - rect.left) * canvas.width / canvas.clientWidth;
last_mousey = (e.clientY - rect.top ) * canvas.height / canvas.clientHeight;
mousedown = true;
});
//Mouseup
$(canvas).on('mouseup', function(e) {
mousedown = false;
});
//Mousemove
$(canvas).on('mousemove', function(e) {
const rect = canvas.getBoundingClientRect();
mousex = (e.clientX - rect.left) * canvas.width / canvas.clientWidth;
mousey = (e.clientY - rect.top ) * canvas.height / canvas.clientHeight;
if(mousedown) {
ctx.clearRect(0,0,canvas.width,canvas.height); //clear canvas
drawGrid();
// save the canvas state
ctx.save();
// move origin to start of line
ctx.translate(last_mousex, last_mousey);
// compute direction of line from start to end
const dx = mousex - last_mousex;
const dy = mousey - last_mousey;
const angle = Math.atan2(dy, dx);
// rotate to point to end of line
ctx.rotate(angle);
// compute length of line
const length = Math.sqrt(dx * dx + dy * dy);
ctx.setLineDash([5, 15]);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(length, 0);
ctx.strokeStyle = 'blue';
ctx.lineDashOffset = 2;
ctx.lineWidth = 5;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
drawPolygon([0, length, length, 0, 0],
[-10, -10, -60, -60],true, 'gray', false, 'black', 2);
drawPolygon([0, length, length, 0, 0],
[+10, +10, +60, +60],true, 'gray', false, 'black', 2);
// restore the canvas state
ctx.restore();
}
//Output
$('#output').html('current: '+mousex+', '+mousey+'<br/>last: '+last_mousex+', '+last_mousey+'<br/>mousedown: '+mousedown);
});
/** */
function drawLine(startX, startY, endX, endY, width, color) {
// width is an integer
// color is a hex string, i.e. #ff0000
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.stroke();
}
function drawPolygon(xArr, yArr, fill, fillColor, stroke, strokeColor, strokeWidth) {
// fillColor is a hex string, i.e. #ff0000
fill = fill || false;
stroke = stroke || false;
ctx.beginPath();
ctx.moveTo(xArr[0], yArr[0]);
for (var i = 1; i < xArr.length; i++) {
ctx.lineTo(xArr[i], yArr[i]);
}
ctx.closePath();
if (fill) {
ctx.fillStyle = fillColor;
ctx.fill();
}
if (stroke) {
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
//console.log(xArr);
//console.log(yArr);
}
/** returns n where -gridSize/2 < n <= gridSize/2 */
function calculateGridOffset(n) {
if (n >= 0) {
return (n + gridSpacing / 2.0) % gridSpacing - gridSpacing / 2.0;
} else {
return (n - gridSpacing / 2.0) % gridSpacing + gridSpacing / 2.0;
}
}
/** */
function drawGrid() {
var offsetX = calculateGridOffset(-originX);
var offsetY = calculateGridOffset(-originY);
var width = canvas.width;
var height = canvas.height;
for (var x = 0; x <= (width / gridSpacing); x++) {
drawLine(gridSpacing * x + offsetX, 0, gridSpacing * x + offsetX, height, gridWidth, gridColor);
}
for (var y = 0; y <= (height / gridSpacing); y++) {
drawLine(0, gridSpacing * y + offsetY, width, gridSpacing * y + offsetY, gridWidth, gridColor);
}
}
canvas {
cursor: crosshair;
border: 1px solid #000000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="canvas" width="800" height="500"></canvas>
<div id="output"></div>

How to draw canvas trailing line with opacity

I'm attempting to draw the rotating line in this canvas animation with trailing opacity but it's not working. I've seen this effect with rectangles and arcs but never with a line, so I'm not sure what I need to add.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
} else {
timer = 0;
}
requestAnimationFrame(sonar);
}
}
sonar();
jsbin example
Here are two ways to do this: with a gradient and by adding translucent lines.
Sidenote, you should try and only redraw what you need to redraw. I separated the canvases and put one on top of the other so that we don't redraw the grid all the time.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function trail() {
var canvas = document.getElementById('trail');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, innerWidth, innerHeight);
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.arc(cx,cy,cy,radians(timer-30),radians(timer));
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
var gradient = ctx.createLinearGradient(
cx+cy*Math.cos(radians(timer)), cy+cy*Math.sin(radians(timer)),
cx+cy*0.9*Math.cos(radians(timer-30)), cy+cy*0.9*Math.sin(radians(timer-30)));
gradient.addColorStop(0,'limegreen');
gradient.addColorStop(1,'transparent');
ctx.strokeStyle='transparent';
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
var fade = 10;
for(var i =0;i<fade;i++)
{
ctx.moveTo(cx, cy);
ctx.lineTo(cx+cy*Math.cos(radians(180+timer-i*1.3)),cy+cy*Math.sin(radians(180+timer-i*1.3)));
ctx.strokeStyle ="rgba(50,205,50,0.1)";
ctx.lineWidth=5;
ctx.closePath();
ctx.stroke();
}
} else {
timer = 0;
}
requestAnimationFrame(trail);
}
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
}
}
sonar();
trail();
canvas{
position: absolute;
}
<canvas id=sonar></canvas>
<canvas id=trail></canvas>
The problem is that to get this effect, you need to draw a triangle with a gradient along an arc, and you can't do that in a canvas. Gradients must be linear or radial.
The other option is to have an inner loop run each time you want to draw the sweeper, and go backwards from your sweeper line, drawing with slightly less opacity each time. But lets say you want your sweep to cover 15 degrees--obviously, if you have a 100% opacity line at d and a 5% opacity line at d - 15, that doesn't do the trick. So start filling in more lines, and more lines...you will have to draw so many lines to make it seem filled your performance would probably suffer.
My suggestion--you shouldn't have to redraw that on every frame. I would just make a PNG that looks like you want it to, and then place it and just rotate it around the center on each frame. No need to redraw it all the time then. That will be much faster than drawing a bunch of lines.
Canvas stack trails.
Below is a quick demo of how to use a stack of canvases to create a trailing effect.
You have a normal on screen canvas (this FX will not effect it) and then a stack of canvases for the trail FX. Each frame you move to the next canvas in the stack, first slightly clearing it then drawing to it what you want to trail. Then you render that canvas and the one just above it to the canvas.
A point to keep in mind is that the trails can also have a hugh range of FX, like blurring (just render each frame stack on itself slightly offset each time you render to it), zoom in and out trails. Trails on top or trails under. You can change the trail distance and much more.
It is overkill but over kill is fun.
The slider above the demo controls the trail length. Also the code need babel because I dont have time to write it for ES5.
Top slider is trail amount.One under that is trail distance. Trail dist does not transition well. Sorry about that.
//==============================================================================
// helper function
function $(query,q1){
if(q1 !== undefined){
if(typeof query === "string"){
var e = document.createElement(query);
if(typeof q1 !== "string"){
for(var i in q1){
e[i] = q1[i];
}
}else{
e.id = q1;
}
return e;
}
return [...query.querySelectorAll(q1)];
}
return [...document.querySelectorAll(query)];
}
function $$(element,e1){
if(e1 !== undefined){
if(typeof element === "string"){
$(element)[0].appendChild(e1);
return e1;
}
element.appendChild(e1);
return e1;
}
document.body.appendChild(element);
return element;
}
function $E(element,types,listener){
if(typeof types === "string"){
types = types.split(",");
}
element = $(element)[0];
types.forEach(t=>{
element.addEventListener(t,listener)
});
return element;
}
function R(I){
if(I === undefined){
return Math.random();
}
return Math.floor(Math.random()*I);
}
//==============================================================================
//==============================================================================
// answer code
// canvas size
const size = 512;
const trailDist = 10; // There is this many canvases so be careful
var trailDistCurrent = 10; // distance between trails
var clearAll = false;
// create a range slider for trail fade
$$($("input",{type:"range",width : size, min:0, max:100, step:0.1, value:50, id:"trail-amount",title:"Trail amount"}));
$("#trail-amount")[0].style.width = size + "px";
$E("#trail-amount","change,mousemove",function(e){fadeAmount = Math.pow(this.value / 100,2);});
// create a range slider trail distance
$$($("input",{type:"range",width : size, min:2, max:trailDist , step:1, value:trailDist , id:"trail-dist",title:"Trail seperation"}));
$("#trail-dist")[0].style.width = size + "px";
$E("#trail-dist","change,mousemove", function(e){
if(this.value !== trailDistCurrent){
trailDistCurrent= this.value;
clearAll = true;
}
});
$$($("br","")) // put canvas under the slider
// Main canvas
var canvas;
$$(canvas = $("canvas",{width:size,height:size})); // Not jquery. Just creates a canvas
// and adds canvas to the document
var ctx = canvas.getContext("2d");
// Trailing canvas
var trailCanvases=[];
var i =0; // create trail canvas
while(i++ < trailDist){trailCanvases.push($("canvas",{width:size,height:size}));}
var ctxT = trailCanvases.map(c=>c.getContext("2d")); // get context
var topCanvas = 0;
var fadeAmount = 0.5;
// Draw a shape
function drawShape(ctx,shape){
ctx.lineWidth = shape.width;
ctx.lineJoin = "round";
ctx.strokeStyle = shape.color;
ctx.setTransform(shape.scale,0,0,shape.scale,shape.x,shape.y);
ctx.rotate(shape.rot);
ctx.beginPath();
var i = 0;
ctx.moveTo(shape.shape[i++],shape.shape[i++]);
while(i < shape.shape.length){
ctx.lineTo(shape.shape[i++],shape.shape[i++]);
}
ctx.stroke();
}
// Create some random shapes
var shapes = (function(){
function createRandomShape(){
var s = [];
var len = Math.floor(Math.random()*5 +4)*2;
while(len--){
s[s.length] = (R() + R()) * 20 * (R() < 0.5 ? -1 : 1);
}
return s;
}
var ss = [];
var i = 10;
while(i--){
ss[ss.length] = createRandomShape();
}
ss[ss.length] = [0,0,300,0]; // create single line
return ss;
})();
// Create some random poits to move the shapes
var points = (function(){
function point(){
return {
color : "hsl("+R(360)+",100%,50%)",
shape : shapes[R(shapes.length)],
width : R(4)+1,
x : R(size),
y : R(size),
scaleMax : R()*0.2 + 1,
scale : 1,
s : 0,
rot : R()*Math.PI * 2,
dr : R()*0.2 -0.1,
dx : R()*2 - 1,
dy : R()*2 - 1,
ds : R() *0.02 + 0.01,
}
}
var line = shapes.pop();
var ss = [];
var i = 5;
while(i--){
ss[ss.length] = point();
}
var s = ss.pop();
s.color = "#0F0";
s.x = s.y = size /2;
s.dx = s.dy = s.ds = 0;
s.scaleMax = 0.5;
s.dr = 0.02;
s.shape = line;
s.width = 6;
ss.push(s);
return ss;
})();
var frameCount = 0; // used to do increamental fades for long trails
function update(){
// to fix the trail distance problem when fade is low and distance high
if(clearAll){
ctxT.forEach(c=>{
c.setTransform(1,0,0,1,0,0);
c.clearRect(0,0,size,size);
});
clearAll = false;
}
frameCount += 1;
// get the next canvas that the shapes are drawn to.
topCanvas += 1;
topCanvas %= trailDistCurrent;
var ctxTop = ctxT[topCanvas];
// clear the main canvas
ctx.setTransform(1,0,0,1,0,0); // reset transforms
// Fade the trail canvas
ctxTop.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,size,size); // clear main canvas
// slowly blendout trailing layer
if(fadeAmount < 0.1){ // fading much less than this leaves perminant trails
// so at low levels just reduce how often the fade is done
if(((Math.floor(frameCount/trailDistCurrent)+topCanvas) % Math.ceil(1 / (fadeAmount * 10))) === 0 ){
ctxTop.globalAlpha = 0.1;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
}else{
ctxTop.globalAlpha = fadeAmount;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
ctxTop.globalCompositeOperation = "source-over";
ctxTop.globalAlpha = 1;
// draw shapes
for(var i = 0; i < points.length; i ++){
var p = points[i];
p.x += p.dx; // move the point
p.y += p.dy;
p.rot += p.dr;
p.s += p.ds;
p.dr += Math.sin(p.s) * 0.001;
p.scale = Math.sin(p.s) * p.scaleMax+1;
p.x = ((p.x % size) + size) % size;
p.y = ((p.y % size) + size) % size;
drawShape(ctxTop,p); // draw trailing layer (middle)
}
// draw the trail the most distance from the current position
ctx.drawImage(trailCanvases[(topCanvas + 1)%trailDistCurrent],0,0);
// do it all again.
requestAnimationFrame(update);
}
update();

Making mouse position (0, 0) the center of the canvas

So I've recently been messing around with html5 canvas and trying to figure out how to make a particle system. While it does work I want to make vx = mouse coordinates(specifically x) but they start from the center.
What I mean by this is basically if the cursor is in the center of the canvas then vx would be equal to 0 and if the cursor is to the right from the center of the canvas it have positive mouse coordinates (and if the cursor is to the left from the center of canvas it have negative mouse coordinates).
Once that is accomplished I would just do p.speed += p.vx
Here is my javascript:
window.onload = function(){
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var W = window.innerWidth, H = window.innerHeight;
canvas.width = W;
canvas.height = H;
var particles = [];
var particle_count = 100;
for(var i = 0; i < particle_count; i++)
{
particles.push(new particle());
}
function particle()
{
this.vx = -1 + Math.random() * 2;
this.speed = {x: 0, y: -15+Math.random()*10};
this.location = {x: W/2, y: H/2};
this.radius = 5+Math.random()*10;
this.life = 20+Math.random()*10;
this.remaining_life = this.life;
this.r = Math.round(Math.random()*255);
this.g = Math.round(Math.random()*55);
this.b = Math.round(Math.random()*5);
}
function draw()
{
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "black";
ctx.fillRect(0, 0, W, H);
ctx.globalCompositeOperation = "lighter";
for(var i = 0; i < particles.length; i++)
{
var p = particles[i];
ctx.beginPath();
p.opacity = Math.round(p.remaining_life/p.life*100)/100
var gradient = ctx.createRadialGradient(
p.location.x, p.location.y, 0, p.location.x, p.location.y, p.radius);
gradient.addColorStop(0, "rgba("+p.r+", "+p.g+", "+p.b+", "+p.opacity+")");
gradient.addColorStop(0.5, "rgba("+p.r+", "+p.g+", "+p.b+", "+p.opacity+")");
gradient.addColorStop(1, "rgba("+p.r+", "+p.g+", "+p.b+", 0)");
ctx.fillStyle = gradient;
ctx.arc(p.location.x, p.location.y, p.radius, Math.PI*2, false);
ctx.fill();
p.remaining_life--;
p.radius--;
p.location.x += p.speed.x += p.vx;
p.location.y += p.speed.y;
if(p.remaining_life < 0 || p.radius < 0) {
particles[i] = new particle();
}
}
}
setInterval(draw, 33); }
Here is my codepen.
You need to translate context to move origin to center.
You also need to reverse the translation on the mouse coordinates as they will still be relative to the upper-left corner (assuming the coordinates are corrected to be relative to canvas).
Example:
var cx = canvas.width * 0.5;
var cy = canvas.height * 0.5;
ctx.translate(cx, cy); // translate globally once
For each mouse coordinate compensate with the translated position:
var pos = getMousePosition(evt); // see below
var x = pos.x - cx;
var y = pos.y - cy;
To adjust mouse position:
function getMousePosition(evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}

filling circle with hexagons

I'm trying to find a way to put as much hexagons in a circle as possible. So far the best result I have obtained is by generating hexagons from the center outward in a circular shape.
But I think my calculation to get the maximum hexagon circles is wrong, especially the part where I use the Math.ceil() and Math.Floor functions to round down/up some values.
When using Math.ceil(), hexagons are sometimes overlapping the circle.
When using Math.floor() on the other hand , it sometimes leaves too much space between the last circle of hexagons and the circle's border.
var c_el = document.getElementById("myCanvas");
var ctx = c_el.getContext("2d");
var canvas_width = c_el.clientWidth;
var canvas_height = c_el.clientHeight;
var PI=Math.PI;
var PI2=PI*2;
var hexCircle = {
r: 110, /// radius
pos: {
x: (canvas_width / 2),
y: (canvas_height / 2)
}
};
var hexagon = {
r: 20,
pos:{
x: 0,
y: 0
},
space: 1
};
drawHexCircle( hexCircle, hexagon );
function drawHexCircle(hc, hex ) {
drawCircle(hc);
var hcr = Math.ceil( Math.sqrt(3) * (hc.r / 2) );
var hr = Math.ceil( ( Math.sqrt(3) * (hex.r / 2) ) ) + hexagon.space; // hexRadius
var circles = Math.ceil( ( hcr / hr ) / 2 );
drawHex( hc.pos.x , hc.pos.y, hex.r ); //center hex ///
for (var i = 1; i<=circles; i++) {
for (var j = 0; j<6; j++) {
var currentX = hc.pos.x+Math.cos(j*PI2/6)*hr*2*i;
var currentY = hc.pos.y+Math.sin(j*PI2/6)*hr*2*i;
drawHex( currentX,currentY, hex.r );
for (var k = 1; k<i; k++) {
var newX = currentX + Math.cos((j*PI2/6+PI2/3))*hr*2*k;
var newY = currentY + Math.sin((j*PI2/6+PI2/3))*hr*2*k;
drawHex( newX,newY, hex.r );
}
}
}
}
function drawHex(x, y, r){
ctx.beginPath();
ctx.moveTo(x,y-r);
for (var i = 0; i<=6; i++) {
ctx.lineTo(x+Math.cos((i*PI2/6-PI2/4))*r,y+Math.sin((i*PI2/6-PI2/4))*r);
}
ctx.closePath();
ctx.stroke();
}
function drawCircle( circle ){
ctx.beginPath();
ctx.arc(circle.pos.x, circle.pos.y, circle.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
}
<canvas id="myCanvas" width="350" height="350" style="border:1px solid #d3d3d3;">
If all the points on the hexagon are within the circle, the hexagon is within the circle. I don't think there's a simpler way than doing the distance calculation.
I'm not sure how to select the optimal fill point, (but here's a js snippet proving that the middle isn't always it). It's possible that when you say "hexagon circle" you mean hexagon made out of hexagons, in which case the snippet proves nothing :)
I made the hexagon sides 2/11ths the radius of the circle and spaced them by 5% the side length.
var hex = {x:0, y:0, r:10};
var circle = {x:100, y:100, r:100};
var spacing = 1.05;
var SQRT_3 = Math.sqrt(3);
var hexagon_offsets = [
{x: 1/2, y: -SQRT_3 / 2},
{x: 1, y: 0},
{x: 1/2, y: SQRT_3 / 2},
{x: -1/2, y: SQRT_3 / 2},
{x: -1, y: 0},
{x: -1/2, y: -SQRT_3 / 2}
];
var bs = document.body.style;
var ds = document.documentElement.style;
bs.height = bs.width = ds.height = ds.width = "100%";
bs.border = bs.margin = bs.padding = 0;
var c = document.createElement("canvas");
c.style.display = "block";
c.addEventListener("mousemove", follow, false);
document.body.appendChild(c);
var ctx = c.getContext("2d");
window.addEventListener("resize", redraw);
redraw();
function follow(e) {
hex.x = e.clientX;
hex.y = e.clientY;
redraw();
}
function drawCircle() {
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.r, 0, 2 * Math.PI, true);
ctx.closePath();
ctx.stroke();
}
function is_in_circle(p) {
return Math.pow(p.x - circle.x, 2) + Math.pow(p.y - circle.y, 2) < Math.pow(circle.r, 2);
}
function drawLine(a, b) {
var within = is_in_circle(a) && is_in_circle(b);
ctx.strokeStyle = within ? "green": "red";
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.closePath();
ctx.stroke();
return within;
}
function drawShape(shape) {
var within = true;
for (var i = 0; i < shape.length; i++) {
within = drawLine(shape[i % shape.length], shape[(i + 1) % shape.length]) && within;
}
if (!within) return false;
ctx.fillStyle = "green";
ctx.beginPath();
ctx.moveTo(shape[0].x, shape[0].y);
for (var i = 1; i <= shape.length; i++) {
ctx.lineTo(shape[i % shape.length].x, shape[i % shape.length].y);
}
ctx.closePath();
ctx.fill();
return true;
}
function calculate_hexagon(x, y, r) {
return hexagon_offsets.map(function (offset) {
return {x: x + r * offset.x, y: y + r * offset.y};
})
}
function drawHexGrid() {
var hex_count = 0;
var grid_space = calculate_hexagon(0, 0, hex.r * spacing);
var y = hex.y;
var x = hex.x;
while (y > 0) {
y += grid_space[0].y * 3;
x += grid_space[0].x * 3;
}
while (y < c.height) {
x %= grid_space[1].x * 3;
while (x < c.width) {
var hexagon = calculate_hexagon(x, y, hex.r);
if (drawShape(hexagon)) hex_count++;
x += 3 * grid_space[1].x;
}
y += grid_space[3].y;
x += grid_space[3].x;
x += 2 * grid_space[1].x;
}
return hex_count;
}
function redraw() {
c.width = window.innerWidth;
c.height = window.innerHeight;
circle.x = c.width / 2;
circle.y = c.height / 2;
circle.r = Math.min(circle.x, circle.y) * 0.9;
hex.r = circle.r * (20 / 110);
ctx.clearRect(0, 0, c.width, c.height);
var hex_count = drawHexGrid();
drawCircle();
ctx.fillStyle = "rgb(0, 0, 50)";
ctx.font = "40px serif";
ctx.fillText(hex_count + " hexes within circle", 20, 40);
}

Change a straight line into a curved line when length is overtaken

I want to display several legs into a rectangular form in canvas.
Based on an array which groups the miles of my legs, I've made the algo to represent them proportionately on a canvas given.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var width = c.width;
var somme = 0;
var prevValue = 0;
var recapProp = [];
function drawArrow(fromx, fromy, tox, toy){
//variables to be used when creating the arrow
var headlen = 5;
var angle = Math.atan2(toy-fromy,tox-fromx);
//starting path of the arrow from the start square to the end square and drawing the stroke
ctx.beginPath();
ctx.moveTo(fromx, fromy);
ctx.lineTo(tox, toy);
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
//starting a new path from the head of the arrow to one of the sides of the point
ctx.beginPath();
ctx.moveTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
//path from the side point of the arrow, to the other side point
ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));
//path from the side point back to the tip of the arrow, and then again to the opposite side point
ctx.lineTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
//draws the paths created above
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
function drawCircle(centerXFrom, centerYFrom){
var radius = 3;
ctx.beginPath();
ctx.arc(centerXFrom, centerYFrom, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = 'green';
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = '#003300';
ctx.stroke();
ctx.beginPath();
}
function sumTab(tabTT){
for (var i = 0; i < tabTT.length; i++){
somme += tabTT[i];
}
return somme;
}
function findProportion(tabTT){
var tailleMax = tabTT.length;
sumTab(tabTT);
for(var i = 0; i < tabTT.length; i++){
var percentLeg = (tabTT[i]/somme)*100;
var tailleLeg = ((width- 20)*percentLeg)/100 ;
recapProp.push(tailleLeg);
}
for(var i = 0; i <= recapProp.length; ++i){
console.log(prevValue);
drawCircle(prevValue +5, 5);
drawArrow(prevValue + 7, 5, prevValue+recapProp[i],5);
prevValue += recapProp[i];
}
}
var tabTT = [0,5,1,8,2];
findProportion(tabTT);
<canvas id="myCanvas" height="200" width="500"></canvas>
Then, I want to display then in a rectangular form, to make a loop (below is not rectangular, but it helps you to understand) :
I've tried to manipulate quadracticCurveTo() but that's not really conclusive..
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
function drawArrow(fromx, fromy, tox, toy, radius){
//variables to be used when creating the arrow
var headlen = 5;
var r = fromx + tox;
var b = fromy + toy;
var angle = Math.atan2(r,b);
//starting path of the arrow from the start square to the end square and drawing the stroke
ctx.beginPath();
ctx.moveTo(fromx+radius, fromy);
ctx.lineTo(r-radius, fromy);
ctx.quadraticCurveTo(r, fromy, r, fromy+radius);
ctx.lineWidth = "2";
ctx.strokeStyle = '#ff0000';
ctx.stroke();
//starting a new path from the head of the arrow to one of the sides of the point
ctx.beginPath();
ctx.moveTo(r, b);
ctx.lineTo(r-headlen*Math.cos(angle-Math.PI/7),b-headlen*Math.sin(angle-Math.PI/7));
//path from the side point of the arrow, to the other side point
ctx.lineTo(r-headlen*Math.cos(angle+Math.PI/7),b-headlen*Math.sin(angle+Math.PI/7));
//path from the side point back to the tip of the arrow, and then again to the opposite side point
ctx.lineTo(r, b);
ctx.lineTo(r-headlen*Math.cos(angle-Math.PI/7),b-headlen*Math.sin(angle-Math.PI/7));
//draws the paths created above
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
drawArrow(50,5, 80,25, 25);
<canvas id="myCanvas" height="2000" width="2000"></canvas>
Finally, I've created the snippet I will need when I'll know how to curve my lines and keep its length !. I've calculated the perimeter of my canvas surface in order to re-calculate the proportions of my legs.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var width = c.width;
var height = c.height;
var perimetre = (width*2 + height*2);
var up = 0;
var right = 0;
var left = 0;
var bot = 0;
var somme = 0;
var prevValue = 0;
var recapProp = [];
/**********************************/
/*****<<Straight>> Arrows*********/
/********************************/
function drawArrow(fromx, fromy, tox, toy){
var headlen = 5;
var angle = Math.atan2(toy-fromy,tox-fromx);
ctx.beginPath();
ctx.moveTo(fromx, fromy);
ctx.lineTo(tox, toy);
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7));
ctx.lineTo(tox, toy);
ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7));
ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = "blue";
ctx.fill();
}
/**********************************/
/************Points***************/
/********************************/
function drawCircle(centerXFrom, centerYFrom){
var radius = 3;
ctx.beginPath();
ctx.arc(centerXFrom, centerYFrom, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = 'green';
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = '#003300';
ctx.stroke();
ctx.beginPath();
}
function sumTab(tabTT){
for (var i = 0; i < tabTT.length; i++){
somme += tabTT[i];
}
return somme;
}
/***************************************************/
/************Get length for each leg***************/
/*************************************************/
function findProportion(tabTT){
var tailleMax = tabTT.length;
sumTab(tabTT);
for(var i = 0; i < tabTT.length; i++){
var percentLeg = (tabTT[i]/somme)*100;
var tailleLeg = ((perimetre - 20)*percentLeg)/100 ;
recapProp.push(tailleLeg);
}
/* For each leg I draw the circle and the arrow, due to the length calculated previously. If the length > the width of the canva, the arrow has to be curved */
for(var i = 0; i <= recapProp.length; ++i){
if(prevValue > width && top == 1){
drawCircle(prevValue +5, 5);
drawArrowBot(prevValue + 7, 5, prevValue+recapProp[i],5);
right = 1;
top = 0;
}
else if(prevValue > height && right == 1){
drawCircle(prevValue +5, 5);
drawArrowLeft(prevValue + 7, 5, prevValue+recapProp[i],5);
bot = 1;
right = 0;
}
else if (prevValue > width && bot == 1){
drawCircle(prevValue +5, 5);
drawArrowTop(prevValue + 7, 5, prevValue+recapProp[i],5);
bot = 0;
left = 0;
}
else {
drawCircle(prevValue +5, 5);
drawArrow(prevValue + 7, 5, prevValue+recapProp[i],5);
}
prevValue += recapProp[i];
}
}
var tabTT = [0,5,1,8,2];
findProportion(tabTT);
<canvas id="myCanvas" height="200" width="500" style="border:1px solid #000000;"></canvas>
I've commented all my code in order to help you understand the logic and what I want.
So, is it possible to curve the lines in a generic way?
I would probably do something like this:
Define a holding array with number of entries based on a resolution
Map the lines into that array setting 1's very there would be a line range, 0's for the gap.
Define a target shape such as an oval (can be any shape really!) which consists of equally many parts as the array resolution. Store each part and it's coordinate in an array (same length as the line array).
Morph each part using interpolation between the shape array and line array
Now you can produce the lines into almost any shape and form you desire.
Tip: you can of course skip one shape by mapping it directly the first time.
Tip 2: the shapes can be defined in normalized coordinates which makes it easier to translate and scale them.
Example
Here we define a rounded square and circle, then map the lines onto either, we can morph between the shapes to find a combination we like and use that (note: as the square in this example starts in "upper-right" corner and not where the circle has it's 0° there will be a small rotation as well, this can be dealt with separately as an exercise).
The rounded square could be a a bunny for that matter (for a more "tight" rounded square you can use cubic Bezier instead of quadratic as here). The key point is that the shape can be defined independently of the lines themselves. This may be overkill, but it's not so complicated and it's versatile, ie. generic.
See this answer for one way to add an arrow to the lines.
var ctx = document.querySelector("canvas").getContext("2d"),
resolution = 2000,
raster = new Uint8Array(resolution), // line raster array
shape = new Float32Array(resolution * 2), // target shape array (x2 for x/y)
shape2 = new Float32Array(resolution * 2),// target shape array 2
lines = [100, 70, 180, 35], // lines, lengths only
tLen = 0, // total length of lines + gaps
gap = 20, // gap in pixels
gapNorm, // normalized gap value for mapping
p = 0, // position in lines array
radius = 100, // target circle radius
angleStep = Math.PI * 2 / resolution, // angle step to reach circle / res.
cx = 150, cy = 150, // circle center
interpolation = 0.5, // t for interpolation
i;
// get total length of lines + gaps so we can normalize
for(i = 0; i < lines.length; i++) tLen += lines[i];
tLen += (lines.length - 2) * gap;
gapNorm = gap / tLen * 0.5;
// convert line and gap ranges to "on" in the lines array
for(i = 0; i < lines.length; i++) {
var sx = p, // start position in lines array
ex = p + ((lines[i] / tLen) * resolution)|0; // end position in lines array (int)
// fill array
while(sx <= ex) raster[sx++] = 1;
// update arrqay pointer incl. gap
p = ex + ((gapNorm * resolution)|0);
}
// Create a circle target shape split into same amount of segments as lines array:
p = 0; // reset pointer for shape array
for(var angle = 0; angle < Math.PI*2; angle += angleStep) {
shape[p++] = cx + radius * Math.cos(angle);
shape[p++] = cy + radius * Math.sin(angle);
}
// create a rounded rectangle
p = i = 0;
var corners = [
{x1: 250, y1: 150, cx: 250, cy: 250, x2: 150, y2: 250}, // bottom-right
{x1: 150, y1: 250, cx: 50, cy: 250, x2: 50, y2: 150}, // bottom-left
{x1: 50, y1: 150, cx: 50, cy: 50, x2: 150, y2: 50}, // upper-left
{x1: 150, y1: 50, cx: 250, cy: 50, x2: 250, y2: 150} // upper-right
],
c, cres = resolution * 0.25;
while(c = corners[i++]) {
for(var t = 0; t < cres; t++) {
var pos = getQuadraticPoint(c.x1, c.y1, c.cx, c.cy, c.x2, c.y2, t / cres);
shape2[p++] = pos.x;
shape2[p++] = pos.y;
}
}
// now we can map the lines array onto our shape depending on the values
// interpolation. Make it a reusable function so we can regulate the "morph"
function map(raster, shape, shape2, t) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
for(var i = 0, x, y, x1, y1, x2, y2, prev = 0; i < resolution; i++) {
x1 = shape[i*2];
y1 = shape[i*2 + 1];
x2 = shape2[i*2];
y2 = shape2[i*2 + 1];
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
// do we have a change?
if (prev !== raster[i]) {
if (raster[i]) { // it's on, was off. create sub-path
ctx.moveTo(x, y);
}
else { // it's off, was on, render and reset path
ctx.stroke();
ctx.beginPath();
// create "arrow"
ctx.moveTo(x + 3, y);
ctx.arc(x, y, 3, 0, 6.28);
ctx.fill();
ctx.beginPath();
}
}
// add segment if on
else if (raster[i]) {
ctx.lineTo(x, y);
}
prev = raster[i];
}
}
ctx.fillStyle = "red";
map(raster, shape, shape2, interpolation);
document.querySelector("input").onchange = function() {
map(raster, shape, shape2, +this.value / 100);
};
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {
var t1 = (1 - t), // (1 - t)
t12 = t1 * t1, // (1 - t) ^ 2
t2 = t * t, // t ^ 2
t21tt = 2 * t1 * t; // 2(1-t)t
return {
x: t12 * z0x + t21tt * cx + t2 * z1x,
y: t12 * z0y + t21tt * cy + t2 * z1y
}
}
<script src="https://cdn.rawgit.com/epistemex/slider-feedback/master/sliderfeedback.min.js"></script>
<label>Interpolation: <input type="range" min=0 max=400 value=50></label><br>
<canvas width=400 height=400></canvas>
Calculate the middle control point that makes a quadratic Bezier curve become a specified length.
Given:
p0, p2: the QCurves starting and ending points.
length: the desired arc-length of the quadratic Bezier Curve.
You can calculate the control point that makes the QCurve's total arc-length equal length:
Calculate the midpoint between p0 & p2.
Calculate the angle of between p0 & p2.
Calculate a point (p1) perpendicular to that midpoint at a specified distance. This is a possible control point. The perpendicular angle is the calculated angle from step#2 minus 90 degrees.
Calculate the QCurve's arc-length using p0, p1 & p2 (calculatedLength).
You've got the right middle control point if calculatedLength equals the desired length.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
var $length=$('#length');
var PI2=Math.PI*2;
var radius=5+1; // 5==fill, 1=added stroke
var p0={x:50,y:100,color:'red'};
var p2={x:175,y:150,color:'gold'};
var p1={x:0,y:0,color:'green'};
var midpoint={x:0,y:0,color:'purple'};
var perpendicularPoint={x:0,y:0,color:'cyan'};
//var points=[p0,p1,p2];
//var draggingPoint=-1;
setQLength(p0,p2,150,1);
draw();
function draw(){
ctx.clearRect(0,0,cw,ch);
ctx.beginPath();
ctx.moveTo(p0.x,p0.y);
ctx.quadraticCurveTo(p1.x,p1.y,p2.x,p2.y);
ctx.strokeStyle='blue';
ctx.lineWidth=3;
ctx.stroke();
dot(p0);
dot(p1);
dot(p2);
dot(midpoint);
dot(perpendicularPoint)
$length.text('Curve length: '+parseInt(QCurveLength(p0,p1,p2)))
}
//
function dot(p){
ctx.beginPath();
ctx.arc(p.x,p.y,radius,0,PI2);
ctx.closePath();
ctx.fillStyle=p.color;
ctx.fill();
ctx.lineWidth=1;
ctx.strokeStyle='black';
ctx.stroke();
}
function setQLength(p0,p2,length,tolerance){
var dx=p2.x-p0.x;
var dy=p2.y-p0.y;
var alength=Math.sqrt(dx*dx+dy*dy);
// impossible to fit
if(alength>length){
alert('The points are too far apart to have length='+length);
return;
}
// fit
for(var distance=0;distance<200;distance++){
// calc the point perpendicular to midpoint at specified distance
var p=pointPerpendicularToMidpoint(p0,p2,distance);
p1.x=p.x;
p1.y=p.y;
// calc the result qCurve length
qlength=QCurveLength(p0,p1,p2);
// draw the curve
draw();
// break if qCurve's length is within tolerance
if(Math.abs(length-qlength)<tolerance){
break;
}
}
return(p1);
}
function pointPerpendicularToMidpoint(p0,p2,distance){
var dx=p2.x-p0.x;
var dy=p2.y-p0.y;
var perpAngle=Math.atan2(dy,dx)-Math.PI/2;
midpoint={ x:p0.x+dx*0.50, y:p0.y+dy*0.50, color:'purple' };
perpendicularPoint={
x: midpoint.x+distance*Math.cos(perpAngle),
y: midpoint.y+distance*Math.sin(perpAngle),
color:'cyan'
};
return(perpendicularPoint);
}
// Attribution: Mateusz Matczak
// http://www.malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
function QCurveLength(p0,p1,p2){
var a={x: p0.x-2*p1.x+p2.x, y: p0.y-2*p1.y+p2.y}
var b={x:2*p1.x-2*p0.x, y:2*p1.y-2*p0.y}
var A=4*(a.x*a.x+a.y*a.y);
var B=4*(a.x*b.x+a.y*b.y);
var C=b.x*b.x+b.y*b.y;
var Sabc=2*Math.sqrt(A+B+C);
var A2=Math.sqrt(A);
var A32=2*A*A2;
var C2=2*Math.sqrt(C);
var BA=B/A2;
if(A2==0 || BA+C2==0){
var dx=p2.x-p0.x;
var dy=p2.y-p0.y;
var length=Math.sqrt(dx*dx+dy*dy);
}else{
var length=(A32*Sabc+A2*B*(Sabc-C2)+(4*C*A-B*B)*Math.log((2*A2+BA+Sabc)/(BA+C2)))/(4*A32)
}
return(length);
};
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id=length>Curve length:</h4>
<h4>Red,Gold == start and end points<br>Purple == midpoint between start & end<br>Cyan == middle control point.</h4>
<canvas id="canvas" width=300 height=300></canvas>
var angle = 0;
draw = function() {
background(0, 0, 0);
for(var j = 0;j<20;j++){
fill(j*100,j*10,j);
var offset = 0;
for(var i =-27;i<20;i++){
var a = angle +offset;
var h = map(sin(a),-1,1,100,300);
ellipse(i*20+j*20,h,20,20);
offset+=10;
}
}
angle+=2;
};

Categories