I'm trying to manipulate a simple rectangle on a HTML5 canvas. The Javascript that does this is here:
var canvas = document.getElementById("mainCanvas");
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, windowWidth, windowHeight);
var halfWidth = (iconWidth / 2);
var halfHeight = (iconHeight / 2);
var centreX = x + halfWidth;
var centreY = y + halfHeight;
ctx.fillStyle = "#FF0000";
ctx.translate(centreX, centreY);
ctx.rotate(rotationDegree * Math.PI / 180);
ctx.fillRect(-halfWidth, -halfHeight, iconWidth, iconHeight);
ctx.translate(-centreX, -centreY);
As I increase y, I can see the rectangle travelling along the screen and, if I rotate the rectangle, it rotates and moves along the new trajectory; however, in order to stop the rectangle leaving the screen, I had a basic boundary check, which was just not working (the rectangle was travelling off the screen, and being "bounced" where it had not reached the edge.
As an experiment, I then tried the following:
var canvas = document.getElementById("mainCanvas");
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, windowWidth, windowHeight);
var halfWidth = (iconWidth / 2);
var halfHeight = (iconHeight / 2);
var centreX = x + halfWidth;
var centreY = y + halfHeight;
ctx.save();
ctx.fillStyle = "#FF0000";
ctx.translate(centreX, centreY);
ctx.rotate(rotationDegree * Math.PI / 180);
ctx.fillRect(-halfWidth, -halfHeight, iconWidth, iconHeight);
// ctx.translate(-centreX, -centreY);
ctx.restore();
This works, but the rotation no longer guides the rectangle. My conclusion is that the rotate function rotates the canvas, but then leaves it in the new, rotated form (like rotating a piece of paper underneath a pen). So, the bug I had was that the rotation was not being reset; however, apart from the boundary check, this "bugged" behaviour was what I was actually aiming for.
Is there a way to get from a canvas 2d context the absolute position, taking into account the rotation so that, even if I leave the canvas in its "rotated" state, I can perform a boundary check?
Here is a fiddle of the site.
To transform a point from local space (the transformed space) to screen space create a matrix that is a shadow (copy) of the context transform then multiply the point with that matrix
function getTransformToScreen(x,y,rotation,posX,posY){
var xAx = Math.cos(rotation); // x axis x
var xAy = Math.sin(rotation); // x axis y
// the equivalent to
// ctx setTransform(xAx, xAy ,-xAy, xAx, posX, posY);
// second two values (y Axis) is at 90 deg of x Axis if it is
// not at 90 (skewed) then you need to calculate the skewed axis (y axis) direction
return {
x : x * xAx - y * xAy + posX,
y : x * xAy + y * xAx + posY
}
}
To use
// your code
ctx.translate(centreX, centreY);
ctx.rotate(rotationDegree * Math.PI / 180);
ctx.fillRect(-halfWidth, -halfHeight, iconWidth, iconHeight);
// get the top left
var topLeft = getTransformToScreen(
-halfWidth, -halfHeight,
rotationDegree * Math.PI / 180,
centreX, centreY
);
Related
I want to make a gradient that covers the whole canvas whatever the angle of it.
So I used a method found on a Stack Overflow post which is finally incorrect. The solution is almost right but, in fact, the canvas is not totally covered by the gradient.
It is this answer: https://stackoverflow.com/a/45628098/5594331
(You have to look at the last point named "Example of best fit.")
In my code example below, the yellow part should not be visible because it should be covered by the black and white gradient. This is mostly the code written in Blindman67's answer with some adjustments to highlight the problem.
I have drawn in green the control points of the gradient. With the right calculations, these should be stretched to the edges of the canvas at any angle.
var ctx = canvas.getContext("2d");
var w = canvas.width;
var h = canvas.height;
function bestFitGradient(angle){
var dist = Math.sqrt(w * w + h * h) / 2; // get the diagonal length
var diagAngle = Math.asin((h / 2) / dist); // get the diagonal angle
// Do the symmetry on the angle (move to first quad
var a1 = ((angle % (Math.PI *2))+ Math.PI*4) % (Math.PI * 2);
if(a1 > Math.PI){ a1 -= Math.PI }
if(a1 > Math.PI / 2 && a1 <= Math.PI){ a1 = (Math.PI / 2) - (a1 - (Math.PI / 2)) }
// get angles from center to edges for along and right of gradient
var ang1 = Math.PI/2 - diagAngle - Math.abs(a1);
var ang2 = Math.abs(diagAngle - Math.abs(a1));
// get distance from center to horizontal and vertical edges
var dist1 = Math.cos(ang1) * h;
var dist2 = Math.cos(ang2) * w;
// get the max distance
var scale = Math.max(dist2, dist1) / 2;
// get the vector to the start and end of gradient
var dx = Math.cos(angle) * scale;
var dy = Math.sin(angle) * scale;
var x0 = w / 2 + dx;
var y0 = h / 2 + dy;
var x1 = w / 2 - dx;
var y1 = h / 2 - dy;
// create the gradient
const g = ctx.createLinearGradient(x0, y0, x1, y1);
// add colours
g.addColorStop(0, "yellow");
g.addColorStop(0, "white");
g.addColorStop(.5, "black");
g.addColorStop(1, "white");
g.addColorStop(1, "yellow");
return {
g: g,
x0: x0,
y0: y0,
x1: x1,
y1: y1
};
}
function update(timer){
var r = bestFitGradient(timer / 1000);
// draw gradient
ctx.fillStyle = r.g;
ctx.fillRect(0,0,w,h);
// draw points
ctx.lineWidth = 3;
ctx.fillStyle = '#00FF00';
ctx.strokeStyle = '#FF0000';
ctx.beginPath();
ctx.arc(r.x0, r.y0, 5, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.arc(r.x1, r.y1, 5, 0, 2 * Math.PI, false);
ctx.stroke();
ctx.fill();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border : 2px solid red;
}
<canvas id="canvas" width="300" height="200"></canvas>
In this fiddle there is a function that calculates the distance between a rotated line and a point:
function distanceToPoint(px, py, angle) {
const cx = width / 2;
const cy = height / 2;
return Math.abs((Math.cos(angle) * (px - cx)) - (Math.sin(angle) * (py - cy)));
}
Which is then used to find the maximum distance between the line and the corner points (only two points are considered, because the distances to the other two points are mirrored):
const dist = Math.max(
distanceToPoint(0, 0, angle),
distanceToPoint(0, height, angle)
);
Which can be used to calculate offset points for the end of the gradient:
const ox = Math.cos(angle) * dist;
const oy = Math.sin(angle) * dist;
const gradient = context.createLinearGradient(
width / 2 + ox,
height / 2 + oy,
width / 2 - ox,
height / 2 - oy
)
I'm trying to create a little circular "equalizer" effect using JavaScript and HTML canvas for a little project I'm working on, and it works great, except one little thing. It's just a series of rectangular bars moving in time to an mp3 - nothing overly fancy, but at the moment all the bars point in one direction (i.e. 0 radians, or 90 degrees).
I want each respective rectangle around the edge of the circle to point directly away from the center point, rather than to the right. I have 360 bars, so naturally, each one should be 1 degree more rotated than the previous.
I thought that doing angle = i*Math.PI/180 would fix that, but it doesn't seem to matter what I do with the rotate function - they always end up pointing in weird and wonderful directions, and being translated a million miles from where they were. And I can't see why. Can anyone see where I'm going wrong?
My frame code, for reference, is as follows:
function frames() {
// Clear the canvas and get the mp3 array
window.webkitRequestAnimationFrame(frames);
musicArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(musicArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
bars = 360;
for (var i = 0; i < bars; i++) {
// Find the rectangle's position on circle edge
distance = 100;
var angle = i * ((Math.PI * 2) / bars);
var x = Math.cos(angle) * distance + (canvas.width / 2);
var y = Math.sin(angle) * distance + (canvas.height / 2);
barWidth = 5;
barHeight = (musicArray[i] / 4);
// Fill with a blue-green gradient
var grd = ctx.createLinearGradient(x, 0, x + 40, 0);
grd.addColorStop(0, "#00CCFF");
grd.addColorStop(1, "#00FF7F");
ctx.fillStyle = grd;
// Rotate the rectangle according to position
// ctx.rotate(i*Math.PI/180); - DOESN'T WORK
// Draw the rectangle
ctx.fillRect(x, y, barHeight, barWidth);
}
For clarity I've removed part of your code. I'm using rotate as you intended. Also I'm using barHeight = (Math.random()* 50); instead your (musicArray[i]/4); because I wanted to have something to show.
Also I've changed your bars to 180. It's very probable that you won't have 360 bars but 32 or 64 or 128 or 256 . . . Now you can change the numbers of bare to one of these numbers to see the result.
I'm drawing everything around the origin of the canvas and translating the context in the center.
I hope it helps.
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 400;
let ch = canvas.height = 400;
let bars = 180;
let r = 100;
ctx.translate(cw / 2, ch / 2)
for (var i = 0; i < 360; i += (360 / bars)) {
// Find the rectangle's position on circle edge
var angle = i * ((Math.PI * 2) / bars);
//var x = Math.cos(angle)*r+(canvas.width/2);
//var y = Math.sin(angle)*r+(canvas.height/2);
barWidth = 2 * Math.PI * r / bars;
barHeight = (Math.random() * 50);
ctx.fillStyle = "green";
// Rotate the rectangle according to position
// ctx.rotate(i*Math.PI/180); - DOESN'T WORK
// Draw the rectangle
ctx.save();
ctx.rotate(i * Math.PI / 180);
ctx.fillRect(r, -barWidth / 2, barHeight, barWidth);
//ctx.fillRect(r ,0, barHeight, barWidth);
ctx.restore();
}
canvas {
border: 1px solid
}
<canvas id="c"></canvas>
Here is another solution, I'm preserving your initial trigonometry approach.
But instead of rectangles I used lines, I don't think it makes a difference for you, if what you need is bars moving in time to an mp3 all you need to do is change the var v = Math.random() + 1; to a reading from the Amplitude, and those bars will be dancing.
const canvas = document.getElementById("c");
canvas.width = canvas.height = 170;
const ctx = canvas.getContext("2d");
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.lineWidth = 2;
let r = 40;
let bars = 180;
function draw() {
ctx.clearRect(-100, -100, 200, 200)
for (var i = 0; i < 360; i += (360 / bars)) {
var angle = i * ((Math.PI * 2) / bars);
var x = Math.cos(angle) * r;
var y = Math.sin(angle) * r;
ctx.beginPath();
var v = Math.random() + 1;
ctx.moveTo(x, y);
ctx.lineTo(x * v, y * v)
grd = ctx.createLinearGradient(x, y, x*2, y*2);
grd.addColorStop(0, "blue");
grd.addColorStop(1, "red");
ctx.strokeStyle = grd;
ctx.stroke();
}
}
setInterval(draw, 100)
<canvas id="c"></canvas>
am trying to draw rectangles it different angles , initially i know rectangle position data it 0 degree , then sometime i get angle greater than 0 so i have to draw on that angle , for which i then have to rotate points. so currently i can rotate points but do not know to calculate width and height after rotation.
am doing it like ...
// Main rotation function
function rotate(originX, originY,pointX, pointY, angle) {
angle = angle * Math.PI / 180.0;
return {
x: Math.cos(angle) * (pointX-originX) - Math.sin(angle) * (pointY-originY) + originX,
y: Math.sin(angle) * (pointX-originX) + Math.cos(angle) * (pointY-originY) + originY
};
}
// initial Rectangle
ctx.fillStyle = "red";
var x = 200
var y = 200
var w = 80
var h = 20
var angle = 90 ;
ctx.fillRect(x, y, w, h);
// Calculate Center
var cx = (x + (w/2));
var cy = (y + (h/2));
// highlight Center
ctx.fillStyle = "black";
ctx.fillRect(cx,cy, 5, 5);
// Rotate starting x y at angle xxx
var r = rotate(cx,cy,x,y, angle - h );
// highlight roate points
ctx.fillStyle = "yellow";
ctx.fillRect(r.x, r.y, 5, 5);
// rotate Width and Height
var r2 = rotate(cx,cy,x+w,y+h, angle - h );
// highlight roate points
ctx.fillStyle = "green";
ctx.fillRect(r2.x, r2.y, 5, 5);
ctx.save();
so it end i rotated width and height which is ok for single line , but am interested in full rotation of width of height , like it 90 angle old with will become new height and old height will become new width. so any idea how to do it
Fiddle : https://jsfiddle.net/047txgox/
fixed it , hope will help someone
var x = 40
var y = 80
var w = 80
var h = 20
var angle = 120 ;
// Calculate Center
var cx = (x + (w/2));
var cy = (y + (h/2));
context.setTransform(1,0,0,1,0,0)
context.translate(cx, cy);
context.rotate(angle*Math.PI/180);
context.translate(-cx, -cy);
context.rect(x, y, w, h);
context.fillStyle = "#FF00FF";
context.fill();
// highlight Center
context.fillStyle = "black";
context.fillRect(cx,cy, 5, 5);
context.save();
Fiddle : http://jsfiddle.net/vorjcbz7/
I have drawn a text on the canvas in coordinates X, Y and saved them. I have a simple method that checks if a mouse click has happened inside the text boundaries. The problem is when I rotate the text 45 Degree I cannot check whether a mouse click has happened within the rotated text or not.
In short, how can I check whether a mouse click is inside a rotated text or shape?
Create a rect object which is rotated at the same angle as the text, but not drawn.
Then use:
// set transforms here.
// add rect representing text region:
ctx.beginPath();
ctx.rect(x, y, w, h); // region for text
if (ctx.isPointInPath(mx, my)) {
// we clicked inside the region
}
ctx.setTransform(1,0,0,1,0,0); // reset transforms after test
Demo:
var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
txt = "ROTATED TEXT", tw, region;
// transform and draw some rotated text:
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "32px sans-serif";
ctx.translate(150, 75);
ctx.rotate(0.33);
ctx.translate(-150, -75);
ctx.fillText(txt, 150, 75);
tw = ctx.measureText(txt).width;
// define a region for text:
region = {x: 150 - tw*0.5, y: 75 - 16, w: tw, h:32}; // approx. text region
// function to check if mouse x/y is inside (transformed) region
function isInText(region, x, y) {
ctx.beginPath();
ctx.rect(region.x, region.y, region.w, region.h);
return ctx.isPointInPath(x, y);
}
// test for demo
canvas.onmousemove = function(e) {
var rect = canvas.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
// just to visualize:
ctx.clearRect(0,0,300,150);
ctx.fillStyle = isInText(region, x, y) ? "red" : "black";
ctx.fillText(txt, 150, 75);
};
<canvas></canvas>
DO NOT USE BELOW CODE DIRECTLY. HERE I'M TRYING TO EXPLAIN ONLY THE LOGIC THAT HOW YOU CAN PROCEED WITH THIS KIND OF PROBLEMS. YOU MAY HAVE TO WRITE CODE ACCORDING TO YOUR PROBLEM, OR EXTEND THIS SOLUTION TO FIT INTO YOUR PROBLEM
This is more of the mathematical problem where you have to find whether a point is inside rotated Rectangle or not(same as what #user1693593 said)
You can find a rectangle that is covering text(rect(x1: text.x1, y1: text.y1, x2: text.x2, y2: text.y2) assuming (text.x1, text.y1) denotes top-left corner of text.
Now rotate the rect about it's centre by theta angle and find out x1,y1,x2,y2:
Assume all angle calculation will happen in +ve angles.
if theta < 0 then theta = (360 - abs(theta))
Now calculate angle between centre point and point(x2, y2)(Angle from centre to any corner of rect will be same, we are calculating for (x2, y2) because it will give us positive angle)
var width = (x2-x1);
var height = (y2-y1)
var hypotenuse = Math.pow(width*width + height*height, 0.5);
var innerAngle = Math.acos(x2 - x2 / hypotenuse);
Now calculate final angle of corners from centre
(x1, y1), angle((Math.PI + baseAngle) + angle) %(Math.PI*2)
(x2, y1), angle((Math.PI*2) - baseAngle + angle) % (Math.PI*2)}
(x1, y2), angle((Math.PI - baseAngle) + angle) % (Math.PI*2)}
(x2, y2), angle(baseAngle + angle) % (Math.PI*2)}
Now find out rotated Points of rectangle:
point.x = centre.x + (hypotenuse * Math.cos(point.angle));
point.y = centre.y + (hypotenuse * Math.sin(point.angle));
After rotating points It may happen that their order get changed, to make sure the correct order sort them based on x-axis, if two points have same x-axis then sort them based on y-axis (use any sorting)
Now we got sorted points. Name them based on their position p0, p1, p2, p3
Find new boundary of rotated rect from these points, find boundary in x direction and y direction:
var boundary_x1 = ( (p1.x - p0.x) / (p1.y - p0.y) ) * (mousePos.y - p0.y) + p0.x;
var boundary_x2 = ( (p3.x - p2.x) / (p3.y - p2.y) ) * (mousePos.y - p2.y) + p2.x;
var boundary_y1 = ( (p2.y - p0.y) / (p2.x - p0.x)) * (mousePos.x - p0.x) + p0.y;
var boundary_y2 = ( (p3.y - p1.y) / (p3.x - p1.x)) * (mousePos.x - p1.x) + p1.y;
after getting boundary points in x and y direction we can easily put mid point condition and can find whether a point(mousepoint) is clicked in rect or not:
var clickedInside = (mousePos.x > boundary_x1 && mousePos.x < boundary_x2) && (mousePos.y > boundary_y1 && mousePos.y < boundary_y2);
if clickedInside is true then mouseclick happened inside rect
I think it's usual question, but I have some problems with displaying dots in canvas. The first thing I'd like to know is how to draw dot like this (please zoom it).
The second thing is, how to draw a shadow to each element of the grid of this dots, with the light source in the center.
What I have at this moment right here:
the part of my code:
context.fillStyle = "#ccc";
context.shadowColor = '#e92772';
context.shadowOffsetX = 15;
context.shadowOffsetY = 15;
while (--e >= 1) {
x -= z;
if(x < 0) {
x = z*w;
y -= z;
}
context.moveTo(x, y);
context.fillRect( x, y, 1, 1 );
outs = a[e];
}
Also, I've tried to use "context.arc();", but I think "context.fillRect();" is more easier. And one else moment, when I use "while (--e >= 0)" instead of "while (--e >= 1)" I have two more dots, on the top. Why?
If you know some articles or tutorials, would you give me the link to them. Preferably without the use of the frameworks. Thanks.
You can use some trigonometry to simulate 3D dots with a light source.
HERE IS AN ONLINE DEMO
This is one way you can do it, there are of course others (this was the first that came to mind):
Draw the grid with some dots on the main canvas
Render a radial gradient to an off-screen canvas
Change composition mode so anything is draw on already existing pixels
Calculate distance and angle to light source and draw the dot to each grid point offset with the angle/dist.
Here is some code from the demo that does this.
Draw the grid with dots
We skip one grid point as we will fill each dot later with the gradient dot which otherwise would paint over the neighbor dot.
/// draw a grid of dots:
for (y = 0; y < ez.width; y += gridSize * 2) {
for (x = 0; x < ez.height; x += gridSize * 2) {
ctx.beginPath();
ctx.arc(x + offset, y + offset, radius, 0, arcStop);
ctx.closePath();
ctx.fill();
}
}
Create a light "reflection"
Prepare the gradient dot to an off-screen canvas (dctx = dot-context). I am using easyCanvas for the setup and to give me an off-screen canvas with center point already calculated, but one can setup this manually too of course:
grd = dctx.createRadialGradient(dot.centerX, dot.centerY, 0,
dot.centerX, dot.centerY, gridSize);
grd.addColorStop(0, '#fff');
grd.addColorStop(0.2, '#777'); // thighten up
grd.addColorStop(1, '#000');
dctx.fillStyle = grd;
dctx.fillRect(0, 0, gridSize, gridSize);
Do the math
Then we do all the calculation and offsetting:
/// change composition mode
ctx.globalCompositeOperation = 'source-atop';
/// calc angle and distance to light source and draw each
/// dot gradient offset in relation to this
for (y = 0; y < ez.width; y += gridSize) {
for (x = 0; x < ez.height; x += gridSize) {
/// angle
angle = Math.atan2(lightY - y, lightX - x);
//if (angle < 0) angle += 2;
/// distance
dx = lightX - x;
dy = lightY - y;
dist = Math.sqrt(dx * dx + dy * dy);
/// map distance to our max offset
od = dist / maxLength * maxOffset * 2;
if (od > maxOffset * 2) od = maxOffset * 2;
/// now get new x and y position based on angle and offset
offsetX = x + od * Math.cos(angle) - maxOffset * 0.5;
offsetY = y + od * Math.sin(angle) - maxOffset * 0.5;
/// draw the gradient dot at offset
ctx.drawImage(dot.canvas, x + offsetX, y + offsetY);
}
}
Shadow
For the shadow you just inverse the offset while using the composition mode destination-over which will draw outside the already drawn pixels:
/// Shadow, same as offsetting light, but in opposite
/// direction and with a different composite mode
ctx.globalCompositeOperation = 'destination-over';
for (y = 0; y < ez.width; y += gridSize) {
for (x = 0; x < ez.height; x += gridSize) {
/// angle
angle = Math.atan2(lightY - y, lightX - x);
//if (angle < 0) angle += 2;
/// distance
dx = lightX - x;
dy = lightY - y;
dist = Math.sqrt(dx * dx + dy * dy);
/// map distance to our max offset
od = dist / maxLength * maxOffset * 2;
if (od > maxOffset * 4) od = maxOffset * 4;
/// now get new x and y position based on angle and offset
offsetX = x - od * Math.cos(angle) + gridSize * 0.5;
offsetY = y - od * Math.sin(angle) + gridSize * 0.5;
ctx.beginPath();
ctx.arc(x + offsetX, y + offsetY, radius, 0, arcStop);
ctx.fill();
}
}
This can all be optimized of-course into a single loop pair but for overview the code is separated.
Additional
In the demo I added mouse tracking so the mouse becomes the light-source and you can see the dot-reflection changes while you move the mouse. For best performance use Chrome.
To match your need just scale down the values I am using - or - draw to a big off-screen canvas and use drawImage to scale it down to a main canvas.