How to detect shape on a transparent canvas? - javascript

I'm looking for a method of detecting a shape in a transparent PNG.
For example, I will create a transparent canvas of 940x680, then place a fully opaque object somewhere in that canvas.
I want to be able to detect the size (w, h), and top + left location of that object.
Here is an example of the original image:
Here is an example of what I would like to achieve (Bounding box overlay, with top + left margin data):
I've found a resource that does some transparency detection, but I'm not sure how I scale something like this to what I'm looking for.
var imgData,
width = 200,
height = 200;
$('#mask').bind('mousemove', function(ev){
if(!imgData){ initCanvas(); }
var imgPos = $(this).offset(),
mousePos = {x : ev.pageX - imgPos.left, y : ev.pageY - imgPos.top},
pixelPos = 4*(mousePos.x + height*mousePos.y),
alpha = imgData.data[pixelPos+3];
$('#opacity').text('Opacity = ' + ((100*alpha/255) << 0) + '%');
});
function initCanvas(){
var canvas = $('<canvas width="'+width+'" height="'+height+'" />')[0],
ctx = canvas.getContext('2d');
ctx.drawImage($('#mask')[0], 0, 0);
imgData = ctx.getImageData(0, 0, width, height);
}
Fiddle

What you need to do:
Get the buffer
Get a 32-bits reference of that buffer (If your other pixels are transparent then you can use a Uint32Array buffer to iterate).
Scan 0 - width to find x1 edge
Scan width - 0 to find x2 edge
Scan 0 - height to find y1 edge
Scan height - 0 to find y2 edge
These scans can be combined but for simplicity I'll show each step separately.
Online demo of this can be found here.
Result:
When image is loaded draw it in (if the image is small then the rest of this example would be waste as you would know the coordinates when drawing it - assuming here the image you draw is large with a small image inside it)
(note: this is a non-optimized version for the sake of simplicity)
ctx.drawImage(this, 0, 0, w, h);
var idata = ctx.getImageData(0, 0, w, h), // get image data for canvas
buffer = idata.data, // get buffer (unnes. step)
buffer32 = new Uint32Array(buffer.buffer), // get a 32-bit representation
x, y, // iterators
x1 = w, y1 = h, x2 = 0, y2 = 0; // min/max values
Then scan each edge. For left edge you scan from 0 to width for each line (non optimized):
// get left edge
for(y = 0; y < h; y++) { // line by line
for(x = 0; x < w; x++) { // 0 to width
if (buffer32[x + y * w] > 0) { // non-transparent pixel?
if (x < x1) x1 = x; // if less than current min update
}
}
}
For the right edge you just reverse x iterator:
// get right edge
for(y = 0; y < h; y++) { // line by line
for(x = w; x >= 0; x--) { // from width to 0
if (buffer32[x + y * w] > 0) {
if (x > x2) x2 = x;
}
}
}
And the same is for top and bottom edges just that the iterators are reversed:
// get top edge
for(x = 0; x < w; x++) {
for(y = 0; y < h; y++) {
if (buffer32[x + y * w] > 0) {
if (y < y1) y1 = y;
}
}
}
// get bottom edge
for(x = 0; x < w; x++) {
for(y = h; y >= 0; y--) {
if (buffer32[x + y * w] > 0) {
if (y > y2) y2 = y;
}
}
}
The resulting region is then:
ctx.strokeRect(x1, y1, x2-x1, y2-y1);
There are various optimizations you could implement but they depend entirely on the scenario such as if you know approximate placement then you don't have to iterate all lines/columns.
You could do a brute force guess of he placement by skipping x number of pixels and when you found a non-transparent pixel you could make a max search area based on that and so forth, but that is out of scope here.
Hope this helps!

