Related
I am working on a raycaster and followed this tutorial: https://dev.opera.com/articles/3d-games-with-canvas-and-raycasting-part-1/
My questions is about a Bug which draws / calculates Walls in diffrent Width depending on where on the canvas its drawn (so how big the ray angle is to the players point of direction (center view)):
The Walls in drawn in the middle (1.) are small, but those on the left or right (2.) side of the screen are drawn wider. Its easiest to understand when you look at the image. I think I just got the Math wrong, maybe rounded up somewhere I shouldnt but I havent found it yet or could think of any reason this error accures. Its made in a HTML Canvas using JavaScript.
In my first function I am sending out a ray for each x pixel of my canvas:
let resolution = Math.ceil(canvas.width / this.resolution); //canvas width = 1600, resolution = 1
let id = 0;
for (let x = 0; x < resolution; x++) {
let viewDist = (canvas.width / this.resolution) / Math.tan((this.fov / 2)); //fov 90 in rad
let rayx = (-resolution / 2 + x) * this.resolution;
let rayDist = Math.sqrt(rayx * rayx + viewDist * viewDist);
let rayAngle = Math.asin(rayx / rayDist);
let wall = this.castWall(this.pod * Math.PI / 180 + rayAngle);
this.drawWall(x, wall);
}
But I dont think theres anything wrong here. In the second function I am castinbg each ray and giving back the distance to the hit wall. My blocks / walls are 50 wide. My map is stored in and 2D number Array -> this.map.grid, this.map.width holds how many block there are in x direction, this.map.height holds the count in y direction.
castWall(angle) {
const PI2 = Math.PI * 2;
angle %= PI2;
if (angle < 0) {
angle += PI2;
}
let right = angle > PI2 * 0.75 || angle < PI2 * 0.25;
let up = angle < 0 || angle > Math.PI;
let sin = Math.sin(angle);
let cos = Math.cos(angle);
let dist = 0;
let textureX;
let texture;
let slope = sin / cos;
let dXVer = right ? 1 : -1;
let dYVer = dXVer * slope;
let px = this.x / 50;
let py = this.y / 50;
let x = right ? Math.ceil(px) : Math.floor(px);
let y = py + (x - px) * slope;
while (x >= 0 && x < this.map.width && y >= 0 && y < this.map.height) {
let wallX = Math.floor(x + (right ? 0 : -1));
let wallY = Math.floor(y);
if (this.map.grid[wallY][wallX] > 0) {
dist = Math.sqrt(Math.pow(x - px, 2) + Math.pow(y - py, 2));
texture = this.map.grid[wallY][wallX];
textureX = (y * 50) % 50;
if (right) {
textureX = 50 - textureX;
}
break;
}
x += dXVer;
y += dYVer;
}
slope = cos / sin;
let dYHor = up ? -1 : 1;
let dXHor = dYHor * slope;
y = up ? Math.floor(py) : Math.ceil(py);
x = px + (y - py) * slope;
while (x >= 0 && x < this.map.width && y >= 0 && y < this.map.height) {
let wallY = Math.floor(y + (up ? -1 : 0));
let wallX = Math.floor(x);
if (this.map.grid[wallY][wallX] > 0) {
let distHor = Math.sqrt(Math.pow(x - px, 2) + Math.pow(y - py, 2));
if (dist === 0 || distHor < dist) {
dist = distHor;
texture = this.map.grid[wallY][wallX];
textureX = (x * 50) % 50;
if (up) {
textureX = 50 - textureX;
}
}
break;
}
x += dXHor;
y += dYHor;
}
return {
distance: dist,
texture: texture,
textureX: textureX
};`
Ive also tried raycasting with other algorithms (Bresenham & DDA) but I never got them really to work. This is the only one which works for me. If you have any questions about the code feel free to ask.
I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
I'd like to make a ball bounce angle change each time it hits a wall.
It will change based on how near the middle of the wall it hits...
Right now, I'm hard coding the change in X and Y when it hits a surface... My goal is to get the degrees from current X and Y, apply a change to the degrees (Right now I'm add a random number to the degrees), then calculate the new incrementing values for X and Y. I know how to get the newX and newY, but not how to get the incrementing values.
The green is the starting x y of (5,5)... the blue is the next frame of (4,4).
So I calculated the degrees to be 45 based on that.
Then added a random number to the degrees.
Then, I want to get the new x and y coordinates. So I followed this method...
currX (5) - wallX (0) = distX (5)
currY (5) - wallY (0) = distY (5)
Take the cosine of my angle + random increment, we'll say 55 degrees, * distX
cos(55 degrees) = .5735... .5735 x distX (5) = 2.86
And sin of my angle * distY
sin(55 degrees) = .8191... .8191 x distY (5) = 4.09
newX = cos result (2.86) + originX (5) = 7.86
newY = sin result (4.09) + originY (5) = 9.09
newX, newY = (7.86, 9.09)
Okay... so I have my new coordinates...
But those don't equate to what my new incrementing value of x and y should be based on my angle in incidence.
Code snippet: You can see that I'm hard coding the x,y increments (dragger.x += 2; )
function tick() {
var rand = Math.floor((Math.random()*10)+1);
console.log("ticking..." + rand);
if (dragger.x >= 400-20) {
dragger.xDir = "right";
}
if (dragger.x < 20) {
dragger.xDir = "left";
}
if (dragger.y >= 150-20) {
dragger.yDir = "up";
}
if (dragger.y < 20) {
dragger.yDir = "down";
}
var oldX = dragger.y;
var oldY = dragger.x;
if (dragger.xDir == "left") {
dragger.x += 2;
}
else {
dragger.x -= 2;
}
if (dragger.yDir == "up") {
dragger.y -= 2;
}
else {
dragger.y += 2;
}
//post update...
var newX = dragger.y;
var newY = dragger.x;
var angle = getAngle(newX, oldX, newY, oldY)
angle+=rand;
$('#getAngle').empty();
$('#getAngle').append("bounce angle (degrees): " + angle);
//console.log(xDir);
// update the stage:
stage.update();
}
function getAngle(x2, x1, y2, y1) {
var deltaX = Math.abs(x2-x1);
var deltaY = Math.abs(y2-y1);
var radians = Math.atan2(deltaX, deltaY);
var degrees = radians * (180/Math.PI);
return degrees;
}
This is a pretty interesting problem due to it's specificity.
Making a ball bounce in a programming language can be done quite easily. Like this example.
But clearly, your question is not about 'making it work'; you want explicit control over the coordinates and the angles such that you can alter them for whatever purpose you had in mind.
Because I am quite vulnerable to nerd sniping, I dusted off my geometric skills and came up with the following scrap of pseudocode (I made this from scratch to make sure I have total control):
Intuition
Pseudocode
theta = starting angle
a = current x-coordinate of ball
b = current y-coordinate of ball
quadrant = quadrant-direction to which ball is moving
/> Determine number between 1 and 360: theta
/> Calculate quadrant
.> 0-90 : quadrant 1: horizontal: 90-a vertical: b alpha: 90 - theta
.> 90-180: quadrant 4: horizontal: 90-a vertical: 30-b alpha: theta - 90
.> 180-270: quadrant 3: horizontal: a vertical: 30-b alpha: 270 - theta
.> 270-360: quadrant 2: horizontal: a vertical: b alpha: theta - 270
/> Calculate distance to side |
/> Calculate distance to top/bottom |
.> to side: n(alpha) = horizontal/cos(alpha)
.> to top/bottom: m(alpha) = vertical /sin(alpha)
/> Determine where ball is going to hit (n = side, m = top/bottom)
.> n >= m : bounces at top/bottom
.> m >= n : bounces at side
.> switch (quadrant)
.> 1 : n = right side m = top
.> 2 : n = left side m = top
.> 3 : n = left side m = bottom
.> 4 : n = right side m = bottom
/> Calculate coordinates of hit
/> Define new angle
// Normally, angle of impact = angle of reflection
// Let's define the angle of impact with respect to the origin (0,0)
.> switch (quadrant)
.> 1 :
.> n >= m (at top/bottom) : x = a + vertical*tan(alpha) y = 0 theta = 180-theta
.> m >= n (at side) : x = 90 y = b - horizontal*tan(alpha) theta = 270+alpha
.> 2 :
.> n >= m (at top/bottom) : x = a - vertical/tan(alpha) y = 0 theta = 270-alpha
.> m >= n (at side) : x = 0 y = b - horizontal*tan(alpha) theta = 90-alpha
.> 3 :
.> n >= m (at top/bottom) : x = a - vertical/tan(alpha) y = 30 theta = 270+alpha
.> m >= n (at side) : x = 0 y = b + horizontal*tan(alpha) theta = 90+alpha
.> 4 :
.> n >= m (at top/bottom) : x = a + vertical/tan(alpha) y = 30 theta = 90-alpha
.> m >= n (at side) : x = 90 y = b + horizontal*tan(alpha) theta = 270-alpha
/> Define new coordinates (for reusage of function)
.> a = x
.> b = y
.> (optional) if you would like the angles to differ, enter extra term here:
.> extra = ...
.> theta = theta + extra
Implementing this code will allow you to work with the easiness of degrees and still be able to determine the coordinates.
It works as follows:
First determine the initial position of the ball (a,b) and it's initial direction (theta)
Now the program will calculate:
Where the ball is going to hit
What the coordinates of the ball at impact are
What the new angle of reflection is (this is the part you want to change)
And then it starts over again to calculate the new hit.
In JavaScript, the code would look like this:
Code
var width = 500;
var height = 200;
var extra = 0;
var a;
var b;
var x;
var y;
var angle;
var n;
var m;
var quadrant;
var horizontal;
var vertical;
var alpha;
var side;
var topbottom;
var sides;
var i = 1;
var txt=document.getElementById("info");
txt.innerHTML="x: "+a+"<br>y: "+b+"<br>angle: "+angle+"<br>quadrant: "+quadrant;
function buttonClick()
{
if (i == 1)
{
a = 75;
b = 75;
//determine first angle randonmly
angle = Math.floor((Math.random()*360)+1);;
} else
{
a = xcoord();
b = ycoord();
}
var oldAngle = angle;
angle = findNewCoordinate(a, b, angle);
sides = hitWhere();
var txt=document.getElementById("info");
txt.innerHTML="x: "+a+"<br>y: "+b+"<br>horizontal: "+horizontal+"<br>vertical: "+vertical+"<br>n: "+n+"<br>m: "+m+"<br>angle: "+oldAngle+"<br>alpha: "+alpha+"<br>quadrant: "+quadrant+"<br>side: "+topbottom+side+"<br>"+sides+"<br>"+i;
i++;
}
function findNewCoordinate(a, b, angle)
{
if (angle >= 0 && angle < 90) { quadrant = 1; horizontal = width-a; vertical = b; alpha = (90 - angle); }
else if (angle >= 90 && angle < 180) { quadrant = 4; horizontal = width-a; vertical = height-b; alpha = (angle-90); }
else if (angle >= 180 && angle < 270) { quadrant = 3; horizontal = a; vertical = height-b; alpha = (270-angle); }
else if (angle >= 270 && angle <= 360) { quadrant = 2; horizontal = a; vertical = b; alpha = (angle-270); }
var cosa = Math.cos(alpha * Math.PI / 180);
var sina = Math.sin(alpha * Math.PI / 180);
var tana = Math.tan(alpha * Math.PI / 180);
var tant = Math.tan(angle * Math.PI / 180);
n = horizontal/cosa;
m = vertical/sina;
switch (quadrant)
{
case 1:
if (m >= n) //hit at side
{
y = b - horizontal*tana;
x = width;
angle = 270+alpha;
} else
{
y = 0;
x = a + vertical*tant;
angle = 180-angle;
}
side = "right side"; topbottom = "top";
break;
case 2:
if (m >= n) //hit at side
{
y = b-horizontal*tana;
x = 0;
angle = 90-alpha;
} else
{
y = 0;
x = a - vertical/tana;
angle = 270-alpha;
}
side = "left side"; topbottom = "top";
break;
case 3: side = "left side"; topbottom = "bottom";
if (m >= n) //hit at side
{
x = 0;
y = b + tana*horizontal;
angle = 90+alpha;
} else
{
y = height;
x = a - vertical/tana;
angle = 270+alpha;
} break;
case 4: side = "right side"; topbottom = "bottom";
if (m >= n) //hit at side
{
y = b+horizontal*tana;
x = width;
angle = 270-alpha;
} else
{
y = height;
x = a + vertical/tana;
angle = 90-alpha;
} break;
}
//add extra degrees to the angle (optional)
angle += extra;
context.beginPath();
context.arc(a, b, 5, 0, Math.PI*2, true);
context.stroke();
context.closePath();
context.fill();
drawLine(a,b,x,y);
return angle;
}
Important
Note that there are many more ways to make a bouncing program. But, because I tackled the question geometrically and without 'shortcuts', the unique characteristics of my program make it very easy for you to alter it to your likings:
You can give an extra angle to the bounce angle easily (use var extra).
You can change the movement of the ball at any time (at bounce, after bounce etc.)
You have explicit access to the coordinates of the ball
All units are conventional (in degrees and coordinates; hence easy to understand and intuitive).
Also note that I did not make the program very concise because this simply wasn't my goal. I wanted to create a bouncing ball program that, although lenghty, is an exact realisation of the geometric intuition behind it.
Demo
You can find a demo of my program in this JSFiddle.
Note that the beginning angle is determined randomly. Hence restarting the program will give a different angle.
Well, that's about it.
Good luck with building the rest of your program!
We know that
distance = average velocity x time //if acceleration is constant
Hence
time = distance / average velocity
Applying this knowledge to a two dimensional field (distance) means we have to do two things:
Apply Pythagoras theorem to find distance to new coordinates
Calculate the 'new' velocity
Before we apply the Pythagoras theorem, we have to know the direction of the move:
Now to find the distance to the new coordinates, we apply pythagoras theorem:
Pseudocode
//Change in coordinates
dx = Math.abs(newX - oldX);
dy = Math.abs(newY - oldY);
//Distance to travel
distance = Math.sqrt( Math.pow(dx, 2) + Math.pow(dy,2) );
//Units per increase
// time = distance / average velocity
velocity = ?;
time = distance / velocity;
//Now to find x+= .. and y+= .. we apply our knowledge of direction
//Together with our knowledge of the time it takes
case north east: x += (dx / time); y += (dy / time);
case south east: x += (dx / time); y -= (dy / time);
case north west: x -= (dx / time); y -= (dy / time);
case south west: x -= (dx / time); y += (dy / time);
Now note that the x and y represent the coordinates of the moving ball.
This means that we must repeat x += .. and y += .. value of time times to reach the new coordinate.
Hence you can do something like:
for (int i = 0; i < time; i ++)
{
switch (direction)
{
case "north east": x += (dx / time); y += (dy / time); break;
case "south east": x += (dx / time); y -= (dy / time); break;
case "north west": x -= (dx / time); y -= (dy / time); break;
case "south west": x -= (dx / time); y += (dy / time); break;
}
}
Also note that velocity = ? is yet to be specified by you. You can let it have a constant velocity (friction = 0), or you can implement some kind of model to mimick friction.
I hope this answers your question.
PS. This answer is actually a derivative of my other answer as I already specify direction and pixel distance in my other answer hence the step to x += .. and y += .. is actually pretty small/ straightforward.
depends on the angle it came in at.. so basically for making the ball bounce off the wall, just inverse the angle it came in at, e.g. if using velocity, if it was 3, then make it -3 when it collides with the wall, therefore the ball will bounce off the wall at the same angle as it was before it collided with the wall...
I hope this helps... Good luck
When I click on paper I store the position in lastX and lastY values:
lastX = e.screenX;
lastY = e.screenY;
On mousemove I update the currentX and currentY values:
currentX = e.screenX;
currentY = e.screenY;
Can I determine somehow what is the degree between this two coordinates? I think the x line is the 0 degree. But here stopped my science.
Assuming the origin is the point (lastX, lastY), and zero degrees is the positive x-axis,
the degree to the point (currentX, currentY) would be:
function degreesToPoint(origin, endP){
if(typeof origin != typeof [] or typeof endP != typeof [])
return false;
else {
var slope = {
x: origin[0] - endP[0],
y: origin[1] - endP[1]
};
var degrees = Math.atan(slope.y / slope.x) * 180 / Math.PI;
if(slope.x < 0 && slope.y >= 0){
degrees += 180;
} else if (slope.x < 0 && slope.y < 0) {
degrees -= 180;
}
return degrees;
}
}
After some other google searches:
var radian = Math.atan((currentY-lastY)/(currentX-lastX));
var degree = radian * (180/Math.PI);
I'm trying to draw a circle with, not radial gradients, but linear gradients that go around the circle... Basically, I'm trying to create a color wheel and it has to be dynamic as the colors will be customizable... However, I'm completely baffled on how to approach this matter...
I thought I could draw my own circle and color it, then loop the process with a larger radius to fill it out. But that proved to not only be extremely ineffecient but very buggy too...
Here was my first attempt: http://jsfiddle.net/gyFqX/1/
I stuck with that method but changed it to fill a 2x2 square for each point on the circle. It worked alright for blending up to 3 colors, but then you begin to notice it's distortion.
Anyway, I've continued working on it a bit and this is what I have now: http://jsfiddle.net/f3SQ2/
var ctx = $('#canvas')[0].getContext('2d'),
points = [],
thickness = 80;
for( var n = 0; n < thickness; n++ )
rasterCircle( 200, 200, (50 + n) );
function fillPixels() {
var size = points.length,
colors = [
hexToRgb( '#ff0000' ), // Red
hexToRgb( '#ff00ff' ), // Magenta
hexToRgb( '#0000ff' ), // Blue
hexToRgb( '#00ffff' ), // Teal
hexToRgb( '#00ff00' ), // Green
hexToRgb( '#ffff00' ), // Yellow
hexToRgb( '#ff0000' ), // Red
],
colorSpan = colors.length - 1;
if ( colors.length > 0 ) {
var lastPadding = size % colorSpan,
stepSize = size / colorSpan,
steps = null,
cursor = 0;
for ( var index = 0; index < colorSpan; index++ ) {
steps = Math.floor( ( index == colorSpan - 1 ) ? stepSize + lastPadding : stepSize );
createGradient( colors[ index ], colors[ index + 1 ], steps, cursor );
cursor += steps;
}
}
function createGradient( start, end, steps, cursor ) {
for ( var i = 0; i < steps; i++ ) {
var r = Math.floor( start.r + ( i * ( end.r - start.r ) / steps ) ),
g = Math.floor( start.g + ( i * ( end.g - start.g ) / steps ) ),
b = Math.floor( start.b + ( i * ( end.b - start.b ) / steps ) );
ctx.fillStyle = "rgba("+r+","+g+","+b+",1)";
ctx.fillRect( points[cursor][0], points[cursor][1], 2, 2 );
cursor++;
}
}
points = [];
}
function setPixel( x, y ) {
points.push( [ x, y ] );
}
function rasterCircle(x0, y0, radius) {
var f = 1 - radius,
ddF_x = 1,
ddF_y = -2 * radius,
x = 0,
y = radius;
setPixel(x0, y0 + radius);
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
setPixel(x0 - x, y0 - y);
}
var temp = [];
f = 1 - radius,
ddF_x = 1,
ddF_y = -2 * radius,
x = 0,
y = radius;
while(x < y) {
if(f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
temp.push( [x0 - y, y0 - x] );
}
temp.push( [x0 - radius, y0] );
for(var i = temp.length - 1; i > 0; i--)
setPixel( temp[i][0], temp[i][1] );
fillPixels();
}
What I'm trying to accomplish is something like this: http://img252.imageshack.us/img252/3826/spectrum.jpg
The 'brightness' (white to black fade) is not an issue as I know it can be accomplished by using a radial gradient after the color spectrum has been drawn. However, I'd appreciate some help in figuring out how to draw the spectrum itself.
I was even thinking I could draw a linear one and then bend (transform) it, but there aren't any native functions to do that and tackling something such as that is above my skill level. :-/
Check this out: http://jsfiddle.net/f3SQ2/5/
var can = $('#canvas')[0],
ctx = can.getContext('2d'),
radius = 120,
thickness = 80,
p = {
x: can.width,
y: can.height
},
start = Math.PI,
end = start + Math.PI / 2,
step = Math.PI / 180,
ang = 0,
grad,
r = 0,
g = 0,
b = 0,
pct = 0;
ctx.translate(p.x, p.y);
for (ang = start; ang <= end; ang += step) {
ctx.save();
ctx.rotate(-ang);
// linear gradient: black->current color->white
grad = ctx.createLinearGradient(0, radius - thickness, 0, radius);
grad.addColorStop(0, 'black');
h = 360-(ang-start)/(end-start) * 360;
s = '100%';
l = '50%';
grad.addColorStop(.5, 'hsl('+[h,s,l].join()+')');
grad.addColorStop(1, 'white');
ctx.fillStyle = grad;
// the width of three for the rect prevents gaps in the arc
ctx.fillRect(0, radius - thickness, 3, thickness);
ctx.restore();
}
Edit: fixed color spectrum. Apparently we can just give it HSL values, no need for conversions or messy calculations!
Modified a few things to handle scaling better: http://jsfiddle.net/f3SQ2/6/
step = Math.PI / 360
ctx.fillRect(0, radius - thickness, radius/10, thickness);
You could for example set the gradient stops like so:
h = 360-(ang-start)/(end-start) * 360;
s = '100%';
grad.addColorStop(0, 'hsl('+[h,s,'0%'].join()+')'); //black
grad.addColorStop(.5,'hsl('+[h,s,'50%'].join()+')'); //color
grad.addColorStop(1, 'hsl('+[h,s,'100%'].join()+')');//white
My first note would be that the image you linked to has all 3 components it doesn't need to change and could just be a static image.
I adapted some code from a project i'm working on:
http://jsfiddle.net/f3SQ2/1/
function drawColourArc(image) {
var data = image.data;
var i = 0;
var w = image.width, h = image.height;
var result = [0, 0, 0, 1];
var outer = 1, inner = 0.5;
var mid = 0.75;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var dx = (x / w) - 1, dy = (y / w) - 1;
var angular = ((Math.atan2(dy, dx) + Math.PI) / (2 * Math.PI)) * 4;
var radius = Math.sqrt((dx * dx) + (dy * dy));
if (radius < inner || radius > outer) {
data[i++] = 255;
data[i++] = 255;
data[i++] = 255;
data[i++] = 0;
}
else {
if (radius < mid) {
var saturation = 1;
var brightness = (radius - 0.5) * 4;
}
else {
var saturation = 1- ((radius - 0.75) * 4);
var brightness = 1;
}
result[0] = angular;
result[1] = saturation;
result[2] = brightness;
result[3] = 1;
//Inline HSBToRGB
if (result[1] == 0) {
result[0] = result[1] = result[2] = result[2];
}
else {
var varH = result[0] * 6;
var varI = Math.floor(varH); //Or ... var_i = floor( var_h )
var var1 = result[2] * (1 - result[1]);
var var2 = result[2] * (1 - result[1] * (varH - varI));
var var3 = result[2] * (1 - result[1] * (1 - (varH - varI)));
if (varI == 0 || varI == 6) {
result[0] = result[2];
result[1] = var3;
result[2] = var1;
}
else if (varI == 1) {
result[0] = var2;
result[1] = result[2];
result[2] = var1;
}
else if (varI == 2) {
result[0] = var1;
result[1] = result[2];
result[2] = var3;
}
else if (varI == 3) {
result[0] = var1;
result[1] = var2;
result[2] = result[2];
}
else if (varI == 4) {
result[0] = var3;
result[1] = var1;
result[2] = result[2];
}
else {
result[0] = result[2];
result[1] = var1;
result[2] = var2;
}
}
//End of inline
data[i++] = result[0] * 255;
data[i++] = result[1] * 255;
data[i++] = result[2] * 255;
data[i++] = result[3] * 255;
}
}
}
};
var canvas = document.getElementsByTagName("canvas")[0];
var ctx = canvas.getContext("2d");
var image = ctx.createImageData(canvas.width, canvas.height);
drawColourArc(image);
ctx.putImageData(image, 0, 0);
This does it per-pixel which is accurate but you may want to draw an outline to combat the aliasing. It could be adapted to use custom colours instead of interpolating hue.