Image Manipulation - add image with corners in exact positions - javascript

I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.

Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>

You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.

This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms

Related

Finding bounding rect of rotated objects on dynamically created canvas or SVG with javascript (typescript)

I have a problem that I have been trying to solve for a while.
I am working on a web app with typescript, react and google maps, and for the markers I want to use a custom image that is created in runtime given more parameters.
The images has two objects, a circle and a triangle that points on direction by previously given angle and based on other parameters, they have different color.The images should be looking like:
Image 1
Image 2 .
The images can range from 1 to 300 depending on user choice. Other thing is that the top of the triangle should begin at the map coordinates, and when more markers are at close proximity, should be looking like in this picture Image 3.
I have succeeded to create the images and place them on the map as desired, but my problem is that the images are too big and are overlapping when they are close, so all markers are not clickable until zoomed in, so I get situation like this: Image 4.
I need to crop out the transparent area so only the circle and the triangle are left to use for the map. I have created two functions, one with canvas element and one with SVG element. They both are similar, First I draw the circle then the triangle and rotate them.
With the SVG solution I was able to find the bounding rect using getBBox() or getBoundingClientRect() but the problem is that the images are not loaded into the DOM and I get nothing from these methods until the SVG is added into the DOM, which is a not good because I should add every image into the DOM, then process it, add to the map and delete it from DOM.
With the canvas method I was able to find a function on the internet that loops through and scans every pixel from the canvas and return the objects, but when there are many elements loaded it is a little slow. I have also tried to find the objects by finding the rotated coordinates using this formula:
x1 = x * Math.cos(angle * Math.PI / 180) - y * Math.sin(angle * Math.PI / 180)
y1 = x * Math.sin(angle * Math.PI / 180) + y * Math.cos(angle * Math.PI / 180)
but not successful.
So what would be the best way to solve this problem?
Thank you.
The code:
// draw canvas
const drawMarkerIconCanvas = (
angle?: number | null,
arrowFillColor?: string,
circleFillColor?: string,
strokeColor?: string,
scale?: number,
text?: string,
textColor?: string) => {
angle = angle || null;
arrowFillColor = arrowFillColor || "#000000";
circleFillColor = circleFillColor || "#FACF00";
strokeColor = strokeColor || "#0050b3";
scale = scale || 0.7;
text = text || " ";
textColor = textColor || "#FFFFFF";
const canvas = document.createElement("canvas");
canvas.width = 180;
canvas.height = 180;
const ctx = canvas.getContext("2d");
if(ctx) {
ctx.scale(scale,scale!)
ctx.strokeStyle=strokeColor!;
if(angle) {
ctx.translate(canvas.width / 2,canvas.height / 2)
ctx.rotate(angle! * Math.PI / 180)
ctx.translate(-30, 0)
}
// draw the circle
ctx.lineWidth=2;
ctx.fillStyle=circleFillColor!;
ctx.beginPath();
ctx.arc(30,60,25,0,2*Math.PI);
ctx.fill();
ctx.stroke();
// draw the triangle
ctx.fillStyle = arrowFillColor!;
ctx.beginPath();
ctx.moveTo(20, 30);
ctx.lineTo(40, 30);
ctx.lineTo(30, 0);
ctx.lineTo(20, 30);
ctx.closePath();
ctx.fill()
ctx.stroke()
// draw the Text
ctx.translate(30, 60)
ctx.rotate(-angle! * Math.PI / 180)
ctx.translate(-30, -60)
ctx.font="14px Arial"
ctx.fillStyle=textColor;
ctx.fillText(text,27-ctx.measureText(text).width/2,63,)
}
return canvas.toDataURL();
}
// draw svg
const drawMarkerIconSVG = (angle?: number | null,
arrowFillColor?: string,
circleFillColor?: string,
strokeColor?: string,
scale?: number,
text?: string,
textColor?: string) => {
angle = angle || 0;
arrowFillColor = arrowFillColor || "#000000";
circleFillColor = circleFillColor || "#FACF00";
strokeColor = strokeColor || "#0050b3";
scale = scale || 0.7;
text = text || " ";
textColor = textColor || "#FFFFFF"
const getBox = (element: any) => {
return (element as SVGGraphicsElement).getBBox()
}
const svg: HTMLElement = document.createElement('svg')
const group = document.createElement('g')
const circle = document.createElement('circle')
const poly = document.createElement('polygon')
svg.setAttribute("xmlns","http://www.w3.org/2000/svg")
circle.setAttribute('cx', '30')
circle.setAttribute('cy', '60')
circle.setAttribute('r', '25')
circle.setAttribute('fill', circleFillColor)
circle.setAttribute('stroke', strokeColor)
circle.setAttribute('stroke-width', '2')
poly.setAttribute('points', '20,30, 40,30,30,0')
poly.setAttribute('fill', arrowFillColor);
poly.setAttribute('stroke', strokeColor);
poly.setAttribute('stroke-width', '2');
group.appendChild(circle)
group.appendChild(poly)
group.setAttribute('transform', 'scale('+scale+') translate(90,90) rotate('+angle+') translate(-30,0)')
const box = getBox(group)
svg.appendChild(group)
svg.setAttribute('viewBox', `${box.x-10} ${box.y-10} ${box.width+2} ${box.height+2}`)
return svg
}
With a little experimenting I was able to solve my problem, not the intended solution to find the bounds, but in my case worked for me. First I was drawing the images on the center of the canvas and then scaling them. After that I placed them on the map with offset based on the angle of the rotation and the translation of the canvas, applying the aforementioned formula for finding points after rotation, so I have created a function like this:
const getCoordinates = (x: number, y: number, angle: number, imgWidth: number, imgHeight: number) => {
let x1, y1;
x1 = x * Math.cos(angle * Math.PI / 180) - y * Math.sin(angle * Math.PI / 180)
y1 = x * Math.sin(angle * Math.PI / 180) + y * Math.cos(angle * Math.PI / 180)
return {
x: x1+imgWidth/2, y: y1+imgHeight/2
}
}

