Related
I have an image that move on my cavas at a set speed on the X axis. When I try to rotate it his trejectory changes and he no longer move only on the x axis but also on the Y axis.
How can I rotate the image and not change its course?
If you control the center of rotation, and perform the calculations according to the book (e.g. use Polar coordinates along the way), it should be fine.
Here is an example of a rotating rectangle that moves with its center of mass along a horizontal line:
class Polygon {
constructor(points) {
this.points = points;
this.center = [0, 0]; // center of rotation & positioning
this.rotation = 0;
this.polar = points.map(([x, y]) => [Math.sqrt(x*x + y*y), Math.atan2(y, x)]);
}
refresh(ctx) {
let [x0, y0] = this.center;
this.points = this.polar.map(([dist, angle]) => {
angle += this.rotation;
return [Math.cos(angle) * dist + x0,
Math.sin(angle) * dist + y0];
});
ctx.beginPath();
ctx.moveTo(...this.points[0]);
for (let point of this.points) ctx.lineTo(...point);
ctx.closePath();
ctx.stroke();
}
}
let shape = new Polygon([[-20, -10], [20, -10], [20, 10], [-20, 10]]);
shape.center = [20, 50];
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
requestAnimationFrame(function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
shape.center[0] = (shape.center[0] + 1) % 700;
shape.rotation += 0.02;
shape.refresh(ctx);
requestAnimationFrame(loop);
});
body { margin: 0; padding: 0 }
<canvas width="600" height="170"></canvas>
I need to apply several matrix transformations before drawing a shape, however (if on somewhere) I use rotate() the coordinates are inverted and/or reversed and cannot continue without knowing if the matrix was previously rotated.
How can solve this problem?
Example:
<canvas width="300" height="300"></canvas>
<script>
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
ctx.fillStyle = "silver";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, canvas.height/2);
ctx.lineTo(canvas.width, canvas.height/2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(canvas.width/2, 0);
ctx.lineTo(canvas.width/2, canvas.height);
ctx.stroke();
ctx.translate(150, 150);
ctx.rotate(-90 * 0.017453292519943295);
ctx.translate(-150, -150);
// move the red rectangle 100px to the left (top-left)
// but instead is moved on y axis (right-bottom)
ctx.translate(-100, 0);
// more matrix transformations
// ....
// ....
// now finally draw the shape
ctx.fillStyle = "red";
ctx.fillRect(150, 150, 100, 50);
</script>
Can be this Translation after rotation the solution?
It looks like you aren't resetting the canvas matrix each time you make a new transformation.
The Canvas API has the save() and restore() methods. Canvas states are stored on a stack. Every time the save() method is called, the current drawing state is pushed onto the stack. A drawing state consists of transformations that have been applied along with the attributes of things like the fillStyle. When you call restore(), the previous settings are restored.
// ...
ctx.save(); // save the current canvas state
ctx.translate(150, 150);
ctx.rotate(-90 * 0.017453292519943295);
ctx.translate(-150, -150);
ctx.restore(); // restore the last saved state
// now the rectangle should move the correct direction
ctx.translate(-100, 0);
Check out this link for more information on the save and restore methods.
OK finally, i solved the problem by rotating the translation point before applying it. This function does the trick:
function helperRotatePoint(point, angle) {
let s = Math.sin(angle);
let c = Math.cos(angle);
return { x: point.x * c - point.y * s, y: point.x * s + point.y * c};
}
rotating the translation point using the inverted angle I obtain the corrected translation
helperRotatePoint(translation_point, -rotation_angle);
working code:
let canvas = document.querySelector("canvas");
// proper size on HiDPI displays
canvas.style.width = canvas.width;
canvas.style.height = canvas.height;
canvas.width = Math.floor(canvas.width * window.devicePixelRatio);
canvas.height = Math.floor(canvas.height * window.devicePixelRatio);
let ctx = canvas.getContext("2d");
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.fillStyle = "whitesmoke";
ctx.fillRect(0, 0, canvas.width, canvas.height);
class UIElement {
constructor(x, y, width, height, color) {
// PoC
this.draw_pos = {x, y};
this.draw_size = {width, height};
this.color = color;
this.rotate = 0;
this.scale = {x: 1, y: 1};
this.translate = {x: 0, y: 0};
this.skew = {x: 0, y: 0};
this.childs = [];
}
addChild(uielement) {
this.childs.push(uielement);
}
helperRotatePoint(point, angle) {
let s = Math.sin(angle);
let c = Math.cos(angle);
return {
x: point.x * c - point.y * s,
y: point.x * s + point.y * c
};
}
draw(cnvs_ctx, parent_x, parent_y) {
// backup current state
cnvs_ctx.save();
let elements_drawn = 1;// "this" UIElement
// step 1: calc absolute coordinates
let absolute_x = parent_x + this.draw_pos.x;
let absolute_y = parent_y + this.draw_pos.y;
// step 2: apply all transforms
if (this.rotate != 0) {
cnvs_ctx.translate(absolute_x, absolute_y)
cnvs_ctx.rotate(this.rotate);
cnvs_ctx.translate(-absolute_x, -absolute_y);
// rotate translate point before continue
let tmp = this.helperRotatePoint(this.translate, -this.rotate);
// apply rotated translate
cnvs_ctx.translate(tmp.x, tmp.y);
} else {
cnvs_ctx.translate(this.translate.x, this.translate.y);
}
cnvs_ctx.scale(this.scale.x, this.scale.y);
cnvs_ctx.transform(1, this.skew.y, this.skew.x, 1, 0, 0);
// step 3: self draw (aka parent element)
cnvs_ctx.fillStyle = this.color;
cnvs_ctx.fillRect(absolute_x, absolute_y, this.draw_size.width, this.draw_size.height);
// step 4: draw childs elements
for (let i = 0; i < this.childs.length ; i++) {
elements_drawn += this.childs[i].draw(
cnvs_ctx, absolute_x, absolute_y
);
}
// done, restore state
cnvs_ctx.restore();
return elements_drawn;
}
}
// spawn some ui elements
var ui_panel = new UIElement(120, 50, 240, 140, "#9b9a9e");
var ui_textlabel = new UIElement(10, 10, 130, 18, "#FFF");
var ui_image = new UIElement(165, 25, 90, 60, "#ea9e22");
var ui_textdesc = new UIElement(17, 46, 117, 56, "#ff2100");
var ui_icon = new UIElement(5, 5, 10, 10, "#800000");
ui_panel.addChild(ui_textlabel);
ui_panel.addChild(ui_image);
ui_panel.addChild(ui_textdesc);
ui_textdesc.addChild(ui_icon);
// add some matrix transformations
ui_textdesc.skew.x = -0.13;
ui_textdesc.translate.x = 13;
ui_image.rotate = -90 * 0.017453292519943295;
ui_image.translate.y = ui_image.draw_size.width;
ui_panel.rotate = 15 * 0.017453292519943295;
ui_panel.translate.x = -84;
ui_panel.translate.y = -50;
// all ui element elements
ui_panel.draw(ctx, 0, 0);
<canvas width="480" height="360"></canvas>
is there any way that you can stretch an image between few given points on the canvas (similar to Transform). When I use Transform , I can definitely stretch it, however it is not giving all the options that I need for what I have in mind. For example, if I am to do a game that should generate objects , such as boxes, I want to make sure that depending on where the player is located, to turn the image on the side of the boxes and transform them as if you are moving around them. In transform, I can do that, however it will always keep the same height on both sides of the image (left and right) while I need to have more control to decrease the height on the right side of the image only (per say). Much appreciated if you can give a hint on how to do that without using any library.
Here is an example on what I have so far (though again the second image on the right still keeps the same height on the it's far right side same as the one on it's left side):
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Reshape images</title>
</head>
<body style="margin:10px;">
<canvas id="canvas" style="background:#000;"></canvas>
<script type="text/javascript">
// INITIAL SETUP
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');
var innerWidth = 1000,
innerHeight = 650;
canvas.width = innerWidth;
canvas.height = innerHeight;
var img = new Image;
img.onload = function() {
var tile1 = [
{x: 10, y: 10}, // upper left corner
{x: 210, y: 50}, // upper right
{x: 230, y: 150}, // bottom right
{x: 30, y: 110} // bottom left
],
tile2 = [
{x: 210, y: 50},
{x: 410, y: 5},
{x: 430, y: 105},
{x: 230, y: 150}
];
renderTile(this, tile1);
renderTile(this, tile2);
function renderTile(img, tile) {
var dx, dy, a1, a2, w, h, i = 1;
// calc horizontal angle
dx = tile[1].x - tile[0].x; // horizontal diff.
dy = tile[1].y - tile[0].y; // vertical diff.
a1 = Math.atan2(dy, dx); // angle, note dy,dx order here
w = dx|0; // width based on diff for x
// calc vertical angle
dx = tile[3].x - tile[0].x;
dy = tile[3].y - tile[0].y;
a2 = Math.atan2(dx, dy); // note dx,dy order here
h = dy|0;
// draw image to fit parallelogram
// ctx.setTransform(1, a1, a2, 1, tile[0].x, tile[0].y);
ctx.setTransform(1, a1, a2, 1, tile[0].x, tile[0].y);
ctx.drawImage(img, 0, 0, w, h);
}
};
img.src = "http://i.imgur.com/rUeQDjE.png";
</script>
<script src="src.js"></script>
</body>
</html>
I am trying to make an animation with texture atlas:
and it works good when the Character is facing right. I try to flip it horizontally, but it gets wrong position:
Here is my current code:
<canvas id="c" width="200" height="100" style="background: #000"></canvas>
var metaData = [
{x:0,y:0,w:35,h:38,offsetX:3,offsetY:9},
{x:37,y:0,w:31,h:37,offsetX:6,offsetY:10},
{x:70,y:0,w:65,h:47,offsetX:0,offsetY:1},
{x:137,y:0,w:65,h:47,offsetX:0,offsetY:1},
{x:204,y:0,w:61,h:46,offsetX:1,offsetY:1},
{x:267,y:0,w:42,h:46,offsetX:1,offsetY:1},
{x:311,y:0,w:43,h:44,offsetX:1,offsetY:3},
{x:356,y:0,w:38,h:37,offsetX:6,offsetY:10},
{x:396,y:0,w:35,h:34,offsetX:6,offsetY:13},
{x:433,y:0,w:33,h:37,offsetX:7,offsetY:10},
{x:468,y:0,w:36,h:40,offsetX:5,offsetY:7},
{x:506,y:0,w:34,h:39,offsetX:6,offsetY:8}
],
dx = 0, //position x
dy = 0, //position y
index = 0; //frame index
(function draw() {
context2D.clearRect(0,0,c.width,c.height);
var cur = metaData[index];
if(facingRight) {
context2D.drawImage(
img,
cur.x, cur.y,
cur.w, cur.h,
dx + cur.offsetX, dy + cur.offsetY,
cur.w, cur.h
);
} else {
context2D.save();
context2D.translate(cur.w,0);
context2D.scale(-1,1);
context2D.drawImage(
img,
cur.x, cur.y,
cur.w, cur.h,
dx, dy + cur.offsetY,
cur.w, cur.h
);
context2D.restore();
}
index = ++index % metaData.length;
setTimeout(draw,100);
})();
I use scale(-1,1) to flip sprite, but I have no idea how to keep it at the same postion like facing right. Should I fix the offset value?
Please, any help will be appreciated :)
You would save you a lot of time taking an Image editor and making all your sprites fit in same-sized areas along the whole sprite-sheet.
No need metadata weirdness, code is way simpler (a single w, a single h, x = w * i etc.
For example, your largest sprite is around 70px wide, so you should fit all your other sprites in one of such boxes :
Now, it seems that all your sprites share the same position of the front foot. So you should use it as the anchor point for aligning all your sprites.
Something like this :
Note how for all the sprites, the front foot is always at the same position relatively to its own box.
Now it's quite easy to code animation of this sprite-sheet, and even to flip it :
const ssheet = new Image();
ssheet.src = 'https://i.stack.imgur.com/kXKIc.png'; // same without borders
ssheet.onload = startSheetAnim;
function startSheetAnim(evt) {
const ctx = c.getContext('2d');
const h = 49;
const w = 70;
let i = 0;
function anim() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.drawImage(ssheet,
(i * w), 1, w, h,
0, 0, w, h
);
// scale (flip-x) and translate
ctx.setTransform(-1, 0, 0, 1, w * 2, 0);
ctx.drawImage(ssheet,
(i * w), 1, w, h,
0, 0, w, h
);
i = (i + 1) % 12
setTimeout(anim, 100);
}
anim();
}
<canvas id="c"></canvas>
I am not good at JavaScript and have been trying to get Ivan Kuckir's Fastest Gaussian Blur code to work with no success. When I load the page, it becomes unresponsive so that I will have to close the window somehow. The code I use follows. What am doing wrong?
<html>
<head>
<script src="Path_To_GaussianBlur.js"></script>
</head>
<body>
<img id="sourceImage" src="SomeImage_200px_x_200px.jpg" />
<canvas id="canvas1" width="200" height="200" style="border: 1px solid blue"></canvas>
<canvas id="canvas2" width="200" height="200" style="border: 1px solid red"></canvas>
<script>
document.getElementById("sourceImage").onload = function () {
var c1 = document.getElementById("canvas1");
var c2 = document.getElementById("canvas2");
var ctx1 = c1.getContext("2d");
var ctx2 = c2.getContext("2d");
var img = document.getElementById("sourceImage");
ctx1.drawImage(img, 0, 0);
ctx2.drawImage(img, 0, 0);
var source = ctx1.getImageData(0, 0, c1.width, c1.height);
var target = ctx2.getImageData(0, 0, c2.width, c2.height);
for (var i = 0; i < source.data.length; i += 4) {
// red chanel
gaussBlur_4(source.data[i], target.data[i], c1.width, c1.height, 2);
// green channel
gaussBlur_4(source.data[i + 1], target.data[i + 1], c1.width, c1.height, 2);
// blue channel
gaussBlur_4(source.data[i + 2], target.data[i + 2], c1.width, c1.height, 2);
}
ctx2.putImageData(target, 0, 0);
};
</script>
</body>
</html>
As stated in comments you need to split the rgba array into separate channels. You can do this in several ways, here is one:
Split
var idata = ctx.getImageData(0, 0, w, h), // assumes ctx/w/h to be defined
rgba = idata.data,
len = w * h,
rSrc = new Uint8Array(len), // source arrays
gSrc = new Uint8Array(len),
bSrc = new Uint8Array(len),
aSrc = new Uint8Array(len),
// define target arrays the same way as above
i = 0, offset = 0;
for(; i < len; i++) {
rSrc[i] = rgba[offset++];
gSrc[i] = rgba[offset++];
bSrc[i] = rgba[offset++];
aSrc[i] = rgba[offset++];
}
Now you can pass each of those arrays to the blur function using target arrays set up in a similar manner as the source arrays, then merge back the result to rgba format.
Apply
gaussBlur_4(rSrc, rTrg, w, h, radius);
gaussBlur_4(gSrc, gTrg, w, h, radius);
gaussBlur_4(bSrc, bTrg, w, h, radius);
gaussBlur_4(aSrc, aTrg, w, h, radius);
Additional tip: if you're using images (as in photos) you can skip blurring the alpha channel as there is none (or technically, is fully opaque in canvas).
Merge
To merge, simply do:
for(i = 0, offset = 0; i < len; i++) {
rgba[offset++] = rTrg[i];
rgba[offset++] = gTrg[i];
rgba[offset++] = bTrg[i];
rgba[offset++] = aTrg[i]; // or just increase offset if you skipped alpha
}
ctx.putImageData(idata, 0, 0);
Due to license conflict on SO a running example can be found in this fiddle (and here is an alternative split/merge using 32-bits approach).