I'm trying to make a simple multiplayer game with HTML, and I can't figure out how to fix the problem with only 1 player being shown. Here's the relevant code:
socket.on('newpos', function(data){
var transform = ctx.getTransform();
var camX, camY;
for(var i = 0 ; i < data.player.length; i++){
ctx.translate(camX, camY);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0,0,10000,10000);
ctx.setTransform(transform);
ctx.drawImage(background, 0, 0, 10000,10000);
ctx.setTransform(1, 0, 0, 1, 0, 0);
if(ID == data.player[i].id){
camX = -data.player[i].x + canvas.width / 2;
camY = -data.player[i].y + canvas.height / 2;
}
ctx.translate(camX,camY);
ctx.drawImage(img, (data.player[i].x),(data.player[i].y),32,32);
});
The code here loops through all the players on the server. It draws the different player's perspectives, but only the last player that joined is visible.
I've tried changing the transforms in different places and everything else I could think of, not sure what to do.
That's because you clear the entire canvas at every iteration, so only the last is visible
Take out/change the position of
ctx.clearRect(0,0,10000,10000)
Also the above code as of now should give an error since it appears the closing bracket of the for loop is missing
Related
This question already has answers here:
HTML Canvas: How to draw a flipped/mirrored image?
(4 answers)
Closed 4 years ago.
I am a beginner on canvas and I have a question:
I have a background that I don’t want to change but I would like to switch a picture vertically (upside down).
I would like to be able to turn my character (a turtle) upside down at some points during my animation
Can Somebody help me, please
This is a part of my code
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var imgbackground = new Image();
img.src = 'BG.png';
ctx.drawImage(img, 0, 0, 457, 542);
var turtle = new Image(); // My character
turtle.src = 'turtle.png';
ctx.drawImage(turtle, 0, 0, 457, 542);
function gameTime (){
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imgbackground, 0, 0, 900, 500);
ctx.drawImage(turtle, 0, 0, 900, 500);
window.requestAnimationFrame(gameTime)
}
window.requestAnimationFrame(gameTime)
So you can use scale(1, -1) to flip something vertically:
var canvas = document.getElementById('myCanvas')
var context = canvas.getContext('2d');
context.scale(1, -1);
The important part being the .scale which is flipping the Y value with -1,
context here would be the element, so change that to your required element.
so try turtle.scale(1, -1)
- To flip your lovely turtle.
You can also flip things horizontally with (-1, 1) - affecting the x value.
I am using ctx.translate(x, y) to move Camera in canvas game. But for some reason, that doesn't work.
This is what I am using:
setCameraPos: function(x, y) {
//ctx.save()
ctx.translate(x, y)
ctx.setTransform(1, 0, 0, 1, 0, 0)
//ctx.restore()
}
It doesn't work at all. It does not change position of camera.
Any errors? No errors at all.
I am using Electron 3.0.3 Beta.
I accept any libraries.
const canvas = document.getElementById('main')
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 30, 30)
// This doesn't work | VVV
ctx.translate(20, 20)
ctx.setTransform(1, 0, 0, 1, 0, 0)
#main {
background-color: black;
}
<canvas id="main">
</canvas>
From what you gave, the translate operation won't work anywhere, not just in Electron.
ctx.setTransform() method sets the transformation matrix to absolute values, the current matrix is discarded and the passed values are the ones to which your matrix will get set.
1, 0, 0, 1, 0, 0 are the values of the native matrix transform (i.e untransformed).
So calling ctx.setTransform(1, 0, 0, 1, 0, 0) will reset your tranform matrix to its default and make all calls to relative translate(), rotate() or transform() useless.
These methods are meant to be relative because they add up to the current matrix values. For instance,
ctx.translate(10, 10);
// here next drawing will be offset by 10px in both x and y direction
ctx.translate(40, -10);
// this adds up to the current 10, 10, so we are now offset by 30, 0
If you want your translate to work, don't call setTransform here, or even replace it with setTransform(1, 0, 0, 1, 20, 20)
Also, in your snippet, you are setting the transformation matrix after you did draw. The transformations will get applied only on next drawings, not on previous ones.
Now, you might be in an animation loop, and need your matrix to get reset at every loop.
In this case, call ctx.setTransform(1,0,0,1,0,0) either at the beginning of your drawing loop, either as the last op, and call translate() before drawing.
const canvas = document.getElementById('main');
const ctx = canvas.getContext('2d');
let x = 0;
ctx.fillStyle = 'red'
anim();
function draw() {
// reset the matrix so we can clear everything
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
//set the transform before drawing
ctx.translate(x - 30, 20)
//which is actually the same as
//ctx.setTransform(1, 0, 0, 1, x, 20);
ctx.fillRect(0, 0, 30, 30);
}
function anim() {
x = (x + 2) % (canvas.width + 60);
draw();
requestAnimationFrame(anim);
}
#main {
background-color: black;
}
<canvas id="main"></canvas>
So, basically, I have created a canvas on which I draw points. The coordinates of those points are obtained via a JSON obtained from a url. Here's the function that draws the points:
function drawDATA() {
getJSON('http://theossrv2.epfl.ch/aiida_assignment2/api/points/',
function (err, data) {
if (err !== null) {
alert('Something went wrong: ' + err);
} else {
for (var i = 0; i < data.circles.length; i++) {
c.beginPath();
c.arc(data.circles[i].x, data.circles[i].y, 5, 0, 2 * Math.PI);
c.strokeStyle = 'red';
c.stroke();
c.fillStyle = 'red';
c.fill();
}
}
});
}
where c is the context of my canvas.
Now, in the HTML code, I have a button that calls this function when it is pressed:
<input id="Refresh" type="button" value="Refresh" onclick="drawDATA();" />
As you can probably imagine, each time I press the button, new points are added to the canvas (while the points that were previously drawn stay). For example, once I open my HTML file and press the button twice, I have 6 points on my canvas (each time I click on the button, three points appear).
My problem is that I want the previous point to fade out in the following manner:
The first time I press the button, the first 3 points (call them A,B,C) appear normally.
The second time I press the button, three new points appear (D,E,F) and the opacity of A,B,C is set to 75%
The third time I press the button, G,H,I appear, the opacity of A,B,C is set to 50% and the opacity of D,E,F is set to 75%
... and so on (so, after 5 clicks, the points A,B,C must completely disappear).
I tried to approach this problem by creating a CSS file that allows me to fadeOut some elements of my canvas, but it was far from the expected result (so I don't think it is necessary for me to show it).
I hope I was clear enough. Thank you for your help.
The simplest way would be to paint your whole canvas with an opaque layer before applying your new points:
c.beginPath();
c.fillStyle = 'rgba(255, 255, 255, 0.25)';
c.fillRect(0, 0, c.canvas.width, c.canvas.height);
c.closePath();
// draw new points here
Here's a live example
Queue Array
If instead of painting the entire canvas with an opaque layer
you could create a queue array of sets of points that also have each a alpha property.
function drawSET( set ) {
for (var i = 0; i < set.circles.length; i++) {
var circle = set.circles[i];
c.beginPath();
c.arc(circle.x, circle.y, 5, 0, 2 * Math.PI);
c.strokeStyle = 'rgba(255, 0, 0,'+ set.alpha +')';
c.stroke();
c.fillStyle = 'rgba(255, 0, 0,'+ set.alpha +')';
c.fill();
c.closePath();
}
}
var queue = [];
var alphaStep = 0.2;
function drawDATA() {
getJSON('http://theossrv2.epfl.ch/aiida_assignment2/api/points/', function (err, data) {
if (err) return console.log('Something went wrong: ' + err);
queue.push( {alpha: 1, circles: data.circles} ); // Append
if (queue.length > 1/alphaStep) queue.splice(0,1); // Remove first
c.clearRect(0, 0, c.canvas.width, c.canvas.height); // clear canvas
for (var i=0; i<queue.length; i++) {
var set = queue[i];
drawSET( set ); // Draw as is
set.alpha -= alphaStep; // And lower alpha for next iteration
}
});
}
Here's a live example - controlling each set opacity
Re-render scene using alpha to fade.
You need to create an array holding the points. Each point has an alpha value that you decrease each time you draw it. When that alpha value reaches zero you remove the point from the array.
When you get new points you add them to the array of points setting their alpha to one and then render all the points.
Each time you draw the circles you must clear the canvas and draw all the circles. If you have other content on the canvas you must either redraw that content or save it as a separate canvas.
const points = []; // array to hold points
// clear canvas and redraw all points
function drawPoints(ctx, points){
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (var i = 0; i < points.length; i ++) {
const point = points[i];
ctx.beginPath();
ctx.globalAlpha = point.alpha;
ctx.fillStyle = "red";
ctx.arc(point.x, point.y, 5, 0, 2 * Math.PI);
ctx.fill();
// decrease alpha. Will fade through 0.75, 0.50, 0.25
point.alpha -= 0.25;
if (point.alpha <= 0) { // remove point
points.splice(i--,1);
}
}
ctx.globalAlpha = 1; // restore the alpha
}
function drawDATA() {
getJSON('http://theossrv2.epfl.ch/aiida_assignment2/api/points/',
function (err, data) { // I am ignoring error
// add new points to points array
points.push(...data.circles.map(circle => {
return {x : circle.x, y : circle.y, alpha : 1};
}));
// Use animation frame to ensure its presented correctly.
requestAnimationFrame(()=> {
drawPoints(c, points); // draw points
});
}
});
}
You could memorize the dots you drew in one iteration and redraw them with a color closer to the background color in the next iteration. You cannot retrieve previously drawn shapes in your canvas so it should be the best way to do it.
use a fillStyle of rgba.
Keep the alpha values in your data.circles array, then decrease them as desired, and use it
c.fillStyle = "rgba(255,0,0,"+data.circles[i].alpha+")";
I've implemented canvas panning using a Fabric JS canvas, using the below code:
canvas.on("mouse:down", function(e) {
panning = true;
});
canvas.on("mouse:up", function(e) {
panning = false;
});
canvas.on("mouse:move", function(e) {
if (panning) {
var delta = new fabric.Point(e.e.movementX, e.e.movementY);
canvas.relativePan(delta);
}
});
This works fine but you can scroll/pan infinitely in any direction. I want to limit this to a boundary so that a smaller canvas is effectively a view on a larger drawing area. For example a 400 X 400 pixel canvas, which doesn't allow you to pan around more than say 1000 X 1000 pixel area. I've seen in Fabric JS canvas object there's a viewportTransform[] array, which holds zoom level in field [0] and X and Y offsets in fields [4] and [5] but not sure how best to implement a panning boundary. Are there Fabric functions that would make this work?
I also have to take account of zoom level (I'm using canvas.setZoom()) and don't want a user dragging objects beyond the panning boundary either (this may be a separate problem!).
Any thoughts?
Thanks!
there's already a tutorial about it in FabricJS webpage, it works with mousewheel, but you can adapt it: http://fabricjs.com/fabric-intro-part-5
I did it with the some buttons.
First of all I have to tell you that zoom and panning affects canvas viewportTransform property, which is a matrix, similar to css transform property works (or I believe so...).
These are some outputs I got in console while I was working:
originalViewporTransform (6) [1, 0, 0, 1, 0, 0]
zoom
Affects index 0 & 3
zoomViewporTransform (6) [1.2, 0, 0, 1.2, 0, 0]
zoomViewporTransform (6) [1.44, 0, 0, 1.44, 0, 0]
panning
Right:
Affects index 4
panningViewporTransform (6) [1.44, 0, 0, 1.44, -82, 0]
panningViewporTransform (6) [1.44, 0, 0, 1.44, -83, 0]
Left:
Affects index 4
panningViewporTransform (6) [1.728, 0, 0, 1.728, 259, 0]
panningViewporTransform (6) [1.728, 0, 0, 1.728, 260, 0]
Bottom:
Affects index 5
panningViewporTransform (6) [1.728, 0, 0, 1.728, 0, -241]
panningViewporTransform (6) [1.728, 0, 0, 1.728, 0, -242]
Top:
Affects index 5
panningViewporTransform (6) [1.728, 0, 0, 1.728, 0, 305]
panningViewporTransform (6) [1.728, 0, 0, 1.728, 0, 306]
And here's my function to control panning with buttons, in four directions. To limit top and left panning you just have to set the respective viewport to 0 whenever is bigger. When is about right and bottom panning, you have to put the number size of the boundaries you are considering. I was considering the size of my canvas, but you can use which ever size you want.
...
else if (this.actualCanvasViewportTransform[4] < this.canvas.getWidth() - (this.canvas.getWidth() * this.actualCanvasZoom))
...
at those lines you would have to change (this.canvas.getWidth() * this.actualCanvasZoom) with the size you want, like so
(pxLimitRight * this.actualCanvasZoom)
hope it helps
whileMouseDown(caseType){
if (this.actualCanvasZoom <= this.originalCanvasZoom) return;
const units = 1;
let delta;
switch (caseType) {
case 'right':
delta = new fabric.Point(-units,0);
break;
case 'left':
delta = new fabric.Point(units,0);
break;
case 'bottom':
delta = new fabric.Point(0,-units);
break;
case 'top':
delta = new fabric.Point(0,units);
break;
}
this.canvas.relativePan(delta);
// console.log('panningViewporTransform', this.canvas.viewportTransform, this.actualCanvasZoom);
this.actualCanvasViewportTransform = this.canvas.viewportTransform;
/*
WE ARE PANNING LEFT AND RIGHT
*/
if (this.actualCanvasViewportTransform[4] >= 0) {
// WE ARE GOING LEFT
this.actualCanvasViewportTransform[4] = 0;
} else if (this.actualCanvasViewportTransform[4] < this.canvas.getWidth() - (this.canvas.getWidth() * this.actualCanvasZoom))
{
// WE ARE GOING RIGHT
this.actualCanvasViewportTransform[4] = this.canvas.getWidth() - (this.canvas.getWidth() * this.actualCanvasZoom);
}
/*
WE ARE PANNING DOWN AND UP
*/
if (this.actualCanvasViewportTransform[5] >= 0) {
// WE ARE GOING UP
this.actualCanvasViewportTransform[5] = 0;
} else if (this.actualCanvasViewportTransform[5] < this.canvas.getHeight() - (this.canvas.getHeight() * this.actualCanvasZoom)) {
// WE ARE GOING DOWN
this.actualCanvasViewportTransform[5] = this.canvas.getHeight() - (this.canvas.getHeight() * this.actualCanvasZoom);
}
}
UPDATE: This entire issue ended up being a problem with the systems graphics driver, and not (seemingly) a browser / API issue. The torn frames came down to the actual display updating. Thank you again to those who were a part of the discussion and attempts to help.
I have page that uses a canvas and 2d context to display a pre-rendered frame at 720p. I'm rendering the frames separately and updating a variable with the new ImageData. Then, within requestAnimationFrame I simply do context.putImageData(cached_image_data);. Despite having the frame fully rendered in advance and effectively double buffered, I still get tearing far too often. There are a few other questions along these lines that I've found on SO, but they all end in "Use RAF". The code comes down to this:
var canvas = document.getElementById("canvas");
var cached_frame = new ImageData(new Uint8ClampedArray(canvas.width * canvas.height * 4), canvas.width, canvas.height);
var context = canvas.getContext("2d");
var framerate = 30;
function draw() {
if (cached_frame)
context.putImageData(cached_frame, 0, 0);
requestAnimationFrame(draw);
}
setInterval(function() {
var frame = context.getImageData(0, 0, canvas.width, canvas.height);
// Do things to manipulate frame.data.
// Save the resultant pixel data for the cached_frame.
cached_frame.data = frame.data;
}, 1000 / framerate);
draw();
Is there anything more that I can do without turning to webgl?
Any suggestions appreciated. TY all :D
I don't think the code is doing what you think it's doing
First off, as far as I know you can't assign new data to an ImageData so this line
cached_frame.data = frame.data;
Doesn't do anything. We can test that which shows it doens't work
var ctx = document.createElement("canvas").getContext("2d");
document.body.appendChild(ctx.canvas);
var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var data = new Uint8ClampedArray(imageData.length);
// fill imageData.data with red
fillWithColor(255, 0, 0, 255, imageData.data);
// fill data with green
fillWithColor(0, 255, 0, 255, data);
// assign imageData.data to data
imageData.data = data;
// Draw. If assigning imageData.data works result will
// be green, if not result will be red
ctx.putImageData(imageData, 0, 0);
function fillWithColor(r, g, b, a, dst) {
for (ii = 0; ii < dst.length; ii += 4) {
dst[ii + 0] = r;
dst[ii + 1] = g;
dst[ii + 2] = b;
dst[ii + 3] = a;
}
}
Second, your draw function is drawing continuously, at least from the code you posted cached_frame is set on line 2 so it's always going to be true and always going to be drawing. If you're somehow partially updating the actual data in cached_frame then it's going to draw when there are only partial results.
I think you want something like this instead
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
function draw() {
context.putImageData(frame, 0, 0);
}
setInterval(function() {
frame = context.getImageData(0, 0, canvas.width, canvas.height);
// Do things to manipulate frame.data
// frame is ready, draw it at next rAF
requestAnimationFrame(draw);
}, 1000 / framerate);
You might want to check if a draw it is already queued if you think decoding will ever happen faster than raf. I don't think you actually need rAF in this case though. I'm pretty sure you could just draw at the end of your setInterval and it will show up the next frame, no tearing.
Here's a test, it's not tearing for me.
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
var frameCount = 0;
setInterval(function() {
++frameCount;
frame = context.getImageData(0, 0, canvas.width, canvas.height);
var data = frame.data;
var width = frame.width;
var height = frame.height;
// Do things to manipulate frame.data
for (var yy = 0; yy < height; ++yy) {
for (var xx = 0; xx < width; ++xx) {
var offset = (yy * width + xx) * 4;
data[offset + 0] = ((xx >> 2 & 0x1) ^ frameCount & 0x1) ? 255 : 0;
data[offset + 3] = 255;
}
}
// frame is ready, draw it at next rAF
context.putImageData(frame, 0, 0);
}, 1000 / framerate);
<canvas id="canvas" width="1280" height="720"></canvas>