How to flip sprites horizontally with HTML5 Canvas? - javascript

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>

Related

Drawing a sine wave from top right to bottom left

I'm trying to design a small project using canvas and I want to design a sine wave in such a way that the wave is generated from the top right corner to the left bottom corner infinitely if possible.
Something like an inverse sine graph.
All I can do is make the sine wave go from left to right but making this work from top right to bottom left is very difficult.
This is my code at the moment
It's looking very sad...
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth - 5;
canvas.height = window.innerHeight - 5;
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
const wave = {
y: canvas.height / 2,
length: 0.02,
amplitude: 100,
frequency: 0.01,
yOffSet: canvas.height,
};
let increment = wave.frequency;
function animate() {
requestAnimationFrame(animate);
ctx.fillStyle = "rgb(0,0,0)";
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let i = 0; i < canvas.width; i++) {
ctx.lineTo(Math.sin(i / wave.length + increment) * wave.amplitude + wave.yOffSet, 0);
}
ctx.stroke();
increment += wave.frequency;
}
animate();
body {
margin: 0;
padding: 0;
}
<div>
<canvas class="canvas"></canvas>
</div>
Desired Output
The problem is in this line:
ctx.lineTo(Math.sin(i / wave.length + increment) * wave.amplitude + wave.yOffSet, 0);
You are only moving in the x co-ordinate.
I have added the motion in the y co-ordinate and rewrote on three lines just for clarity.
x=i+wave.amplitude*Math.sin(i/wave.length);
y=canvas.height-(i-(wave. amplitude * Math.sin(i/wave.length)));
ctx.lineTo(x,y);
The result it produces is like what you describe, if I understood correctly. There are many more waves than you show in the drawing, but that can be cahnged by the wave.length parameter.

2D lighting with HTML canvas [duplicate]

