I want to have it so that when i create a "component" i can set its radius to make it curved. Below is my code for component create:
function component(width, height, color, x, y) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.color = color;
this.update = function() {
ctx = GameArena.context;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
as you can see it specifies the width height, color and x and y positions but i can't find a way to give it a radius. The other end of my code that uses this component function is here:
PaintBrush = new component(30, 30, "Blue", 30, 320);
Help would be appreciated!
Drawing a rectangle with rounded corners can be done using arcs instead:
The arc takes the arguments:
arc(x, y, radius, startAngle, endAngle [,ccw]); // we won't need counter-clockwise
For example:
var pi2 = Math.PI * 2; // 360 deg.
var r = this.radius, w = this.width, h = this.height;
// ...
// draw rounded rectangle
ctx.beginPath();
ctx.arc(r, r, r, pi2*0.5, pi2*0.75); // top-left
ctx.arc(r+w-r*2, r, r, pi2*0.75, pi2); // top-right
ctx.arc(r+w-r*2, r+h-r*2, r, 0, pi2*0.25); // bottom-right
ctx.arc(r, r+h-r*2, r, pi2*0.25, pi2*0.5); // bottom-left
This simply draws four arc in each corner using radius and start and end angle. Since we use a single path lines will be drawn between each arc from the end of the previous arc to the beginning of the new one - which is why the order matters.
Simply fill() to close the path and fill the shape. If you want to stroke() it as well remember to use closePath() first. If you have paths added later via other objects etc., also remember to use beginPath() before you add them.
The line setting radius will also clamp it to the minimum size possible:
this.radius = Math.min(radius, Math.min(width, height)/2);
First the minimum of height and width is used divided on two. Then the minimum of radius and this result. This makes sure the radius can't be larger then half of the shortest side which would be "impossible".
A note on the setTransform() usage below - if you don't have accumulated transforms this should work fine. If you do and can't easily change it replace the setTransform()s with ctx.translate(this.x, this.y) and after finished reverse it by calling ctx.translate(-this.x, -this.y);. I would recommend using setTransforms for all your objects hover if they are transformed (rotated, scaled etc.) somehow.
Demo
var GameArena = {context: c.getContext("2d")}; // dummy
var PaintBrush = new component(200, 100, "#37f", 10, 10, 16);
PaintBrush.update();
function component(width, height, color, x, y, radius) {
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.radius = Math.min(radius, Math.min(width, height)/2); // clamp radius
this.color = color;
this.update = function() {
var pi2 = Math.PI * 2; // 360 deg.
var r = this.radius, w = this.width, h = this.height;
ctx = GameArena.context;
ctx.fillStyle = this.color;
ctx.setTransform(1,0,0,1,this.x, this.y); // transform (absolute here)
// draw rounded rectangle
ctx.beginPath();
ctx.arc(r , r , r, pi2*0.5 , pi2*0.75); // top-left
ctx.arc(w-r, r , r, pi2*0.75, pi2); // top-right
ctx.arc(w-r, h-r, r, 0 , pi2*0.25); // bottom-right
ctx.arc(r , h-r, r, pi2*0.25, pi2*0.5); // bottom-left
ctx.fill();
ctx.setTransform(1,0,0,1,0,0); // reset transform
}
}
<canvas id=c></canvas>
Related
I would like to create an arc distortion of an image with canvas.
My goal is to do the same thing as imagemagick but in javascript with canvas: https://legacy.imagemagick.org/usage/distorts/#circular_distorts
Here is the expected result with the angle parameter that corresponds to the images below:
60°, 120°, 180°, 270°, 360°
I only found two interesting codes that go in the right direction:
This experimental script
which works directly on the pixel array but does not keep the aspect ratio of the original image and the angle given as a parameter does not work well:
https://github.com/sergiks/canvas-text-arc
This other script
which makes a rotation on each column of the image with drawimage but does not allow to configure the angle of the arc, it is a 360° rotation by default:
http://jsfiddle.net/hto1s6fy/
var cv = document.getElementById('cv');
var ig = document.getElementById('ig');
var ctx = cv.getContext('2d');
// draw the part of img defined by the rect (startX, startY, endX, endY) inside
// the circle of center (cx,cy) between radius (innerRadius -> outerRadius)
// - no check performed -
function drawRectInCircle(img, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY) {
var angle = 0;
var step = 1 * Math.atan2(1, outerRadius);
var limit = 2 * Math.PI;
ctx.save();
ctx.translate(cx, cy);
while (angle < limit) {
ctx.save();
ctx.rotate(angle);
ctx.translate(innerRadius, 0);
ctx.rotate(-Math.PI / 2);
var ratio = angle / limit;
var x = startX + ratio * (endX - startX);
ctx.drawImage(img, x, startY, 1, (endY - startY), 0, 0, 1, (outerRadius - innerRadius));
ctx.restore();
angle += step;
}
ctx.restore();
}
var cx = 300,
cy = 300;
var innerRadius = 0;
var outerRadius = 300;
var startX = 0,
endX = 1361,
startY = 0,
endY = 681;
drawRectInCircle(ig, cx, cy, innerRadius, outerRadius, startX, startY, endX, endY);
Imagemagick source code
Finally, I also looked at the C source code of imagemagick but I don't have the skills to transpose it:
https://github.com/imagemagick/imagemagick/blob/main/magickcore/distort.c
(to see what concerns arc distortion, use the keyword "ArcDistortion")
Though this is an interesting topic and I also like to re-invent the wheel sometimes, it isn't necessary in this case. Someone else had a go at it yet and released a JavaScript library called lens, which replicates some of ImageMagick's filters. Luckily the 'Arc distortion' is among those.
Lens offers a method called distort() which accepts an input like a <canvas> element, applies the transformation requested and outputs raw pixel data, which you can then use to make another <canvas>.
Here's a quick example:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "48px sans";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#0";
ctx.fillText("Around the World", canvas.width / 2, canvas.height / 2);
async function makeArc(image, angle, rotate = 0) {
let result = await lens.distort(
image,
lens.Distortion.ARC, [angle, rotate], {
imageVirtualPixelMethod: angle === 360 ? lens.VirtualPixelMethod.HORIZONTAL_TILE : lens.VirtualPixelMethod.TRANSPARENT
}
);
let tempCanv = document.createElement("canvas");
let tempCtx = tempCanv.getContext("2d");
tempCanv.width = result.image.width;
tempCanv.height = result.image.height;
tempCtx.putImageData(result.image.imageData, 0, 0);
document.body.appendChild(tempCanv);
}
makeArc(canvas, 120, 0);
<script src="https://cdn.jsdelivr.net/npm/#alxcube/lens#1.0.0/dist/lens.min.js"></script>
<canvas id="canvas" width="450" height="50"></canvas>
Link to JSFiddle for entire code: https://jsfiddle.net/u4mk0gdt/
I read the Mozilla docs on save() and restore() and I thought that "save" saved the current state of the entire canvas and "restore" restored the canvas to the most recent "save" state. Hence I placed the saves and restores in such a way that it should clear the white line that is drawn to canvas after is is drawn. However when I run this code the white line is never cleared from the canvas and is drawn continually without clearing.
ctx.restore();
ctx.save(); // <--should save blank canvas
//DRAW LINE
ctx.moveTo(tMatrix.x1, tMatrix.y1);
ctx.lineTo(w/2,h/2);
ctx.strokeStyle = "white";
ctx.stroke();
ctx.restore(); // <-- should restore to the "save()" above
ctx.save(); // <-- <--should save blank canvas again
As you can see, I made a lot of modifications to your code:
console.log("rotating_recs");
// create canvas and add resize
var canvas, ctx;
function createCanvas() {
canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
}
function resizeCanvas() {
if (canvas === undefined) {
createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
var Player = function(x, y, height, width, rot) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.rot = rot;
this.objWinX = 0; //translate the window object and then apply to this
this.objWinY = 0;
this.draw = function() {
//rotate by user.rot degrees, from the players center
ctx.translate(this.x + this.width / 2, this.y + this.height / 2)
ctx.rotate(this.rot * Math.PI / 180)
ctx.translate(-this.x - this.width / 2, -this.y - this.height / 2)
ctx.fillStyle = "grey";
ctx.fillRect(this.x, this.y, this.height, this.width);
ctx.translate(this.x + this.width / 2, this.y + this.height / 2)
ctx.rotate(-this.rot * Math.PI / 180)
ctx.translate(-this.x - this.width / 2, -this.y - this.height / 2)
}
}
var user = new Player(0, 0, 40, 40, 0);
var user2 = new Player(0, 0, 40, 40, 0);
let rot = 0;
function update(time) {
var w, h;
w = canvas.width; // get canvas size incase there has been a resize
h = canvas.height;
ctx.clearRect(0, 0, w, h); // clear the canvas
//MIDDLE RECT
/*
if you don't want this you can just translate by w/2 and h/2, but I would recommend just making the p layers position the middle
*/
user.x = w / 2 - 20;
user.y = h / 2 - 20;
user.rot += 0.5 // or whatever speed
user.draw(); //draw player -- look at the draw function I added some stuff
//LINE
/*
I don't know what you are trying to do, but I just drew the line to the user2's position,
if this doesn't work for your scenario you can change it back
*/
ctx.beginPath()
ctx.moveTo(user2.x + user2.width/2, user2.y + user2.height/2);
ctx.lineTo(w / 2, h / 2);
ctx.strokeStyle = "white";
ctx.stroke();
//FAST SPIN RECT
/*
There are multiple ways to do this, the one that I think you should do, is actually change the position of user two, this uses some very simple trigonometry, if you know this, this is a great way to do this, if not, you can do it how you did previously, and just translate to the center, rotate, and translate back. Similar to what I did with the player draw function. I am going to demonstrate the trig way here:
*/
user2.rot += 5
rot += 2;
user2.x = w/2 + (w/2) * Math.cos(rot * (Math.PI/180))
user2.y = h/2 + (w/2) * Math.sin(rot * (Math.PI/180))
user2.draw();
//RED RECT
ctx.fillStyle = 'red';
ctx.fillRect(140, 60, 40, 40);
requestAnimationFrame(update); // do it all again
}
requestAnimationFrame(update);
While I think you should add some of these modifications into you code, they are not super necessary. To fix you line problem, all you had to do was add ctx.beginPath() before you drew it. The demonstration that I made was not very good (hence demonstration), and you probably shouldn't use it exactly, but definitely look over it. The modified code for you line drawing would look like:
//LINE
ctx.beginPath()
ctx.moveTo(tMatrix.x1, tMatrix.y1);
ctx.lineTo(w/2,h/2);
ctx.strokeStyle = "white";
ctx.stroke();
ctx.restore();
ctx.save();
Hope this helps :D
Sorry for bad spelling
I'm trying to create a JavaScript object that has a method which allows a rectangle to rotate around its own origin during a rAF callback.
Things I have done:
Calculating the origin of an object within the canvas space.
Using ctx.save() and ctx.restore() - this is where my issues arise.
When I use the save() and restore() methods to push and pop the saved states within method calls for different objects it either doesn't change anything, or stops the entire animation.
The rotation in my example appears to be applied globally to the canvas (which is how the functionality is specified on MDN). I'm trying to translate around origin around multiple instances. I've spent hours on this.
Is there something going on with the inheritance mechanism in JavaScript that's not resetting my transforms for different instances of the rectangle objects in the code example?
// author: Nicholas Fazzolari
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var xCenterCanvas = innerWidth/2;
var yCenterCanvas = innerHeight/2;
// custom rectangle object
function RectangleCustom(x, y, w, h, color) {
this.w = w;
this.h = h;
this.x = x;
this.y = y;
this.color = color;
this.radians = (Math.PI/180) * 2; // change the last value to change speed
// draws a rectangle at given coordinates
this.draw = function() {
ctx.save();
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.restore();
}
// rotates the rectangle around it's center relative to a given xy position
this.rotateRect = function() {
ctx.save();
ctx.translate(this.x + this.w * 0.5, this.y + this.h * 0.5);
ctx.rotate(this.radians);
ctx.translate(-this.x -this.w * 0.5, -this.y - this.h * 0.5);
//ctx.restore()
}
}
// singleton rectangles
var bkgRectangle = new RectangleCustom(0, 0, innerWidth, innerHeight, "#212121");
var redRectangle = new RectangleCustom(xCenterCanvas - 64, yCenterCanvas - 64, 128, 128, "#F44336");
// main animation loop
function mainAnimationLoop() {
// runs animation and clears the canvas each call
requestAnimationFrame(mainAnimationLoop);
ctx.clearRect(0, 0, innerWidth, innerHeight);
bkgRectangle.draw();
redRectangle.draw();
redRectangle.rotateRect();
}
mainAnimationLoop();
I have tried rotating multiple rectangles around their own origin at different positions without animation using save() and restore() - which worked.
Additionally, I have tried moving the rotate method inside of the draw method and the results were the same. My rationale was that the rotation would be applied as a function call within draw() - the rationale was clearly wrong.
Any insight towards a solution would be greatly helpful. I have included a link to the pen on codepen to see the concept in motion.
Instead of drawing the rects at (this.x, this.y) you may draw them at 0,0 and translate them to (this.x, this.y);
// author: Nicholas Fazzolari
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var xCenterCanvas = innerWidth/2;
var yCenterCanvas = innerHeight/2;
// custom rectangle object
function RectangleCustom(x, y, w, h, color) {
this.w = w;
this.h = h;
this.x = x;
this.y = y;
this.color = color;
this.radians = (Math.PI/180) * 2; // change the last value to change speed
this.rotation = 0;
// draws a rectangle at given coordinates
this.draw = function() {
this.rotation += this.radians;
ctx.save();
ctx.fillStyle = this.color;
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
ctx.fillRect(0,0, this.w, this.h);
ctx.restore();
}
this.update = function() {
// animation updates
}
}
// singleton rectangles
var bkgRectangle = new RectangleCustom(0, 0, innerWidth, innerHeight, "#212121");
var redRectangle = new RectangleCustom(xCenterCanvas - 64, yCenterCanvas - 64, 128, 128, "#F44336");
// main animation loop
function mainAnimationLoop() {
// runs animation and clears the canvas each call
requestAnimationFrame(mainAnimationLoop);
ctx.clearRect(0, 0, innerWidth, innerHeight);
bkgRectangle.draw();
redRectangle.draw();
}
mainAnimationLoop();
<canvas></canvas>
I'm trying to use a gradient to fill an area of a canvas, but I would like to be able to set the angle of the gradient.
I know this is possible by using different values in the creation of the gradient (ctx.createLinearGradient(x1, y1, x2, y2)) as seen here:
But I can't seem to get my head around the maths needed to convert an angle (radians) to a gradient size that will produce the same angle (The angle I'm referring to is perpendicular to the direction of the gradient, so a 0 radian angle would show the gradient on the right)
In short, how can I convert (quantity) of radians into an X by Y shape?
$(document).ready(function(){
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
var angle = 0.5;
ctx.beginPath();
ctx.moveTo(100, 100);
ctx.arc(100, 100, 100, 0, -angle, true);
ctx.lineTo(100, 100);
ctx.closePath();
// Convert angle into coordinates to tilt the grad
// grad should be perpendicular to the top edge of the arc
var grad = ctx.createLinearGradient(0, 0, 0, 100);
grad.addColorStop(0, "rgba(0,0,0,0)");
grad.addColorStop(1, "rgba(0,0,0,0.8)");
ctx.fillStyle = grad;
ctx.fill();
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id="test" width="500" height="500"></canvas>
(So no one wastes their time: I specifically don't want to use a context.rotate() in this case)
You can use the angle with cos and sin to define the line that gives the gradient. The only thing left then is to give the length:
var length = 100, angle = 0;
ctx.createLinearGradient(x, y, x + Math.cos(angle) * length, y + Math.sin(angle) * length);
The gradient will be rendered along (perpendicular) to the line given.
Not stated, but if you need to calculate the length of the line depending on the angle and box you can use the law of sines to do so (used in this way). The example below uses a fixed radius. You can also use max length from (x1, x2) by calculating the hypotenuse: length = Math.sqrt(diffX*diffX + diffY*diffY);.
Example
var ctx = c.getContext("2d"),
x1 = 150, y1 = 150, x2, y2, angle,
length = 150;
render();
cAngle.oninput = render;
function render() {
angle = +cAngle.value / 180 * Math.PI;
// calculate gradient line based on angle
x2 = x1 + Math.cos(angle) * length;
y2 = y1 + Math.sin(angle) * length;
// create and render gradient
ctx.fillStyle = ctx.createLinearGradient(x1, y1, x2, y2);
ctx.fillStyle.addColorStop(0, "#fff");
ctx.fillStyle.addColorStop(1, "#07f");
ctx.fillRect(0, 0, 300, 300);
// show definition line
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
<label>Angle: <input id=cAngle max=359 type=range value=0></label><br>
<canvas id=c height=300></canvas>
I am drawing two rectangles in html5 canvas element. One of the edges on rectangle a is on the edge of rectangle b.
Rectangle a is green and rectangle b is blue.
The result is that the common edge is neither blue nor green : Its color is some blend of the two.
I tried setting globalCompositeOperation to source over, but it did not help.
That's because lines are drawed over more than one screen pixel.
The drawing model is based on float coordinates, rounded values being between the screen pixels.
To avoid that blending, always draw lines whose line width is one pixel at coordinates Math.round(x)+0.5.
Here's a related answer with a picture.
And here's some code I made to help drawing thin lines and rectangles :
function drawThinHorizontalLine(c, x1, x2, y) {
c.lineWidth = 1;
var adaptedY = Math.floor(y)+0.5;
c.beginPath();
c.moveTo(x1, adaptedY);
c.lineTo(x2, adaptedY);
c.stroke();
}
function drawThinVerticalLine(c, x, y1, y2) {
c.lineWidth = 1;
var adaptedX = Math.floor(x)+0.5;
c.beginPath();
c.moveTo(adaptedX, y1);
c.lineTo(adaptedX, y2);
c.stroke();
}
function Rect(x,y,w,h){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
Rect.prototype.drawThin = function(context) {
drawThinHorizontalLine(context, this.x, this.x+this.w, this.y);
drawThinHorizontalLine(context, this.x, this.x+this.w, this.y+this.h);
drawThinVerticalLine(context, this.x, this.y, this.y+this.h);
drawThinVerticalLine(context, this.x+this.w, this.y, this.y+this.h);
}
Example :
context.strokeColor = 'red';
var r = new Rect(20, 23, 433, 14);
r.drawThin(context);