I was in need of something similar to this, just recently. Although the question is answered, I wanted to post my code for a future reference.
In my case, I'm drawing a (font) icon on a blank/transparent canvas, and want to get the bounding box. Even if I know the height of the icon (using font-size, i.e., height), I can't know the width. So I have to calculate it manually.
I'm not sure if there's a clever way to calculate this. First thing that popped into my head was doing it the hard way: manually checking every pixel, and that's what I did.
I think the code is pretty self-explanatory, so I won't do any explanation. I tried to keep the code as clean as possible.
/* Layer 3: The App */
let canvas = document.querySelector("#canvas");
let input = document.querySelector("#input");
let output = document.querySelector("#output");
canvas.width = 256;
canvas.height = 256;
let context = canvas.getContext("2d");
context.font = "200px Arial, sans-serif";
let drawnLetter = null;
drawLetter(input.value);
function drawLetter(letter) {
letter = letter ? letter[0] : null;
if (!letter) {
// clear canvas
context.clearRect(0, 0, canvas.width, canvas.height);
output.textContent = null;
return;
}
if (letter == drawnLetter) {
return;
}
drawnLetter = letter;
// clear canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// draw letter
context.fillText(letter, 50, canvas.height - 50);
// find edges
let boundingBox = findEdges(context);
// mark the edges
context.beginPath();
context.rect(boundingBox.left, boundingBox.top, boundingBox.width, boundingBox.height);
context.lineWidth = 2;
context.strokeStyle = "red";
context.stroke();
// output the values
output.textContent = JSON.stringify(boundingBox, null, " ");
}
/* Layer 2: Interacting with canvas */
function findEdges(context) {
let left = findLeftEdge(context);
let right = findRightEdge(context);
let top = findTopEdge(context);
let bottom = findBottomEdge(context);
// right and bottom are relative to top left (0,0)
return {
left,
top,
right,
bottom,
width : right - left,
height : bottom - top,
};
}
function findLeftEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let x = 0; x < context.canvas.width; x++) {
for (let y = 0; y < context.canvas.height; y++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return x;
}
}
}
}
function findRightEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let x = context.canvas.width - 1; x >= 0; x--) {
for (let y = 0; y < context.canvas.height; y++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return x;
}
}
}
}
function findTopEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let y = 0; y < context.canvas.height; y++) {
for (let x = 0; x < context.canvas.width; x++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return y;
}
}
}
}
function findBottomEdge(context) {
let imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
let emptyPixel = [0, 0, 0, 0].join();
for (let y = context.canvas.height - 1; y >= 0; y--) {
for (let x = 0; x < context.canvas.width; x++) {
let pixel = getPixel(imageData, x, y).join();
if (pixel != emptyPixel) {
return y;
}
}
}
}
/* Layer 1: Interacting with ImageData */
/**
* Returns the pixel array at the specified position.
*/
function getPixel(imageData, x, y) {
return getPixelByIndex(imageData, pos2index(imageData, x, y));
}
/**
* Returns the RGBA values at the specified index.
*/
function getPixelByIndex(imageData, index) {
return [
imageData.data[index + 0],
imageData.data[index + 1],
imageData.data[index + 2],
imageData.data[index + 3],
];
}
/**
* Returns the index of a position.
*/
function pos2index(imageData, x, y) {
return 4 * (y * imageData.width + x);
}
body {
background-color: hsl(0, 0%, 95%);
}
canvas {
background: white;
image-rendering: pixelated;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEXMzMz////TjRV2AAAAEUlEQVQI12P4z8CAFWEX/Q8Afr8P8erzE9cAAAAASUVORK5CYII=);
zoom: 0.8; /* this counters the scale up (125%) of my screen; can be removed */
}
input {
padding: 0.2em;
margin-top: 0.5em;
}
<canvas id="canvas"></canvas>
<br>
<input type="text" id="input" placeholder="type a letter" value="A" onkeyup="drawLetter(this.value)" />
<pre id="output"></pre>

Related

Perlin noise grid transformation question