I am creating a game using the HTML5 Canvas element, and as one of the visual effects I would like to create a glow (like a light) effect. Previously for glow effects I found solutions involving creating shadows of shapes, but these require a solid shape or object to cast the shadow. What I am looking for is a way to create something like an ambient light glow with a source location but no object at the position.
Something I have thought of was to define a centerpoint x and y and create hundreds of concentric circles, each 1px larger than the last and each with a very low opacity, so that together they create a solid center and a transparent edge. However, this is very computationally heavy and does not seem elegant at all, as the resulting glow looks awkward.
While this is all that I am asking of and I would be more than happy to stop here, bonus points if your solution is A) computationally light, B) modifiable to create a focused direction of light, or even better, C) if there was a way to create an "inverted" light system in which the entire screen is darkened by a mask and the shade is lifted where there is light.
I have done several searches, but none have turned up any particularly illuminating results.
So I'm not quite sure what you want, but I hope the following snippet will help.
Instead of creating a lot of concentric circles, create one radialGradient.
Then you can combine this radial gradient with some blending, and even filters to modify the effect as you wish.
var img = new Image();
img.onload = init;
img.src = "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/car.svg";
var ctx = c.getContext('2d');
var gradCtx = c.cloneNode().getContext('2d');
var w, h;
var ratio;
function init() {
w = c.width = gradCtx.canvas.width = img.width;
h = c.height = gradCtx.canvas.height = img.height;
draw(w / 2, h / 2)
updateGradient();
c.onmousemove = throttle(handleMouseMove);
}
function updateGradient() {
var grad = gradCtx.createRadialGradient(w / 2, h / 2, w / 8, w / 2, h / 2, 0);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, 'white');
gradCtx.fillStyle = grad;
gradCtx.filter = "blur(5px)";
gradCtx.fillRect(0, 0, w, h);
}
function handleMouseMove(evt) {
var rect = c.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
draw(x, y);
}
function draw(x, y) {
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(gradCtx.canvas, x - w / 2, y - h / 2);
ctx.globalCompositeOperation = 'lighten';
ctx.fillRect(0, 0, w, h);
}
function throttle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function() { // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
<canvas id="c"></canvas>

p5.js WebGL 3d graphics covered by 2d background when rotated

I have two things that I want to display with p5, one is a 2D background and the other is a 3D WebGL foreground, both generated by p5. What I noticed is that even if I draw the 2D background before the 3D stuff in the draw() function, the 3D stuff will still be partially covered by the background when rotateX() or rotateY() is called. It looks kind of like this:
I suspect what's happening is that the 2d and 3d stuff are both on the same z-plane, therefore when the foreground is rotated some of it gets covered by the background which now is in the front compared to the covered parts.
So my question is how can I keep the background completely in the back (i.e. not covering foreground regardless of the rotation)?
Below is my current implementation, the 2d background is generated in an offscreen canvas then put onto the main canvas with image() where the 3d stuff is generated, but I'll take any other approaches.
let bg;
p.setup = () => {
p.createCanvas(width,height,p.WEBGL);
bg = p.createGraphics(width,height);
}
p.draw = () => {
... // draw background bg
p.image(bg,x,y); // draw background on canvas
... // draw foreground
p.rotateX(degrees);//rotate
}
The best way to accomplish this is by clearing the WebGL depth buffer. This is buffer stores the depth for every pixel that has been draw so far so that as subsequent triangles are drawn they can be clipped if some or all of them is behind whatever was previously drawn at that location. This buffer is automatically cleared in between calls to draw() in p5.js but you can also call it yourself mid-frame:
let bg;
let zSlider;
let glContext;
function setup() {
let c = createCanvas(200, 200, WEBGL);
glContext = c.GL;
bg = createGraphics(width, height);
bg.background('red');
for (let y = 0; y < height; y += 20) {
for (let x = 0; x < width; x += 20) {
if ((x / 20 + y / 20) % 2 === 0) {
bg.fill('black');
} else {
bg.fill(
map(x + y, 0, width + height, 0, 360),
map(y, 0, height, 50, 100),
map(x, 0, width, 50, 100)
);
}
bg.square(x, y, 20)
}
}
zSlider = createSlider(0, width * 2, width);
zSlider.position(10, 10);
}
function draw() {
image(bg, -width / 2, -height / 2, width, height);
// Clear the z-buffer, subsequent drawing commands will not clip, even if they
// intersect with or are behind previously drawn elements (like our background
// image)
glContext.clear(glContext.DEPTH_BUFFER_BIT);
push();
translate(0, 0, (zSlider.value() - width) * 2);
rotateX(millis() / 1000 * PI / 4);
rotateY(millis() / 1000 * PI / 8);
box(100);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>
There is also kludgy solution that doesn't rely on calling WebGL internals, but I have only been able to make it work for square canvases:
Switch to an orthographic camera mode before drawing your background image.
Translate in the negative Z direction as far as possible without going beyond the "far" clipping plane.
Draw your background image.
Pop the state back to the normal perspective camera.
This uses orthographic projection to allow you to draw the background image behind the rest of the scene without diminishing size due to perspective. However I haven't come up with a fool proof way to determine what the perfect translation value is, nor how to reliably setup the orthographic project to control where the "far" clipping plane is.
let bg;
let zSlider;
function setup() {
createCanvas(200, 200, WEBGL);
bg = createGraphics(width, height);
bg.background('red');
for (let y = 0; y < height; y += 20) {
for (let x = 0; x < width; x += 20) {
if ((x / 20 + y / 20) % 2 === 0) {
bg.fill('black');
} else {
bg.fill(
map(x + y, 0, width + height, 0, 360),
map(y, 0, height, 50, 100),
map(x, 0, width, 50, 100)
);
}
bg.square(x, y, 20)
}
}
zSlider = createSlider(0, width * 2, width);
zSlider.position(10, 10);
}
function draw() {
push();
ortho();
translate(0, 0, min(width, height) * -0.13);
image(bg, -width / 2, -height / 2, width, height);
pop();
push();
translate(0, 0, (zSlider.value() - width) * 2);
rotateX(millis() / 1000 * PI / 4);
rotateY(millis() / 1000 * PI / 8);
box(100);
pop();
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.3.1/lib/p5.js"></script>

How can I animate multiple instances of drawImage() on a Canvas?

I am working with canvas animations for the first time and I am having an issue when trying to animate multiple images at once.
I am able to draw multiple images on a canvas and position them randomly. I can get a single image to animate on the canvas but only the last image drawn from an array.
I know that the issue is with clearRect() clearing all previously drawn images from said array but can't figure out how to only clearRect once all images have been drawn in each animation frame, I was wondering if anyone has dealt with something like this before and if they could point me in the right direction of how to only clearRect() after all images are drawn?
function animate() {
srequestAnimationFrame(animate);
for(let i = 0; i < images.length; i++) {
let y = images[i].y;
let img = new Image();
img.src = images[i].url;
img.onload = function() {
// This is clearing all images drawn before the last image
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this, images[i].x, y, images[i].width, images[i].height);
}
images[i].y -= 1;
if(images[i].y < (0 - images[i].height)) {
images[i].y = window.innerHeight;
images[i].x = (Math.random() * (canvas.width - 160));
}
}
}
I would like to animate all the images vertically up the page with them resetting to the bottom after reaching the top of the screen, I have this working but only for the last image as mentioned above
In the animate() function, after updating the value for the y you need to draw again every image. Also, you need to clear the canvas with every frame (the animate() function), not after drawing every image, since clearRect will delete everything previously drawn on the canvas. The reason why you need to clear the rect is that you have to delete the images drawn at the previous position.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = window.innerWidth);
cx = cw / 2;
let ch = (canvas.height = window.innerHeight),
cy = ch / 2;
let images = [
{
url: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/222579/puppy150x150.jpg",
x: Math.random() * cw,
y: Math.random() * ch,
width: 50,
height: 50
},
{
url: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/222579/puppy200x200.jpg",
x: Math.random() * cw,
y: Math.random() * ch,
width: 40,
height: 40
},
{
url:
"https://s3-us-west-2.amazonaws.com/s.cdpn.io/222579/puppyBeagle300.jpg",
x: Math.random() * cw,
y: Math.random() * ch,
width: 60,
height: 60
}
];
for (let i = 0; i < images.length; i++) {
let y = images[i].y;
images[i].img = new Image();
images[i].img.src = images[i].url;
images[i].img.onload = function() {
// This is clearing all images drawn before the last image
//ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this, images[i].x, y, images[i].width, images[i].height);
};
}
function animate() {
requestAnimationFrame(animate);
// clear the canvas here!
ctx.clearRect(0, 0, cw, ch);
for (let i = 0; i < images.length; i++) {
//update the y value
images[i].y -= 1;
if (images[i].y < -images[i].height) {
images[i].y = window.innerHeight;
images[i].x = Math.random() * (canvas.width - 160);
}
//draw again the image
ctx.drawImage(
images[i].img,
images[i].x,
images[i].y,
images[i].width,
images[i].height
);
}
}
animate();
<canvas id="canvas"></canvas>

javascript html5 scaling pixels

I can't seem to figure out how to scale pixels on an html5 canvas. Here's where I am so far.
function draw_rect(data, n, sizex, sizey, color, pitch) {
var c = Color2RGB(color);
for( var y = 0; y < sizey; y++) {
var nn = n * 4 * sizex;
for( var x = 0; x < sizex; x++) {
data[nn++] = c[0];
data[nn++] = c[1];
data[nn++] = c[2];
data[nn++] = 0xff;
}
n = n + pitch;;
}
}
function buffer_blit(buffer, width, height) {
var c_canvas = document.getElementById("canvas1");
var context = c_canvas.getContext("2d");
context.scale(2, 2);
var imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
var n = width * height - 1;
while((n--)>=0) draw_rect(imageData.data, n, pixel, pixel, buffer[n], width);
context.putImageData(imageData, 0, 0);
}
Edits:
I updated the code here. Not sure what changed.
Maybe some images will help.
First image has pixel size of one, second pixel size of 2. Note that the image doubles and only fills half the canvas.
Edit2:
I made a webpage showing at least one of the strange behaviors I'm experiencing.
Live example
drawImage can be used to scale any image or image-like thing ( or for instance) without using ImageData -- unless you explicitly want to handle pixel specific scaling you should probably use native support. eg.
var myContext = myCanvas.getContext("2d");
// scale image up
myContext.drawImage(myImage, 0, 0, myImage.naturalWidth * 2, myImage.naturalHeight * 2);
// scale canvas up, can even take the source canvas
myContext.drawImage(myCanvas, 0, 0, myCanvas.width * 2, myCanvas.height * 2);
// scale up a video
myContext.drawImage(myVideo, 0, 0, myVideo.width * 2, myVideo.height * 2);
alternatively you could just do:
myContext.scale(2, 2); // say
//.... draw stuff ...
myContext.scale(.5, .5);
Depending on your exact goal.
You could temporarly use an image with canvas.toDataURL(), then draw it resized with drawImage().
context.putImageData() should do the trick, but the dimensions parameters are not yet implemented in Firefox.
The code, with two canvas for demo purposes:
var canv1 = document.getElementById("canv1"),
canv2 = document.getElementById("canv2"),
ctx1 = canv1.getContext("2d"),
ctx2 = canv2.getContext("2d"),
tmpImg = new Image();
/* Draw the shape */
ctx1.beginPath();
ctx1.arc(75,75,50,0,Math.PI*2,true);
ctx1.moveTo(110,75);
ctx1.arc(75,75,35,0,Math.PI,false);
ctx1.moveTo(65,65);
ctx1.arc(60,65,5,0,Math.PI*2,true);
ctx1.moveTo(95,65);
ctx1.arc(90,65,5,0,Math.PI*2,true);
ctx1.stroke();
/* On image load */
tmpImg.onload = function(){
/* Draw the image on the second canvas */
ctx2.drawImage(tmpImg,0,0, 300, 300);
};
/* Set src attribute */
tmpImg.src = canv1.toDataURL("image/png");
EDIT: olliej is right, there is no need to use a temp image

Categories