JavaScript draw using mouse Canvas with 3 axis - javascript

How is it / is it possible to draw using the mouse a canvas using 3 axis(x,y,z).
I know that one can draw a canvas on 2 axis and I have done that successfully.
But I have no idea of how I shall draw it on 3 axis (for example a cube).
Following shows some 2d canvas drawing functionallity
$(canvas).on('mousemove', function(e) {
mousex = parseInt(e.clientX-canvasx);
mousey = parseInt(e.clientY-canvasy);
if(mousedown) {
ctx.beginPath();
if(tooltype=='draw') {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
} else {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 10;
}
ctx.moveTo(last_mousex,last_mousey);
ctx.lineTo(mousex,mousey);
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
}
last_mousex = mousex;
last_mousey = mousey;
//Output
$('#output').html('current: '+mousex+', '+mousey+'<br/>last: '+last_mousex+', '+last_mousey+'<br/>mousedown: '+mousedown);
});
The full code https://jsfiddle.net/ArtBIT/kneDX/.
But how can I add a z axis and draw a 3d canvas for instance a cube.

With 2D it is simple, you have the X and Y coordinate of the mouse, and when a mouse button is clicked you can change pixels at that location in the canvas.
3D on the other hand is quite hard. Because of the extra dimension that does not exist on the 2D surface, you need to know how to control the 3D positions. And to make matters worse, with that third dimension comes all kinds of extra's that everyone likes to have: lightning and shadows, effects, focus, etc.
Simple drawing
In its most basic form, (set aside some arithmic) you can flatten the Z axis on the 2D surface with a single division. Suppose that you have a point in 3D which consists of three points on three axis (x3d, y3d, z3d) then you can do:
var x2d = x3d / z3d;
var y2d = y3d / z3d;
If you're new to 3D, you will want to play with this first. Here is a tutorial.
Advanced drawing
For just particles and lines this is rather straightforward, although you might want to use another perspective. But it gets more complicated soon when you use objects and want to rotate them in 3D space. This is why most people rely on an engine like three.js to do the 3D drawing for them.
Control 3D space
When drawing with the mouse, you need to map the 2D mouse movement to 3D for control. For examples, have a look a these 3D GUI's: Microsoft's Paint 3D, Google's Sketchup, and Blender. Note that the more kinds of mappings needs to be implemented (like scaling and other transformations) the more math is required.

Using three.js would help you out. See here: https://jsfiddle.net/bn890dtc/
The core code for drawing the line as your click and drag:
function onMouseMove(evt) {
if (renderer) {
var x = (event.clientX / window.innerWidth) * 2 - 1;
var y = -(event.clientY / window.innerHeight) * 2 + 1;
var z = 0
var vNow = new THREE.Vector3(x, y, z);
vNow.unproject(camera);
splineArray.push(vNow);
}
}
The line
vNow.unproject(camera);
will project your drawing into 3D space.
This function will update the line in 3D space:
function updatePositions() {
var positions = line.geometry.attributes.position.array;
var index = 0;
for ( var i = 0; i < splineArray.length; i ++ ) {
positions[ index ++ ] = splineArray[i].x;
positions[ index ++ ] = splineArray[i].y;
positions[ index ++ ] = splineArray[i].z;
}
}

Related

How to join 2 Bezier curves smoothly & continuously with HTML5 Canvas

