I have a paint-style application that works with touch events.
The JavaScript code is...
var RADIUS = 10;
var ctx = document.querySelector("canvas").getContext("2d");
var getRandomColorFragment = function () {
return Math.floor(Math.random() * 255);
};
document.body.addEventListener("touchstart", function (event) {
ctx.fillStyle = "rgb(" + [getRandomColorFragment(), getRandomColorFragment(), getRandomColorFragment()].join() + ")";
});
document.body.addEventListener("touchmove", function (event) {
// Prevent default behavior of scrolling elements.
event.preventDefault();
// Get a reference to the first touch placed.
var touch = event.touches[0];
// Draw a circle at the location of the finger.
ctx.beginPath();
ctx.arc(touch.pageX - RADIUS, touch.pageY - RADIUS, RADIUS, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
});
jsFiddle.
You can test this on a platform that doesn't support touch events by using Chrome and opening the Web Inspector's settings and choosing Emulate touch events.
When the finger/pointer moves very fast, it fails to paint the canvas continually like which would be expected (see screenshot above). It seems I can only get the touches' coordinates only as fast as the touchmove event is triggered.
I thought that I could determine if there is a big enough gap between arcs using the Distance Formula (c2 = a2 + b2) and determining if it's larger than the RADIUS constant. This part worked well. The next thing I needed to figure out is how to interpolate between the two points: the previous registered touch's coordinates and the newly registered coordinates'.
So, essentially, I'd like to know how I could interpolate between the two points I have to determine how to fill-in the gap.
You want to draw a line from last mouse position to the new mouse postition. But you are drawing a circle at the new position instead. Why?
Why don't you do it this way?
onTouchStart: ctx.moveTo(x,y);
onTouchMove: ctx.lineTo(x,y);
All interpolation, antialiasing etc. is already included in "lineTo" function. Use ctx.lineCap = "round"; to have round corners.
If you still want to interpolate numbers, here is the function:
function interpolate(a, b, frac) // points A and B, frac between 0 and 1
{
var nx = a.x+(b.x-a.x)*frac;
var ny = a.y+(b.y-a.y)*frac;
return {x:nx, y:ny};
}
interpolate({x:2, y:2}, {x:4,y:4}, 0.5); // returns {x:3, y:3}
Related
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.
I'm trying to build an app where user can add various objects (rectangles, circles) and he can use mouse wheel to zoom-in and zoom-out.
For this zooming I set up event handler like this:
TheCanvas.on('mouse:wheel', function(options){
var p = new fabric.Point(
options.e.clientX,
options.e.clientY
);
var direction = (options.e.deltaY > 0) ? 0.9 : 1.1;
var newZoom = TheCanvas.getZoom() * direction;
// restrict too big/small zoom here:
if ((newZoom > 50) || (newZoom < 0.7)) return false;
TheCanvas.zoomToPoint( p, newZoom );
}
Everything worked fine until now. Now I want to draw a crosshair over all objects on the canvas. Something like this:
So I made my own custom object like:
CrossHairClass = fabric.util.createClass(fabric.Object, {
strokeDashArray: [1,2], // I want lines to be dashed
........
My problem is:
When user zooms with the mouse wheel, my cross-hair lines zoom their thickness too and also small dashes get bigger. But I don't want that. I want my cross-hair lines be a "hair" lines = ideally 1 pixel thick all the time regardless zoom factor of the canvas. And fine dashed line too.
Render function of my Class:
_render: function (ctx) {
// I tried it like this
var zoom = TheCanvas.getZoom();
var scale = (1/zoom) * 3.333; // with this scale it visually looked the best
// I have to scale it in X and Y while I want small dashes to stay small and also thickness of the line to stay "hair-line"
this.scaleX = this.scaleY = scale;
this.width = CROSSHAIR_SIZE / scale; // my constant from elsewhere
ctx.lineWidth = 1;
ctx.beginPath();
// this example is for horizontal line only
ctx.moveTo(-this.width / 2, 0);
ctx.lineTo(this.width / 2, 0);
this._renderStroke(ctx);
}
I tried various combinations of multiplying or dividing by scale factor or zoom factor but if I finally had lines thin, I couldn't keep their size, which must be constant (in pixels) regardless of canvas zoom. Please help.
P.S.: now I got an idea. Maybe I should create another canvas, over my current canvas and draw this crosshair on the upper canvas, which will not zoom?
EDIT 1
Based on the answer from #andreabogazzi I tried various approaches, but this finally worked out! Thanks! :)
_render: function (ctx) {
var zoom = TheCanvas.getZoom();
// ctx.save(); // this made no difference
// ctx.setTransform(1/zoom, 0, 0, 1/zoom, 0, 0); // this didn't work
this.setTransformMatrix([1/zoom, 0, 0, 1/zoom, 0, 0]);
ctx.strokStyle = 'red';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-this.widthHalf, 0); // widthHalf computed elsewhere
ctx.lineTo(this.widthHalf, 0);
this._renderStroke(ctx); // I use this instead of ctx.stroke() while this ensures my line is still nicely dashed
// ctx.restore(); // this made no difference
}
Since you created a custom class, you have to invert the zoom of your canvas before drawing.
On the _render function of your subclass, since you should be positioned in the center of your crosshair, apply a transform matrix of scale type, with scale factor of 1/zoomLevel and everything should work.
I would say the correct way is:
_render: function (ctx) {
var zoom = TheCanvas.getZoom();
ctx.save(); // this is done anyway but if you add custom ctx transform is good practice to wrap it in a save/restore couple
ctx.transform(1/zoom, 0, 0, 1/zoom, 0, 0);
ctx.strokStyle = 'red';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-this.widthHalf, 0); // widthHalf computed elsewhere
ctx.lineTo(this.widthHalf, 0);
this._renderStroke(ctx); // I use this instead of ctx.stroke() while this ensures my line is still nicely dashed
ctx.restore(); // this is done anyway but if you add custom ctx transform is good practice to wrap it in a save/restore couple
}
Now it happens that this object get cached from the fabricJS cache system that will probably create the cache depending on the canvas zoom too.
I have no understanding of the final use of this object, but you should include this calculation also in the cache canvas size calculation.
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;
}
}
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.
I have short code, that draws circles (orbits) dots (satellites) on these orbits. Satellites are moving around orbits. In fact code isn't mine, but I was asked to solve the problem.
According to profiler in chrome and firefox, function drawSatellite eats 50%-100% cpu and I would like to know why.
Canvas is same big as your window (1920x1080). There are around 160 orbits (increasing with time page is online).
This is drawSatellite:
OrbitBackground.prototype.drawSatellite = function(ctx, satellite) {
ctx.fillStyle = satellite.satellite.fill;
ctx.beginPath();
if (++satellite.satellite.angularPosition == 360)
satellite.satellite.angularPosition = 0;
// 1 FPS = 60 calls => 180 / 6 (6-times faster # 60 FPS) = 30
var radians = satellite.satellite.angularPosition * Math.PI / 30 / satellite.rps;
if (satellite.backward)
radians = -radians;
ctx.arc(
satellite.satellite.x + satellite.orbit.radius * Math.cos(radians),
satellite.satellite.y + satellite.orbit.radius * Math.sin(radians),
satellite.satellite.radius,
0,
Math.PI*2,
true
);
ctx.closePath();
ctx.fill();
};
Function that calls it:
OrbitBackground.prototype.drawFrame = function() {
if (this.running)
requestAnimationFrame(this.drawFrame.bind(this));
this.dynamicStageCtx.clearRect(0, 0, this.pageWidth, this.pageHeight);
for (var i=0; i < this.orbits.length; i++) {
this.drawSatellite(this.dynamicStageCtx, this.orbits[i]);
}
};
You're doing this:
Loop:
set fill style
begin path
make path
end path
fill
You would be much faster doing this:
set fill style (just once, before loop)
begin path (just one path, with loop-number of subpaths)
Loop:
moveTo (start of subpath)
make path
close path
fill (just once, after loop)
But that requires the fill style to be the same on every satellite. If there are only a few colors, you could try to bunch them by their colors.
Note also that calcluating cosine and sine are slow (all trig functions and square root calls are slow), and if you can avoid their use you will be better off.
The size (pixel count) of your canvas also matters. Consider making the canvas half-size or quarter-size (960x540 or 480x270) and scaling it up with CSS.
Might be a problem:
I don't see a ctx.beginPath before your ctx.arc command.
Without ctx.beginPath all your previous arcs are being redrawn along with your current arc.
A small optimization
Assign Math.PI*2 to a variable since it's used very often
var PI2=Math.PI*2;
How to eliminate the slowest part of your code ( Math.cos and Math.sin ).
Since your nodes are going in repeating orbits, you can precalculate all the untranslated [x,y] for a complete orbit.
var statellite.orbitTrig=[];
for(var i=0;i<360;i++){
var radians=PI2/360*i;
var x=satellite.orbit.radius * Math.cos(radians)
var y=satellite.orbit.radius * Math.sin(radians)
satellite.orbitTrig.push({x:x,y:y});
}
Then you can refer to the precalculated values in your animation loop.
var trig=satellite.orbitTrig[satellite.satellite.angularPosition];
var x=satellite.satellite.x + trig.x;
var y=satellite.satellite.y + trig.y;
ctx.beginPath();
ctx.arc(x,y,satellite.satellite.radius,0,PI2);
ctx.closePath();
ctx.fill();