Incorrectly drawing line to edge of ellipse given angle - javascript

Sorry for the confusing title, I don't know how to succinctly describe my question.
I'm drawing an ellipse on a canvas element using javascript and I'm trying to figure out how to detect if the mouse is clicked inside of the ellipse or not. The way I'm trying to do this is by comparing the distance from the center of the ellipse to the mouse to the radius of the ellipse at the same angle as the mouse click. Here's a terrible picture representing what I just said if it's still confusing:
Obviously this isn't working, otherwise I wouldn't be asking this, so below is a picture of the computed radius line (in red) and the mouse line (in blue). In this picture, the mouse has been clicked at a 45° angle to the center of the ellipse and I've calculated that the radius line is being drawn at about a 34.99° angle.
And below is the calculation code:
//This would be the blue line in the picture above
var mouseToCenterDistance = distanceTo(centerX, centerY, mouseX, mouseY);
var angle = Math.acos((mouseX - centerX) / mouseToCenterDistance);
var radiusPointX = (radiusX * Math.cos(angle)) + centerX;
var radiusPointY = (radiusY * Math.sin(-angle)) + centerY;
//This would be the red line in the picture above
var radius = distanceTo(centerX, centerY, radiusPointX, radiusPointY);
var clickedInside = mouseToCenterDistance <= radius;
I'm really not sure why this isn't working, I've been staring at this math forever and it seems correct. Is it correct and there's something about drawing on the canvas that's making it not work? Please help!