I'm trying to join two separate bezier curves into one continuous curve. Currently, what I have looks like this:
The problem is that they aren't joined, so the points at which they meet look pointy/sharp instead of curvy and smooth. I've looked into documentation for joining bezier curves in P5.js, but am unsure of how to translate this into HTML5 Canvas. How do I join these two bezier curves so that they look like one smooth and continuous curve?
This is my code:
const canvas = document.getElementById('canvas');
const c = canvas.getContext("2d");
width = 800;
height = 500;
canvas.width = width;
canvas.height = height;
let face;
let centerX = width / 2;
let centerY = height / 3;
setup();
function setup() {
c.clearRect(0, 0, canvas.width, canvas.height);
face = new Face();
draw();
};
function draw() {
setBackground(`rgba(250, 250, 250, 1)`);
c.beginPath();
c.moveTo(centerX - face.hsx, centerY + face.hsy);
c.bezierCurveTo(centerX - face.hcp1x / 10, centerY - face.hsy2,
centerX + face.hcp1x / 10, centerY - face.hsy2,
centerX + face.hsx, centerY + face.hsy);
c.moveTo(centerX - face.hsx, centerY + face.hsy);
c.bezierCurveTo(centerX - face.hcp1x, centerY + face.hcp1y,
centerX + face.hcp1x, centerY + face.hcp1y,
centerX + face.hsx, centerY + face.hsy);
c.stroke();
c.fillStyle = (`rgba(25, 250, 211, 0)`);
c.fill();
}
function setBackground(color) {
c.fillStyle = color;
c.fillRect(0, 0, width, height);
}
function Face() {
this.hsx = 150;
this.hsy = 0;
this.hsy2 = 120;
this.hcp1x = 120;
this.hcp1y = 250;
}
Common tangent
To join two beziers smoothly you need to make the lines from the common point parallel thus defining the tangent at the end and start of the two beziers to be the same.
The following image illustrates this
The line that is defined by the two control points (C2, C1) and the common point (P) is the tangent of the curve at P. The length of the line segments have no constraints.
How?
There are dozens of ways to do this and how you do it is dependent on the requirements of the curve, the type of curve, and much more.
Example
I am not going to give a full example as it requires an understanding of vector maths and a cover all solution on the assumption you are not familiar with vector maths would be huge.
Thus the most basic pseudo code example uses the previous control and end points to calculate the next control point. ? represents unknowns which are not bound by constraints required to keep the lines parallel
// From illustration in answer
corner = ? // Distance to next control point as fraction of distance
// from previous control point
C2 = {x:?, y:?} // Last control point of previous bezier
P = {x:?, y:?} // Start of next bezier
C1 = { // Next control point along line from previous and scaled
x: P.x + (P.x - C2.x) * corner,
y: P.y + (P.y - C2.y) * corner,
}
// two beziers with common point P
ctx.bezierCurveTo(?,?, C2.x, C2.y, P.x, P.y)
ctx.bezierCurveTo(C1.x, C1.y, ?, ?, ?, ?)
In the below page:
https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_canvas_beziercurveto
You change the width and height of the canvas to 1000.
Then you replace the two lines between beginpath and stroke with the below code.
points=[
{x:0, y:300},//0
{x:100,y:500},//1
{x:200,y:300},//2
{x:300,y:100},//3
{x:400,y:300},//4
{x:100,y:500},//5
{x:100,y:300},//6
];
ctx.rect(points[0].x-5, points[0].y-5, 10,10);
var smoother={};
smoother.x=((points[1].x-points[0].x)/10)+points[0].x;
smoother.y=((points[1].y-points[0].y)/10)+points[0].y;
ctx.rect(smoother.x-5, smoother.y-5, 10,10);
ctx.rect(points[1].x-5, points[1].y-5, 10,10);
ctx.rect(points[2].x-5, points[2].y-5, 10,10);
ctx.moveTo(points[0].x,points[0].y);
ctx.bezierCurveTo(
smoother.x, smoother.y,
points[1].x, points[1].y,
points[2].x, points[2].y
);
var smoother={};
var dx=(points[2].x-points[1].x);
var dy=(points[2].y-points[1].y);
var yperx=(dy/dx);
travel_x=dx;
travel_y=(dx*yperx);
smoother.x=points[2].x+travel_x/3;
smoother.y=points[2].y+travel_y/3;
ctx.rect(smoother.x-5, smoother.y-5, 10,10);
ctx.rect(points[3].x-5, points[3].y-5, 10,10);
ctx.rect(points[4].x-5, points[4].y-5, 10,10);
ctx.moveTo(points[2].x,points[2].y);
ctx.bezierCurveTo(
smoother.x, smoother.y,
points[3].x, points[3].y,
points[4].x, points[4].y
);
var smoother={};
var dx=(points[4].x-points[3].x);
var dy=(points[4].y-points[3].y);
var yperx=(dy/dx);
travel_x=dx;
travel_y=(dx*yperx);
smoother.x=points[4].x+travel_x/3;
smoother.y=points[4].y+travel_y/3;
ctx.rect(smoother.x-5, smoother.y-5, 10,10);
ctx.rect(points[5].x-5, points[5].y-5, 10,10);
ctx.rect(points[6].x-5, points[6].y-5, 10,10);
ctx.moveTo(points[4].x,points[4].y);
ctx.bezierCurveTo(
smoother.x, smoother.y,
points[5].x, points[5].y,
points[6].x, points[6].y
);
You can also run it here by pressing the run button:
https://www.w3schools.com/code/tryit.asp?filename=GSP1RKBFHGGK
At that you can manipulate the pixels in points[], and notice that the bezier curves always connect kinda smoothly.
That's because in each new bezier curve, the system automatically makes the first bezier point, which only serves the role of smoothing the line. Which is basically just a point that continues in whatever direction the previous bezier was heading, for a little bit. The next pixel in the bezier is then an actual destination, which the given bezier curve then takes care of smoothing.
There is the number 3 in there, it represents how quickly you want to start going in the actual direction. If it's too large we start to too quickly head in the needed direction and the smoothness suffers.
If it's too small we are ignoring too much where the line needs to be going, in favor of smoothness.