Objective: Amend the compassGrid() function to create a grid of 25x25 lines of length stepSize. Make sure each compass is at the center of each tile. By default they should all be pointing up. You should use translate() to move to the center of each grid.
I've tried all kinds of stuff, but none of my attempts seem to reach the objective outlined above. Here’s my code:
var stepSize = 20;
function setup() {
createCanvas(500, 500);
}
///////////////////////////////////////////////////////////////////////
function draw() {
background(125);
colorGrid();
compassGrid();
}
///////////////////////////////////////////////////////////////////////
function colorGrid() {
for (var x = 0; x < width; x += width / 25) {
for (var y = 0; y < height; y += height / 25) {
let from = color(255, 0, 0);
let to = color(0, 255, 0);
let interA = lerpColor(from, to, n);
var n = noise(x / 50, y / 50, frameCount / 100);
var c = map(n, 0, 1, 0, 255);
fill(interA);
stroke(c);
rect(x, y, stepSize, stepSize);
}
}
}
///////////////////////////////////////////////////////////////////////
function compassGrid() {
translate(10, 10);
for (var x = 0; x < width; x += width / 25) {
for (var y = 0; y < height; y += height / 25) {
push();
translate(x, y);
stroke("black");
line(0, 0, 0, stepSize - 5);
pop();
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>
What's my mistake?

Changing the Direction of an Object in a Diagonal orientation

I'm trying to make an object move in the shape of a triangle however, I'm running into some trouble with changing the orientation. I've been referencing my lecture notes but I can't seem to change what I want.
function aniLoop(){
clear();
circlesUpdate(aC);
requestAnimationFrame(aniLoop)
}
function circlesUpdate(a){
for (var i = 0; i<a.length; i++){
circle(a[i]);
updateData(a[i]);
}
}
function circle(o){
ctx.beginPath();
ctx.arc(o.x, o.y, o.r, 0, 2*Math.PI);
ctx.fillStyle = "hsla("+o.c+", 100%, 50%, "+o.a+")";
ctx.fill();
}
function randn(r){
return Math.random()*r - r/2
}
function rand(r){
return Math.random()*r
}
I'm trying to make a loop of the circle to move in a triangle starting from w/2 and h/5 of the canvas size.
function createData(num){
for (var i = 0; i<num; i++){
aC.push({
x: w/2,
dx: randn(0),
y: h/5,
dy: 2+rand(3),
r: 50,
c: 200+rand(60),
a: 0.5,
})
}
}
function clear(){
ctx.clearRect(0, 0, w, h);
}
function updateData(o){
var i;
o.x += o.dx;
o.y += o.dy;
o.a -= o.da;
if(o.x > w || o.x < 0){
o.dx *= -1;
}
if(o.y > h || o.y < 0){
o.dy *= -1;
}
}
I know that the function is set up so that the circle starts from w/2,h/5 and moves down then bounces back up, but I'm not sure how to change that so the circle moves in a triangle orientation from the top corner to the bottom left to the bottom right and back to the top corner.
If I understood correctly...
You want to create an animation where a circle moves in a triangle loop.
I see too much code on that example, no clue why we need random for this, also it does not show how/where the createData function is used...
Here is what I would do:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var i = 0, n = 1
var points = [[10,10], [80,10], [10,80]]
var speed = [ [1,0], [-1, 1], [0,-1]]
var p = points[i]
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(p[0], p[1], 5, 2* Math.PI, 0);
ctx.fill()
p = [p[0]+speed[i][0], p[1]+speed[i][1]]
if (Math.hypot(p[0]-points[n][0], p[1]-points[n][1]) < 2) {
i = n
n = (n+1) % points.length
}
}
setInterval(draw, 30);
<canvas id="canvas" width=150 height=150></canvas>
You don't really specify what type of triangle so for simplicity, I coded a right triangle...
On the points = [[10,10], [80,10], [10,80]]
those are the corners of our triangle
And on speed = [ [1,0], [-1, 1], [0,-1]]
that is the speed for our object (wanna go faster change those 1 to 2)
We have minimal math:
p = [p[0]+speed[i][0], p[1]+speed[i][1]]
Just increasing the position by current speed
And on the if statement:
if (Math.hypot(p[0]-points[n][0], p[1]-points[n][1]) < 2)
We are just checking the distance to the next point, if we are less than 2 pixels away we will change our "current" index to next i = n

My grid quickly disappears on page load due to incorrect force being applied?

So I'm recreating the warping grid from Geometry Wars in a web page to further test my skills with JavaScript and I've hit another snag. I'm following a tutorial written in C# over on TutsPlus that I used a long time ago to recreate it while learning XNA Framework. The tutorial is straight forward, and most of the code is self-explanatory, but I think my lack of superior education in mathematics is letting me down once again.
I've successfully rendered the grid in a 300x300 canvas with no troubles, and even replicated all of the code in the tutorial, but since they're using the XNA Framework libraries, they have the advantage of not having to write the mathematical functions of the Vector3 type. I've implemented only what I need, but I believe I may have gotten my math incorrect or perhaps the implementation.
The initial grid (above) should look like this until I begin interacting with it, and it does, as long as I disable the Update function of my Grid. I've stepped through the code and the issue seems to be related to my calculation for the magnitude of my vectors. The XNA Framework libraries always called it Length and LengthSquared, but each Google search I performed was returning results for calculating magnitude as:
Now, this is incredibly simple to recreate in code, and my Vector3 class accounts for Magnitude and MagnitudeSquared since the tutorial calls for both. I've compared the results of my magnitude calculation to that of an online calculator and the results were the same:
V = (2, 3, 4)
|V| = 5.385164807134504
To top this off, the URL for this calculator says that I'm calculating the length of the vector. This is what leads me to believe that it may be my implementation here that is causing the whole thing to go crazy. I've included my snippet below, and it is unfortunately a bit long, but I assure you it has been trimmed as much as possible.
class Vector3 {
constructor(x, y, z) {
this.X = x;
this.Y = y;
this.Z = z;
}
Add(val) {
this.X += val.X;
this.Y += val.Y;
this.Z += val.Z;
}
Subtract(val) {
this.X -= val.X;
this.Y -= val.Y;
this.Z -= val.Z;
}
MultiplyByScalar(val) {
let result = new Vector3(0, 0, 0);
result.X = this.X * val;
result.Y = this.Y * val;
result.Z = this.Z * val;
return result;
}
DivideByScalar(val) {
let result = new Vector3(0, 0, 0);
result.X = this.X / val;
result.Y = this.Y / val;
result.Z = this.Z / val;
return result;
}
Magnitude() {
if (this.X == 0 && this.Y == 0 && this.Z == 0)
return 0;
return Math.sqrt(Math.pow(this.X, 2) +
Math.pow(this.Y, 2) +
Math.pow(this.Z, 2));
}
MagnitudeSquared() {
return Math.pow(this.Magnitude(), 2);
}
DistanceFrom(to) {
let x = Math.pow(this.X - to.X, 2);
let y = Math.pow(this.Y - to.Y, 2);
let z = Math.pow(this.Z - to.Z, 2);
return Math.sqrt(x + y + z);
}
}
class PointMass {
Acceleration = new Vector3(0, 0, 0);
Velocity = new Vector3(0, 0, 0);
Damping = 0.95;
constructor(position, inverseMass) {
this.Position = position;
this.InverseMass = inverseMass;
}
IncreaseDamping(factor) {
this.Damping *= factor;
}
ApplyForce(force) {
this.Acceleration.Add(force.MultiplyByScalar(this.InverseMass));
}
Update() {
this.Velocity.Add(this.Acceleration);
this.Position.Add(this.Velocity);
this.Acceleration = new Vector3(0, 0, 0);
if (this.Velocity.MagnitudeSquared() < 0.001 * 0.001)
Velocity = new Vector3(0, 0, 0);
this.Velocity.MultiplyByScalar(this.Damping);
this.Damping = 0.95;
}
}
class Spring {
constructor(startPoint, endPoint, stiffness, damping) {
this.StartPoint = startPoint;
this.EndPoint = endPoint;
this.Stiffness = stiffness;
this.Damping = damping;
this.TargetLength = startPoint.Position.DistanceFrom(endPoint.Position) * 0.95;
}
Update() {
let x = this.StartPoint.Position;
x.Subtract(this.EndPoint.Position);
let magnitude = x.Magnitude();
if (magnitude < this.TargetLength || magnitude == 0)
return;
x = x.DivideByScalar(magnitude).MultiplyByScalar(magnitude - this.TargetLength);
let dv = this.EndPoint.Velocity;
dv.Subtract(this.StartPoint.Velocity);
let force = x.MultiplyByScalar(this.Stiffness)
force.Subtract(dv.MultiplyByScalar(this.Damping));
this.StartPoint.ApplyForce(force);
this.EndPoint.ApplyForce(force);
}
}
class Grid {
Springs = [];
Points = [];
constructor(containerID, spacing) {
this.Container = document.getElementById(containerID);
this.Width = this.Container.width;
this.Height = this.Container.height;
this.ColumnCount = this.Width / spacing + 1;
this.RowCount = this.Height / spacing + 1;
let columns = [];
let fixedColumns = [];
let rows = [];
let fixedRows = [];
let fixedPoints = [];
for (let y = 0; y < this.Height; y += spacing) {
for (let x = 0; x < this.Width; x += spacing) {
columns.push(new PointMass(new Vector3(x, y, 0), 1));
fixedColumns.push(new PointMass(new Vector3(x, y, 0), 0));
}
rows.push(columns);
fixedRows.push(fixedColumns);
columns = [];
fixedColumns = [];
}
this.Points = rows;
for (let y = 0; y < rows.length; y++) {
for (let x = 0; x < rows[y].length; x++) {
if (x == 0 || y == 0 || x == rows.length - 1 || x == rows[y].length - 1)
this.Springs.push(new Spring(fixedRows[x][y], this.Points[x][y], 0.1, 0.1));
else if (x % 3 == 0 && y % 3 == 0)
this.Springs.push(new Spring(fixedRows[x][y], this.Points[x][y], 0.002, 0.002));
const stiffness = 0.28;
const damping = 0.06;
if (x > 0)
this.Springs.push(new Spring(this.Points[x - 1][y], this.Points[x][y], stiffness, damping));
if (y > 0)
this.Springs.push(new Spring(this.Points[x][y - 1], this.Points[x][y], stiffness, damping));
}
}
}
ApplyDirectedForce(force, position, radius) {
this.Points.forEach(function(row) {
row.forEach(function(point) {
if (point.Position.DistanceFrom(position) < Math.pow(radius, 2))
point.ApplyForce(force.MultiplyByScalar(10).DivideByScalar(10 + point.Position.DistanceFrom(position)));
});
});
}
ApplyImplosiveForce(force, position, radius) {
this.Points.forEach(function(point) {
let distance_squared = Math.pow(point.Position.DistanceFrom(position));
if (distance_squared < Math.pow(radius, 2)) {
point.ApplyForce(force.MultiplyByScalar(10).Multiply(position.Subtract(point.Position)).DivideByScalar(100 + distance_squared));
point.IncreaseDamping(0.6);
}
});
}
ApplyExplosiveForce(force, position, radius) {
this.Points.forEach(function(point) {
let distance_squared = Math.pow(point.Position.DistanceFrom(position));
if (distance_squared < Math.pow(radius, 2)) {
point.ApplyForce(force.MultiplyByScalar(100).Multiply(point.Position.Subtract(position)).DivideByScalar(10000 + distance_squared));
point.IncreaseDamping(0.6);
}
});
}
Update() {
this.Springs.forEach(function(spring) {
spring.Update();
});
this.Points.forEach(function(row) {
row.forEach(function(point) {
point.Update();
});
});
}
Draw() {
const context = this.Container.getContext('2d');
context.clearRect(0, 0, this.Width, this.Height);
context.strokeStyle = "#ffffff";
context.fillStyle = "#ffffff";
for (let y = 1; y < this.Points.length; y++) {
for (let x = 1; x < this.Points[y].length; x++) {
let left = new Vector3(0, 0, 0);
let up = new Vector3(0, 0, 0);
if (x > 1) {
left = this.Points[x - 1][y].Position;
context.beginPath();
context.moveTo(left.X, left.Y);
context.lineTo(this.Points[x][y].Position.X, this.Points[x][y].Position.Y);
context.stroke();
}
if (y > 1) {
up = this.Points[x][y - 1].Position;
context.beginPath();
context.moveTo(up.X, up.Y);
context.lineTo(this.Points[x][y].Position.X, this.Points[x][y].Position.Y);
context.stroke();
}
let radius = 3;
if (y % 3 == 1)
radius = 5;
context.beginPath();
context.arc(this.Points[x][y].Position.X, this.Points[x][y].Position.Y, radius, 0, 2 * Math.PI);
context.fill();
}
}
}
}
var grid = new Grid("grid", 40);
setInterval(function() {
grid.Update();
grid.Draw();
}, 5);
var mouseX = 0;
var mouseY = 0;
function updateMouseCoordinates(evt) {
var rect = grid.Container.getBoundingClientRect();
mouseX = evt.clientX - rect.left;
mouseY = evt.clientY - rect.top;
const context = grid.Container.getContext('2d');
context.clearRect(0, 0, this.Width, this.Height);
context.strokeStyle = "#ffffff";
context.fillStyle = "#ff3333";
context.beginPath();
context.arc(mouseX, mouseY, 15, 0, 2 * Math.PI);
context.fill();
grid.ApplyDirectedForce(new Vector3(0, 0, 5000), new Vector3(mouseX, mouseY, 0), 50);
}
html,
body {
margin: 0;
height: 100%;
background: #213;
background: linear-gradient(45deg, #213, #c13);
background: -webkit-linear-gradient(45deg, #213, #c13);
}
.container {
position: relative;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
<div class="container">
<canvas onmousemove="updateMouseCoordinates(event)" id="grid" class="grid" width="300" height="300"></canvas>
</div>
I believe the issue has something to do with the Update method in the Spring and PointMass classes as when I stepped through my code, I kept finding that the PointMass objects seemed to have acceleration when they shouldn't (as in, I haven't interacted with them yet). In all honesty, I think it's the implementation of my custom Vector3 functions in those update functions that are causing the issue but for the life of me, I can't figure out what I've done incorrectly here.
Perhaps I just need to take a break and come back to it, but I'm hoping someone here can help spot an incorrect implementation.
How do I prevent my grid from immediately dissipating due to forces that have not yet been exerted (as in they are just miscalculations)?
My advice is reduce the problem down. Have only a single point, slow the interval down, step through to see what's happening. The mouse doesn't appear to be doing anything. Commenting out the line grid.ApplyDirectedForce(new Vector3(0, 0, 5000), new Vector3(mouseX, mouseY, 0), 50); doesn't change the output. It goes wrong in grid.Update(), for some reason grid.Update() does something even if there's no force applied, maybe that means the spring code has a bug. The bottom right point doesn't move frame one maybe that means something. The debugger is your friend. Add a breakpoint to grid.Update() and see what the code is actually doing. I know this isn't a direct answer but I hope this guides you in the right direction.
I also want to point out that usually the whole point of Magnitude Squared is so that you can compare vectors or distances without having to do a square root operation. That is, in your Magnitude function you do a Square root operation and then in your Magnitude Squared function you square it. This is is the same as simply going x^2 + y^2 + z^2
frame 1:
frame 2:

How to avoid repeated?

Good day,
I am generating some circles with colors, sizes and positions. All of this things randomly.
But, my problem is that I do not want them to collide, so that no circle is inside another, not even a little bit.
The logic explained in detail within the code, I would like to know why the failure and why the infinite loop.
The important functions are:
checkSeparation and setPositions
window.addEventListener("load", draw);
function draw() {
var canvas = document.getElementById("balls"), // Get canvas
ctx = canvas.getContext("2d"); // Context
canvas.width = document.body.clientWidth; // Set canvas width
canvas.height = document.documentElement.scrollHeight; // Height
var cW = canvas.width, cH = canvas.height; // Save in vars
ctx.fillStyle = "#fff022"; // Paint background
ctx.fillRect(0, 0, cW, cH); // Coordinates to paint
var arrayOfBalls = createBalls(); // create all balls
setPositions(arrayOfBalls, cW, cH);
arrayOfBalls.forEach(ball => { // iterate balls to draw
ctx.beginPath(); // start the paint
ctx.fillStyle = ball.color;
ctx.arc(ball.x, ball.y, ball.radius, 0, (Math.PI/180) * 360, false); // draw the circle
ctx.fill(); // fill
ctx.closePath(); // end the paint
});
}
function Ball() {
this.x = 0; // x position of Ball
this.y = 0; // y position of Ball
this.radius = Math.floor(Math.random() * ( 30 - 10 + 1) + 10);
this.color = "";
}
Ball.prototype.setColor = function(){
for(var j = 0, hex = "0123456789ABCDEF", max = hex.length,
random, str = ""; j <= 6; j++, random = Math.floor(Math.random() * max), str += hex[random])
this.color = "#" + str;
};
function random(val, min) {
return Math.floor(Math.random() * val + min); // Random number
}
function checkSeparation(value, radius, toCompare) {
var min = value - radius, // Min border of circle
max = value + radius; // Max border of circle
// Why ? e.g => x position of circle + this radius it will be its right edge
for(; min <= max; min++) {
if(toCompare.includes(min)) return false;
/*
Since all the positions previously obtained, I add them to the array, in order to have a reference when verifying the other positions and that they do NOT collide.
Here I check if they collide.
In the range of:
[pos x - its radius, pos x + its radius]
*/
}
return true; // If they never collided, it returns true
}
function createBalls() {
var maxBalls = 50, // number of balls
balls = []; // array of balls
for(var j = 0; j < maxBalls; j++) { // create 50 balls
var newBall = new Ball(); // create ball
newBall.setColor(); // set the ball color
balls.push(newBall); //push the ball to the array of balls
}
return balls; // return all balls to draw later
}
function setPositions(balls, canvasW, canvasH) {
var savedPosX = [], // to save x pos of balls
savedPosY = []; // to save y pos of balls
for(var start = 0, max = balls.length; start < max; start++) {
var current = balls[start], // current ball
randomX = random(canvasW, current.radius), // get random value for x pos
randomY = random(canvasH, current.radius); // get random value for y pos
if(checkSeparation(randomX, current.radius, savedPosX)) {
current.x = randomX; // If it position, along with your radio does not touch another circle, I add the position
} else {
// start--; continue;
console.log("X: The above code causes an infinite loop");
}
if(checkSeparation(randomY, current.radius, savedPosY)) {
current.y = randomY;
} else {
// start--; continue;
console.log("Y: The above code causes an infinite loop");
}
}
}
body,html {
margin: 0; border: 0; padding: 0; overflow: hidden;
}
<canvas id="balls"></canvas>
In your code, you test possible collisions by means of arrays of already used x and y positions, but you never add new positions to these arrays. You also check the x and y coordinates separately, which means you are really testing a collision of a bounding box.
Two circles collide when the distance between their centres is smaller than the sum of their radii, so you could use:
function collides(balls, n, x, y, r) {
for (let i = 0; i < n; i++) {
let ball = balls[i];
let dx = ball.x - x;
let dy = ball.y - y;
let dd = dx*dx + dy*dy;
let rr = r + ball.radius;
if (dd < rr * rr) return true;
}
return false;
}
function setPositions(balls, canvasW, canvasH) {
for (let i = 0, max = balls.length; i < max; i++) {
let ball = balls[i],
r = ball.radius,
maxTries = 20;
ball.x = -canvasW;
ball.y = -canvasH;
for (let tries = 0; tries = maxTries; tries++) {
let x = random(canvasW - 2*r, r),
y = random(canvasH - 2*r, r);
if (!collides(balls, i, x, y, r)) {
ball.x = x;
ball.y = y;
break;
}
}
}
}
This is reasonably fast for 50 balls, but will be slow if you have more balls. In that case, some spatial data structures can speed up the collision search.
You must also guard against the case that no good place can be found. The code above gives up after 20 tries and moves the ball outside the visible canvas. You can improve the chances of placing balls by sorting the balls by radius and plaing the large balls first.
Finally, you add one hex digit too many to your random colour. (That for loop, where everything happens in the loop control is horrible, by the way.)

How to create a camera view in canvas that will follow a players rotation and rotation?

I'm trying to create a game in canvas with javascript where you control a spaceship and have it so that the canvas will translate and rotate to make it appear like the spaceship is staying stationary and not rotating.
Any help would be greatly appreciated.
window.addEventListener("load",eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasSupport() {
return Modernizr.canvas;
}
function canvasApp() {
if (!canvasSupport()) {
return;
}
var theCanvas = document.getElementById("myCanvas");
var height = theCanvas.height; //get the heigth of the canvas
var width = theCanvas.width; //get the width of the canvas
var context = theCanvas.getContext("2d"); //get the context
var then = Date.now();
var bgImage = new Image();
var stars = new Array;
bgImage.onload = function() {
context.translate(width/2,height/2);
main();
}
var rocket = {
xLoc: 0,
yLoc: 0,
score : 0,
damage : 0,
speed : 20,
angle : 0,
rotSpeed : 1,
rotChange: 0,
pointX: 0,
pointY: 0,
setScore : function(newScore){
this.score = newScore;
}
}
function Star(){
var dLoc = 100;
this.xLoc = rocket.pointX+ dLoc - Math.random()*2*dLoc;
this.yLoc = rocket.pointY + dLoc - Math.random()*2*dLoc;
//console.log(rocket.xLoc+" "+rocket.yLoc);
this.draw = function(){
drawStar(this.xLoc,this.yLoc,20,5,.5);
}
}
//var stars = new Array;
var drawStars = function(){
context.fillStyle = "yellow";
if (typeof stars !== 'undefined'){
//console.log("working");
for(var i=0;i< stars.length ;i++){
stars[i].draw();
}
}
}
var getDistance = function(x1,y1,x2,y2){
var distance = Math.sqrt(Math.pow((x2-x1),2)+Math.pow((y2-y1),2));
return distance;
}
var updateStars = function(){
var numStars = 10;
while(stars.length<numStars){
stars[stars.length] = new Star();
}
for(var i=0; i<stars.length; i++){
var tempDist = getDistance(rocket.pointX,rocket.pointY,stars[i].xLoc,stars[i].yLoc);
if(i == 0){
//console.log(tempDist);
}
if(tempDist > 100){
stars[i] = new Star();
}
}
}
function drawRocket(xLoc,yLoc, rWidth, rHeight){
var angle = rocket.angle;
var xVals = [xLoc,xLoc+(rWidth/2),xLoc+(rWidth/2),xLoc-(rWidth/2),xLoc-(rWidth/2),xLoc];
var yVals = [yLoc,yLoc+(rHeight/3),yLoc+rHeight,yLoc+rHeight,yLoc+(rHeight/3),yLoc];
for(var i = 0; i < xVals.length; i++){
xVals[i] -= xLoc;
yVals[i] -= yLoc+rHeight;
if(i == 0){
console.log(yVals[i]);
}
var tempXVal = xVals[i]*Math.cos(angle) - yVals[i]*Math.sin(angle);
var tempYVal = xVals[i]*Math.sin(angle) + yVals[i]*Math.cos(angle);
xVals[i] = tempXVal + xLoc;
yVals[i] = tempYVal+(yLoc+rHeight);
}
rocket.pointX = xVals[0];
rocket.pointY = yVals[0];
//rocket.yLoc = yVals[0];
//next rotate
context.beginPath();
context.moveTo(xVals[0],yVals[0])
for(var i = 1; i < xVals.length; i++){
context.lineTo(xVals[i],yVals[i]);
}
context.closePath();
context.lineWidth = 5;
context.strokeStyle = 'blue';
context.stroke();
}
var world = {
//pixels per second
startTime: Date.now(),
speed: 50,
startX:width/2,
startY:height/2,
originX: 0,
originY: 0,
xDist: 0,
yDist: 0,
rotationSpeed: 20,
angle: 0,
distance: 0,
calcOrigins : function(){
world.originX = -world.distance*Math.sin(world.angle*Math.PI/180);
world.originY = -world.distance*Math.cos(world.angle*Math.PI/180);
}
};
var keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
addEventListener("keyup", function (e) {
delete keysDown[e.keyCode];
}, false);
var update = function(modifier) {
if (37 in keysDown) { // Player holding left
rocket.angle -= rocket.rotSpeed* modifier;
rocket.rotChange = - rocket.rotSpeed* modifier;
//console.log("left");
}
if (39 in keysDown) { // Player holding right
rocket.angle += rocket.rotSpeed* modifier;
rocket.rotChange = rocket.rotSpeed* modifier;
//console.log("right");
}
};
var render = function (modifier) {
context.clearRect(-width*10,-height*10,width*20,height*20);
var dX = (rocket.speed*modifier)*Math.sin(rocket.angle);
var dY = (rocket.speed*modifier)*Math.cos(rocket.angle);
rocket.xLoc += dX;
rocket.yLoc -= dY;
updateStars();
drawStars();
context.translate(-dX,dY);
context.save();
context.translate(-rocket.pointX,-rocket.pointY);
context.translate(rocket.pointX,rocket.pointY);
drawRocket(rocket.xLoc,rocket.yLoc,50,200);
context.fillStyle = "red";
context.fillRect(rocket.pointX,rocket.pointY,15,5);
//context.restore(); // restores the coordinate system back to (0,0)
context.fillStyle = "green";
context.fillRect(0,0,10,10);
context.rotate(rocket.angle);
context.restore();
};
function drawStar(x, y, r, p, m)
{
context.save();
context.beginPath();
context.translate(x, y);
context.moveTo(0,0-r);
for (var i = 0; i < p; i++)
{
context.rotate(Math.PI / p);
context.lineTo(0, 0 - (r*m));
context.rotate(Math.PI / p);
context.lineTo(0, 0 - r);
}
context.fill();
context.restore();
}
// the game loop
function main(){
requestAnimationFrame(main);
var now = Date.now();
var delta = now - then;
update(delta / 1000);
//now = Date.now();
//delta = now - then;
render(delta / 1000);
then = now;
// Request to do this again ASAP
}
var w = window;
var requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;
//start the game loop
//gameLoop();
//event listenters
bgImage.src = "images/background.jpg";
} //canvasApp()
Origin
When you need to rotate something in canvas it will always rotate around origin, or center for the grid if you like where the x and y axis crosses.
You may find my answer here useful as well
By default the origin is in the top left corner at (0, 0) in the bitmap.
So in order to rotate content around a (x,y) point the origin must first be translated to that point, then rotated and finally (and usually) translated back. Now things can be drawn in the normal order and they will all be drawn rotated relative to that rotation point:
ctx.translate(rotateCenterX, rotateCenterY);
ctx.rotate(angleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
Absolute angles and positions
Sometimes it's easier to keep track if an absolute angle is used rather than using an angle that you accumulate over time.
translate(), transform(), rotate() etc. are accumulative methods; they add to the previous transform. We can set absolute transforms using setTransform() (the last two arguments are for translation):
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY); // absolute
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
The rotateCenterX/Y will represent the position of the ship which is drawn untransformed. Also here absolute transforms can be a better choice as you can do the rotation using absolute angles, draw background, reset transformations and then draw in the ship at rotateCenterX/Y:
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY);
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
// update scene/background etc.
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(ship, rotateCenterX, rotateCenterY);
(Depending on orders of things you could replace the first line here with just translate() as the transforms are reset later, see demo for example).
This allows you to move the ship around without worrying about current transforms, when a rotation is needed use the ship's current position as center for translation and rotation.
And a final note: the angle you would use for rotation would of course be the counter-angle that should be represented (ie. ctx.rotate(-angle);).
Space demo ("random" movements and rotations)
The red "meteors" are dropping in one direction (from top), but as the ship "navigates" around they will change direction relative to our top view angle. Camera will be fixed on the ship's position.
(ignore the messy part - it's just for the demo setup, and I hate scrollbars... focus on the center part :) )
var img = new Image();
img.onload = function() {
var ctx = document.querySelector("canvas").getContext("2d"),
w = 600, h = 400, meteors = [], count = 35, i = 0, x = w * 0.5, y, a = 0, a2 = 0;
ctx.canvas.width = w; ctx.canvas.height = h; ctx.fillStyle = "#555";
while(i++ < count) meteors.push(new Meteor());
(function loop() {
ctx.clearRect(0, 0, w, h);
y = h * 0.5 + 30 + Math.sin((a+=0.01) % Math.PI*2) * 60; // ship's y and origin's y
// translate to center of ship, rotate, translate back, render bg, reset, draw ship
ctx.translate(x, y); // translate to origin
ctx.rotate(Math.sin((a2+=0.005) % Math.PI) - Math.PI*0.25); // rotate some angle
ctx.translate(-x, -y); // translate back
ctx.beginPath(); // render some moving meteors for the demo
for(var i = 0; i < count; i++) meteors[i].update(ctx); ctx.fill();
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(img, x - 32, y); // draw ship as normal
requestAnimationFrame(loop); // loop animation
})();
};
function Meteor() { // just some moving object..
var size = 5 + 35 * Math.random(), x = Math.random() * 600, y = -200;
this.update = function(ctx) {
ctx.moveTo(x + size, y); ctx.arc(x, y, size, 0, 6.28);
y += size * 0.5; if (y > 600) y = -200;
};
}
img.src = "http://i.imgur.com/67KQykW.png?1";
body {background:#333} canvas {background:#000}
<canvas></canvas>

Categories

Resources