Ellipse line intercept
Finding the intercept includes solving if the point is inside.
If it is the ellipse draw via the 2D context the solution is as follows
// defines the ellipse
var cx = 100; // center
var cy = 100;
var r1 = 20; // radius 1
var r2 = 100; // radius 2
var ang = 1; // angle in radians
// rendered with
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke()
To find the point on the ellipse that intersects the line from the center to x,y. To solve I normalise the ellipse so that it is a circle (well the line is moved so that the ellipse is a circle in its coordinate space).
var x = 200;
var y = 200;
var ratio = r1 / r2; // need the ratio between the two radius
// get the vector from the ellipse center to end of line
var dx = x - cx;
var dy = y - cy;
// get the vector that will normalise the ellipse rotation
var vx = Math.cos(-ang);
var vy = Math.sin(-ang);
// use that vector to rotate the line
var ddx = dx * vx - dy * vy;
var ddy = (dx * vy + dy * vx) * ratio; // lengthen or shorten dy
// get the angle to the line in normalise circle space.
var c = Math.atan2(ddy,ddx);
// get the vector along the ellipse x axis
var eAx = Math.cos(ang);
var eAy = Math.sin(ang);
// get the intercept of the line and the normalised ellipse
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;
// rotate the intercept to the ellipse space
var ix = nx * eAx - ny * eAy
var iy = nx * eAy + ny * eAx
// cx,cy to ix ,iy is from the center to the ellipse circumference
The procedure can be optimised but for now that will solve the problem as presented.
Is point inside
Then to determine if the point is inside just compare the distances of the mouse and the intercept point.
var x = 200; // point to test
var y = 200;
// get the vector from the ellipse center to point to test
var dx = x - cx;
var dy = y - cy;
// get the vector that will normalise the ellipse rotation
var vx = Math.cos(ang);
var vy = Math.sin(ang);
// use that vector to rotate the line
var ddx = dx * vx + dy * vy;
var ddy = -dx * vy + dy * vx;
if( 1 >= (ddx * ddx) / (r1 * r1) + (ddy * ddy) / (r2 * r2)){
// point on circumference or inside ellipse
}
Example use of method.
function path(path){
ctx.beginPath();
var i = 0;
ctx.moveTo(path[i][0],path[i++][1]);
while(i < path.length){
ctx.lineTo(path[i][0],path[i++][1]);
}
if(close){
ctx.closePath();
}
ctx.stroke();
}
function strokeCircle(x,y,r){
ctx.beginPath();
ctx.moveTo(x + r,y);
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.stroke();
}
function display() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
var cx = w/2;
var cy = h/2;
var r1 = Math.abs(Math.sin(globalTime/ 4000) * w / 4);
var r2 = Math.abs(Math.sin(globalTime/ 4300) * h / 4);
var ang = globalTime / 1500;
// find the intercept from ellipse center to mouse on the ellipse
var ratio = r1 / r2
var dx = mouse.x - cx;
var dy = mouse.y - cy;
var dist = Math.hypot(dx,dy);
var ex = Math.cos(-ang);
var ey = Math.sin(-ang);
var c = Math.atan2((dx * ey + dy * ex) * ratio, dx * ex - dy * ey);
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;
var ix = nx * ex + ny * ey;
var iy = -nx * ey + ny * ex;
var dist = Math.hypot(dx,dy);
var dist2Inter = Math.hypot(ix,iy);
ctx.strokeStyle = "Blue";
ctx.lineWidth = 4;
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke();
if(dist2Inter > dist){
ctx.fillStyle = "#7F7";
ctx.globalAlpha = 0.5;
ctx.fill();
ctx.globalAlpha = 1;
}
// Display the intercept
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
path([[cx,cy],[mouse.x,mouse.y]])
ctx.strokeStyle = "red";
ctx.lineWidth = 5;
path([[cx,cy],[cx + ix,cy+iy]])
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
strokeCircle(cx + ix, cy + iy, 6)
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
strokeCircle(cx, cy, 6)
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
strokeCircle(mouse.x, mouse.y, 4)
ctx.fillStyle = "white";
ctx.fill();
}
/** SimpleFullCanvasMouse.js begin **/
//==============================================================================
// Boilerplate code from here down and not related to the answer
//==============================================================================
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
var c,
cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
if ((m.buttonRaw & 2) && m.crashRecover !== null) {
if (typeof m.crashRecover === "function") {
setTimeout(m.crashRecover, 0);
}
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
// Clean up. Used where the IDE is on the same page.
var done = function () {
window.removeEventListener("resize", resizeCanvas)
mouse.remove();
document.body.removeChild(canvas);
canvas = ctx = mouse = undefined;
}
function update(timer) { // Main update loop
if(ctx === undefined){ return; }
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
//mouse.crashRecover = done;
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/

If you have an ellipse of the form (x-x0)2/a2 + (y-y0)2/b2 = 1, then a point (x, y) is inside the ellipse if and only if (x-x0)2/a2 + (y-y0)2/b2 < 1. You can just test that inequality to see if the mouse is inside the ellipse.
To be able to draw a line to the edge of the ellipse: get the theta of the mouse with atan2 (don't use acos, you'll get incorrect results in quadrants III & IV), use the polar equation of the ellipse to solve for r, then convert back to rectangular coordinates and draw.

Related

hyperdrive effect in canvas across randomly placed circles

I'm trying to create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}

How to shift pixel value to the next mousemove position in canvas?

I am creating a smudging tool with HTML5 canvas. Now I have to shift the pixel color at the point of mouse pointer to the next position where mouse pointer moves. Is it possible to do with javascript?
<canvas id="canvas"><canvas>
var canvas = document.getElementById("canvas");
var context = canvas.getContext('2d');
var url = 'download.jpg';
var imgObj = new Image();
imgObj.src = url;
imgObj.onload = function(e) {
context.drawImage(imgObj, 0, 0);
}
function findPos(obj) {
var curleft = 0,
curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return {
x: curleft,
y: curtop
};
}
return undefined;
}
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
$('#canvas').mousemove(function(e) {
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
console.log(x, y);
var c = this.getContext('2d');
var p = c.getImageData(x, y, 1, 1).data;
var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
console.log(hex)
});
I am very short on time ATM so code only.
Uses an offscreen canvas brush to get a copy of the background canvas background where the mouse was last frame. Then use a radial gradient to feather the brush using ctx.globalCompositeOperation = "destination-in". Then draw the updated brush at the next mouse position.
The main canvas is use just to display, the canvas being smeared is called background You can put whatever content you want on that canvas (eg image) and it can be any size, and you can zoom, pan, rotate the background though you will have to convert the mouse coordinates to match the background coordinates
Click drag mouse to smear colours.
const ctx = canvas.getContext("2d");
const background = createCanvas(canvas.width,canvas.height);
const brushSize = 64;
const bs = brushSize;
const bsh = bs / 2;
const smudgeAmount = 0.25; // values from 0 none to 1 full
// helpers
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// brush gradient for feather
const grad = ctx.createRadialGradient(bsh,bsh,0,bsh,bsh,bsh);
grad.addColorStop(0,"black");
grad.addColorStop(1,"rgba(0,0,0,0)");
const brush = createCanvas(brushSize)
// creates an offscreen canvas
function createCanvas(w,h = w){
var c = document.createElement("canvas");
c.width = w;
c.height = h;
c.ctx = c.getContext("2d");
return c;
}
// get the brush from source ctx at x,y
function brushFrom(ctx,x,y){
brush.ctx.globalCompositeOperation = "source-over";
brush.ctx.globalAlpha = 1;
brush.ctx.drawImage(ctx.canvas,-(x - bsh),-(y - bsh));
brush.ctx.globalCompositeOperation = "destination-in";
brush.ctx.globalAlpha = 1;
brush.ctx.fillStyle = grad;
brush.ctx.fillRect(0,0,bs,bs);
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var globalTime;
var lastX;
var lastY;
// update background is size changed
function createBackground(){
background.width = w;
background.height = h;
background.ctx.fillStyle = "white";
background.ctx.fillRect(0,0,w,h);
doFor(64,()=>{
background.ctx.fillStyle = `rgb(${randI(255)},${randI(255)},${randI(255)}`;
background.ctx.fillRect(randI(w),randI(h),randI(10,100),randI(10,100));
});
}
// main update function
function update(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
createBackground();
}else{
ctx.clearRect(0,0,w,h);
}
ctx.drawImage(background,0,0);
// if mouse down then do the smudge for all pixels between last mouse and mouse now
if(mouse.button){
brush.ctx.globalAlpha = smudgeAmount;
var dx = mouse.x - lastX;
var dy = mouse.y - lastY;
var dist = Math.sqrt(dx*dx+dy*dy);
for(var i = 0;i < dist; i += 1){
var ni = i / dist;
brushFrom(background.ctx,lastX + dx * ni,lastY + dy * ni);
ni = (i+1) / dist;
background.ctx.drawImage(brush,lastX + dx * ni - bsh,lastY + dy * ni - bsh);
}
}else{
brush.ctx.clearRect(0,0,bs,bs); /// clear brush if not used
}
lastX = mouse.x;
lastY = mouse.y;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>

Canvas perpendicular points to line

I'm using Konva library to draw some stuff on HTML5 canvas.
I have given 2 points from user interaction by mouse click:
var A={x:'',y:''};
var B={x:'',y:''};
1) How to draw line line this?
My question is:
1) How to get perpendicular lines on each interval?
2) How to get distance from A to B point?
3) How to get all points on line from A to B?
4) How to get red points?
You have not explained what your line is so I am assuming it is a sin wave (though the image looks like circles stuck together???)
As MBo has given the basics this is just applying it to the wavy line.
// normalize a vector
function normalize(vec){
var length = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= length;
vec.y /= length;
return vec;
}
// creates a wavy line
function wavyLine(start, end, waves, amplitude){
return ({
start,
end,
waves,
amplitude,
update(){
if(this.vec === undefined){
this.vec = {};
this.norm = {};
}
this.vec.x = this.end.x - this.start.x;
this.vec.y = this.end.y - this.start.y;
this.length = Math.sqrt(this.vec.x * this.vec.x + this.vec.y * this.vec.y);
this.norm.x = this.vec.x / this.length;
this.norm.y = this.vec.y / this.length;
return this;
}
}).update();
}
// draws a wavy line
function drawWavyLine(line) {
var x, stepSize, i, y, phase, dist;
ctx.beginPath();
stepSize = ctx.lineWidth;
ctx.moveTo(line.start.x, line.start.y);
for (i = stepSize; i < line.length; i+= stepSize) {
x = line.start.x + line.norm.x * i; // get point i pixels from start
y = line.start.y + line.norm.y * i; // get point i pixels from start
phase = (i / (line.length / line.waves)) * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
x -= line.norm.y * dist;
y += line.norm.x * dist;
ctx.lineTo(x, y);
}
phase = line.waves * Math.PI * 2; // get the wave phase at this point
dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
ctx.lineTo(line.end.x - line.norm.y * dist, line.end.y + line.norm.x * dist);
ctx.stroke();
}
// find the closest point on a wavy line to a point returns the pos on the wave, tangent and point on the linear line
function closestPointOnLine(point,line){
var x = point.x - line.start.x;
var y = point.y - line.start.y;
// get the amount the line vec needs to be scaled so tat point is perpendicular to the line
var l = (line.vec.x * x + line.vec.y * y) / (line.length * line.length);
x = line.vec.x * l; // scale the vec
y = line.vec.y * l;
return pointAtDistance(Math.sqrt(x * x + y * y), line);
}
// find the point at (linear) distance along wavy line and return coordinate, coordinate on wave, and tangent
function pointAtDistance(distance,line){
var lenScale = line.length / line.waves; // scales the length into radians
var phase = distance * Math.PI * 2 / lenScale; // get the wave phase at this point
var dist = Math.sin(phase) * line.amplitude; // get the distance from the line to the point on the wavy curve
var slope = Math.cos(phase ) * Math.PI * 2 * line.amplitude / lenScale; // derivitive of sin(a*x) is -a*cos(a*x)
// transform tangent (slope) into a vector along the line. This vector is not a unit vector so normalize it
var tangent = normalize({
x : line.norm.x - line.norm.y * slope,
y : line.norm.y + line.norm.x * slope
});
// move from the line start to the point on the linear line at distance
var linear = {
x : line.start.x + line.norm.x * distance,
y : line.start.y + line.norm.y * distance
}
// move out perpendicular to the wavy part
return {
x : linear.x - line.norm.y * dist,
y : linear.y + line.norm.x * dist,
tangent,linear
};
}
// create a wavy line
var wLine = wavyLine({x:10,y:100},{x:300,y:100},3,50);
// draw the wavy line and show some points on it
function display(timer){
globalTime = timer;
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
var radius = Math.max(ch,cw);
// set up the wavy line
wLine.waves = Math.sin(timer / 10000) * 6;
wLine.start.x = Math.cos(timer / 50000) * radius + cw;
wLine.start.y = Math.sin(timer / 50000) * radius + ch;
wLine.end.x = -Math.cos(timer / 50000) * radius + cw;
wLine.end.y = -Math.sin(timer / 50000) * radius + ch ;
wLine.update();
// draw the linear line
ctx.lineWidth = 0.5;
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(wLine.start.x, wLine.start.y);
ctx.lineTo(wLine.end.x, wLine.end.y);
ctx.stroke();
// draw the wavy line
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
drawWavyLine(wLine);
// find point nearest mouse
var p = closestPointOnLine(mouse,wLine);
ctx.lineWidth = 1;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x + p.tangent.x * 20,p.y + p.tangent.y * 20);
ctx.lineTo(p.x - p.tangent.y * 10,p.y + p.tangent.x * 10);
ctx.lineTo(p.x + p.tangent.y * 10,p.y - p.tangent.x * 10);
ctx.closePath();
ctx.stroke();
// find points at equal distance along line
ctx.lineWidth = 1;
ctx.strokeStyle = "blue";
ctx.beginPath();
for(var i = 0; i < w; i += w / 10){
var p = pointAtDistance(i,wLine);
ctx.moveTo(p.x + 5,p.y);
ctx.arc(p.x,p.y,5,0,Math.PI * 2);
ctx.moveTo(p.x,p.y);
ctx.lineTo(p.linear.x,p.linear.y);
ctx.moveTo(p.x + p.tangent.x * 40, p.y + p.tangent.y * 40);
ctx.lineTo(p.x - p.tangent.x * 40, p.y - p.tangent.y * 40);
}
ctx.stroke();
}
/******************************************************************************
The code from here down is generic full page mouse and canvas boiler plate
code. As I do many examples which all require the same mouse and canvas
functionality I have created this code to keep a consistent interface. The
Code may or may not be part of the answer.
This code may or may not have ES6 only sections so will require a transpiler
such as babel.js to run on legacy browsers.
*****************************************************************************/
// V2.0 ES6 version for Stackoverflow and Groover QuickRun
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0;
// You can declare onResize (Note the capital R) as a callback that is also
// called once at start up. Warning on first call canvas may not be at full
// size.
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var resizeTimeoutHandle;
var firstRun = true;
function createCanvas () {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas () {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") { setGlobals() }
if (typeof onResize === "function") {
clearTimeout(resizeTimeoutHandle);
if (firstRun) { onResize() }
else { resizeTimeoutHandle = setTimeout(onResize, RESIZE_DEBOUNCE_TIME) }
firstRun = false;
}
}
function setGlobals () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position and wheel
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
eventNames : "mousemove,mousedown,mouseup".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
},
start(element) {
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
},
}
m = mouse;
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(timer); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
canvas = createCanvas();
mouse.start(canvas);
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
We have points A and B. Difference vector
D.X = B.X - A.X
D.Y = B.Y - A.Y
Length = Sqrt(D.X * D.X + D.Y * D.Y)
normalized (unit) vector
uD.X = D.X / Length
uD.Y = D.Y / Length
perpendicular unit vector
P.X = - uD.Y
P.Y = uD.X
some red point:
R.X = A.X + uD.X * Dist + P.X * SideDist * SideSign
R.Y = A.Y + uD.Y * Dist + P.Y * SideDist * SideSign
where Dist is in range 0..Length
Dist = i / N * Length for N equidistant points
SideSign is +/- 1 for left and right side