Set allowed drawing area in JavaScript canvas API

I'm using the JavaScript canvas API for free drawing. I'm stuck at masking the area that is allowed to be drawn on - in my example it should only be the speechbubble area.
I'm using this Vue component: https://github.com/sametaylak/vue-draw/blob/master/src/components/CanvasDraw.vue
draw(event) {
this.drawCursor(event);
if (!this.isDrawing) return;
if (this.tools[this.selectedToolIdx].name === 'Eraser') {
this.canvasContext.globalCompositeOperation = 'destination-out';
} else {
this.canvasContext.globalCompositeOperation = 'source-over';
this.canvasContext.strokeStyle = this.tools[this.selectedToolIdx].color;
}
this.canvasContext.beginPath();
this.canvasContext.moveTo(this.lastX, this.lastY);
this.canvasContext.lineTo(event.offsetX, event.offsetY);
this.canvasContext.stroke();
[this.lastX, this.lastY] = [event.offsetX, event.offsetY];
},
drawCursor(event) {
this.cursorContext.beginPath();
this.cursorContext.ellipse(
event.offsetX, event.offsetY,
this.brushSize, this.brushSize,
Math.PI / 4, 0, 2 * Math.PI
);
this.cursorContext.stroke();
setTimeout(() => {
this.cursorContext.clearRect(0, 0, this.width, this.height);
}, 100);
},
There is a built-in clip() method which sets a path as the clipping region.
var ctx=document.getElementById("cnv").getContext("2d");
ctx.lineWidth=2;
ctx.strokeStyle="red";
ctx.moveTo(0,0);
ctx.lineTo(100,100);
ctx.stroke(); // 1.
ctx.strokeStyle="black";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(100,10);
ctx.lineTo(100,60);
ctx.lineTo(30,60);
ctx.lineTo(10,80);
ctx.closePath();
ctx.stroke(); // 2.
ctx.clip(); // 3.
ctx.strokeStyle="green";
ctx.beginPath();
ctx.moveTo(0,100);
ctx.lineTo(100,0);
ctx.stroke(); // 4.
<canvas id="cnv"></canvas>
red line is drawn between 0,0 and 100,100, without clipping
bubble is drawn in black
bubble is set as clipping region
green line is drawn between 0,100 and 100,0, and correctly clipped into the bubble.
In practice you may want to have the clipping region one pixel inside the bubble, so a separate path (which is not stroke()-d, just clip()-ped), so drawing can not modify the bubble itself. If you zoom in now as it is, you will see that the green line actually overdraws the inner pixels of the bubble (linewidth is 2 pixels, and the outer one is "unharmed").
Establishing if a given point belongs to polygon's area is a quite tricky and solved problem in Computer Science.
In this concrete scenario, where you have canvas with above image set as background and 500x300 dimension you don't really need to use ray casting algorithm.
You can for example divide speech bubble area to a rectangle and a triangle, and then using event.offsetX, event.offsetY check if any given point lies inside of any of these two figures.
Code example:
isPointInArea(event) {
const x = event.offsetX;
const y = event.offsetY;
// For rectangle it is straightforward
if (x >= 60 && x <= 325 && y >= 60 && y <= 215) {
return true;
}
/* Since two sides of this triangle are parallel to canvas
It is enough to check y coordinate with one linear function of a third one
in form of y = ax + b */
if(x >= 60 && x <= 120 && y >= 215) {
const boundaryY = -0.81818181818 * x + 313.181818182;
if (y <= boundaryY) {
return true;
}
}
return false;
}
In draw function of CanvasDraw.vue
draw(event) {
if(!this.isPointInArea(event)) {
return;
}
this.drawCursor(event);
if (!this.isDrawing) return;
...
Working example on codesandbox
Result:
Edit:
As pointed out by #tevemadar you can also use simply clip() method of Canvas API. If there is nothing else you want to render (And since it is not a game, so probably that is the case), then you can just execute clip() once and you are all set. Otherwise remember to use save() method (and then of course restore(), so that you can render stuff also outside of speech bubble clipping region.

coding half a million circles in a pattern

I´ve been trying to make a desktop app (javascript, canvas) and draw 413.280 clickable circles in a certain pattern, but I can´t really figure out how to do it. I´m not convinced canvas is the best solution but I dont know how to solve this and get an app with a reasonable performance.
Here´s the layout I´m trying to get:
circle layout
I want 2 rows of circles within each line. the division in the middle is to be left empty.
Every left row has to be 588 circles.
Every right row has to be 560 circles
There are 180 lines on each side which means there's (588*2*180)= 211680 circles on the left side.
There's (560*2*180)=201600 circles on the right side.
can anyone point me in the right direction, maybe have a clue how I can solve this in the most efficient way possible? Thanks in advance.
EDIT: here's the JSFiddle I've got so far jsfiddle.net/cmxLoqej/2/
JavaScript
window.onload = draw;
function draw() {
var canvas = document.getElementById('canvas');
var c = canvas.getContext('2d');
var ycoordinate = 20;
//draw the line 180 times
for (var x = 1; x <= 180; x++) {
// draw the left side
for (var i = 1; i <= 1; i++){
c.strokeStyle = 'black';
c.moveTo(0,ycoordinate);
c.lineTo(6468,ycoordinate);
c.stroke();
ycoordinate = ycoordinate + 40;
}
}
var ycoordinate = 20;
//draw right side
for (var x = 1; x <= 180; x++) {
for (var j = 1; j <= 1; j++){
c.strokeStyle = 'black';
c.moveTo(6776,ycoordinate);
c.lineTo(canvas.width,ycoordinate);
c.stroke();
ycoordinate = ycoordinate + 40;
}
}
}
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var canvasPattern = document.createElement("canvas");
canvasPattern.width=11;
canvasPattern.height=20;
var contextPattern = canvasPattern.getContext("2d");
contextPattern.beginPath();
contextPattern.arc(5, 10, 5, 0, 2 * Math.PI, false);
contextPattern.strokeStyle = '#003300';
contextPattern.stroke();
var pattern = context.createPattern(canvasPattern,"repeat");
context.fillStyle = pattern;
context.fillRect(0, 20, 6468, 7160);
context.fill();
var canvas2 = document.getElementById('canvas');
var context2 = canvas.getContext('2d');
var canvasPattern2 = document.createElement("canvas");
canvasPattern2.width=11;
canvasPattern2.height=20;
var contextPattern2 = canvasPattern.getContext("2d");
contextPattern2.beginPath();
contextPattern2.arc(5, 10, 5, 0, 2 * Math.PI, false);
contextPattern2.strokeStyle = '#003300';
contextPattern2.stroke();
var pattern2 = context2.createPattern(canvasPattern2,"repeat");
context2.fillStyle = pattern;
context2.fillRect(6776, 20, 6160, 7160);
context2.fill();
HTML
<!DOCTYPE html>
<html>
<body>
<canvas {
id="canvas";
width= "12936" ;
height ="7400";
style= "border: 1px solid black;";
padding: 0;
margin: auto;
display: block;
}>
</canvas>
</body>
</html>
Use fill patterns of circles to create rectangular canvas images of
a single row of the left hand side
a single row of the right hand side
a combined row of each side
a single canvas of 180 rows
Use temporary CANVAS objects along the way as necessary to use the context2D.createPattern method. You should not need to add them to the DOM just to manipulate pixels.
Modify the algorithm if needed as you learn. Happy coding!
Update (edit)
Running the code added to the question shows all circles being evenly spaced horizontally and vertically.
A simpler way of drawing the canvas may be to fill two rectangles that exactly cover the left and right areas of the canvas with the circle pattern, and draw the grid lines on the canvas afterwards instead of before.
Finding the circle clicked
A click event listener on the canvas is passed a mouse event object.
The classical way to determine which circle was clicked was to first perform arithmetic on the screenX and screenY event properties for screen position, window.scrollX and window.scrollY for document scroll amounts, and the position of the canvas within the document, to find where the click occured in the canvas.
Although not yet fully standardized, offsetX and offsetY properties of the mouse event object provide the result directly. The MDN reference shows fairly good cross browser support.
Then knowledge of canvas layout can be used to determine which rectangular circle pattern was clicked, and with a bit of algebra if the click is inside the circle.

Can I optimize this canvas draw call that takes up 30% of my memory profiling?

My game has many Laser objects. mx & my represent velocity. I use the following code to draw a line from behind the Laser 2 pixels to ahead of the Laser in the direction it's going 2 pixels.
Removing the first line of the function adjusted the % of the Profiling by ~1% but I don't like the way it looks. I think I could optimize the drawing by sorting by Linewidth but that doesn't appear to get me much.
How else could I optimize this?
Laser.prototype.draw = function(client, context) {
context.lineWidth = Laser.lineWidth;
context.beginPath();
context.moveTo(this.x - this.mx * 2, this.y - this.my * 2);
context.lineTo(this.x + this.mx * 2, this.y + this.my * 2);
context.strokeStyle = this.teamColor;
context.closePath();
context.stroke();
}
Instead of multiplying things by two, why not add them?
E.g.
context.moveTo(this.x - this.mx - this.mx, this.y - this.my - this.my);
context.lineTo(this.x + this.mx + this.mx, this.y + this.my - this.my);
Testing shows that addition is an order of magnitude faster on an imac over multiplication
https://jsfiddle.net/1c85r2pq/
Dont use moveTo or lineTo as they do not use the hardware to render and are very slow. Also your code is drawing the line twice
ctx.beginPath(); // starts a new path
ctx.moveTo(x,y); // sets the start point of a line
ctx.lineTo(xx,yy); // add a line from x,y to xx,yy
// Not needed
ctx.closePath(); // This is not like beginPath
// it is like lineTo and tells the context
// to add a line from the last point xx,yy
// back to the last moveTo which is x,y
This would half the already slow render time.
A quick way to draw lines using bitmaps.
First at the start create an image to hold the bitmap used to draw the line
function createLineSprite(col,width){
var lineSprite = document.createElement("canvas");
var lineSprite.width = 2;
var lineSprite.height = width;
lineSprite.ctx = lineSprite.getContext("2d");
lineSprite.ctx.fillStyle = col;
lineSprite.ctx.fillRect(0,0,2,width);
return lineSprite;
}
var line = createLineSprite("red",4); // create a 4 pixel wide red line sprite
Or you can use an image that you load.
To draw a line you just need to create a transform that points in the direction of the line, and draw that sprite the length of the line.
// draw a line with sprite from x,y,xx,yy
var drawLineSprite = function(sprite,x,y,xx,yy){
var nx = xx-x; // get the vector between the points
var ny = yy-y;
if(nx === 0 && ny === 0){ // nothing to draw
return;
}
var d = Math.hypot(nx,ny); // get the distance. Note IE does not have hypot Edge does
// normalise the vector
nx /= d;
ny /= d;
ctx.setTransform(nx,ny,-ny,nx,x,y); // create the transform with x axis
// along the line and origin at line start x,y
ctx.drawImage(sprite, 0, 0, sprite.width, sprite.height, 0, -sprite.height / 2, d, sprite.height);
}
To draw the line
drawSpriteLine(line,0,0,100,100);
When you are done drawing all the lines you can get the default transform back with
ctx.setTransform(1,0,0,1,0,0);
The sprite can be anything, this allows for very detailed lines and great for game lasers and the like.
If you have many different colours to draw then create one sprite (image) that has many colour on it, then in the line draw function simply draw only the part of the sprite that has the colour you want. You can stretch out a single pixel to any size so you can get many colours on a small bitmap.

why is my strokeStyle transparent?

I am drawing onto an HTML5 canvas with stroke() and regardless of how or when I set globalAlpha, the stroke is being drawn with some measure of transparency. I'd like for the stroke to be completely opaque (globalAlpha=1). Is there somewhere else where the alpha is being set?
In this jsfiddle, I am drawing a grid of solid black lines onto a canvas. For me, the result shows dots at the intersections, confirming that the lines are partially transparent. Here's the gist of it:
context.globalAlpha=1;
context.strokeStyle="#000";
context.beginPath();
/* draw the grid */
context.stroke();
context.closePath;
The especially weird thing (to me) is that this problem was not occurring in my code before my last computer restart, so I'm guessing there was something hanging around in the cache that was keeping the alpha at my desired level.
I'm obviously missing something here... thanks for any help you can provide.
Real answer :
Each point in a canvas has its center in its (+0.5, +0.5) coordinate.
So to avoid artifacts, start by translating the context by (0.5, 0.5) ,
then round the coordinates.
css scaling creates artifact, deal only with canvas width and height, unless
you want to deal with hidpi devices with webGL, or render at a lower resolution
with both webGL and context2D.
-> in your case, your setup code would be (with NO css width/height set ) :
( http://jsfiddle.net/gamealchemist/x9bTX/8/ )
// parameters
var canvasHorizontalRatio = 0.9;
var canvasHeight = 300;
var hCenterCanvas = true;
// setup
var canvasWidth = Math.floor(window.innerWidth * canvasHorizontalRatio);
var cnv = document.getElementById("myCanvas");
cnv.width = canvasWidth;
cnv.height = canvasHeight;
if (hCenterCanvas)
cnv.style['margin-left'] = Math.floor((window.innerWidth - canvasWidth) * 0.5) + 'px';
var ctx = cnv.getContext("2d");
ctx.translate(0.5, 0.5);
gridContext();
The rest of the code is the same as your original code, i just changed the size of you squares to get quite the same visual aspect.
ctx.beginPath();
for (var i=60; i<canvasHeight; i+=60) {
ctx.moveTo(0,i);
ctx.lineTo(canvasWidth,i);
}
for (i=60; i<canvasWidth; i+=60) {
ctx.moveTo(i,0);
ctx.lineTo(i,canvasHeight);
}
ctx.strokeStyle="#000";
ctx.stroke();
ctx.closePath();
With those changes we go from :
to :
Edit : to ensure rounding, in fact i think most convenient is to inject the context and change moveTo, lineTo :
function gridContext() {
var oldMoveTo = CanvasRenderingContext2D.prototype.moveTo;
CanvasRenderingContext2D.prototype.moveTo = function (x,y) {
x |= 0; y |= 0;
oldMoveTo.call(this, x, y);
}
var oldLineTo = CanvasRenderingContext2D.prototype.lineTo;
CanvasRenderingContext2D.prototype.lineTo = function (x,y) {
x |= 0; y |= 0;
oldLineTo.call(this, x, y);
}
}
Obviously, you must do this for all drawing functions you need.
When drawing lines on a canvas, the line itself is exactly on the pixel grid. But because the line is one pixel wide, half of it appears in each of the pixels to either side of the grid, resulting in antialising and a line that is basically 50% transparent over two pixels.
Instead, offset your line by 0.5 pixels. This will cause it to appear exactly within the pixel.
Demo

Categories

Resources