I have a webpage that contains a canvas object, I should make an animation of a figure pictured by canvas.
On the same area I already have other figures pictured, so I can't use the drawRect function to erase the figure in each loop of the animation. How can I resolve?
Complete code of my webpage:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animazioni con canvas</title>
<link rel="stylesheet" type="text/css" href="stile.css">
<script type="text/javascript" src="codice.js"></script>
</head>
<body>
<canvas id="mycanvas">canvas not supported.</canvas>
<script>
let mycanvas = document.getElementById("mycanvas");
let ctx = mycanvas.getContext("2d");
mycanvas.width = 600;
mycanvas.height = 400;
ctx.fillStyle = "greenyellow";
ctx.fillRect(10,10,100,100);
const step = 5;
let x = 0;
let loop = () => {
ctx.clearRect(0, 0, mycanvas.width, mycanvas.height);
ctx.beginPath();
ctx.arc(x, 200, 50, 0, 2 * Math.PI);
ctx.stroke();
x = x + step;
x === mycanvas.width + 50 && (x = -50)
requestAnimationFrame(loop)
};
requestAnimationFrame(loop)
</script>
</body>
</html>
I'm beginner with the html,css and javascript technologies. I tried different solutions, looking for them on the internet, but I don't know how to proceed, so ask in this website how to do to resolve the problem.
Here are three potential ways of not having to redraw everything. Which one works best for you will depend upon everything else you need to do with the canvas. In each example, there's a background, which is a gradient, and four figures. The bottom right figure is animated and redrawn every frame, the others are not.
The first is in this fiddle. It creates a clipping rectangle around the anmiated figure:
context.beginPath();
context.rect(figureCenter - figureSize, figureCenter - figureSize, figureCenter * 2, figureSize * 2);
context.clip();
By doing that it can call the drawBackground function, which would overwrite everything in the canvas, but only the part of the background behind the figure (inside the clipping rect) is redrawn. Then the animated figure is then drawn. The other figures do not have to be redrawn.
So you can redraw everything that might be in the area you want to update, without worrying that anything that would be drawn outside the updating area. The clipping area does not have to be a square.
The second example is in this fiddle. It has two canvas elements overlaid. The bottom one has the background and the top contains the figures. This allows it to clear the rect around the area that is animated without erasing the background.
context.clearRect(figureCenter - figureSize, figureCenter - figureSize, figureCenter * 2, figureSize * 2);
By using layers like this, you can put animated things on one canvas and static things on other canvases.
Finally there's this fiddle. It uses an OffscreenCanvas, and everything that isn't animated is drawn to it, then the contents of the offscreen canvas are redrawn to the visible canvas every frame:
context.drawImage(offscreenCanvas,0,0);
With this method you can store drawn things to reuse in later frames.
This answer is longer than I intended, but there's no one solution that is best in all situations.
Related
I am creating a game, I need to achieve a perfect canvas line on HTML5 under different types of screen resolutions and zooms.
To easily understand I am talking about, simply paste the two different codes into an HTML file(not jsFiddle, as it is too small to notice):
With fabric.js:
<canvas id = "c" width = "600" height = "300"></canvas>
<script src = "https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<script> var c=document.getElementById("c");
var context=c.getContext("2d");
new fabric.Canvas('c', { selection: false });
context.moveTo(0, 0);
context.lineTo(0, 300);
context.stroke();
</script>
Without fabric.js:
<canvas id = "c" width = "600" height = "300"></canvas>
<script> var c=document.getElementById("c");
var context=c.getContext("2d");
context.moveTo(0, 0);
context.lineTo(0, 300);
context.stroke();
</script>
Now as you can see, fabric.js removes the blurriness that you get under different kind of browser zooms(Mouse wheel) once the page loads.
I have two problems with it though:
1) Once you click on the canvas the line is gone
2) It's a big framework/library, and I only need it to draw lines(Maybe not if it can achieve the same thing with PNG images)
So, is there a way to achieve the same sharpness result with a clean, short javascript code, without using fabric.js?
If not, how can I fix the clicking problem?
Thanks.
All lines drawn on the canvas are automatically given anti-aliasing to lessen the visual effect of "jaggies". This anti-aliasing also makes the line appear blurry.
If you ONLY are drawing horizontal and vertical lines you can make them crisp:
Before drawing the lines, context.translate(0.50,0.50),
Draw the lines using only integer coordinates,
After drawing the lines, context.translate(-0.50,-0.50),
If you are drawing non-horizontal and non-vertical lines, then you can use Bresenhan's Line Algorithm to draw crisp lines on the canvas by drawing lines pixel-by-pixel. This previous Q&A has example code using Bresenhan's algorithm.
In html5, when you draw to a canvas using putImageData(), if some of the pixels you are drawing are transparent (or semi-transparent), how do you keep old pixels in the canvas unaffected?
example:
var imgData = context.createImageData(30,30);
for(var i=0; i<imgData.data.length; i+=4)
{
imgData.data[i]=255;
imgData.data[i+1]=0;
imgData.data[i+2]=0;
imgData.data[i+3]=255;
if((i/4)%30 > 15)imgData.data[i+3] = 0;
}
context.putImageData(imgData,0,0);
The right half of the 30x30 rect is transparent.
If this is drawn over something on the canvas, pixels behind the right half are removed (or become thransparent). How do I keep them?
You can use getImageData to create a semi-transparent overlay:
create a temporary offscreen canvas
getImageData to get the pixel data from the offscreen canvas
modify the pixels as you desire
putImageData the pixels back on the offscreen canvas
use drawImage to draw the offscreen canvas to the onscreen canvas
Here's example code and a Demo: http://jsfiddle.net/m1erickson/CM7uY/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var context=canvas.getContext("2d");
// draw an image on the canvas
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/stack1/landscape1.jpg";
function start(){
canvas.width=img.width;
canvas.height=img.height;
context.drawImage(img,0,0);
// overlay a red gradient
drawSemiTransparentOverlay(canvas.width/2,canvas.height)
}
function drawSemiTransparentOverlay(w,h){
// create a temporary canvas to hold the gradient overlay
var canvas2=document.createElement("canvas");
canvas2.width=w;
canvas2.height=h
var ctx2=canvas2.getContext("2d");
// make gradient using ImageData
var imgData = ctx2.getImageData(0,0,w,h);
var data=imgData.data;
for(var y=0; y<h; y++) {
for(var x=0; x<w; x++) {
var n=((w*y)+x)*4;
data[n]=255;
data[n+1]=0;
data[n+2]=0;
data[n+3]=255;
if(x>w/2){
data[n+3]=255*(1-((x-w/2)/(w/2)));
}
}
}
// put the modified pixels on the temporary canvas
ctx2.putImageData(imgData,0,0);
// draw the temporary gradient canvas on the visible canvas
context.drawImage(canvas2,0,0);
}
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=200 height=200></canvas>
</body>
</html>
Alternatively, you might check out using a linear gradient to do your effect more directly.
http://jsfiddle.net/m1erickson/j6wLR/
Problem
As you know, your statement
if((i/4)%30 > 15)imgData.data[i+3] = 0;
will make pixels on the right half of the image be transparent, so that any other object on the page behind the canvas can be seen through the canvas at that pixel position. However, you are still overwriting the pixel of the canvas itself with context.putImageData, which replaces all of its previous pixels. The transparency that you add will not cause the previous pixels of to show through, because the result of putImageData is not a second set of pixels on top of the previous pixels in the canvas, but rather the replacement of existing pixels.
Solution
I suggest that you begin your code not with createImageData which will begin with a blank set of data, but rather with getImageData which will give you a copy of the existing data to work with. You can then use your conditional statement to avoid overwriting the portion of the image that you wish to preserve. This will also make your function more efficient.
var imgData = context.getImageData(30,30);
for(var i=0; i<imgData.data.length; i+=4)
{
if((i/4)%30 > 15) continue;
imgData.data[i]=255;
imgData.data[i+1]=0;
imgData.data[i+2]=0;
imgData.data[i+3]=255;
}
context.putImageData(imgData,0,0);
Something that tripped me up that may be of use... I had problems with this because I assumed that putImageData() and drawImage() would work in the same way but it seems they don't. putImageData() will overwrite existing pixels with its own transparent data while drawImage() will leave them untouched.
When looking into this I just glanced at the docs for CanvasRenderingContext2D.globalCompositeOperation (should have read more closely), saw that source-over is the default and didn't realise this would not apply to putImageData()
Drawing into a temporary canvas then and using drawImage() to add the temp canvas to the main context was the solution I needed so cheers for that.
I wanted to copy a CRISP, un modified version of the canvas on top of itself. I eventually came up with this solution, which applies.
https://jsfiddle.net/4Le454ak/1/
The copy portion is in this code:
var imageData = canvas.toDataURL(0, 0, w, h);
var tmp = document.createElement('img');
tmp.style.display = 'none'
tmp.src = imageData;
document.body.appendChild(tmp);
ctx.drawImage(tmp, 30, 30);
What's happening:
copy image data from canvas
set image data to a non-displayed <img> (<img> has to be in dom though)
draw that image back onto the canvas
you can delete or reuse the <img> at this point
It is an old question, but I had a similar issue and came up with another solution that fits me better (similar to #popClingwrap's answer, but I'll elaborate a bit more). I have a WebWorker and I want it to copy and paste an svg file multiple times in an existing canvas. If the source of your ImageData is another Canvas, and you want to copy the data to another canvas, there is an easier way than manipulating pixel values in a loop. the ctx.drawImage() function does overlay images respecting transparency and can also take another canvas as source.
So I used Canvg to create a source canvas containing my source image ( For your application this will look different)
const cnv = new OffscreenCanvas(100, 100);
const loadCanvas = async () => {
const v = await Canvg.from(cnv.getContext("2d"), src, preset);
await v.render();
};
For your example this would probably look something like this
var cnv = document.createElement('canvas');
var ctx = cnv.getContext('2d');
cnv.width = 30;
cnv.height = 30;
ctx.putImageData(imgData, 0, 0);
And then you can draw this transparent image on top of an existing image as often as needed with:
ctx.drawImage(cnv, 0, 0);
I'm new to EaselJS and I've been having trouble positioning shapes correctly on the stage for days now. It's only today that I discovered that, while I set the canvas width and height to 800px and 450px respectively, easeljs seems to assume that the width is 400px and the height 225px (half of the set values).
I'm pretty sure I must be doing something wrong so could someone please look at this code and see what's causing the issue:
index.html
<!doctype html>
<html>
<head>
<title>Ludacris Labs</title>
<meta charset='utf-8'></meta>
<link rel='stylesheet' type='text/css' href='css/index.css'/>
<script type='text/javascript' src='//code.jquery.com/jquery-1.10.2.min.js'></script>
<script type='text/javascript' src="http://code.createjs.com/easeljs-0.6.0.min.js"></script>
<script type='text/javascript' src='js/TestTube.js'></script>
<script type='text/javascript' src='js/index.js'></script>
</head>
<body>
<canvas id='canvas' width='800' height='450'>
This application will not run on your browser because your browser is out of date.
</canvas>
</body>
</html>
index.css
#canvas{
border: 1px solid #cccccc;
}
index.js
function init()
{
var stage = new createjs.Stage("canvas");
var dimensions = {
width: $('#canvas').width(),//800
height: $('#canvas').height()//450
};
var solution = new TestTube('water','blue');
stage.addChild(solution);
solution.x = dimensions.width/2-100;//300
solution.y = 0.44*dimensions.height/2;//99
solution.width = 50;
solution.height = 0.55*dimensions.height;//247.5
solution.level = 90;
solution.render();
stage.update();
}
$(document).ready(function(){
init();
});
TestTube.render
TestTube.prototype.render = function(){
console.log(this.name,this.x,this.y,this.level);
this.graphics.clear();
this.graphics.setStrokeStyle(2,1,1);
//set stroke color black.
this.graphics.beginStroke("#000000");
//set fill color
this.graphics.beginFill(this.color);
//START DRAWING------------------------------------
//move to origin.
this.graphics.moveTo(this.x,this.y);
//draw line uptil point of liquid in test tube [liquid start point]
_level = (100 - this.level)/100;
this.graphics.lineTo(this.x,this.y+_level*this.height);
//draw line uptil bottom of test tube.
this.graphics.lineTo(this.x,this.y+(this.height-this.width/2));
//draw the round part of test tube.
//graphics.arc(centerX,centerY,radius,startAngle,endAngle,anticlockwise);
this.graphics.arc(this.x + this.width/2,this.y+(this.height-this.width/2),this.width/2,Math.PI,0,true);
//go back to upto level of liquid in tube [liquid end point]
this.graphics.lineTo(this.x+this.width,this.y+_level*this.height);
//connect liquid start point with liquid end point to fill in liquid colour.
this.graphics.lineTo(this.x,this.y+_level*this.height);
this.graphics.endFill();
//go back to liquid end point
this.graphics.moveTo(this.x+this.width,this.y+_level*this.height);
//draw the rest of the test tube.
this.graphics.lineTo(this.x+this.width,this.y);
//stop drawing.
this.graphics.endStroke();
}
Screenshot
Based on the code on index.js, you'd expect the test tube to appear near the centre of the canvas, but it appears on the bottom-right because the canvas size used is half of the canvas size set.
Your issue is with the drawing-coordinates inside your shape:
Each object (Bitmap, Shape, MovieClip, ect.) in EaselJS has its' own coordinate-space.
That means: If you place the shape at: 100|100 AND draw a dot at this.x|this.y (100|100) your dot will appear on the stage at 200|200.
What you should do instead is to use 0|0 as your base and not this.x|this.y.
//move to origin.
this.graphics.moveTo(0,0);
//draw line uptil point of liquid in test tube [liquid start point]
_level = (100 - this.level)/100;
this.graphics.lineTo(0,0+_level*this.height);
//...and so on...
Trivia
The benefits of that are: If you remove the Shape and add it to a different container, it will be automatically positioned relative to that new containers' position.
Also: You do not have to worry about the absolut position of an object, this comes in very handy with more complex constructs with multiple shapes and containers. You only need to know the position relative to the parent-container.
I have currently two circles in a <canvas> tag with HTML5 & JavaScript.
Now I'm trying to add an image (done) that changes based on mouse-over and click.
It's basically an implementation of a play / pause button with an extra color change when the user mouse-overs the button.
I can't seem to figure out how events work on shapes in HTML5 since they are not objects ... Here is my code at the moment :
window.onload = function() {
var canvas = document.getElementsByTagName('canvas')[0];
var context = canvas.getContext('2d');
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
//Outer circle
context.beginPath();
context.arc(centerX, centerY, 150, 0, Math.PI * 2, false);
context.fillStyle = "#000";
context.fill();
context.stroke();
//Inner cicle
context.beginPath();
context.arc(centerX, centerY, 75, 0, Math.PI * 2, false);
context.fillStyle = "#fff";
context.fill();
context.stroke();
//Play / Pause button
var imagePlay = new Image();
var imageHeight = 48/2;
imagePlay.onload = function() {
context.drawImage(imagePlay, centerX - imageHeight, centerY - imageHeight);
};
imagePlay.src = "images/play.gif";
}
How to handle events on shapes created with <canvas>?
How to clean-up / remove images on the <canvas> when replacing it with another one?
There is technically no way to register mouse events on canvas-drawn shapes. However, if you use a library, like Raphael (http://raphaeljs.com/), it can keep track of shape positions and thus figure out what shape is receiving the mouse event. here's an example:
var circle = r.circle(50, 50, 40);
circle.attr({fill: "red"});
circle.mouseover(function (event) {
this.attr({fill: "red"});
});
As you can see, it's very simple this way. For modifying shapes, this library will also come in handy. Without it you would need to remember how to redraw everything each time you make a change
Well The simple answer is you can't. You either will have to find the coordinates of the click event and calculate whether you want to perform an option or not or you can use area and map tags and overlay the canvas element with it. To change a canvas use the clearRect function to draw paint a rectangle over everything and then redraw what you want.
There is no "built-in" way of keeping track of shapes drawn on the canvas. They are not treated as objects, but rather just pixels in an area. If you want to handle events on shapes drawn on the canvas, you would need to keep track of the area each shape covers, and then determine which shape you're triggering the event for based on the mouse position.
You can just draw over other shapes if you want to replace it with something. You might want to take a look at globalCompositeOperation.
If you want to treat your drawings as objects, I would recommend using SVG instead of canvas.
Another option is to use buttons, and then style them using CSS.
Basically, what you're doing now really wasn't the intended purpose or use of the canvas. It's like using a pencil to hammer in nails - you're using the wrong tool for the job.
While it's true that you cannot create click events for objects drawn on the canvas there is a workaround: Wrap the canvas in a DIV tag and then add the images within the DIV tag above the CANVAS tag.
<div id="wrapper">
<img src="img1.jpg" id="img1"></img>
<canvas id="thecanvas"></canvas>
</div>
Then use CSS to make the images position:absolute and use left:*px and top:*px attributes to place the image over the canvas where you would have normally drawn it.
#img1{
position:absolute;
left: 10px;
top: 10px;
}
You can then add click events to the image which is placed over the canvas giving the impression that you are clicking on the canvas(the below example uses the jQuery click() function)
$( "#img1" ).click(function(){
alert("Thanks for clicking me");
});
You can cast a ray into the canvas and manually test your images for intersection with the ray. You should look at it like you press, and send a ray into the screen. You should write a
objectsUnderPoint( x, y );
function that returns an array of all the images that intersect with the ray at x, y.
This is the only real right answer, and this is how it is usually done in 3D engines as well.
I'm trying to build a puzzle game in javascript, using raphael to take care of the drag and drop and rotation of the pieces.
The problem I'm facing is: Whenever I rotate an image, its coordinate system is rotated as well and the drag and drop doesn't work anymore as expected.
Edit2: Here is a simple test case. Drag around the ellipse, rotate it, and then try to drag again:
http://www.tiagoserafim.com/tests/dragdrop.html
Edit: To clarify, whenever I move 1 pixel with the mouse to the right (x++), the image also moves 1 pixel on the x-coord, but on its own coordinate system, that maybe rotate, as the image below shows.
(source: oreilly.com)
As explained on SVG Essentials, this is the expected behavior.
My question is: Is there an elegant way to do what I want, or I'll be forced to manually calculate the correct coords by using rotation matrix?
Other JS libraries or suggestions will be very welcome, even if they mean losing the IE support.
As also noted in that article, the order of transformations is important.
Translate object so the center point (Or whatever other point you want to rotate around) is at 0,0
Rotate
Translate back to previous position
Also note that there is an overload of rotate that already does this.
<html>
<head>
<script type="text/javascript" src="http://github.com/DmitryBaranovskiy/raphael/blob/master/raphael-min.js?raw=true"></script>
<script type="text/javascript">
function Draw()
{
var x = 150, y = 150;
var rotation = 0;
var paper = Raphael(0, 0, 800, 800);
var e = paper.ellipse(x, y, 30, 10);
paper.path("M150 150L800 150");
window.setInterval(function()
{
x += 10;
rotation += 10;
e.translate(10, 0);
e.rotate(rotation, x, y);
}, 500);
}
</script>
</head>
<body onload="Draw()">
</body>
</html>