Resizing rotated rectangle on HTML canvas

I have an instance of HTML 5 canvas and a rectangle drawn on it.
My drawing function takes a resizing angle into account and uses relative coordinates.
Relative coordinates're based upon three variables: top left rectangle point, rectangle width and rectangle height.
Rectangle width and rectangle height're calculated using two points: top left rectangle point and bottom right rectangle point.
To sum up, drawing function depends on top left rectangle point, bottom right rectangle point and rotation. It's an important point for the following text!
Here's a code snippet:
var canvas = document.getElementById('imageCanvas');
var ctx = canvas.getContext('2d');
var xTopLeft = 550;
var yTopLeft = 200;
var xBottomRight = 750;
var yBottomRight = 450;
var w = Math.max(xTopLeft, xBottomRight) - Math.min(xTopLeft, xBottomRight);
var h = Math.max(yTopLeft, yBottomRight) - Math.min(yTopLeft, yBottomRight);
var r = 1;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save()
ctx.translate(xTopLeft + w / 2, yTopLeft + h / 2);
ctx.rotate(r);
ctx.fillStyle = "yellow";
ctx.fillRect(w / 2 * (-1), h / 2 * (-1), w, h);
ctx.restore()
}
Here's my rectangle with a bunch of controls: eight resizing handles (white) and one rotation handle (green):
Rotating works fine.
Resizing works fine.
And I also try to implement resizing after rotation. Here's my approach with a humble illustration:
Grab the coordinates of the red point (it's mouse cursor coordiantes)
Derotate the point using negative angle to get derotated coordinates
function rotatePoint(x, y, center_x, center_y, angle) {
var new_x = (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx;
var new_y = (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy;
return [new_x, new_y]
}
Update xTopLeft, yTopLeft and redraw
Done
The idea behind this approach is simple. Since my drawing function depeneds on top left rectangle point and bottom right rectangle point I just need to get their new coordinates.
For instance, here's a simplified code for B point:
if (point == 'B') {
var newPointB = rotatePoint(mouse.x, mouse.y, center_x, center_y, -r);
xBottomRight = newPointB[0];
yTopLeft = newPointB[1];
}
But it doesn't work as expected: while resizing my rotated rectangle shifts, jumps and totally misbehaves.
In search of insights I've stumbled upon this article. The article covers my problem, but I don't get author's approach and can't implement it.
Why should I always lock the coordinates of the A point? My top left handle is intended to resize the rectangle in a north-west direction, so it would be necessary to change the coordinates of the A point...
Why should we recalculate the center point before derotation? It breaks the idea of uniform matrix transformations...
What's the correct algorithm in my case?
I was also facing same problem. It turned out that the angle I was using was in degree. Try multiplying angle in rotatePoint function with (Math.PI / 180).

Html canvas - rotate individual shapes on center axis

As you can see in the demo the L shape is getting cropped off the top of the screen and should be rotated 180 degrees and flush with the top left corner. I noticed two things that don't work as expected, the first is when I change ctx.translate(x, y) to ctx.moveTo(x, y) and increase the shape position to 100, 100 it moves more than 100px with translate, where as moveTo seems accurate. The second is that using a negative translate after ctx.stroke() has no affect on the shapes position.
var shape = {};
function draw(shape) {
var canvas = document.getElementById('canvas');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
var x = shape.position.x + 0.5;
var y = shape.position.y + 0.5;
ctx.translate(x, y);
ctx.translate(shape.width * shape.scale/2, shape.height * shape.scale/2);
ctx.rotate(shape.orientation * Math.PI/180);
ctx.beginPath();
for (var i = 0; i < shape.points.length; i++) {
x = shape.points[i].x * shape.scale + shape.position.x + 0.5;
y = shape.points[i].y * shape.scale + shape.position.y + 0.5;
ctx.lineTo(x, y);
}
ctx.strokeStyle = '#fff';
ctx.stroke();
ctx.translate(-shape.width * shape.scale/2, -shape.height * shape.scale/2);
ctx.restore();
}
}
// L Shape
shape.points = [];
shape.points.push({ x:0, y:0 });
shape.points.push({ x:0, y:3 });
shape.points.push({ x:2, y:3 });
shape.points.push({ x:2, y:2 });
shape.points.push({ x:1, y:2 });
shape.points.push({ x:1, y:0 });
shape.points.push({ x:0, y:0 });
shape.position = {x: 0, y: 0};
shape.scale = 30;
shape.width = 3;
shape.height = 2;
shape.orientation = 180;
draw(shape);
#canvas {
background: #272B34; }
<canvas id="canvas" width="400" height="600"></canvas>
The easiest way to do 2D tranforms is via the setTransform function which takes 6 numbers, 2 vectors representing the direction and scale of the X and y axis, and one coordinate representing the new origin.
Unlike the other transform functions which are dependent of the current state setTransform is not effected by any transform done before it is called.
To set the transform for a matrix that has a square aspect (x and y scale are the same) and that the y axis is at 90 deg to the x ( no skewing) and a rotation is as follows
// x,y the position of the orign
function setMatrix(x,y,scale,rotate){
var xAx = Math.cos(rotate) * scale; // the x axis x
var xAy = Math.sin(rotate) * scale; // the x axis y
ctx.setTransform(xAx, xAy, -xAy, xAx, x, y);
}
//use
setMatrix(100,100,20,Math.PI / 4);
ctx.strokeRect(-2,-2,4,4); // draw a square centered at 100,100
// scaled 20 times
// and rotate clockwise 45 deg
Update
In response to the questions in the comments.
Why sin and cos?
Can you also explain why you used cos and sin for the axis?
I use Math.sin and Math.cos to calculate the X axis and thus the Y axis as well (because y is at 90 deg to x) because it is slightly quicker than adding the rotation as a separate transform.
When you use any of the transform functions apart from setTransform you are doing a matrix multiplication. The next snippet is the JS equivalent minimum calculations done when using ctx.rotate, ctx.scale, ctx.translate, or ctx.transform
// mA represent the 2D context current transform
mA = [1,0,0,1,0,0]; // default transform
// mB represents the transform to apply
mB = [0,1,-1,0,0,0]; // Rotate 90 degree clockwise
// m is the resulting matrix
m[0] = mA[0] * mB[0] + mA[2] * mB[1];
m[1] = mA[1] * mB[0] + mA[3] * mB[1];
m[2] = mA[0] * mB[2] + mA[2] * mB[3];
m[3] = mA[1] * mB[2] + mA[3] * mB[3];
m[4] = mA[0] * mB[0] + mA[2] * mB[1] + mA[4];
m[5] = mA[1] * mB[0] + mA[3] * mB[1] + mA[5];
As you can see there are 12 multiplications and 6 additions plus the need for memory to hold the intermediate values and if the call was to ctx.rotation the sin and cos of the angle would also be done. This is all done in native code in the JavaScript engine so is quicker than doing in JS, but side stepping the matrix multiplication by calculating the axis in JavaScript results in less work. Using setTransform simply replaces the current matrix and does not require a matrix multiplication to be performed.
The alternative to the answer's setMatrix function can be
function setMatrix(x,y,scale,rotate){
ctx.setTransform(scale,0,0,scale, x, y); // set current matrix
ctx.rotate(rotate); // multiply current matrix with rotation matrix
}
which does the same and does look cleaner, though is slower and when you want to do things like games where performance is very important often called functions should be as quick as possible.
To use the setMatrix function
So how would I use this for custom shapes like the L in my demo?
Replacing your draw function. BTW you should be getting the context outside any draw function.
// assumes ctx is the 2D context in scope for this function.
function draw(shape) {
var i = 0;
setMatrix(shape.position.x, shape.position.y, shape.scale, shape.orientation); // orientation is in radians
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(shape.points[i].x, shape.points[i++].y)
while (i < shape.points.length) {
ctx.lineTo(shape.points[i].x, shape.points[i++].y);
}
ctx.closePath(); // draw line from end to start
ctx.stroke();
}
In your code you have the line stored such that its origin (0,0) is at the top left. When defining shapes you should define it in terms of its local coordinates. This will define the point of rotation and scaling and represents the coordinate that will be at the transforms origin (position x,y).
Thus you should define your shape at its origin
function createShape(originX, originY, points){
var i;
const shape = [];
for(i = 0; i < points.length; i++){
shape.push({
x : points[i][0] - originX,
y : points[i][1] - originY,
});
}
return shape;
}
const shape = {};
shape.points = createShape(
1,1.5, // the local origin relative to the coords on next line
[[0,0],[0,3],[2,3],[2,2],[1,2],[1,0]] // shape coords
);

Is it possible to draw an image on a path in HTML5 canvas?

For instance, say I have the following path.
<canvas id="main" width="500" height="250"></canvas>
var canvas = document.getElementById("main");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(100,20);
ctx.arcTo(150,20,150,70,50);
ctx.lineTo(150,120);
ctx.lineWidth = 3;
ctx.stroke();
Is it possible to draw images on the arc of the line? If so, How?
Slice an image to draw on curves.
Yes it is possible, though ideally this would be a job for WebGL. The next best solution is a scan line render but that is way to much CPU load for poor Javascript to manage.
The next best I mean "OK sort of." option is a little image slicing.
You simply draw the image in thin slices around the arc. The 2D renderer is not perfect and tries to draw half pixels as best it can. The result is some noise along the edge of each slice where you can see through. To overcome this I draw each slice slightly wider to cover up any holes.
If you need high quality, rendering it all at double the size on an offscreen canvas and then scale down to a onscreen canvas (don't forget smoothing) will make most think it was drawn that way.
As the inner and outer edges of the arc have different circumferences some of the image must be squashed or stretched. In the demo I keep the inner edge of the image to the correct width and stretch the outer edge. It is easy to change but ensure that you use the outer edge to workout how many slices to draw.
WARNING the radius given is for the inner edge. It is vetted to stop the for loop getting too long and blocking the page. You may want to limit the radius so the inner circumference is the same as the image width. radius = radius < img.width / (Math.PI * 2) ? img.width / (Math.PI * 2) : radius;
It is easy to adapt to lines and curves. All you need is the tangent or curve normal (should be unit vector ie length 1) Use this vector to set the transform ctx.setTransform(nx,ny,tx,ty,px,py). THe first two values point out from the bottom of the image to the top, the next two numbers are along the tangent from left to right. The last two are the point on the curve to draw the slice.
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
// create a canvas and add to dom
var can = createImage(512,512);
document.body.appendChild(can);
var ctx = can.ctx;
// create a image (canvas) to draw on the arc.
const textToDisplay = "<<Image on arc>>"
ctx.font = "64px arial";
var w = ctx.measureText(textToDisplay).width + 8;
var text = createImage(w + 64,84);
text.ctx.fillStyle = "#F90";
text.ctx.strokeStyle = "black";
text.ctx.lineWidth = 16;
text.ctx.fillRect(0,0,text.width,text.height);
text.ctx.strokeRect(0,0,text.width,text.height);
text.ctx.font = "64px arial";
text.ctx.fillStyle = "#0F0";
text.ctx.strokeStyle = "Black";
text.ctx.lineWidth = 4;
text.ctx.strokeText(textToDisplay,38,58);
text.ctx.fillText(textToDisplay,38,58);
// draws image on arc
// img image to render
// x,y center of arc
// radius the inner edge (bottom of image) radius
// fromAng The angle to start drawing the image in radians
// toAng (optional if not given image width will be used to get toAng)
// returns undefined
function drawArcImage(img,x,y,radius,fromAng,toAng){
// WARNING if you let the radius get to small the ratio between the inner and out circumference
// gets very large. This will result in the image being stretched over a quintabazzilon pixels.
// so must vet the radius or you will block the page and upset the browser gods.
radius = Math.abs(radius); // only positive
radius = radius < img.height / 8 ? img.height / 8 : radius;
var outRad = radius + img.height;
var cir = Math.PI * 2 * radius; // get inner circumference
if(toAng === undefined){
var toAng = (img.width / cir) * Math.PI * 2 ; // get the angle the image will cover
}
var cirOut = toAng * outRad; // get the out edge distance in pixels
var imgStep = img.width / cirOut; // the image step per slice
var imgX = 0; // track the image line to draw
var angStep = toAng / cirOut; // the angle steps
// For each pixel on the out edge draw a slice
for(var i = 0; i < toAng; i += angStep){
var dx = Math.cos(fromAng + i);
var dy = Math.sin(fromAng + i);
// set up the transform to draw a slice from the inner to outer edges
ctx.setTransform(dy,-dx,-dx,-dy,dx * radius + x,dy * radius + y);
// get and draw the slice. I stretch it a little (2pix) to cover imperfect rendering
ctx.drawImage(img,imgX,0,imgStep,img.height,-1,-img.height,2,img.height);
// move to next slice
imgX += imgStep;
}
ctx.setTransform(1,0,0,1,0,0); // reset the transform
}
// animate the image to prove it is real.. LOL
var animTick = 0;
var animRate = 0.01;
var pos = 0;
// update function call via RAF
function update(){
animTick += animRate; // update tick
// random anim sin waves.
var rad = Math.sin(animTick) * (256-text.height - 20) + 20;
pos += Math.sin(animTick*10) * 0.02;
pos += Math.sin(animTick/ 3) * 0.02;
pos += Math.sin(animTick/ 7) * 0.05;
// clear
ctx.clearRect(0,0,can.width,can.height)
// draw
drawArcImage(text,256,256,rad,pos)
// do again and again and again
requestAnimationFrame(update);
}
update();
This is an answer to a similar question:
You could, in the draw loop implement a "line drawing algorithm" that does not exactly draw a line but draws an item at a place where that point would be. Except, replace the line algorithm here to draw an arc instead.
function line(x0, y0, x1, y1){
var dx = Math.abs(x1-x0);
var dy = Math.abs(y1-y0);
var sx = (x0 < x1) ? 1 : -1;
var sy = (y0 < y1) ? 1 : -1;
var err = dx-dy;
while(true){ // put draw loop here.
drawImage(image,x0,y0);//setPixel(x0,y0); // Do what you need to for this
if ((x0==x1) && (y0==y1)) break;
var e2 = 2*err;
if (e2 >-dy){ err -= dy; x0 += sx; }
if (e2 < dx){ err += dx; y0 += sy; }
}
}
code taken from: Bresenham algorithm in Javascript
I would suggest using a library like p5.js to do something like this. http://p5js.org

Understanding HTML5 Canvas

I am trying to get to grips and understand how to use and create colliding balls with HTML5 canvas,examples I have looked at have a lot of JavaScript, but I need to break it down into much smaller chunks to get a better understanding of what's going on.
In my example what I understand so far is that I am redrawing the circles every 40 milliseconds onto the canvas, and calling the animate function each time. Every time this is called the position of the circle changes as I am changing it with
circles[0].x+=1;
circles[0].y+=-1.5;
So my circle objects are in an array, and there are 2 things I would like to achieve:
1) not to let the balls escape the canvas area
2) if the balls collide then bounce off each other and reverse in direction.
What I want to tackle first though is not letting the balls escape the canvas and how I would go about working that out.
I have access to the window.width and window.height, so it's a case of understanding how to get the position of each ball in the array, and ensure that it does not cross those boundaries.
I don't want to just have it work, would much prefer to understand what is happening.
This will check collisions on the bounds of the canvas. I updated your objects to store vx and vy (velocity) and the draw() function to move based on these properties. I added checkBounds() which reverses the velocity when the circle goes outside the bounds.
EDIT: modified so that it takes into account the radius of the circles too.
Doing a collision detect between the circles could follow a similar pattern
http://jsfiddle.net/3tfUN/5/
var canvas = document.getElementById('ball-canvas');
var context = canvas.getContext('2d')
var radius = 50;
var strokewidth = 2;
var strokestyle = '#666';
var frameCount = 0;
var w = canvas.width;
var h = canvas.height;
// Circle Objects
var yellowCircle = {
x: 50,
y: h / 2,
radius: radius,
color: 'yellow',
vx: 1,
vy: 1.5
}
var redCircle = {
x: 450,
y: h / 2,
radius: radius,
color: 'red',
vx: 1,
vy: -1
}
var blueCircle = {
x: 850,
y: h / 2,
radius: radius,
color: 'blue',
vx: -1,
vy: -1.5
}
// Create empty array and then push cirlce objects into array
var circles = [];
circles.push(yellowCircle, blueCircle, redCircle);
function checkBounds() {
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
if (c.x > w - c.radius || c.x < c.radius) {
c.vx = -c.vx;
}
if (c.y > h - c.radius || c.y < c.radius) {
c.vy = -c.vy;
}
}
}
// Clear last circle and draw again
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the circle from the from page
for (var i = 0; i < circles.length; i++) {
var c = circles[i];
context.beginPath();
context.fillStyle = c.color // Set the color of the circle using key:valuecontext.fill();
context.lineWidth = strokewidth;
context.strokeStyle = strokestyle;
context.stroke();
context.arc(c.x, c.y, c.radius, 0, Math.PI * 2); // X-axis Position, y-axis Position, radius, % of fill, ?
context.closePath();
context.fill();
}
}
function animate() {
for (i = 0; i <= 2; i++) {
circles[i].x += circles[i].vx;
circles[i].y += circles[i].vy;
}
checkBounds();
draw();
}
var canvas = document.getElementById('ball-canvas');
var context = canvas.getContext('2d')
var radius = 50;
setInterval(animate, 40);
circles[0].x+=1;
circles[0].y+=-1.5;
That's pretty tough to maintain. Instead, I'd suggest you have properties for X and Y speeds (I used moveX and moveY in the example).
Next, you need to check whether the position of the ball + the radius compensation is touching the canvas edges, and if so, reverse the speed value. So, for example, the X speed of the ball is 4 and now it hits the left or the right canvas egde, the X speed now becomes -4.
This is it, in a nutshell:
var c = circles[i];
// check rebounds
if (c.x - c.radius <= 0 || c.x + c.radius >= canvas.width)
c.moveX = -c.moveX; // flip the horizontal speed component
if (c.y - c.radius <= 0 || c.y + c.radius >= canvas.height)
c.moveY = -c.moveY; // flip the vertical speed component
// Yellow Circle
c.x += c.moveX; // here we don't have to worry
c.y += c.moveY; // about directions anymore
See my example here: http://jsfiddle.net/3tfUN/8/
The same principle applies for collisions between balls. I'm assuming you want to do simple collisions without angle changes.
But if you wish to simulate real ball collisions, that would require some more serious trigonometry to calculate when exactly the pixel-perfect collision happens, and to calculate the new X and Y speed components.
UPDATE
An example featuring slightly improved collision detection and speed transfer between balls: http://jsfiddle.net/3tfUN/12/
The canvas is just a "canvas" where you draw the circles. What you need to accomplish what you want is to model a "world" where the circles are object with width and height dimensions and their current position, and where the bounds are well defined. Once you have the width and height of each circle and their position, you can calculate where they are in respect to the bounds you set and see if you need to change direction or keep going.
Collisions stem from the same principle but are somewhat harder to model if you want them to be "realistic" (in the bounds problem you are only interested in the width and height of the circles because the bounding area is box shaped and the circle will always collide in the furthest point from its center, while when two circles collide you should take into account the radius of each circle instead of the "bounding box" around them.
I don't have time right now to show you this concepts with examples, but hopefully I sent you in the right track :).

Categories