I'm trying to do an isometric projection in HTML5 and the closest I can get is by scaling and rotating the viewport
window.addEventListener('DOMContentLoaded', (event) => {
class Point {
constructor() {
this.x = 0;
this.y = 0;
}
}
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function tick(time) {
ctx.save();
ctx.translate(250,0);
ctx.scale(1, 0.5);
ctx.rotate(45 * Math.PI /180);
let p = new Point();
p.x = 4*32;
p.y = 2*32;
ctx.fillStyle = 'green';
ctx.fillRect(p.x,p.y,32,32);
for (var x = 0; x < 10; ++x) {
for (var y = 0; y < 10; ++y) {
ctx.strokeStyle = 'black';
ctx.strokeRect(x*32, y*32, 32, 32);
}
}
ctx.restore();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
And it does work..but this isn't a practical solution because then when I try to draw isometric sprites within that scaled and rotated view, it looks really distorted. So I'd rather not go that route unless I can get it to work without distorting my sprites.
I also tried this website https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-primer-for-game-developers-updated--cms-28392
Which provided an equation for generating isometric grids. I used their cartesianToIsometric function to try and make an iso grid
window.addEventListener('DOMContentLoaded', (event) => {
class Point {
constructor() {
this.x = 0;
this.y = 0;
}
}
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function cartesianToIsometric(cartPt) {
var tempPt = new Point();
tempPt.x = cartPt.x - cartPt.y;
tempPt.y = (cartPt.x + cartPt.y) / 2;
return (tempPt);
}
function tick(time) {
for (var x = 0; x < 50; ++x) {
for (var y = 0; y < 50; ++y) {
let p = new Point();
p.x = x * 32;
p.y = y * 32;
let iso = cartesianToIsometric(p);
ctx.strokeStyle = 'black';
ctx.strokeRect(iso.x, iso.y, 32, 32);
}
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
but it still looked wrong Unless I rotate or scale the viewport to correct it.
So my question is..how can I draw an isometric grid without scaling and rotating my viewport(if possible)
If I have to scale my viewport like the first example, how can I do it without distorting my sprites.... Sorry if this question is confusing to read my knowledge on this is iffy..
Here's my html file
index.html
<html>
<head>
<script src="index.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Looks like you are so close to the solution but a little bit confused. Let me explain:
Normally, you don't have to rotate and scale your canvas viewport. This is something you do when you create your isometric sprites. You should already have some isometric sprites and all you have to do just to put them in correct isometric coordinates.
Your second approach will do exactly what I mean, calculating the isometric points by using cartesian coordinates. It should work, no need to draw rectangles just place the images on the isometric coordinates.
The only trick here you should place your isometric sprites from their bottom center points:
for (var x = 0; x < 50; ++x) {
for (var y = 0; y < 50; ++y) {
let p = new Point();
p.x = x * 32;
p.y = y * 32;
let iso = cartesianToIsometric(p);
// Apply offset to place each isometric image from its bottom center.
// The default pivot point (top left) won't do good
// because we need to stack them up according to their heights.
let offset = {x: floorImageWidth/2, y: floorImageHeight}
ctx.drawImage(floor, iso.x - offset.x, iso.y - offset.y);
}
}
I also suggest using a separate function to draw iso points (not the rectangles) so that you can debug your positions:
let debug = true;
function debugDraw(time) {
for (var x = 0; x < 50; ++x) {
for (var y = 0; y < 50; ++y) {
let p = new Point();
p.x = x * tileWidth;
p.y = y * tileWidth;
let iso = cartesianToIsometric(p);
// draw pivot point for each tile
ctx.fillStyle = 'yellow';
ctx.fillRect(iso.x - 5, iso.y - 5, 10, 10);
}
}
}
if(debug){
requestAnimationFrame(debugDraw)
}
Plus, here is a working demo for you to experiment further:
https://codepen.io/justintc/pen/eYpMabx
Hope it helps, cheers!
Related
I have some code which generates collages from sets of photos, im doing this by averaging the pixels' colors of the images themselves and eventually, after a certain manipulation i just point(x,y) with the averaged color.
The only problem is that when I zoom on retina screens (above certain resolution) it is very visible that this is indeed a bunch of points on the screen and not 1 complete image.
I guess it has something to do with pixelDensity() but after a lot of experimentations with that, it didn't help as well.
attached here is an example of a zoomed crop -
The main loop which combines the pixels is very basic and looks like this -
for (let y = 0; y <= imgOne.height; y++) {
for (let x = 0; x <= imgOne.width; x++) {
// Get the colors.
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo) ) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo) ) / 2;
stroke(avgRed,avgGreen,avgBlue);
point(x, y);
}
}
The point function is specifically going to draw a tiny round shape with a diameter equal to the pixelDensity. When you scale up the canvas either by CSS transform or by using your browsers zoom function you are going to start to see the sub-pixel artifacts of this. There are two ways to make sure your "points" of color are square and completely fill the plane even when zoomed in: 1) use the set() function to explicitly draw pixels, 2) use the square() or rect() functions to deliberately draw a square. (Theoretically you could also directly manipulate the pixels array, but this would be significantly more complicated).
Here is an example that demonstrates the original issue, as well as the different solutions.
// only show a portion of the image.
const W = 120;
const H = 120;
let imgOne;
let imgTwo;
function preload() {
// "Recursive raytrace of a sphere" by Tim Babb is licensed under CC BY-SA 4.0
// https://creativecommons.org/licenses/by-sa/4.0/
imgOne = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/32/Recursive_raytrace_of_a_sphere.png/240px-Recursive_raytrace_of_a_sphere.png");
//
imgTwo = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Rainbow-gradient-fully-saturated.svg/240px-Rainbow-gradient-fully-saturated.svg.png");
}
function setup() {
createCanvas(W * 3, H);
noLoop();
}
function draw() {
background(220);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
// Get the colors.
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo)) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo)) / 2;
stroke(avgRed, avgGreen, avgBlue);
point(x, y);
}
}
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo)) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo)) / 2;
set(x + W, y, color(avgRed, avgGreen, avgBlue));
}
}
updatePixels();
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const colorOne = imgOne.get(x, y);
const colorTwo = imgTwo.get(x, y);
let avgRed = (red(colorOne) + red(colorTwo)) / 2;
let avgGreen = (green(colorOne) + green(colorTwo)) / 2;
let avgBlue = (blue(colorOne) + blue(colorTwo)) / 2;
fill(avgRed, avgGreen, avgBlue);
noStroke();
square(x + W * 2, y, 1);
}
}
}
// This work is licensed under a CC BY-SA 4.0 License.
// https://creativecommons.org/licenses/by-sa/4.0/
// Author: Paul Wheeler
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
transform-origin: top left;
transform: scale(4);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
In order for the problem to be reproduced you need to use your browsers zoom capability first and then run the code. I'm not sure exactly why the behavior differs when you run the code and then zoom.
I need to draw multiples balls inside a rect. I have a rect and 4 informations. Width and height of the rect.. numbers of balls per line and numbers of lines. That's been said I have to draw, for example, 4 balls at the same line. starting by the corners(That I was able to do) but I can't figure out how to draw more than 2 balls, example: If I have 3 balls, I need to draw 2 in the corners and 1 in the middle, if I have 4 balls... 2 in the corners and 2 in the middle. I had the idea of think about the rect as a matrix but having no luck.. link to see what I mean
If you need to drawn for example n dragon balls on line then you can divide length with n + 1 to get spacing between center of balls, or if you want different offset on start and end then you would divide (width - 2*offset) / (n - 1).
<canvas id="canvas" width="300" height="100">
</canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
class Rect {
constructor(x, y, width, heght) {
this.x = x;
this.y = y;
this.width = width;
this.heght = heght;
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
}
class Scene
{
constructor() {
this.items = [];
}
clear() {
this.items = [];
}
add(item) {
this.items.push(item);
}
draw(ctx) {
for(let item of this.items) {
if (item instanceof Rect) {
ctx.beginPath();
ctx.rect(item.x, item.y, item.width, item.heght);
ctx.stroke();
} else if (item instanceof Circle) {
ctx.beginPath();
ctx.arc(item.x, item.y, item.radius, 0, 2 * Math.PI);
ctx.stroke();
}
}
}
}
const scene = new Scene();
scene.clear();
scene.add(new Rect(0, 0, 300, 100));
let n = 5;
let offset = 30;
let spacing = ((300 - 2 * offset ) / (n - 1));
for (let i = 0; i < n; i++) {
scene.add(new Circle(i * spacing + offset, 50, 25))
}
scene.draw(ctx);
</script>
I loved it and i'm using it now... although i'm having trouble trying to positioning the balls inside my draw... see what I got so far and if you have a little more time to give me a hand in this <3 (I need to put the balls inside the third rect only no matter what width or height the user enter)
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
scene.clear();
context.beginPath();
context.strokeRect(zoomedX(0), zoomedY(0), zoomed(width), zoomed(height));
context.strokeRect(zoomedX(55), zoomedY(55), zoomed(width-10), zoomed(height-10));
context.strokeRect(zoomedX(60), zoomedY(60), zoomed(width-20), zoomed(height-20));
context.closePath();
let radius = 8;
let n = 3;
let lines = 3;
let offset = 68;
let offsetY = 68;
let spacing = ((width - 2 * offset ) / (n - 1));
let spacingY = ((height - 2 * offset ) / (lines - 1));
for (let i = 0; i < n; i++) {
for(let j = 0; j < lines ;j++){
scene.add(new Circle(i * spacing + offset, j * spacingY + offset, radius))
}
}
scene.draw(context);
}
Am trying to calculate width and height of object i loaded into canvas. When object is not rotated i get correct left right top bottom values, but when i load rotated object in canvas then i not get correct values , so i wonder what will be the logic or math formula to do achieve it.
how am doing.
initially load image into canvas
get image data from canvas
loop through image data to get only colored pixels by using alpha check
from colored pixel array find min max xy values
var temp_ray = []; // pixel array
for (var y = 0; y < imgData.height; ++y) {
for (var x = 0; x < imgData.width; ++x) {
var index = (y * imgData.width + x) * 4;
if(imgData.data[index+3]){
var xc = (index / 4) % imgData.width;
var yc = Math.floor((index / 4) / imgData.width);
temp_ray.push([xc,yc]);
}
}
}
if(temp_ray.length > 0){
var Xind = MaxMin2darray(temp_ray,0);
var Yind = MaxMin2darray(temp_ray,1);
var W = parseFloat(Xind['max']) - parseFloat(Xind['min']);
var H = parseFloat(Yind['max']) - parseFloat(Yind['min']);
var center_x = Xind['min'] + (W/2);
var center_y = Yind['min'] + (H/2);
// find corners of object
// find *min x , min y
let top_left = temp_ray[Xind['imin']]; // min X priority , min Y // top left
// find max x , *min y
let top_right = temp_ray[Yind['imin']]; // max X, min Y priority , // top right
// find *max x , min y
let bot_right = temp_ray[Xind['imax']]; // max X priority , min Y // bottom right
// find max x , *max y
let bot_left = temp_ray[Yind['imax']]; // max X , max Y priority // bottom left
var dim = {'W':W,'H':H,'CenterX':center_x,'CenterY':center_y,'top_left':top_left,'top_right':top_right,'bot_right':bot_right,'bot_left':bot_left,'Xend':Xind['max'],'Yend':Yind['max'],'Xstart':Xind['min'],'Ystart':Yind['min'],'Xend':Xind['max'],'Yend':Yind['max']};
console.log(dim);
}
and then using min max xy value find corners of object which works with none rotated objects but not work with rotated/tilted objects.
so any idea how to solve this problem
openpnp project is achieving this through opencv, but i think in js we do not have opencv library nor am that pro of java :(.
https://github.com/openpnp/openpnp/blob/develop/src/main/java/org/openpnp/vision/pipeline/stages/DrawRotatedRects.java
jsfiddle: http://jsfiddle.net/4L13vtaj/
In some simple cases (like rectangular objects), you could try to rotate the image until you minimize the number of uncolored pixels.
So you start with your image, and for each of the possible 360°, you compute the ratio. This is not perfect, but "doable" simply in pure js.
Here's a pseudoCode that might help you:
for degree in [0,365]{
rotateOriginalImageBy(degree);
cost[degree] = NemptyPixels/NfilledPixels;
}
predictedDegree = Math.min(cost);
rotateOriginalImageBy(predictedDegree);
compute 2 dimensions;
width = largerDimension;
height = shorterDimension;
Begining of an implementation (I edited your jsfiddle):
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var rotatioDegree = 45;
var imageObject = new Image();
imageObject.onload = function() {
var canvasWidth = imageObject.width;
var canvasHeight = canvasWidth; // not useful since width==height
document.getElementById('canvas').width = canvasWidth;
document.getElementById('canvas').height = canvasWidth;
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
// Move registration point to the center of the canvas
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
ctx.drawImage(imageObject,0,0);
ctx.translate(canvasWidth/2, canvasWidth/2)
ctx.rotate(-rotatioDegree*3.1415/180);
ctx.translate(-canvasWidth/2,-canvasWidth/2)
var imgData = ctx.getImageData(0, 0, canvasWidth, canvasWidth);
http://jsfiddle.net/4L13vtaj/17/
If this doesn't work, you could implement some image detection techniques (Mathematical morphology for example). But i think this is outside the scope of stackoverflow.
If you work with some approximation, you can have something like that; I hope at least it can provide to you some ideas:
// some pixels in this image are not transparent, so we add a tollerance
// you can try to remove the second condition.
const isNotEmpty = (color) => color && color < 0xffaaaaaa;
function getTop(buff, w, h) {
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getRight(buff, w, h) {
for (let x = w; x >=0; x--) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getBottom(buff, w, h) {
for (let y = h; y >= 0; y--) {
for (let x = 0; x < w; x++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
function getLeft(buff, w, h) {
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let i = y * w + x;
if (isNotEmpty(buff[i])) {
return {x, y}
}
}
}
}
async function main(imageSource) {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const imageObject = new Image();
imageObject.src = imageSource;
await new Promise(r => imageObject.onload = r);
const w = canvas.width = imageObject.width;
const h = canvas.height = imageObject.height;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(imageObject, 0, 0);
const imgData = ctx.getImageData(0, 0, w, h);
const buff = new Uint32Array(imgData.data.buffer);
const points = [
getTop(buff, w, h),
getRight(buff, w, h),
getBottom(buff, w, h),
getLeft(buff, w, h)
];
ctx.strokeStyle = "#0000ff"
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.lineTo(points[2].x, points[2].y);
ctx.lineTo(points[3].x, points[3].y);
ctx.closePath();
ctx.stroke();
}
main(/* image's url*/);
Here the link for testing: https://codepen.io/zer0/pen/zLxyQV
There are several problem with this approach: as said, with irregular images, it's not precise, in fact you will see the pin are making the image's bounding box a little bit smaller.
But the thing can be worse: try in the link above to use the 2nd image, that is quite irregular, and you will see.
Of course we can compensate, using also a bit more complex algorithm instead this simple one, but the question is: what the expected result for something like the 2nd image? Depends by that you can decide how to proceed.
I'm trying to practice using matter.js to create top down levels Bomberman style.
Right now I want to get my circle, which is controlled by arrow keys to move and bump into static squares but it is just going through them. Did I set it up incorrectly? I have been coding for three months, so I might be quite slow sorry!
var Engine = Matter.Engine,
World = Matter.World,
Bodies = Matter.Bodies;
var engine = Engine.create();
var world = engine.world;
var player;
var rocks = [];
var cols = 7;
var rows = 7;
function setup() {
createCanvas(750, 750);
Engine.run(engine);
player = new Player(300, 300, 25);
var spacing = width / cols;
for (var j = 0; j < rows; j++) {
for (var i = 0; i < cols; i++) {
var r = new Rocks(i * spacing, j * spacing);
rocks.push(r);
}
}
}
function draw() {
background(51);
Engine.update(engine);
for (var i = 0; i < rocks.length; i++) {
rocks[i].show();
}
player.show();
player.move();
}
function Player(x, y, r) {
this.body = Bodies.circle(x, y, r);
this.r = r;
World.add(world, this.body);
this.show = function () {
ellipse(x, y, this.r * 2);
}
this.move = function () {
if (keyIsDown(RIGHT_ARROW))
x += 10;
if (keyIsDown(LEFT_ARROW))
x -= 10;
if (keyIsDown(UP_ARROW))
y -= 10;
if (keyIsDown(DOWN_ARROW))
y += 10;
x = constrain(x, this.r, height - this.r);
y = constrain(y, this.r, width - this.r);
}
}
function Rocks(x, y, w, h, options) {
var options = {
isStatic: true
}
this.body = Bodies.rectangle(x, y, h, w, options);
this.w = w;
this.h = h;
this.size = player.r * 2;
World.add(world, this.body);
this.show = function () {
rect(x, y, this.size, this.size);
}
}
I think the problem is that you player is not drawn in the same position that the physics engine thinks its at.
in your Player function after the initialization of x and y the rest all need to be this.body.position.x and this.body.position.y. Otherwise you're changing where the picture is drawn at but not where the player actually is.
I'm not entirely sure what all you want me to point out besides that but also I think you want to disable gravity with engine.world.gravity.y = 0 and I was trying to fix the constrain function because as I tested it it wasn't working, I wasn't able to fix it but I'd recommend just making static boundary objects for the walls and just don't draw them.
Also matter.js processes the locations of objects from their centers. When drawing the objects you either have to take that into consideration or switch the mode to ellipseMode(CENTER);, 'rectMode(CENTER);` .. etc.
I hope this helps
I am trying to create a grid-based shadow engine using JavaScript. My algorithm shades squares based on whether their position is 'behind' a block, relative to a light source.
This is my algorithm so far: https://jsfiddle.net/jexqpfLf/
var canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var light_x = 90;
var light_y = 110;
var block_x = 120;
var block_y = 120;
requestAnimationFrame(render);
function render() {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var vec1_x = block_x - light_x;
var vec1_y = block_y - light_y;
var vec1_mag = Math.sqrt(vec1_x * vec1_x + vec1_y * vec1_y);
ctx.fillStyle = 'black';
for (var x = 0; x < canvas.width; x += 10)
for (var y = 0; y < canvas.width; y += 10) {
var vec2_x = x - light_x;
var vec2_y = y - light_y;
var vec2_mag = Math.sqrt(vec2_x * vec2_x + vec2_y * vec2_y);
var dotproduct = vec1_x * vec2_x + vec1_y * vec2_y;
var angle = Math.acos(dotproduct / (vec1_mag * vec2_mag));
if (vec2_mag > vec1_mag && angle < Math.PI / 8 / vec1_mag * 10)
ctx.fillRect(x, y, 10, 10);
}
ctx.fillStyle = 'green';
ctx.fillRect(light_x, light_y, 10, 10);
ctx.fillStyle = 'red';
ctx.fillRect(block_x, block_y, 10, 10);
requestAnimationFrame(render);
}
onkeydown = function (e) {
if (e.which == 65)
light_x -= 10;
if (e.which == 68)
light_x += 10;
if (e.which == 87)
light_y -= 10;
if (e.which == 83)
light_y += 10;
}
Unfortunately, as you can see in the demonstration, I'm finding some angles problematic. Some squares which should be shaded are left unshaded. This happens for some angles and distances (between the light source and the block) but not others. For example, placing the light source at (60, 90) shows these artifacts as well.
I am using the vectors LP (from light to point) and LB (from light to block), taking their dot product and dividing by the product of their magnitudes to find the shading angle, then scaling this angle depending on the distance between the block and light source.
Could these artifacts be due to rounding errors? Or is there a problem with the algorithm itself? Any help would be appreciated :-)
Great question. You're not gonna like this one.
It's a floating point math issue.
What's the value of Math.acos(1.000000000000000)?
0.
Whats the value of Math.acos(1.0000000000000003)?
NaN.
That's annoying, isn't it?
At some values, your dotproduct is 6000 and your (vec1_mag * vec2_mag) is 5999.999999999999, leading to the issue above.
Changing (vec1_mag * vec2_mag) to Math.round(vec1_mag * vec2_mag) will solve your problem.
While we're staring at this fiddle together you should know that there's another bug:
for (var x = 0; x < canvas.width; x += 10) {
for (var y = 0; y < canvas.width; y += 10) {
You use canvas.width here twice. I imagine the second one ought to be canvas.height, so make sure what you wrote there is what you want.
Working fiddle for you!