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);
}
}
Related
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
I've recently seen a challenge picture on brillant.org's Instagram account:
The instructions:
The robot takes 4 random steps (can't go diagonal).
In which area is it most likely to land?
Obviously there are 44 = 256 possible paths for the robot.
I tried to write a program (Javascript) to solve that problem but my approaches didn't work.
Actually I don't have useful code to show here because I got stuck pretty early on.
So my question:
How would you write a program that:
Checks all 256 possible paths and
Tells me how many (%) landed in which area
This is a very cool question!
And thanks for letting me discover brillant.org's Instagram account.
So, I would proceed as following:
Write a function to calculate all possible permutation with repetition (n^k)
Generate a map where to execute all possible moves calculated in the step 1
Check the area where the robot would land on with the final step and store it
Calculate the percentage based on the counting in step 3
The first step is a problem by itself, and it's not part of this scope. You can use or adapt the code here: https://rosettacode.org/wiki/Permutations_with_repetitions
Then, to generate the map, I simply used an array:
const map = [
0, 0, 0, 0, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 2, 1, 1, 0, 0,
0, 1, 1, 2, 2, 2, 1, 1, 0,
1, 1, 3, 3, 2, 3, 3, 1, 1,
0, 1, 1, 3, 3, 3, 1, 1, 0,
0, 0, 1, 1, 3, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0, 0,
];
This is a representation of the image you gave, each area is marked with a different number, that we will reuse later.
At this point I defined an array of the 4 possible moves:
const moves = [
-1, // left
1, // right,
-9, // top
9, // bottom
];
The values indicates the offset needed to move in the direction wrote in in the comment: left and right I guess are self explanatory. For the top and bottom, since we're using an array as "matrix", we basically need to translate the y value to a index value in the array. The formula is simple: index = x + y * width there fore it means if you want to specify a y to move up by one cell you have -1 * 9, and to move down is 1 * 9.
For the same reason the robot's starting position (at the center of the map) is calculate as follow: 4 + 4 * 9.
Now I calculate all the possible moves combination with the permutation function:
const allmoves = permutationsWithRepetition(4, moves);
And create an array to store the results:
let results = [0, 0, 0, 0];
After that, I just iterate all the possible moves array, and calculate the position at the end of the moves:
for (let j = 0; j < allmoves.length; j++) {
// set the robot's initial position at the center
// before executing any moves' list
let pos = 4 + 4 * 9;
// calculate the new position using the current moves
for (let i = 0; i < moves.length; i++) {
let move = allmoves[j][i];
pos += move;
}
// now `map[pos]` points to a number between 1 and 3
// that identify the area where the robot is.
// we use that number as index of the results
// to increment its value.
// Notice that it's impossible land to any 0 area
// if we start from the center and we can only perform
// 4 moves.
// We use that number as `index` for our `results`
results[map[pos]]++;
}
Now in results you will have how many times the robot ended up in which area:
console.log(results); // [0, 60, 100, 96]
As mentioned is impossible given the starting position and the number of moves for the robot to land in any of the 0 area, so the first index would have 0 as value.
You can see that it landed in the area 1 (the orange one) 60 times, in the area 2 100 times (the smallest area, the green / aqua one), and in the area 3, 96 times (the blue / purple one).
At this point you can calculate the percentage (times / total * 100) and display it with a proper formatting:
// we skip the first element of results since
// it's not an area (and we'll be always 0)
for (let k = 1; k < results.length; k++) {
console.log(k, `${(results[k] / allmoves.length * 100).toFixed(2)}%`)
}
And you'll get:
1 "23.44%"
2 "39.06%"
3 "37.50%"
You can also do an empiric check, and actually generate ten thousands of moves randomly and make the program apply those instead of allmoves, and you'll see that you end always with similar number (obviously, but that also the fun part of math, verify that is actually what you will expect!).
Here the a working code that also implement the permutation code mentioned at the beginning, from rosettacode.org, and the code explained in this post: https://codepen.io/zer0/pen/RwWPZmE
(You need to open the console to see the results)
I would create different objects representing the different possibilities like below:
function Path(x, y, movesLeft) {
this.x = x;
this.y = y;
this.movesLeft = movesLeft;
this.paths = [];
if (movesLeft > 0) {
this.paths.push(new Path(x - 1, y, movesLeft - 1));
this.paths.push(new Path(x + 1, y, movesLeft - 1));
this.paths.push(new Path(x, y - 1, movesLeft - 1));
this.paths.push(new Path(x, y + 1, movesLeft - 1));
}
this.getArray = function() {
if (this.movesLeft > 0) {
var output = [];
for (var i = 0; i < this.paths.length; i++) {
output = output.concat(this.paths[i].getArray());
}
return output;
}
return [this];
}
}
Now, you can create an object and test the results:
var endPosArray = new Path(0, 0, 4).getArray();
All you need to do is loop through the array and calculate the chances.
I am using the HTML5 canvas API to draw a tile map for a pixel art game. The rendered tile map is comprised of many smaller images that are cut out of a single source image called a tile sheet. I am using drawImage(src_img, sx, sy, sw, sh, dx, dy, dw, dh) to cut the individual tiles out of the source image and draw them onto the destination canvas. I am using setTransform(sx, 0, 0, sy, tx, ty) to apply scale and translation to the final rendered image.
The color "bleeding" issue I need to fix is caused by the sampler, which uses interpolation to blend colors during scale operations in order to make things not look pixelated. This is great for scaling digital photographs, but not for pixel art. While this doesn't do much visual damage to the centers of the tiles, the sampler is blending colors along the edges of adjacent tiles in the source image which creates unexpected colors in the rendered tile map. Instead of only using colors that fall within the source rectangle passed to drawImage, the sampler blends in colors from just outside of its boundaries causing what appear to be gaps between the tiles.
Below is my tile sheet's source image. Its actual size is 24x24 pixels, but I scaled it up to 96x96 pixels in GIMP so you could see it. I used the "Interpolation: None" setting on GIMP's scaling tool. As you can see there are no gaps or blurred borders around the individual tiles because the sampler did not interpolate the colors. The canvas API's sampler apparently does interpolate colors even when imageSmoothingEnabled is set to false.
Below is a section of the rendered tile map with imageSmoothingEnabled set to true. The left arrow points to some red bleeding into the bottom of the gray tile. This is because the red tile is directly below the gray tile in the tile sheet. The sampler is blending the red into the bottom edge of the gray tile.
The arrow on the right points to the right edge of the green tile. As you can see, no color is bleeding into it. This is because there is nothing to the right of the green tile in the source image and therefore nothing for the sampler to blend.
Below is the rendered tile map with imageSmoothingEnabled set to false. Depending on the scale and translation, texture bleeding still occurs. The left arrow is pointing to red bleeding in from the red tile in the source image. The visual damage is reduced, but still present.
The right arrow points to an issue with the far right green tile, which has a thin gray line bleeding in from the gray tile in the source image, which is to the left of the green tile.
The two images above were screen captured from Edge. Chrome and Firefox do a better job of hiding the bleeding. Edge seems to bleed on all sides, but Chrome and Firefox seem to only bleed on the right and bottom sides of the source rectangle.
If anyone knows how to fix this please let me know. People ask about this problem in a lot of forums and get work around answers like:
Pad your source tiles with border color so the sampler blends in the same color along the edges.
Put your source tiles in individual files so the sampler has nothing to sample past the borders.
Draw everything to an unscaled buffer canvas and then scale the buffer, ensuring that the sampler is blending in colors from adjacent tiles that are part of the final image, mitigating the visual damage.
Draw everything to the unscaled canvas and then scale it using CSS using image-rendering:pixelated, which basically works the same as the previous work around.
I would like to avoid work arounds, however if you know of another one, please post it. I want to know if there is a way to turn off sampling or interpolation or if there is any other way to stop texture bleeding that isn't one of the work arounds I listed.
Here is a fiddle showcasing the issue: https://jsfiddle.net/0rv1upjf/
You can see the same example on my Github Pages page: https://frankpoth.info/pages/javascript-projects/content/texture-bleeding/texture-bleeding.html
Update:
The problem arose due to floating point numbers being used when plotting pixels. The solution is to avoid floats and only draw on integers. Unfortunately, this means setTransform cannot be used efficiently because scaling generally results in floats, but I still managed to keep a good bit of math out of the tile rendering loop. Here's the code:
function drawRounded(source_image, context, scale) {
var offset_x = -OFFSET.x * scale + context.canvas.width * 0.5;
var offset_y = -OFFSET.y * scale + context.canvas.height * 0.5;
var map_height = (MAP_HEIGHT * scale)|0; // Similar to Math.trunc(MAP_HEIGHT * scale);
var map_width = (MAP_WIDTH * scale)|0;
var tile_size = TILE_SIZE * scale;
var rendered_tile_size = (tile_size + 1)|0; // Similar to Math.ceil(tile_size);
var map_index = 0; // Track the tile index in the map. This increases once per draw loop.
/* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */
for (var y = 0; y < map_height; y += tile_size) { // y first so we draw rows from top to bottom
for (var x = 0; x < map_width; x += tile_size) {
var frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image.
// We have to keep the dx, dy truncation inside the loop to ensure the highest level of accuracy possible.
context.drawImage(source_image, frame.x, frame.y, TILE_SIZE, TILE_SIZE, (offset_x + x)|0, (offset_y + y)|0, rendered_tile_size, rendered_tile_size);
map_index ++;
}
}
}
I'm using Bitwise OR or the | operator to do my rounding. Bitwise Or returns a 1 in each bit position for which the corresponding bits of either or both operands are 1s. Bitwise operations will convert a float to an int. Using 0 as the right operand will match all the bits in the left operand and truncate the decimals. The downside to this is it only supports 32 bits, but I doubt I'll ever need more than 32 bits for my tile positions.
For example:
-10.5 | 0 == -10
10.1 | 0 == 10
10.5 | 0 == 10
In binary:
1010 | 0000 == 1010
This is a rounding issue.
There was already that question about this issue experienced on Safari browser when the context is translated to exactly n.5, Edge an IE are even worse and always bleed one way or an other, Chrome for macOs bleeds on n.5 too, but only when drawing an <img>, <canvas> are fine.
Least to say, that's a buggy area.
I didn't check the specs to know exactly what they should do, but there is an easy workaround.
Compute yourself the transformation of your coordinates so you can control exactly how they'll get rounded and ensure crisp pixels.
// First calculate the scaled translations
const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5;
const scaled_offset_top = -OFFSET.y * scale + context.canvas.height * 0.5;
// when drawing each tile
const dest_x = Math.floor( scaled_offset_left + (x * scale) );
const dest_y = Math.floor( scaled_offset_top + (y * scale) );
const dest_size = Math.ceil( TILE_SIZE * scale );
context.drawImage( source_image,
frame.x, frame.y, TILE_SIZE, TILE_SIZE,
dest_x, dest_y, dest_size, dest_size,
);
/* This is the tile map. Each value is a frame index in the FRAMES array. Each frame tells drawImage where to blit the source from */
const MAP = [
0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
0, 1, 0, 1, 2, 2, 1, 2, 3, 2,
0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
3, 4, 3, 4, 5, 5, 4, 5, 6, 5,
3, 4, 3, 4, 5, 5, 4, 5, 6, 5,
3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
6, 6, 6, 7, 7, 7, 7, 8, 8, 8,
6, 7, 6, 7, 8, 8, 7, 8, 0, 8,
6, 6, 6, 7, 7, 7, 7, 8, 8, 8
];
const TILE_SIZE = 8; // Each tile is 8x8 pixels
const MAP_HEIGHT = 80; // The map is 80 pixels tall by 80 pixels wide
const MAP_WIDTH = 80;
/* Each frame represents the source x, y coordinates of a tile in the source image. They are indexed according to the map values */
const FRAMES = [
{ x:0, y:0 }, // map value = 0
{ x:8, y:0 }, // map value = 1
{ x:16, y:0 }, // map value = 2
{ x:0, y:8 }, // etc.
{ x:8, y:8 },
{ x:16, y:8},
{ x:0, y:16},
{ x:8, y:16},
{ x:16, y:16}
];
/* These represent the state of the keyboard keys being used. false is up and true is down */
const KEYS = {
down: false,
left: false,
right: false,
scale_down: false, // the D key
scale_up: false, // the F key
up: false
}
/* This is the scroll offset. You can also think of it as the position of the red dot in the map. */
const OFFSET = {
x: MAP_WIDTH * 0.5,
y: MAP_HEIGHT * 0.5
}; // It starts out centered in the map.
const MAX_SCALE = 75; // Max scale is 75 times larger than the actual image size.
const MIN_SCALE = 0; // Texture bleeding seems to only occur on upscale, but min scale is 0 in case you want to try it.
var scale = 4.71; // some arbitrary number that will hopefully cause the issue in your browser
/* Get the canvas drawing context. */
var context = document.querySelector('canvas').getContext('2d', {
alpha: false,
desynchronized: true
});
/* The toggle button is the div */
var toggle = document.querySelector('div');
/* The source image is a 24x24 square with 9 tile images of various colors in it. */
var base_64_image_source = 'data:image/png;base64,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';
var source_image = new Image(); // This will be the source image
/* The keyboard event handler */
function keyDownUp(event) {
var state = event.type == 'keydown' ? true : false;
switch (event.keyCode) {
case 37:
KEYS.left = state;
break;
case 38:
KEYS.up = state;
break;
case 39:
KEYS.right = state;
break;
case 40:
KEYS.down = state;
break;
case 68:
KEYS.scale_down = state;
break;
case 70:
KEYS.scale_up = state;
}
}
/* This is the update and rendering loop. It handles input and draws the images. */
function loop() {
window.requestAnimationFrame(loop); // Perpetuate the loop
/* Prepare to move and scale the image with the keyboard input */
if (KEYS.left) OFFSET.x -= 0.5;
if (KEYS.right) OFFSET.x += 0.5;
if (KEYS.up) OFFSET.y -= 0.5;
if (KEYS.down) OFFSET.y += 0.5;
if (KEYS.scale_down) scale -= 0.5 * scale / MAX_SCALE;
if (KEYS.scale_up) scale += 0.5 * scale / MAX_SCALE;
/* Keep the scale size within a defined range */
if (scale > MAX_SCALE) scale = MAX_SCALE;
else if (scale < MIN_SCALE) scale = MIN_SCALE;
/* Clear the canvas to gray. */
context.setTransform(1, 0, 0, 1, 0, 0); // Set the transform back to the identity matrix
context.fillStyle = "#202830"; // Set the fill color to gray
context.fillRect(0, 0, context.canvas.width, context.canvas.height); // fill the entire canvas
/* [EDIT]
Don't set the transform, we will calculate it ourselves
// context.setTransform(scale, 0, 0, scale, -OFFSET.x * scale + context.canvas.width * 0.5, -OFFSET.y * scale + context.canvas.height * 0.5);
First step is calculating the scaled translation
*/
const scaled_offset_left = -OFFSET.x * scale + context.canvas.width * 0.5;
const scaled_offset_top = -OFFSET.y * scale + context.canvas.height * 0.5;
let map_index = 0; // Track the tile index in the map. This increases once per draw loop.
/* Loop through all tile positions in actual coordinate space so no additional calculations based on grid index are needed. */
for (let y = 0; y < MAP_HEIGHT; y += TILE_SIZE) { // y first so we draw rows from top to bottom
for (let x = 0; x < MAP_WIDTH; x += TILE_SIZE) {
const frame = FRAMES[MAP[map_index]]; // The frame is the source location of the tile in the source_image.
/* [EDIT]
We transform the coordinates ourselves
We can control a uniform rounding by using floor and ceil
*/
const dest_x = Math.floor( scaled_offset_left + (x * scale) );
const dest_y = Math.floor( scaled_offset_top + (y * scale) );
const dest_size = Math.ceil(TILE_SIZE * scale);
context.drawImage( source_image,
frame.x, frame.y, TILE_SIZE, TILE_SIZE,
dest_x, dest_y, dest_size, dest_size
);
map_index++;
}
}
/* Draw the red dot in the center of the screen. */
context.fillStyle = "#ff0000";
/* [EDIT]
Do the same kind of calculations for the "dot" if you don't want antialiasing
// const dot_x = Math.floor( scaled_offset_left + ((OFFSET.x - 0.5) * scale) );
// const dot_y = Math.floor( scaled_offset_top + ((OFFSET.y - 0.5) * scale) );
// const dot_size = Math.ceil( scale );
// context.fillRect( dot_x, dot_y, dot_size, dot_size ); // center on the dot
But if you do want antialiasing for the dot, then just set the transformation for this drawing
*/
context.setTransform(scale, 0, 0, scale, scaled_offset_left, scaled_offset_top);
context.fillRect( (OFFSET.x - 0.5), (OFFSET.y - 0.5), 1, 1 ); // center on the dot
var smoothing = context.imageSmoothingEnabled; // Get the current smoothing value because we are going to ignore it briefly.
/* Draw the source image in the top left corner for reference. */
context.setTransform(4, 0, 0, 4, 0, 0); // Zoom in on it so it's visible.
context.imageSmoothingEnabled = false; // Set smoothing to false so we get a crisp source image representation (the real source image is not scaled at all).
context.drawImage( source_image, 0, 0 );
context.imageSmoothingEnabled = smoothing; // Set smoothing back the way it was according to the toggle choice.
}
/* Turn image smoothing on and off when you press the toggle. */
function toggleSmoothing(event) {
context.imageSmoothingEnabled = !context.imageSmoothingEnabled;
if (context.imageSmoothingEnabled) toggle.innerText = 'Smoothing Enabled'; // Make sure the button has appropriate text in it.
else toggle.innerText = 'Smoothing Disabled';
}
/* The main loop will start after the source image is loaded to ensure there is something to draw. */
source_image.addEventListener('load', (event) => {
window.requestAnimationFrame(loop); // Start the loop
}, { once: true });
/* Add the toggle smoothing click handler to the div. */
toggle.addEventListener('click', toggleSmoothing);
/* Add keyboard input */
window.addEventListener('keydown', keyDownUp);
window.addEventListener('keyup', keyDownUp);
/* Resize the canvas. */
context.canvas.width = 480;
context.canvas.height = 480;
toggleSmoothing(); // Set imageSmoothingEnabled
/* Load the source image from the base64 string. */
source_image.setAttribute('src', base_64_image_source);
* {
box-sizing: border-box;
margin: 0;
overflow: hidden;
padding: 0;
user-select: none;
}
body,
html {
background-color: #202830;
color: #ffffff;
height: 100%;
width: 100%;
}
body {
align-items: center;
display: grid;
justify-items: center;
}
p {
max-width: 640px;
}
div {
border: #ffffff 2px solid;
bottom: 4px;
cursor: pointer;
padding: 8px;
position: fixed;
right: 4px
}
<div>Smoothing Disabled</div>
<p>Use the arrow keys to scroll and the D and F keys to scale. The source image is represented on the top left. Notice the vertical and horizontal lines that appear between tiles as you scroll and scale. They are the color of the tile's neighbor in the source
image. This may be due to color sampling that occurs during scaling. Click the toggle to set imageSmoothingEnabled on the drawing context.</p>
<canvas></canvas>
Note that to draw your "player" dot, you can either choose to do the same caulcations manually to avoid the blurring caused by antialiasing, or if you actually want that blurring, then you can simply set the transform only for this dot. In your position I would probably even make something modular like after a certain scale round, and below smoothen, but I'll let the reader do that implementation.
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>
I am reading an image using a command such as
gl.readPixels(0, 0, gl.width, gl.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
Now pixels has a length of width*height*4 in a 1D array. I am unsure along which axis are the image values collapsed? Intuitively I would expect it to read each row, moving down the column for Red first, then G, B, A (I call this collapsing along width, then height, than RGBA).
For instance, if I would like to access the RED value in the second-from-the-right pixel at the bottom of the image, would I use:
<br>
pixels[width*height-2] (collapse along width, then height, then RGBA)<br>
pixels[width*height-1-height] (collapse along height, then width, then RGBA)<br>
pixels[width*height*4-8] (collapse along RGBA, then width, then height)<br>
or some other order.
The order is the standard GL order which is the first pixel corresponds to the 0,0 position in the texture, renderbuffer, canvas.
For the canvas itself 0,0 is the bottom left corner. For textures there is no concept of bottom, there's the first pixel (0,0), the pixel at (1,0), the pixel at (0,1) and the last pixel at (1,1).
For the canvas (since it's the only thing with a set direction),
The right most pixel in the first row (bottom) is data[(width - 1) * pixelSize]
The right most pixel in the 3rd row is data[(width * 3 - 1) * pixelSize]
The right most pixel in the last row (top) is data[(width * height - 1) * pixelSize]
A simple test
const gl = document.querySelector("canvas").getContext("webgl");
gl.enable(gl.SCISSOR_TEST);
drawRectUsingScissor(0, 0, 1, 1, [ 1, 0, 0, 1]);
drawRectUsingScissor(1, 0, 1, 1, [ 0, 1, 0, 1]);
drawRectUsingScissor(0, 1, 1, 1, [ 0, 0, 1, 1]);
drawRectUsingScissor(1, 1, 1, 1, [ 1, .5, 0, 1]);
const width = 2;
const height = 2;
const pixelSize = 4;
const data = new Uint8Array(width * height * pixelSize);
gl.readPixels(0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE, data);
log("raw: ", data);
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const offset = (y * width + x) * pixelSize;
log(x, ',', y, ':', data.slice(offset, offset + pixelSize));
}
}
function drawRectUsingScissor(x, y, width, height, color) {
gl.clearColor(...color);
gl.scissor(x, y, width, height);
gl.clear(gl.COLOR_BUFFER_BIT);
}
function log(...args) {
const elem = document.createElement("pre");
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
canvas {
width: 100px;
height: 100px;
image-rendering: pixelated;
}
pre { margin: 0 }
<canvas width="2" height="2"></canvas>
Note that the above is not strictly true since you have to take gl.PACK_ALIGNMENT setting into account which defaults to 4 but can be set to 1, 2, 4, or 8. For RGBA/UNSIGNED_BYTE reading you don't have to worry about PACK_ALIGNMENT