How to draw a smooth continuous line with mouse using html canvas and javascript

I'm attempting to create a simple draw/paint programme using html5 canvas and plain javascript. I've got it working ok, but when drawing and moving the mouse too fast the line disconnects and I just end up with a line of dots - how can I make this a smooth continuous line?
Advice would be much appreciated! I'm quite new to JS so code examples would be really useful, thanks in advance.
Current JS is:
var canvas, ctx
var mouseX, mouseY, mouseDown = 0
function draw(ctx,x,y,size) {
ctx.fillStyle = "#000000"
ctx.beginPath()
ctx.arc(x, y, size, 0, Math.PI*2, true)
ctx.closePath()
ctx.fill()
}
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
function onMouseDown() {
mouseDown = 1
draw(ctx, mouseX, mouseY, 2)
}
function onMouseUp() {
mouseDown = 0
}
function onMouseMove(e) {
getMousePos(e)
if (mouseDown == 1) {
draw(ctx, mouseX, mouseY, 2)
}
}
function getMousePos(e) {
if (!e)
var e = event
if (e.offsetX) {
mouseX = e.offsetX
mouseY = e.offsetY
}
else if (e.layerX) {
mouseX = e.layerX
mouseY = e.layerY
}
}
function init() {
canvas = document.getElementById('sketchpad')
ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', onMouseDown, false)
canvas.addEventListener('mousemove', onMouseMove, false)
window.addEventListener('mouseup', onMouseUp, false)
}
init();
<canvas id="sketchpad" width="500" height="500"></canvas>
Drawing a smooth curve with the mouse.
Sadly it is not that easy if you wish to stay true to the artists intended line.
It involves recording the whole mouse stroke. When the stroke is complete, reduce the number of points to the detail limit (set by artist) then apply a bezier smoothing function on the remaining points.
It can be done as the stroke is drawn but for some devices this can become too much if the line becomes very long. As the line detail reduction looks at all points when showing the smoothed line live some people dont like the way it slightly changes as the line gets longer.
Demo
The code below demonstrates a solution I have found useful.
Use the left button to draw with smoothing done one button release.
Use the right button to draw with live smoothing (blue line).
Middle mouse button click to clear.
Use the two sliders at the top to set the amount of smoothing, and the amount of detail. Left click to drag out a stroke, the raw line is shown. When the mouse is released the line is then simplified, smoothed, and added to the background image.
var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");
// mouse stuff
var mouse = {
x:0,
y:0,
buttonLastRaw:0, // user modified value
buttonRaw:0,
buttons:[1,2,4,6,5,3], // masks for setting and clearing button raw bits;
};
function mouseMove(event){
mouse.x = event.offsetX; mouse.y = event.offsetY;
if(mouse.x === undefined){ mouse.x = event.clientX; mouse.y = event.clientY;}
if(event.type === "mousedown"){ mouse.buttonRaw |= mouse.buttons[event.which-1];
}else if(event.type === "mouseup"){mouse.buttonRaw &= mouse.buttons[event.which+2];
}else if(event.type === "mouseout"){ mouse.buttonRaw = 0; mouse.over = false;
}else if(event.type === "mouseover"){ mouse.over = true; }
event.preventDefault();
}
canvas.addEventListener('mousemove',mouseMove);
canvas.addEventListener('mousedown',mouseMove);
canvas.addEventListener('mouseup' ,mouseMove);
canvas.addEventListener('mouseout' ,mouseMove);
canvas.addEventListener('mouseover' ,mouseMove);
canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false);
// Line simplification based on
// the Ramer–Douglas–Peucker algorithm
// referance https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
// points are and array of arrays consisting of [[x,y],[x,y],...,[x,y]]
// length is in pixels and is the square of the actual distance.
// returns array of points of the same form as the input argument points.
var simplifyLineRDP = function(points, length) {
var simplify = function(start, end) { // recursize simplifies points from start to end
var maxDist, index, i, xx , yy, dx, dy, ddx, ddy, p1, p2, p, t, dist, dist1;
p1 = points[start];
p2 = points[end];
xx = p1[0];
yy = p1[1];
ddx = p2[0] - xx;
ddy = p2[1] - yy;
dist1 = (ddx * ddx + ddy * ddy);
maxDist = length;
for (var i = start + 1; i < end; i++) {
p = points[i];
if (ddx !== 0 || ddy !== 0) {
t = ((p[0] - xx) * ddx + (p[1] - yy) * ddy) / dist1;
if (t > 1) {
dx = p[0] - p2[0];
dy = p[1] - p2[1];
} else
if (t > 0) {
dx = p[0] - (xx + ddx * t);
dy = p[1] - (yy + ddy * t);
} else {
dx = p[0] - xx;
dy = p[1] - yy;
}
}else{
dx = p[0] - xx;
dy = p[1] - yy;
}
dist = dx * dx + dy * dy
if (dist > maxDist) {
index = i;
maxDist = dist;
}
}
if (maxDist > length) { // continue simplification while maxDist > length
if (index - start > 1){
simplify(start, index);
}
newLine.push(points[index]);
if (end - index > 1){
simplify(index, end);
}
}
}
var end = points.length - 1;
var newLine = [points[0]];
simplify(0, end);
newLine.push(points[end]);
return newLine;
}
// This is my own smoothing method
// It creates a set of bezier control points either 2nd order or third order
// bezier curves.
// points: list of points
// cornerThres: when to smooth corners and represents the angle between to lines.
// When the angle is smaller than the cornerThres then smooth.
// match: if true then the control points will be balanced.
// Function will make a copy of the points
var smoothLine = function(points,cornerThres,match){ // adds bezier control points at points if lines have angle less than thres
var p1, p2, p3, dist1, dist2, x, y, endP, len, angle, i, newPoints, aLen, closed, bal, cont1, nx1, nx2, ny1, ny2, np;
function dot(x, y, xx, yy) { // get do product
// dist1,dist2,nx1,nx2,ny1,ny2 are the length and normals and used outside function
// normalise both vectors
dist1 = Math.sqrt(x * x + y * y); // get length
if (dist1 > 0) { // normalise
nx1 = x / dist1 ;
ny1 = y / dist1 ;
}else {
nx1 = 1; // need to have something so this will do as good as anything
ny1 = 0;
}
dist2 = Math.sqrt(xx * xx + yy * yy);
if (dist2 > 0) {
nx2 = xx / dist2;
ny2 = yy / dist2;
}else {
nx2 = 1;
ny2 = 0;
}
return Math.acos(nx1 * nx2 + ny1 * ny2 ); // dot product
}
newPoints = []; // array for new points
aLen = points.length;
if(aLen <= 2){ // nothing to if line too short
for(i = 0; i < aLen; i ++){ // ensure that the points are copied
newPoints.push([points[i][0],points[i][1]]);
}
return newPoints;
}
p1 = points[0];
endP =points[aLen-1];
i = 0; // start from second poitn if line not closed
closed = false;
len = Math.hypot(p1[0]- endP[0], p1[1]-endP[1]);
if(len < Math.SQRT2){ // end points are the same. Join them in coordinate space
endP = p1;
i = 0; // start from first point if line closed
p1 = points[aLen-2];
closed = true;
}
newPoints.push([points[i][0],points[i][1]])
for(; i < aLen-1; i++){
p2 = points[i];
p3 = points[i + 1];
angle = Math.abs(dot(p2[0] - p1[0], p2[1] - p1[1], p3[0] - p2[0], p3[1] - p2[1]));
if(dist1 !== 0){ // dist1 and dist2 come from dot function
if( angle < cornerThres*3.14){ // bend it if angle between lines is small
if(match){
dist1 = Math.min(dist1,dist2);
dist2 = dist1;
}
// use the two normalized vectors along the lines to create the tangent vector
x = (nx1 + nx2) / 2;
y = (ny1 + ny2) / 2;
len = Math.sqrt(x * x + y * y); // normalise the tangent
if(len === 0){
newPoints.push([p2[0],p2[1]]);
}else{
x /= len;
y /= len;
if(newPoints.length > 0){
var np = newPoints[newPoints.length-1];
np.push(p2[0]-x*dist1*0.25);
np.push(p2[1]-y*dist1*0.25);
}
newPoints.push([ // create the new point with the new bezier control points.
p2[0],
p2[1],
p2[0]+x*dist2*0.25,
p2[1]+y*dist2*0.25
]);
}
}else{
newPoints.push([p2[0],p2[1]]);
}
}
p1 = p2;
}
if(closed){ // if closed then copy first point to last.
p1 = [];
for(i = 0; i < newPoints[0].length; i++){
p1.push(newPoints[0][i]);
}
newPoints.push(p1);
}else{
newPoints.push([points[points.length-1][0],points[points.length-1][1]]);
}
return newPoints;
}
// creates a drawable image
var createImage = function(w,h){
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d");
return image;
}
// draws the smoothed line with bezier control points.
var drawSmoothedLine = function(line){
var i,p;
ctx.beginPath()
ctx.moveTo(line[0][0],line[0][1])
for(i = 0; i < line.length-1; i++){
p = line[i];
p1 = line[i+1]
if(p.length === 2){ // linear
ctx.lineTo(p[0],p[1])
}else
if(p.length === 4){ // bezier 2nd order
ctx.quadraticCurveTo(p[2],p[3],p1[0],p1[1]);
}else{ // bezier 3rd order
ctx.bezierCurveTo(p[2],p[3],p[4],p[5],p1[0],p1[1]);
}
}
if(p.length === 2){
ctx.lineTo(p1[0],p1[1])
}
ctx.stroke();
}
// smoothing settings
var liveSmooth;
var lineSmooth = {};
lineSmooth.lengthMin = 8; // square of the pixel length
lineSmooth.angle = 0.8; // angle threshold
lineSmooth.match = false; // not working.
// back buffer to save the canvas allowing the new line to be erased
var backBuffer = createImage(canvas.width,canvas.height);
var currentLine = [];
mouse.lastButtonRaw = 0; // add mouse last incase not there
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.strokeStyle = "black";
ctx.clearRect(0,0,canvas.width,canvas.height);
var drawing = false; // if drawing
var input = false; // if menu input
var smoothIt = false; // flag to allow feedback that smoothing is happening as it takes some time.
function draw(){
// if not drawing test for menu interaction and draw the menus
if(!drawing){
if(mouse.x < 203 && mouse.y < 24){
if(mouse.y < 13){
if(mouse.buttonRaw === 1){
ctx.clearRect(3,3,200,10);
lineSmooth.angle = (mouse.x-3)/200;
input = true;
}
}else
if(mouse.buttonRaw === 1){
ctx.clearRect(3,14,200,10);
lineSmooth.lengthMin = (mouse.x-3)/10;
input = true;
}
canvas.style.cursor = "pointer";
}else{
canvas.style.cursor = "crosshair";
}
if(mouse.buttonRaw === 0 && input){
input = false;
mouse.lastButtonRaw = 0;
}
ctx.lineWidth = 0.5;
ctx.fillStyle = "red";
ctx.clearRect(3,3,200,10);
ctx.clearRect(3,14,200,10);
ctx.fillRect(3,3,lineSmooth.angle*200,10);
ctx.fillRect(3,14,lineSmooth.lengthMin*10,10);
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.fillStyle = "#000"
ctx.strokeRect(3,3,200,10);
ctx.fillText("Smooth "+(lineSmooth.angle * (180 / Math.PI)).toFixed(0)+"deg",5,2)
ctx.strokeRect(3,14,200,10);
ctx.fillText("Detail "+lineSmooth.lengthMin.toFixed(0) + "pixels",5,13);
}else{
canvas.style.cursor = "crosshair";
}
if(!input){
ctx.lineWidth = 3;
if(mouse.buttonRaw === 4 && mouse.lastButtonRaw === 0){
currentLine = [];
drawing = true;
backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height);
backBuffer.ctx.drawImage(canvas,0,0);
currentLine.push([mouse.x,mouse.y])
}else
if(mouse.buttonRaw === 4){
var lp = currentLine[currentLine.length-1]; // get last point
// dont record point if no movement
if(mouse.x !== lp[0] || mouse.y !== lp[1] ){
currentLine.push([mouse.x,mouse.y]);
ctx.beginPath();
ctx.moveTo(lp[0],lp[1])
ctx.lineTo(mouse.x,mouse.y);
ctx.stroke();
liveSmooth = smoothLine(
simplifyLineRDP(
currentLine,
lineSmooth.lengthMin
),
lineSmooth.angle,
lineSmooth.match
);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(backBuffer,0,0);
ctx.strokeStyle = "Blue";
drawSmoothedLine(liveSmooth );
ctx.strokeStyle = "black";
}
}else
if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 4){
ctx.textAlign = "center"
ctx.fillStyle = "red"
ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5);
smoothIt = true;
}else
if(smoothIt){
smoothIt = false;
var newLine = smoothLine(
simplifyLineRDP(
currentLine,
lineSmooth.lengthMin
),
lineSmooth.angle,
lineSmooth.match
);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(backBuffer,0,0);
drawSmoothedLine(newLine);
drawing = false;
}
if(mouse.buttonRaw === 1 && mouse.lastButtonRaw === 0){
currentLine = [];
drawing = true;
backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height);
backBuffer.ctx.drawImage(canvas,0,0);
currentLine.push([mouse.x,mouse.y])
}else
if(mouse.buttonRaw === 1){
var lp = currentLine[currentLine.length-1]; // get last point
// dont record point if no movement
if(mouse.x !== lp[0] || mouse.y !== lp[1] ){
currentLine.push([mouse.x,mouse.y]);
ctx.beginPath();
ctx.moveTo(lp[0],lp[1])
ctx.lineTo(mouse.x,mouse.y);
ctx.stroke();
}
}else
if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 1){
ctx.textAlign = "center"
ctx.fillStyle = "red"
ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5);
smoothIt = true;
}else
if(smoothIt){
smoothIt = false;
var newLine = smoothLine(
simplifyLineRDP(
currentLine,
lineSmooth.lengthMin
),
lineSmooth.angle,
lineSmooth.match
);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(backBuffer,0,0);
drawSmoothedLine(newLine);
drawing = false;
}
}
// middle button clear
if(mouse.buttonRaw === 2){
ctx.clearRect(0,0,canvas.width,canvas.height);
}
mouse.lastButtonRaw = mouse.buttonRaw;
requestAnimationFrame(draw);
}
draw();
.canC { width:1000px; height:500px; border:1px black solid;}
<canvas class="canC" id="canV" width=1000 height=500></canvas>
You could save the last position and draw a line between the last point and the actual point.
if (lastX && lastY && (x !== lastX || y !== lastY)) {
ctx.fillStyle = "#000000";
ctx.lineWidth = 2 * size;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
// ...
lastX = x;
lastY = y;
}
On mouseup event set the two variables to zero.
var canvas, ctx
var mouseX, mouseY, mouseDown = 0,
lastX, lastY;
function draw(ctx,x,y,size) {
if (lastX && lastY && (x !== lastX || y !== lastY)) {
ctx.fillStyle = "#000000";
ctx.lineWidth = 2 * size;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
}
ctx.fillStyle = "#000000";
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
lastX = x;
lastY = y;
}
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
function onMouseDown() {
mouseDown = 1
draw(ctx, mouseX, mouseY, 2)
}
function onMouseUp() {
mouseDown = 0;
lastX = 0;
lastY = 0;
}
function onMouseMove(e) {
getMousePos(e)
if (mouseDown == 1) {
draw(ctx, mouseX, mouseY, 2)
}
}
function getMousePos(e) {
if (!e)
var e = event
if (e.offsetX) {
mouseX = e.offsetX
mouseY = e.offsetY
}
else if (e.layerX) {
mouseX = e.layerX
mouseY = e.layerY
}
}
function init() {
canvas = document.getElementById('sketchpad')
ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', onMouseDown, false)
canvas.addEventListener('mousemove', onMouseMove, false)
window.addEventListener('mouseup', onMouseUp, false)
}
init();
<canvas id="sketchpad" width="600" height="300"></canvas>
Good question! And I recommend you a site https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API to learn more canvas API.
I think using lineTo is better than arc.So I hope this code will help you.
var canvas, ctx;
var mouseDown = 0, lastX, lastY;
function draw(ctx,x,y) {
ctx.beginPath();
ctx.moveTo(lastX,lastY);
ctx.lineTo(x,y);
ctx.closePath();
ctx.stroke();
}
function clearCanvas(canvas,ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
function onMouseDown(e) {
var xy = getMousePos(e);
lastX = xy.mouseX;
lastY = xy.mouseY;
mouseDown = 1;
}
function onMouseUp() {
mouseDown = 0
}
function onMouseMove(e) {
if (mouseDown == 1) {
var xy = getMousePos(e);
draw(ctx, xy.mouseX, xy.mouseY);
lastX = xy.mouseX, lastY = xy.mouseY;
}
}
function getMousePos(e) {
var o = {};
if (!e)
var e = event
if (e.offsetX) {
o.mouseX = e.offsetX
o.mouseY = e.offsetY
}
else if (e.layerX) {
o.mouseX = e.layerX
o.mouseY = e.layerY
}
return o;
}
function init() {
canvas = document.getElementById('sketchpad')
ctx = canvas.getContext('2d')
canvas.addEventListener('mousedown', onMouseDown, false)
canvas.addEventListener('mousemove', onMouseMove, false)
canvas.addEventListener('mouseup', onMouseUp, false)
}
init();

Optimizing arcs to draw sectors only visible on canvas

I have an arc which is rather large in size with a stroke that uses rgba values. It has a 50% alpha value and because of that, it is causing a big hit on my cpu profile for my browser.
So i want to find a way to optimize this so that where ever the arc is drawn in a canvas, it will only draw from one angle to another of which is visible on screen.
What i am having difficulty with, is working out the correct angle range.
Here is a visual example:
The top image is what the canvas actually does even if you don't see it, and the bottom one is what I am trying to do to save processing time.
I created a JSFiddle where you can click and drag the circle, though, the two angles are currently fixed:
https://jsfiddle.net/44tawd81/
Here is the draw code:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
var radius = 50;
var pos = {
'x': canvas.width - 20,
'y': canvas.height /2
};
function draw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
ctx.arc(pos.x,pos.y,radius,0,2*Math.PI); //need to adjust angle range
ctx.stroke();
requestAnimationFrame(draw);
}
draw();
What is the simplest way to find the angle range to draw based on it's position and size in a canvas?
Clipping a Circle
This is how to clip a circle to a rectangular region aligned to the x and y axis.
To clip the circle I search for the list of points where the circle intersects the clipping region. Starting from one side I go in a clockwise direction adding clip points as they are found. When all 4 sides are tested I then draw the arc segments that join the points found.
To find if a point has intercepted a clipping edge you find the distance the circle center is from that edge. Knowing the radius and the distance you can complete the right triangle to find the coordinates of the intercept.
For the left edge
// define the clip edge and circle
var clipLeftX = 100;
var radius = 200;
var centerX = 200;
var centerY = 200;
var dist = centerX - clipLeftX;
if(dist > radius) { // circle inside }
if(dist < -radius) {// circle completely outside}
// we now know the circle is clipped
Now calculate the distance from the circle y that the two clip points will be
// the right triangle with hypotenuse and one side know can be solved with
var clipDist = Math.sqrt(radius * radius - dist * dist);
So the points where the circle intercept the clipping line
var clipPointY1 = centerY - clipDist;
var clipPointY2 = centerY + clipDist;
With that you can work out if the two points are inside or outside the left side top or bottom by testing the two points against the top and bottom of the left line.
You will end up with either 0,1 or 2 clipping points.
Because arc requires angles to draw you need to calculate the angle from the circle center to the found points. You already have all the info needed
// dist is the x distance from the clip
var angle = Math.acos(radius/dist); // for left and right side
The hard part is making sure all the angles to the clipping point are in the correct order. The is a little fiddling about with flags to ensure that the arcs are in the correct order.
After checking all four sides you will end up with 0,2,4,6, or 8 clipping points representing the start and ends of the various clipped arcs. It is then simply iterating the arc segments and rendering them.
// Helper functions are not part of the answer
var canvas;
var ctx;
var mouse;
var resize = function(){
/** fullScreenCanvas.js begin **/
canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
}
/** MouseFull.js end **/
resize();
// Answer starts here
var w = canvas.width;
var h = canvas.height;
var d = Math.sqrt(w * w + h * h); // diagnal size
var cirLWidth = d * (1 / 100);
var rectCol = "black";
var rectLWidth = d * (1 / 100);
const PI2 = Math.PI * 2;
const D45_LEN = 0.70710678;
var angles = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // declared outside to stop GC
// create a clipArea
function rectArea(x, y, x1, y1) {
return {
left : x,
top : y,
width : x1 - x,
height : y1 - y
};
}
// create a arc
function arc(x, y, radius, start, end, col) {
return {
x : x,
y : y,
r : radius,
s : start,
e : end,
c : col
};
}
// draws an arc
function drawArc(arc, dir) {
ctx.strokeStyle = arc.c;
ctx.lineWidth = cirLWidth;
ctx.beginPath();
ctx.arc(arc.x, arc.y, arc.r, arc.s, arc.e, dir);
ctx.stroke();
}
// draws a clip area
function drawRect(r) {
ctx.strokeStyle = rectCol;
ctx.lineWidth = rectLWidth;
ctx.strokeRect(r.left, r.top, r.width, r.height);
}
// clip and draw an arc
// arc is the arc to clip
// clip is the clip area
function clipArc(arc, clip){
var count, distTop, distLeft, distBot, distRight, dist, swap, radSq, bot,right;
// cir1 is used to draw the clipped circle
cir1.x = arc.x;
cir1.y = arc.y;
count = 0; // number of clip points found;
bot = clip.top + clip.height; // no point adding these two over and over
right = clip.left + clip.width;
// get distance from all edges
distTop = arc.y - clip.top;
distBot = bot - arc.y;
distLeft = arc.x - clip.left;
distRight = right - arc.x;
radSq = arc.r * arc.r; // get the radius squared
// check if outside
if(Math.min(distTop, distBot, distRight, distLeft) < -arc.r){
return; // nothing to see so go home
}
// check inside
if(Math.min(distTop, distBot, distRight, distLeft) > arc.r){
drawArc(cir1);
return;
}
swap = true;
if(distLeft < arc.r){
// get the distance up and down to clip
dist = Math.sqrt(radSq - distLeft * distLeft);
// check the point is in the clip area
if(dist + arc.y < bot && arc.y + dist > clip.top){
// get the angel
angles[count] = Math.acos(distLeft / -arc.r);
count += 1;
}
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distLeft / -arc.r); // get the angle
if(count === 0){ // if first point then set direction swap
swap = false;
}
count += 1;
}
}
if(distTop < arc.r){
dist = Math.sqrt(radSq - distTop * distTop);
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distTop / arc.r);
count += 1;
}
if(arc.x+dist < right && arc.x+dist > clip.left){
angles[count] = PI2-Math.asin(distTop/arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distRight < arc.r){
dist = Math.sqrt(radSq - distRight * distRight);
if(arc.y - dist < bot && arc.y - dist > clip.top){
angles[count] = PI2 - Math.acos(distRight / arc.r);
count += 1;
}
if(dist + arc.y < bot && arc.y + dist > clip.top){
angles[count] = Math.acos(distRight / arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
if(distBot < arc.r){
dist = Math.sqrt(radSq - distBot * distBot);
if(arc.x + dist < right && arc.x + dist > clip.left){
angles[count] = Math.asin(distBot / arc.r);
count += 1;
}
if(arc.x - dist < right && arc.x - dist > clip.left){
angles[count] = Math.PI + Math.asin(distBot / -arc.r);
if(count === 0){
swap = false;
}
count += 1;
}
}
// now draw all the arc segments
if(count === 0){
return;
}
if(count === 2){
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1,swap);
}else
if(count === 4){
if(swap){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[0];
drawArc(cir1);
}else{
cir1.s = angles[2];
cir1.e = angles[3];
drawArc(cir1);
cir1.s = angles[0];
cir1.e = angles[1];
drawArc(cir1);
}
}else
if(count === 6){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[0];
drawArc(cir1);
}else
if(count === 8){
cir1.s = angles[1];
cir1.e = angles[2];
drawArc(cir1);
cir1.s = angles[3];
cir1.e = angles[4];
drawArc(cir1);
cir1.s = angles[5];
cir1.e = angles[6];
drawArc(cir1);
cir1.s = angles[7];
cir1.e = angles[0];
drawArc(cir1);
}
return;
}
var rect = rectArea(50, 50, w - 50, h - 50);
var circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
var cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
var counter = 0;
var countStep = 0.03;
function update() {
var x, y;
ctx.clearRect(0, 0, w, h);
circle.x = mouse.x;
circle.y = mouse.y;
drawArc(circle, "#888"); // draw unclipped arc
x = Math.cos(counter * 0.1);
y = Math.sin(counter * 0.3);
rect.top = h / 2 - Math.abs(y * (h * 0.4)) - 5;
rect.left = w / 2 - Math.abs(x * (w * 0.4)) - 5;
rect.width = Math.abs(x * w * 0.8) + 10;
rect.height = Math.abs(y * h * 0.8) + 10;
cir1.col = "RED";
clipArc(circle, rect); // draw the clipped arc
drawRect(rect); // draw the clip area. To find out why this method
// sucks move this to before drawing the clipped arc.
requestAnimationFrame(update);
if(mouse.buttonRaw !== 1){
counter += countStep;
}
ctx.font = Math.floor(w * (1 / 50)) + "px verdana";
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.lineWidth = Math.ceil(w * (1 / 300));
ctx.textAlign = "center";
ctx.lineJoin = "round";
ctx.strokeText("Left click and hold to pause", w/ 2, w * (1 / 40));
ctx.fillText("Left click and hold to pause", w/ 2, w * (1 / 40));
}
update();
window.addEventListener("resize",function(){
resize();
w = canvas.width;
h = canvas.height;
rect = rectArea(50, 50, w - 50, h - 50);
circle = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "#AAA");
cir1 = arc(w * (1 / 2), h * (1 / 2), w * (1 / 5), 0, Math.PI * 2, "red");
});
The quickest way to clip a circle.
That is the quickest I could manage to do it in code. There is some room for optimization but not that much in the agorithum.
The best solution is of course to use the canvas 2D context API clip() method.
ctx.save();
ctx.rect(10,10,200,200); // define the clip region
ctx.clip(); // activate the clip.
//draw your circles
ctx.restore(); // remove the clip.
This is much quicker than the method I showed above and should be used unless you have a real need to know the clip points and arcs segments that are inside or outside the clip region.
To find the angle to draw based on circle position, canvas position, circle size, and canvas size:
Determine intersection of circle and canvas
Calculate points on the circle at which intersection occurs
You then have an isosceles triangle.
You can use cosine formula for calculation of the angle.
c^2=a^2+b^2−2abcos(α)
a and b are sides adjacent to the angle α, which are the radius of the center r. c is the distance between the two points P1 and P2. So we get:
|P1−P2|^2=2r^2−2r^2cos(α)
2r^2−|P1−P2|^2/2r2=cos(α)
α=cos−1(2r^2−|P1−P2|^2/2r^2)

Categories