I am trying to add a normal up and down motion to my two lines drawn in html5 using canvas . but i am unable to..
Kindly suggest me a way to overcome this and is there any better way to do this.
I am adding the function here
function create_blades()
{
canvas_context.moveTo(60+x_pos,80);
canvas_context.lineTo(125+x_pos,40+y_factor);
canvas_context.lineWidth = 3;
canvas_context.stroke();
canvas_context.moveTo(60+x_pos,110);
canvas_context.lineTo(125+x_pos,60-y_factor);
canvas_context.lineWidth = 3;
canvas_context.stroke();
//motion();
}
You are not describing the motion so here is a generic approach to animate lines (or any objects).
Update positions
Check boundaries/criterias
Clear canvas
Redraw shape
Loop
Here is a live example.
var xpos = 0, // holds current position (here, actually delta to fixed pos)
ypos = 0,
dltY = 2, // delta values
dltX = 2;
(function loop() {
xpos += dltX; // add delta values
ypos += dltY;
// check boundaries, here canvas edges
if (xpos < 0 || xpos > canvas.width) dltX = -dltX;
if (ypos < 0 || ypos > canvas.height) dltY = -dltY;
// redraw graphics
create_blades(xpos, ypos);
// loop
requestAnimationFrame(loop);
})();
Then the draw function:
function create_blades(x_pos, y_factor) {
// clear canvas
canvas_context.clearRect(0, 0, canvas.width, canvas.height);
canvas_context.beginPath(); // need this first
canvas_context.moveTo(60 + x_pos, 80);
canvas_context.lineTo(125 + x_pos, 40 + y_factor);
//canvas_context.lineWidth = 3; // not needed here
//canvas_context.stroke();
canvas_context.moveTo(60 + x_pos, 110);
canvas_context.lineTo(125 + x_pos, 60 - y_factor);
canvas_context.lineWidth = 3;
canvas_context.stroke();
//motion();
}
As you are using fixed values in the draw position the passed arguments with positions will of course just add to that, but you should be able to see the principle in how you move the shape.
Related
I've been attempting to create a triangle shaped canvas for a day or so now and I'm having no luck. The canvas is always square/rectangle. I'm using FabricJS but I can also manipulate the canvas directly if that's an option.
I've attempted using .clipTo(ctx) to clip the canvas as described here: Canvas in different shapes with fabricjs plugin
I've also attempted manipulating the canvas directly as I saw here: https://www.html5canvastutorials.com/tutorials/html5-canvas-custom-shapes/
What I'm trying to accomplish is for a user to drag-drop images onto a triangle shaped canvas so there's no "bleed" of the image outside the triangle shape. I accomplished this easily with a rectangle but I can't figure out how to change the canvas shape. OR if anyone has a "trick" solution that would look like the canvas was a triangle but under the hood remain a square, that would work as well.
Using pure API's
I don't use fabric (especially if its just for simple image manipulation) so you will have to locate the appropriate fabric functions to match this answer.
The canvas is always 4 sided. 2D and 3D transforms can change the shape but that also changes the shape of the contained pixels.
You have 2 simple options. There are other ways to do this but they are complex and have compatibility issues.
Visual only
Masking
To get the appearance of irregular shaped canvas you can use a mask (second canvas has mask). Draw the content to the main canvas and then mask that canvas with the mask.
Use the property CanvasRenderingContext2D.globalCompositeOperation to define how the mask is applied.
eg
function createTriangleMask(w, h) {
const mask = document.createElement("canvas");
mask.width = w;
mask.height = h;
mask.ctx = mask.getContext("2d");
mask.ctx.beginPath();
mask.ctx.lineTo(w / 2, 0);
mask.ctx.lineTo(w , h);
mask.ctx.lineTo(0 , h);
mask.ctx.fill();
return mask;
}
const mask = createTriangleMask(ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(myImg, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(mask, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = "source-over";
Using 2D clip
Or you can use the 2D API CanvasRenderingContext2D.clip to create a clip region and draw the content while the clip is active. Don't forget to pop the 2D state when done with the clip,
function triangleClip(ctx, w, h) {
ctx.save();
ctx.beginPath();
mask.ctx.lineTo(w / 2, 0);
mask.ctx.lineTo(w , h);
mask.ctx.lineTo(0 , h);
ctx.clip();
}
triangleClip(ctx, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(myImg, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.restore(); // Turn off clip. Must do before calling triangle clip again.
Still rectangular!
This has not changed the canvas shape. It is still a rectangle, just that some pixels are transparent. The DOM still sees a rectangle and user interactions with the canvas will still use the whole rectangular canvas.
CSS clip-path
You can use the style property clip-path to define the shape of a element. This will clip the elements visual content and the elements interactive area. Effectively turning any applicable element to an irregular shaped element.
Using JS Declarative
canvas.style.clipPath = "polygon(50% 0, 100% 100%, 0% 100%)"
Using JS
function clipElement(el, shape) {
var rule = "polygon(", i = 0, comma = "";
while (i < shape.length) {
rule += comma + shape[i++] + "% " + shape[i++] + "%";
comma = ",";
}
el.style.clipPath = rule + ")";
}
clipElement(canvas, [50, 0, 100, 100, 0, 100]);
Using CSS rule
canvas {
clip-path: polygon(50% 0, 100% 100%, 0% 100%);
}
With the clipped path in place the canvas will obey its shape via UI
canvas.style.cursor = "pointer"; // Pointer change only inside clipped area
canvas.title = "foo"; // appears only when over clipped area
canvas.addEventListener("mouseover", () => console.log("foo")); // fires when crossing
// clip boundary
Demo
Creates an animated clip via JS on the canvas element with content rendered once.
There are limitations
Note that the CSS defined background color (yellow) and shadow are also clipped. Many other visual properties will also be clipped.
Note that JS animation does not update UI events if there are no intervening user iteration.
The animation can also be achieved via CSS.
Compatibility with fabric is unknown to me, check their documentation.
var clearConsole = 0;
const s = 2 ** 0.5 * 0.25, clipPath = [0.5, 0, 0.5 + s, 0.5 + s, 0.5 - s, 0.5 + s], img = new Image;
img.src = "https://i.stack.imgur.com/C7qq2.png?s=328&g=1";
img.addEventListener("load",() => canvas.getContext("2d").drawImage(img, 0, 0, 300, 300), {once: true});
requestAnimationFrame(animateLoop);
function clipRotate(el, ang, scale, path) {
const dx = Math.cos(ang) * scale;
const dy = Math.sin(ang) * scale;
var clip = "polygon(", i = 0, comma = "";
while (i < path.length) {
const x = path[i++] - 0.5;
const y = path[i++] - 0.5;
clip += comma;
clip += ((x * dx - y * dy + 0.5) * 100) + "% ";
clip += ((x * dy + y * dx + 0.5) * 100) + "%";
comma = ",";
}
el.style.clipPath = clip + ")";
}
function animateLoop(time) {
clipRotate(canvas, time / 1000 * Math.PI, 0.9, clipPath);
requestAnimationFrame(animateLoop);
if (clearConsole) {
clearConsole --;
!clearConsole && console.clear();
}
}
canvas.addEventListener("pointerenter", () => (clearConsole = 30, console.log("Pointer over")));
body {
background-color: #49C;
}
canvas {
cursor: pointer;
background-color: yellow;
box-shadow: 12px 12px 4px rgba(0,0,0,0.8);
}
<canvas id="canvas" width="300" height="300" title="You are over the clipped canvas"></canvas>
I'm trying to draw in a canvas in a react component. The component draws a line and a number of squares depending on the length of an array passed to it as props inclining rotating all of them depending on another prop.
I have a loop that draws it perfectly until it reaches the 5th iteration, then something happens and it start to mess with the context restoration after the rotation. There is only one change of value in that loop ( initialX) Debugging the loop in the browser the rotate method is called the same times as the restore. I'm really confused by this behaviour, it is a very simple draw and I can't see where is my mistake.
This is what I'm getting
This is the same sketch without applying rotation
class Sketch extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
let canvas = document.getElementById("plano");
let detector = this.props.detector
let X, Y;
if (canvas && canvas.getContext && detector) {
inicializarCanvas(detector);
function inicializarCanvas(detector) {
let ctx = canvas.getContext("2d");
let s = getComputedStyle(canvas);
let w = s.width;
let h = s.height;
canvas.width = w.split("px")[0];
canvas.height = h.split("px")[0];
X = canvas.width / 2;
Y = canvas.height / 2;
//draw beam
ctx.moveTo( canvas.width / 3, canvas.height / 2);
ctx.lineTo(0, canvas.height / 2);
ctx.strokeStyle = "#f00";
ctx.stroke();
ctx.restore();
ctx.restore();
ctx.save();
drawBlades(ctx, canvas.width, canvas.height, detector)
}
function drawBlades(ctx, x, y, detector) {
let initialX = x / 3
let initialY = y / 4
let thick = 20
let margin = 5
let rotation = (90 - detector.angle) * Math.PI / 180
let i = 0
ctx.save();
let canvas = document.getElementById("plano");
let ctx2 = canvas.getContext("2d");
ctx2.save();
console.log("blade draw")
//This loop is messing up at the 5th iteration
for (; i < detector.blades.length; i++) {
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}
ctx2.save()
}
}
}
render() {
return (
<div className='sketch'>
<canvas width="400" height="150" id="plano">
Canvas not compatible with your browser
</canvas>
</div>
)
}
};
You're restoring your context in each iteration but you don't save it.
Try to add a ctx2.save() and it will work.
for (; i < detector.blades.length; i++) {
ctx2.save(); // save the context
ctx2.strokeStyle = "#000000";
ctx2.translate(initialX, initialY);
ctx2.rotate(rotation);
ctx2.strokeRect(0, 0, thick, y / 2);
ctx2.restore()
// this is the only variable in that changes of
// value in the loop
initialX = margin + thick + initialX
}
I'm very new to Javascript and I've started a simple game. I want the character's gun to rotate to follow the mouse. So far, movement and everything else works fine, except that when I added the rotation functionality the character seems to rotate in a huge circle around the screen. Here's the jsfiddle: https://jsfiddle.net/jvwr8bug/#
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
var mouseX = evt.clientX - rect.top;
var mouseY = evt.clientY - rect.left;
return {
x: mouseX,
y: mouseY
};
}
window.addEventListener('load', function() {
canvas.addEventListener('mousemove', function(evt) {
var m = getMousePos(canvas, evt);
mouse.x = m.x;
mouse.y = m.y;
}, false);
}, false);
The error seems to be somewhere there but obviously it could be something else
**Edit: Thanks to Blindman67 for the fix.
You were rotating the current transform by rotation each frame. ctx.rotate(a) rotates the current transform so each time it is called you increase the rotation amount by a. Think of it as a relative rotation rather than setting the absolute rotation.
To fix your code replace the canon rendering with
//cannon
//ctx.rotate(rotation); // << you had
// setTransform overwrites the current transform with a new one
// The arguments represent the vectors for the X and Y axis
// And are simply the direction and length of one pixel for each axis
// And a coordinate for the origin.
// All values are in screen/canvas pixels coordinates
// setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY)
ctx.setTransform(1,0,0,1,x,y); // set center of rotation (origin) to center of gun
ctx.rotate(rotation); // rotate about that point.
ctx.fillStyle = "#989898";
ctx.fillRect(15, - 12.5, 25, 25); // draw relative to origin
ctx.lineWidth = 2;
ctx.strokeStyle = "#4f4f4f";
ctx.strokeRect( 15,- 12.5, 25, 25); // draw relative to origin
//body
ctx.fillStyle = "#5079c4";
ctx.beginPath();
ctx.arc(0, 0, size, 0, Math.PI * 2); // draw relative to origin
ctx.fill();
ctx.stroke();
// can't leave the transformed state as is because that will effect anything else
// that will be rendered. So reset to the default.
ctx.setTransform(1,0,0,1,0,0); // restore the origin to the default
And a few more problems to get it working
Just above rendering the canon get the direction to the mouse
// you had coordinates mixed up
// rotation = Math.atan2(mouse.x - y, mouse.y - x); // you had (similar)
rotation = Math.atan2(mouse.y - y, mouse.x - x);
And your mouse event listener is mixing up coordinates and not running very efficiently
Replace all your mouse code with. You don't need onload as the canvas already exists.
canvas.addEventListener('mousemove', function(evt) {
var rect = this.getBoundingClientRect();
mouse.x = evt.clientX - rect.left; // you had evt.clientX - rect.top
mouse.y = evt.clientY - rect.top; // you had evt.clientY - rect.left
}, false);
In trying to find documentation for Canvas context's putImageData() method, I've found things like this:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
(from http://www.w3schools.com/tags/canvas_putimagedata.asp)
According to the documentation I've read, x and y are an index into the source image, whereas dirtyX and dirtyY specify coordinates in the target canvas where to draw the image. Yet, as you'll see from the example below (and JSFiddle) a call to putImageData(imgData,x,y) works while putImageData(imgData, 0, 0, locX, locY) doesn't. I'm not sure why.
EDIT:
I guess my real question is why the top row of the image is black, and there are only 7 rows, not 8. The images should start at the top-left of the Canvas. They DO start at the left (and have 8 columns). Why do they not start at the top?
Answer: that's due to divide by 0 on this line when yLoc is 0:
xoff = imgWidth / (yLoc/3);
The JSFiddle:
http://jsfiddle.net/WZynM/
Code:
<html>
<head>
<title>Canvas tutorial</title>
<script type="text/javascript">
var canvas;
var context; // The canvas's 2d context
function setupCanvas()
{
canvas = document.getElementById('myCanvas');
if (canvas.getContext)
{
context = canvas.getContext('2d');
context.fillStyle = "black"; // this is default anyway
context.fillRect(0, 0, canvas.width, canvas.height);
}
}
function init()
{
loadImages();
startGating();
}
var images = new Array();
var gatingTimer;
var curIndex, imgWidth=0, imgHeight;
// Load images
function loadImages()
{
for (n = 1; n <= 16; n++)
{
images[n] = new Image();
images[n].src = "qxsImages/frame" + n + ".png";
// document.body.appendChild(images[n]);
console.log("width = " + images[n].width + ", height = " + images[n].height);
}
curIndex = 1;
imgWidth = images[1].width;
imgHeight = images[1].height;
}
function redrawImages()
{
if (imgWidth == 0)
return;
curIndex++;
if (curIndex > 16)
curIndex = 1;
// To do later: use images[1].width and .height to layout based on image size
for (var x=0; x<8; x++)
{
for (var y=0; y<8; y++)
{
//if (x != 1)
// context.drawImage(images[curIndex], x*150, y*100);
// context.drawImage(images[curIndex], x*150, y*100, imgWidth/2, imgHeight/2); // scale
// else
self.drawCustomImage(x*150, y*100);
}
}
}
function drawCustomImage(xLoc, yLoc)
{
// create a new pixel array
imageData = context.createImageData(imgWidth, imgHeight);
pos = 0; // index position into imagedata array
xoff = imgWidth / (yLoc/3); // offsets to "center"
yoff = imgHeight / 3;
for (y = 0; y < imgHeight; y++)
{
for (x = 0; x < imgWidth; x++)
{
// calculate sine based on distance
x2 = x - xoff;
y2 = y - yoff;
d = Math.sqrt(x2*x2 + y2*y2);
t = Math.sin(d/6.0);
// calculate RGB values based on sine
r = t * 200;
g = 125 + t * 80;
b = 235 + t * 20;
// set red, green, blue, and alpha:
imageData.data[pos++] = Math.max(0,Math.min(255, r));
imageData.data[pos++] = Math.max(0,Math.min(255, g));
imageData.data[pos++] = Math.max(0,Math.min(255, b));
imageData.data[pos++] = 255; // opaque alpha
}
}
// copy the image data back onto the canvas
context.putImageData(imageData, xLoc, yLoc); // Works... kinda
// context.putImageData(imageData, 0, 0, xLoc, yLoc, imgWidth, imgHeight); // Doesn't work. Why?
}
function startGating()
{
gatingTimer = setInterval(redrawImages, 1000/25); // start gating
}
function stopGating()
{
clearInterval(gatingTimer);
}
</script>
<style type="text/css">
canvas { border: 1px solid black; }
</style>
</head>
<body onload="setupCanvas(); init();">
<canvas id="myCanvas" width="1200" height="800"></canvas>
</body>
</html>
http://jsfiddle.net/WZynM/
You just had your coordinates backwards.
context.putImageData(imageData, xLoc, yLoc, 0, 0, imgWidth, imgHeight);
Live Demo
xLoc, and yLoc are where you are putting it, and 0,0,imgWidth,imgHeight is the data you are putting onto the canvas.
Another example showing this.
A lot of the online docs seem a bit contradictory but for the seven param version
putImageData(img, dx, dy, dirtyX, dirtyY, dirtyRectWidth, dirtyRectHeight)
the dx, and dy are your destination, the next four params are the dirty rect parameters, basically controlling what you are drawing from the source canvas. One of the most thorough descriptions I can find was in the book HTML5 Unleashed by Simon Sarris (pg. 165).
Having been using this recently, I've discovered that Loktar above has hit upon a VERY important issue. Basically, some documentation of this method online is incorrect, a particularly dangerous example being W3Schools, to which a number of people will turn to for reference.
Their documentation states the following:
Synopsis:
context.putImageData(imgData,x,y,dirtyX,dirtyY,dirtyWidth,dirtyHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas
x : The x-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
y : The y-coordinate, in pixels, of the upper-left corner of the ImageData object [WRONG]
dirtyX : Optional. The horizontal (x) value, in pixels, where to place the image on the canvas [WRONG]
dirtyY : Optional. The vertical (y) value, in pixels, where to place the image on the canvas [WRONG]
dirtyWidth : Optional. The width to use to draw the image on the canvas
dirtyHeight: Optional. The height to use to draw the image on the canvas
As Loktar states above, the CORRECT synopsis is as follows:
Correct Synopsis:
context.putImageData(imgData, canvasX, canvasY, srcX ,srcY, srcWidth, srcHeight);
Arguments:
imgData: Specifies the ImageData object to put back onto the canvas (as before);
canvasX : The x coordinate of the location on the CANVAS where you are plotting your imageData;
canvasY : The y coordinate of the location on the CANVAS where you are plotting your ImageData;
srcX : Optional. The x coordinate of the top left hand corner of your ImageData;
srcY : Optional. The y coordinate of the top left hand corner of your ImageData;
srcWidth : Optional. The width of your ImageData;
srcHeight : Optional. The height of your ImageData.
Use the correct synopsis above, and you won't have the problems that have been encountered above.
I'll give a big hat tip to Loktar for finding this out initially, but I thought it apposite to provide an expanded answer in case others run into the same problem.
I have a quadratic curve rendered on a canvas. I want to animate it by means of window.setInterval and changing it's dimensions (note not simply changing it's scale) thereafter.
How do I retain an editable reference to the path after calling context.closePath()?
I'd recommend that you maintained a reference to the path in a new Path object; that way you could modify x, y, points etc on the fly and then render it each animation step.
var testPath = new Path(100, 100, [[40, 40], [80, 80]]);
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
function Path(x, y, points)
{
this.x = x;
this.y = y;
this.points = points;
}
function update()
{
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'red';
ctx.moveTo(testPath.points[0][0], testPath.points[0][1]);
for (var i = 1; i < testPath.points.length; i++)
{
ctx.lineTo(testPath.points[i][0], testPath.points[i][1]);
}
ctx.stroke();
testPath.points[1][1]++; // move down
// loop
requestAnimationFrame(update);
}
update();
For some reason JSFiddle doesn't play nice with Paul Irish's requestAnimationFrame polyfill but it should work locally. I'd definitely recommend this over setInterval.
http://jsfiddle.net/d2sSg/1/