Cannot get the coord of a projected point on an axis - javascript

I'm trying to do a function who take a point position and an axis (a line) and return the projection of the point on the axis.
Point are simply represented by {x, y} coords
Axis are represented by an f(x) = ax + b linear function (or f(x) = C for vertical line) and the rad value for the a (where 0rad is right, and turn clockwise). So axis = {a, b, rad} or axis = {c, rad}.
It should be a simple mathematical problem using Pythagorean but i cannot get the right solution.. Projections are always looking on the other directions (but look at the good distance).
Code demo:
I create a dark point on 25;100 and multiples coloured axis. Every axis have a related point (same color) who represent the projection of the dark point on it. Both grey are vertical and horizontal examples and work well but 3 others axis (Red, Green and Blue) are wrong.
I tried to redo getProjectionOnAxis function multiple times, and even try to get the good results by testing all possibility (by inverse vars, using another cos/sin/tan function) and try to catch my issue like that but nothing to do I never get the good result.
const point = {x:25, y:100};
const axis = [
{a: 1, b: 25, rad:Math.PI/4, rgb: '255,0,0'}, // 45deg
{a: -.58, b: 220, rad:Math.PI/6, rgb: '0,255,0'}, // -30deg
{a: 3.73, b: -50, rad:5*Math.PI/12, rgb: '0,0,255'}, // 75deg
// Gray
{c: 150, rad:Math.PI/2, rgb: '100,100,100'},
{b: 150, rad:0},
];
const execute = () => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
drawPoint({...point, rgb:'0,0,0', ctx});
axis.forEach(_axis => {
drawAxis({axis:_axis, rgb: _axis.rgb, ctx, canvas});
const projected = getProjectionOnAxis({...point, axis: _axis});
drawPoint({...projected, rgb:_axis.rgb, ctx});
});
}
// --- Issue come from here ---
const getProjectionOnAxis = ({x,y,axis}) => {
// On vertical axis |
if (axis.c) {
return { x: axis.c, y };
}
// On horizontal axis _
if (!axis.a) {
return { x, y: axis.b };
}
// Projected on axis but perpendicular to X axis
const projectedOnX = {
x,
y: axis.a * x + axis.b,
}
// The distance between both points is the hypothenus of the triangle:
// point, projectedOnAxis, projectedOnX
// where point <-> projectedOnX is the hypothenus
// and point angle is same that axis
const distCornerToProjectedOnX = Math.abs(y - projectedOnX.y);
const distCornerToProjectedOnAxis = distCornerToProjectedOnX * Math.cos(axis.rad);
const projectedVector = {
x: distCornerToProjectedOnAxis * Math.cos(axis.rad),
y: distCornerToProjectedOnAxis * Math.sin(axis.rad),
};
return {
x: x + projectedVector.x,
y: y + projectedVector.y,
};
}
// --- Draw Functions ---
// Not really important for the issue
const drawPoint = ({x,y,rgb, ctx}) => {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
ctx.fillStyle = `rgba(${rgb},1)`;
ctx.arc(0, 0, 2, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
ctx.restore();
};
const drawAxis = ({axis, rgb, ctx, canvas}) => {
if (axis.c) {
// Vertical axis
drawLine({
from: {x: axis.c, y:0},
to: {x:axis.c, y:canvas.height},
rgb, ctx
});
}
else if (!axis.a) {
// Horizontal axis
drawLine({
from: {x:0, y:axis.b},
to: {x:canvas.width, y:axis.b},
rgb, ctx
});
}
else {
// ax + b (here a != 0)
let to = {
x: canvas.width,
y: axis.a * canvas.width + axis.b,
};
if (to.y < 0) {
to = {
x: axis.b / - axis.a,
y: 0,
}
}
drawLine({
from: {x:0, y:axis.b},
to,
rgb, ctx
});
}
}
const drawLine = ({
from, to, rgb=null, ctx
}) => {
ctx.save();
ctx.translate(from.x, from.y);
ctx.beginPath();
ctx.strokeStyle = `rgba(${rgb},1)`;
ctx.moveTo(0, 0);
ctx.lineTo(to.x - from.x, to.y - from.y);
ctx.stroke();
ctx.restore();
};
execute();
html, body, canvas { margin: 0; padding: 0;}
<canvas id="canvas" width="500" height="500"></canvas>
This is the positions I should get:
PS: I don't really like the way I manage my axis, maybe they are another (simple) way to do it ?

Represent line (your axis) in parametric form as base point A and unit direction vector d = (dx, dy). It is universal representation suitable for all slopes. If you have slope angle fi relative to OX axis, then dx=cos(fi), dy=sin(fi)
L = P0 + d * t
Then projection of point C onto line is (using scalar product)
AC = C - A
P = A + d * (d.dot.AC)
In coordinates
dotvalue = dx * (C.x - A.x) + dy * (C.y - A.y)
P.x = A.x + d.x * dotvalue
P.y = A.y + d.y * dotvalue

Related

How to displace a circle minimally outside a rectangle?

This seems like it should be pretty simple but I could not find any clear answers on it. Say I have a single circle and rectangle. If the circle is outside of the rectangle, it should maintain its current position. However, if it is inside the rectangle at all, it should be displaced minimally such that it is barely outside the rectangle.
I have created a full demo below that demonstrates my current work-in-progress. My initial idea was to clamp the circle to the closest edge, but that seemed to not be working properly. I think there might be a solution involving Separating Axis Theorem, but I'm not sure if that applies here or if it's overkill for this sort of thing.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
function draw() {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
ctx.fillStyle = "#fff";
drawCircle(circlePos.x, circlePos.y, circleR);
drawSquare(squarePos.x, squarePos.y, squareW, squareH);
}
function drawCircle(xCenter, yCenter, radius) {
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawSquare(x, y, w, h) {
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.stroke();
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getCircleRectangleDisplacement(rX, rY, rW, rH, cX, cY, cR) {
let nearestX = clamp(cX, rX, rX + rW);
let nearestY = clamp(cY, rY, rY + rH);
let newX = nearestX - cR / 2;
let newY = nearestY - cR / 2;
return { x: newX, y: newY };
}
function displace() {
circlePos = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR);
draw();
}
let circlePos = { x: 280, y: 70 };
let squarePos = { x: 240, y: 110 };
let circleR = 50;
let squareW = 100;
let squareH = 100;
draw();
setTimeout(displace, 500);
canvas { display: flex; margin: 0 auto; }
<canvas width="800" height="800"></canvas>
As you can see in the demo, after 500 milliseconds the circle jumps a bit in an attempt to displace itself properly, but it does not move to the correct location. Is there an algorithm to find the circle's new location that would require as little movement as possible to move it outside of the bounds of the rectangle?
Have a look here, core is in calc() function, it's Java, not JavaScript , but I think that you can easily translate it.
package test;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class CircleOutside extends JComponent {
protected Rectangle2D rect;
protected Point2D originalCenter;
protected double radius;
protected Point2D movedCenter;
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2=(Graphics2D) g;
g2.draw(rect);
g.setColor(Color.red);
g2.draw(new Ellipse2D.Double(originalCenter.getX()-radius, originalCenter.getY()-radius, 2*radius, 2*radius));
g.setColor(Color.green);
g2.draw(new Ellipse2D.Double(movedCenter.getX()-radius, movedCenter.getY()-radius, 2*radius, 2*radius));
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
originalCenter=e.getPoint();
calc();
repaint();
}
});
}
public void calc() {
movedCenter=originalCenter;
//Circle center distance from edges greater than radius, do not move
if (originalCenter.getY()+radius<=rect.getY()) {
return;
}
if (originalCenter.getY()-radius>=rect.getY()+rect.getHeight()) {
return;
}
if (originalCenter.getX()+radius<=rect.getX()) {
return;
}
if (originalCenter.getX()-radius>=rect.getX()+rect.getWidth()) {
return;
}
double moveX=0;
double moveY=0;
boolean movingY=false;
boolean movingX=false;
//Center projects into rectangle's width, move up or down
if (originalCenter.getX()>=rect.getX()&&originalCenter.getX()<=rect.getX()+rect.getWidth()) {
System.out.println("X in width");
double moveUp=rect.getY()-originalCenter.getY()-radius;
double moveDown=rect.getY()+rect.getHeight()-originalCenter.getY()+radius;
if (Math.abs(moveUp)<=Math.abs(moveDown)) {
moveY=moveUp;
} else {
moveY=moveDown;
}
System.out.println("UP "+moveUp+" DOWN "+moveDown);
movingY=true;
}
//Center projects into rectangle's height, move left or right
if (originalCenter.getY()>=rect.getY()&&originalCenter.getY()<=rect.getY()+rect.getHeight()) {
double moveLeft=rect.getX()-originalCenter.getX()-radius;
double moveRight=rect.getX()+rect.getWidth()-originalCenter.getX()+radius;
if (Math.abs(moveLeft)<=Math.abs(moveRight)) {
moveX=moveLeft;
} else {
moveX=moveRight;
}
movingX=true;
}
//If circle can be moved both on X or Y, choose the lower distance
if (movingX&&movingY) {
if (Math.abs(moveY)<Math.abs(moveX)) {
moveX=0;
} else {
moveY=0;
}
}
//Note that the following cases are mutually excluding with the previous ones
//Center is in the arc [90-180] centered in upper left corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()<rect.getX()&&originalCenter.getY()<rect.getY()) {
double dist=originalCenter.distance(rect.getX(),rect.getY());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX());
moveY=factor*(originalCenter.getY()-rect.getY());
}
}
//Center is in the arc [0-90] centered in upper right corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()>rect.getX()+rect.getWidth()&&originalCenter.getY()<rect.getY()) {
double dist=originalCenter.distance(rect.getX()+rect.getWidth(),rect.getY());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX()-rect.getWidth());
moveY=factor*(originalCenter.getY()-rect.getY());
}
}
//Center is in the arc [270-360] centered in lower right corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()>rect.getX()+rect.getWidth()&&originalCenter.getY()>rect.getY()+rect.getHeight()) {
double dist=originalCenter.distance(rect.getX()+rect.getWidth(),rect.getY()+rect.getHeight());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX()-rect.getWidth());
moveY=factor*(originalCenter.getY()-rect.getY()-rect.getHeight());
}
}
//Center is in the arc [180-270] centered in lower left corner with same radius as circle, calculate distance from corner and adjust both axis
if (originalCenter.getX()<rect.getX()&&originalCenter.getY()>rect.getY()+rect.getHeight()) {
double dist=originalCenter.distance(rect.getX(),rect.getY()+rect.getHeight());
if (dist<radius) {
double factor=(radius-dist)/dist;
moveX=factor*(originalCenter.getX()-rect.getX());
moveY=factor*(originalCenter.getY()-rect.getY()-rect.getHeight());
}
}
movedCenter=new Point2D.Double(originalCenter.getX()+moveX,originalCenter.getY()+moveY);
}
public static void main(String[] args) {
Rectangle2D rect=new Rectangle2D.Double(240, 110, 100, 100);
Point2D center=new Point2D.Double(280, 70);
double radius=50;
CircleOutside o=new CircleOutside();
o.rect=rect;
o.originalCenter=center;
o.radius=radius;
o.calc();
o.setPreferredSize(new Dimension(800,600));
JFrame frame=new JFrame("Test circle");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(o);
frame.pack();
frame.setVisible(true);
}
}
Made the following modifications to the code:
Added function pointToSegmentDistance which calculates both the distance of a point to a line segment in addition to the corresponding perpendicular point on the line segment.
Added function pointInPolygon which determines whether a point resides inside or outside of a polygon.
Modified function getCircleRectangleDisplacement to perform the following:
Create a bounding polygon that extends the edges of the rectangle by the length of the radius. Then, if the circle center resides inside this bounding polygon, it needs to be moved to one of the four (4) extended edges. Function pointInPolygon determines whether the circle center is in the bounding polygon, and if so, then pointToSegmentDistance is used to find the closest point on one of the four (4) extended edges, a point which now represents the new circle center.
Otherwise, if the circle center is outside the bounding polygon, then the function checks if the circle center is less than the length of the radius to one of the four vertices, and if so, moves the circle center away from the vertex such that the distance is now the radius.
<html><head>
<style>
canvas { display: flex; margin: 0 auto; }
</style>
</head><body>
<canvas width="800" height="800"></canvas>
<script>
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
function draw() {
ctx.fillStyle = "#fff";
drawCircle(circlePos.x, circlePos.y, circleR);
drawSquare(squarePos.x, squarePos.y, squareW, squareH);
}
function drawCircle(xCenter, yCenter, radius) {
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI);
ctx.fill();
}
function drawSquare(x, y, w, h) {
ctx.fillStyle = "#f0f";
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.fill();
}
// Sourced and adapted from https://stackoverflow.com/a/6853926/7696162
function pointToSegmentDistance(point, segBeg, segEnd) {
var A = point.x - segBeg.x;
var B = point.y - segBeg.y;
var C = segEnd.x - segBeg.x;
var D = segEnd.y - segBeg.y;
var dot = A * C + B * D;
var len_sq = C * C + D * D;
var param = -1;
if (len_sq != 0) //in case of 0 length line
param = dot / len_sq;
let intersectPoint;
if (param < 0) {
intersectPoint = segBeg;
}
else if (param > 1) {
intersectPoint = segEnd;
}
else {
intersectPoint = { x: segBeg.x + param * C, y:segBeg.y + param * D };
}
var dx = point.x - intersectPoint.x;
var dy = point.y - intersectPoint.y;
return { intersect: intersectPoint, distance: Math.sqrt(dx * dx + dy * dy) };
}
// Sourced and adapted from https://www.algorithms-and-technologies.com/point_in_polygon/javascript
function pointInPolygon( point, polygon ) {
let vertices = polygon.vertex;
//A point is in a polygon if a line from the point to infinity crosses the polygon an odd number of times
let odd = false;
//For each edge (In this case for each point of the polygon and the previous one)
for (let i = 0, j = polygon.length - 1; i < polygon.length; i++) {
//If a line from the point into infinity crosses this edge
if (((polygon[i].y > point.y) !== (polygon[j].y > point.y)) // One point needs to be above, one below our y coordinate
// ...and the edge doesn't cross our Y corrdinate before our x coordinate (but between our x coordinate and infinity)
&& (point.x < ((polygon[j].x - polygon[i].x) * (point.y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x))) {
// Invert odd
odd = !odd;
}
j = i;
}
//If the number of crossings was odd, the point is in the polygon
return odd;
}
function getCircleRectangleDisplacement( rX, rY, rW, rH, cX, cY, cR ) {
let rect = [
{ x: rX, y:rY },
{ x: rX + rW, y:rY },
{ x: rX + rW, y:rY + rH },
{ x: rX, y:rY + rH }
];
let boundingPolygon = [
{ x: rX, y: rY },
{ x: rX, y: rY - cR },
{ x: rX + rW, y: rY - cR },
{ x: rX + rW, y: rY },
{ x: rX + rW + cR, y: rY },
{ x: rX + rW + cR, y: rY + rH },
{ x: rX + rW, y: rY + rH },
{ x: rX + rW, y: rY + rH + cR },
{ x: rX, y: rY + rH + cR },
{ x: rX, y: rY + rH },
{ x: rX - cR, y: rY + rH },
{ x: rX - cR, y: rY }
];
// Draw boundingPolygon... This can be removed...
ctx.setLineDash([2,2]);ctx.beginPath();ctx.moveTo(boundingPolygon[0].x,boundingPolygon[0].y);for (let p of boundingPolygon) {ctx.lineTo(p.x,p.y);} ctx.lineTo(boundingPolygon[0].x,boundingPolygon[0].y);ctx.stroke();
circleCenter = { x: cX, y: cY };
// If the circle center is inside the bounding polygon...
if ( pointInPolygon( circleCenter, boundingPolygon ) ) {
let newCircleCenter;
let minDistance = Number.MAX_VALUE;
// ...then loop through the 4 segments of the bounding polygon that are
// extensions of the original rectangle, looking for the point that is
// closest to the circle center.
for ( let i = 1; i < boundingPolygon.length; i += 3 ) {
let pts = pointToSegmentDistance( circleCenter, boundingPolygon[ i ], boundingPolygon[ i + 1 ] );
if ( pts.distance < minDistance ) {
newCircleCenter = pts.intersect;
minDistance = pts.distance;
}
}
circleCenter = newCircleCenter;
} else {
// ...otherwise, if the circle center is outside the bounding polygon,
// let's check to see if the circle center is closer than the radius
// to one of the corners of the rectangle.
let newCircleCenter;
let minDistance = Number.MAX_VALUE;
for ( let i = 0; i < boundingPolygon.length; i += 3 ) {
let d = Math.sqrt( ( circleCenter.x - boundingPolygon[ i ].x ) ** 2 + ( circleCenter.y - boundingPolygon[ i ].y ) ** 2 );
if ( d < cR && d < minDistance ) {
// Okay, the circle is too close to a corner. Let's move it away...
newCircleCenter = {
x: boundingPolygon[ i ].x + ( circleCenter.x - boundingPolygon[ i ].x ) * cR / d,
y: boundingPolygon[ i ].y + ( circleCenter.y - boundingPolygon[ i ].y ) * cR / d
}
minDistance = d;
}
}
if ( newCircleCenter ) {
circleCenter = newCircleCenter;
}
}
return circleCenter;
}
function displace() {
ctx.fillStyle = "#b2c7ef";
ctx.fillRect(0, 0, 800, 800);
circlePos.x += 1;
circlePos.y += 1;
circlePos = getCircleRectangleDisplacement(squarePos.x, squarePos.y, squareW, squareH, circlePos.x, circlePos.y, circleR);
draw();
if ( maxIterations < iterations++ ) {
clearInterval( timer );
}
}
let circlePos = { x: 280, y: 40 };
circlePos={ x: 240, y: 110 };
let squarePos = { x: 240, y: 110 };
let circleR = 50;
let squareW = 100;
let squareH = 100;
let iterations = 0;
let maxIterations = 200;
let timer = setInterval(displace, 50);
</script>
</body></html>
I believe this algorithm can be extended to simple polygons (ie, convex polygons, not concave polygons) although with a bit more trigonometry and/or matrix math...

Konva: get corners coordinate of a rotated rectangle

How can i get corners coordinate of a rotated rectangle (with center of rectangle as pivot) ?
i already tried all of the solution from the link below but seems haven't got any luck.
Rotating a point about another point (2D)
Find corners of a rotated rectangle given its center point and rotation
https://gamedev.stackexchange.com/questions/86755/how-to-calculate-corner-positions-marks-of-a-rotated-tilted-rectangle
here's the code
// make a rectangle with zero rotation
const rect1 = new Konva.Rect({
x: 200,
y: 200,
width: 100,
height: 50,
fill: "#00D2FF",
draggable: true,
rotation: 0,
name: "rect"
});
// convert degree to rad
const degToRad = (deg: number) => deg * (Math.PI / 180);
// here's the code i use to rotate it around its center (from https://konvajs.org/docs/posts/Position_vs_Offset.html)
const rotateAroundCenter = (node: Rect, rotation: number) => {
const topLeft = {
x: -node.width() / 2,
y: -node.height() / 2
};
console.log(`current X: ${node.x()}, current Y: ${node.y()},`)
const currentRotatePoint = rotatePoint(topLeft, degToRad(node.rotation()));
const afterRotatePoint = rotatePoint(topLeft, degToRad(rotation));
const dx = afterRotatePoint.x - currentRotatePoint.x;
const dy = afterRotatePoint.y - currentRotatePoint.y;
node.rotation(rotation);
node.x(node.x() + dx);
node.y(node.y() + dy);
layer.draw();
console.log(`the actual position x: ${node.x()}, y: ${node.y()}`);
};
// the code that i expected to give me the corner point
const computeCornerPoint = (r:Rect) => {
// for now we want to compute top left corner point(as it's the easiest corner to get)
let corner = {
x: r.x(),
y: r.y()
};
// the coordinate of rectangle's center (in stage coordinate)
const cx = r.x() + r.width() / 2;
const cy = r.y();
// sine and cosine of the rectangle's rotation
const s = Math.sin(degToRad(r.rotation()));
const c = Math.cos(degToRad(r.rotation()));
// rotate the corner point
let xnew = c * (corner.x - cx) - s * (corner.y - cy) + cx;
let ynew = s * (corner.x - cx) + c * (corner.y - cy) + cy;
console.log(`based on this function calculation: xnew : ${xnew}, ynew: ${ynew}`);
return [xnew, ynew];
}
based on the code above, if the initial rotation is 0, and i rotate the rectangle 30 degree clockwise,
then the actual position would be same as the value from computeCornerPoint, which is (219, 178) and if i rotate it again by 30 degree clockwise, the actual position would be (246, 169) while the value from computeCornerPoint would be (275, 175).
Life is rectangular in the world of the canvas so all we need to do to predict the corner positions is know the shape top-left and the rotation angle, then apply some high-school math. The math to rotate a point is in function rotatePoint(). The rest of the snippet is setup for its use and illustration of the outcome.
Maybe better running the snippet in full screen mode.
// Function to rotate a point.
// pt = {x,y} of point to rotate,
// o = {x, y} of rotation origin,
// a = angle of rotation in degrees.
// returns {x, y} giving the new point.
function rotatePoint(pt, o, a){
var angle = a * (Math.PI/180); // Convert to radians
var rotatedX = Math.cos(angle) * (pt.x - o.x) - Math.sin(angle) * (pt.y - o.y) + o.x;
var rotatedY = Math.sin(angle) * (pt.x - o.x) + Math.cos(angle) * (pt.y - o.y) + o.y;
return {x: rotatedX, y: rotatedY};
}
// This is just about drawing the circles at the corners.
function drawCorners(rect, angle){
var rectPos = rect.position();
var x = 0, y = 0;
for (var i = 0; i < 4; i = i + 1){
switch (i){
case 0:
x = rectPos.x; y = rectPos.y;
break;
case 1:
x = rectPos.x + rect.width(); y = rectPos.y;
break;
case 2:
x = rectPos.x + rect.width(); y = rectPos.y + rect.height();
break;
case 3:
x = rectPos.x; y = rectPos.y + rect.height();
break;
}
var pt = rotatePoint({x: x, y: y}, {x: rectPos.x, y: rectPos.y}, angle)
circles[i].position(pt)
}
}
// rotate and redraw the rectangle
function rotateUnderMouse(){
// Get the stage position of the mouse
var mousePos = stage.getPointerPosition();
// get the stage position of the mouse
var shapePos = rect.position();
// compute the vector for the difference
var rel = {x: mousePos.x - shapePos.x, y: mousePos.y - shapePos.y}
// Now apply the rotation
angle = angle + 90;
circle.position({x: mousePos.x, y: mousePos.y});
circle.show();
// and reposition the shape to keep the same point in the shape under the mouse
var newPos = ({x: mousePos.x + rel.y , y: mousePos.y - rel.x})
rect.position(newPos);
rect.rotation(angle);
// re-calculate and draw the circle positions.
drawCorners(rect, angle)
stage.draw()
}
function setup() {
// Set up a stage and a shape
stage = new Konva.Stage({
container: 'canvas-container',
width: 650,
height: 300
});
layer = new Konva.Layer();
stage.add(layer);
newPos = {x: 80, y: 100};
rect = new Konva.Rect({
width: 140, height: 50, x: newPos.x, y: newPos.y, draggable: true, stroke: 'silver', fill: 'cyan'
})
// not very dry, setting up the corner circles.
circle = new Konva.Circle({x: newPos.x, y: newPos.y, radius: 10, fill: 'magenta'})
circles[0] = circle.clone();
circles[0].fill('lime')
layer.add(circles[0]);
circles[1] = circle.clone();
circles[1].fill('gold')
layer.add(circles[1]);
circles[2] = circle.clone();
circles[2].fill('blue')
layer.add(circles[2]);
circles[3] = circle.clone();
circles[3].fill('darkviolet')
layer.add(circles[3]);
layer.add(rect);
layer.add(circle);
circle.hide()
drawCorners(rect, 0)
stage.draw()
rect.on('mousedown', function(){
rotateUnderMouse()
})
}
var stage, layer, rect, circles = [], angle = 0;
setup()
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Click the rectangle - it will rotate 90 degrees clockwise under the mouse and coloured circles will be drawn consistently at the corners. These circles have their position calculated rather than derived from the visual rectangle corner positions. NB: No dragging !</p>
<div id="canvas-container"></div>
I recently learned of a new, easier way to achieve this using a built-in Konva function. The node.getTransform and its close relation node.getAbsoluteTransform methods will retrieve the transform applied to the node (shape). The absolute version gets the transform including the parent transform, while the plain getTransform gets the transform relative to the node's parent.
Both return a Konva.Trasform object, which itself has the point() method that will take a given {x, y} object and apply the transform to it.
Using the transform applied to the shape means that we do not have to be concerned with how to mimic the steps of that transform - we just ask for the same transform to be applied to our points.
Which means that we can do this...
// assuming we have a Konva rect already...
let rect = new Konva.Rect({
x: 100,
y: 80,
width: 60,
height: 20,
fill: 'cyan'
rotation: 45
})
let corners = [],
size = rect.size();
// Now get the 4 corner points
corners[0] = {x: 0, y: 0 }; // top left
corners[1] = {x: size.width, y: 0 }; // top right
corners[2] = {x: size.width, y: size.height }; // bottom right
corners[4] = {x: 0, y: size.height }; // bottom left
// And rotate the corners using the same transform as the rect.
for (let i = 0; i < 4; i++){
// Here be the magic
corners[i] = rect.getAbsoluteTransform().point(corners[i]); // top left
}
// At this point we have the rotated positions of the corners.
IMPORTANT NOTE
You will have seen that the corners of the rect in the above code are set relative to the origin and not the rect position. In other words the top-left corner is {x: 0, y: 0} and not {x: rect.x(), y: rect.y()}, and the bottom-right is {x: rect.width, y: rect.height}. This is because the rect's transform is:
moveto(x, y)
rotate(angle)
If we do not negate the moveto when deciding our unrotated corner points then they will appear to have experienced 2 times the moveto transform.
The moveto transform is not obvious - it is the effect of setting the shape.x() and shape.y() in the initial declaration of the shape.
Here is a working snippet using the getAbsoluteTransform() method.
// Function to rotate a point.
// node = the shape we are using to deterine the transform of the pt.
// pt = {x,y} of point to rotate,
// returns {x, y} giving the new point.
function rotatePoint(node, pt){
return node.getAbsoluteTransform().point(pt);
}
// This is just about drawing the circles at the corners.
function drawCorners(rect, angle){
var rectPos = rect.position();
var x = 0, y = 0;
for (var i = 0; i < 4; i = i + 1){
switch (i){
case 0:
x = 0; y = 0;
break;
case 1:
x = rect.width(); y = 0;
break;
case 2:
x = rect.width(); y = rect.height();
break;
case 3:
x = 0; y = rect.height();
break;
}
var pt = rotatePoint(rect, {x: x, y: y})
circles[i].position(pt)
}
}
// rotate and redraw the rectangle
function rotateUnderMouse(){
// Get the stage position of the mouse
var mousePos = stage.getPointerPosition();
// get the stage position of the mouse
var shapePos = rect.position();
// compute the vector for the difference
var rel = {x: mousePos.x - shapePos.x, y: mousePos.y - shapePos.y}
// Now apply the rotation
angle = angle + 90;
circle.position({x: mousePos.x, y: mousePos.y});
circle.show();
// and reposition the shape to keep the same point in the shape under the mouse
var newPos = ({x: mousePos.x + rel.y , y: mousePos.y - rel.x})
rect.position(newPos);
rect.rotation(angle);
// re-calculate and draw the circle positions.
drawCorners(rect, angle)
stage.draw()
}
function setup() {
// Set up a stage and a shape
stage = new Konva.Stage({
container: 'canvas-container',
width: 650,
height: 300
});
layer = new Konva.Layer();
stage.add(layer);
newPos = {x: 80, y: 100};
rect = new Konva.Rect({
width: 140, height: 50, x: newPos.x, y: newPos.y, draggable: true, stroke: 'silver', fill: 'cyan'
})
// not very dry, setting up the corner circles.
circle = new Konva.Circle({x: newPos.x, y: newPos.y, radius: 10, fill: 'magenta', listening: false})
circles[0] = circle.clone();
circles[0].fill('lime')
layer.add(circles[0]);
circles[1] = circle.clone();
circles[1].fill('gold')
layer.add(circles[1]);
circles[2] = circle.clone();
circles[2].fill('blue')
layer.add(circles[2]);
circles[3] = circle.clone();
circles[3].fill('darkviolet')
layer.add(circles[3]);
layer.add(rect);
layer.add(circle);
circle.hide()
drawCorners(rect, 0)
stage.draw()
rect.on('mousedown', function(){
rotateUnderMouse()
})
}
var stage, layer, rect, circles = [], angle = 0;
setup()
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Click the rectangle - it will rotate 90 degrees clockwise under the mouse and coloured circles will be drawn consistently at the corners. These circles have their position derived via the rect.transform - not calculated. NB: No dragging !</p>
<div id="canvas-container"></div>

Get bounds of unrotated rotated rectangle

I have a rectangle that has a rotation already applied to it. I want to get the the unrotated dimensions (the x, y, width, height).
Here is the dimensions of the element currently:
Bounds at a 90 rotation: {
height 30
width 0
x 25
y 10
}
Here are the dimensions after the rotation is set to none:
Bounds at rotation 0 {
height 0
width 30
x 10
y 25
}
In the past, I was able to set the rotation to 0 and then read the updated bounds . However, there is a bug in one of the functions I was using, so now I have to do it manually.
Is there a simple formula to get the bounds at rotation 0 using the info I already have?
Update: The object is rotated around the center of the object.
UPDATE:
What I need is something like the function below:
function getRectangleAtRotation(rect, rotation) {
var rotatedRectangle = {}
rotatedRectangle.x = Math.rotation(rect.x * rotation);
rotatedRectangle.y = Math.rotation(rect.y * rotation);
rotatedRectangle.width = Math.rotation(rect.width * rotation);
rotatedRectangle.height = Math.rotation(rect.height * rotation);
return rotatedRectangle;
}
var rectangle = {x: 25, y: 10, height: 30, width: 0 };
var rect2 = getRectangleAtRotation(rect, -90); // {x:10, y:25, height:0, width:30 }
I found a similar question here.
UPDATE 2
Here is the code I have. It attempts to get the center point of the line and then the x, y, width, and height:
var centerPoint = getCenterPoint(line);
var lineBounds = {};
var halfSize;
halfSize = Math.max(Math.abs(line.end.x-line.start.x)/2, Math.abs(line.end.y-line.start.y)/2);
lineBounds.x = centerPoint.x-halfSize;
lineBounds.y = centerPoint.y;
lineBounds.width = line.end.x;
lineBounds.height = line.end.y;
function getCenterPoint(node) {
return {
x: node.boundsInParent.x + node.boundsInParent.width/2,
y: node.boundsInParent.y + node.boundsInParent.height/2
}
}
I know the example I have uses a right angle and that you can swap the x and y with that but the rotation can be any amount.
UPDATE 3
I need a function that returns the unrotated bounds of a rectangle. I have the bounds at a specific rotation already.
function getUnrotatedRectangleBounds(rect, currentRotation) {
// magic
return unrotatedRectangleBounds;
}
I think I can handle the calculation of the bounds size without too much effort (few equations), I'm not sure, instead, how you would like x and y to be handled.
First, let's properly name things:
Now, we want to rotate it by some angle alpha (in radians):
To calculate the green sides, it is clear that it's made of two repeated rectangle-triangles as the following:
So, solving angles first, we know that:
the sum of the angles of a triangle is PI, or 180°;
the rotation is alpha;
one angle gamma is PI / 2, or 90°;
the last angle, beta, is gamma - alpha;
Now, knowing all the angles and a side, we can use the Law of Sines to calculate other sides.
As a brief recap, the Law of Sines tells us that there is an equality between the ratio of a side length and it's opposite angle. More info here: https://en.wikipedia.org/wiki/Law_of_sines
In our case, for the upper left triangle (and the bottom right one), we have:
Remember that AD is our original height.
Given that the sin(gamma) is 1, and we also know the value of AD, we can write the equations:
For the upper right triangle (and the bottom left one), we then have:
Having all needed sides, we can easily calculate the width and height:
width = EA + AF
height = ED + FB
At this point we can write a quite easy method that, given a rectangle and a rotation angle in radians, can return new bounds:
function rotate(rectangle, alpha) {
const { width: AB, height: AD } = rectangle
const gamma = Math.PI / 4,
beta = gamma - alpha,
EA = AD * Math.sin(alpha),
ED = AD * Math.sin(beta),
FB = AB * Math.sin(alpha),
AF = AB * Math.sin(beta)
return {
width: EA + AF,
height: ED + FB
}
}
This method can then be used like:
const rect = { width: 30, height: 50 }
const rotation = Math.PI / 4.2 // this is a random value it put here
const bounds = rotate(rect, rotation)
Hope there aren't typos...
I think I might get a solution but, for safety, I prefer to prior repeat what we have and what we need to be sure I understood everything correctly. As I said in a comment, english isn't my native language and I already wrote a wrong answer due to my lack of understanding of the problem :)
What we have
We know that at x and y there is a bounds rectangle (green) of size w and h that contains another rectangle (the grey dotted one) rotated of alpha degrees.
We know that the y axis is flipped relatively to the Cartesian one, and that makes the angle to be considered clockwise instead of counter-clockwise.
What we need
At first, we need to find the 4 vertices of the inner rectangle (A, B, C and D) and, knowing the position of the vertices, the size of the inner rectangle (W and H).
As a second step, we need to counter rotate the inner rectangle to 0 degrees, and find it's position X and Y.
Find the vertices
Generally speaking for each vertex we know only one coordinate, the x or the y. The other one "slides" along the side of the bounding box in relation to the angle alpha.
Let's start with A: we know Ay, we need Ax.
We know that the Ax lies between x and x + w in relation to the angle alpha.
When alpha is 0°, Ax is x + 0. When alpha is 90°, Ax is x + w. When alpha is 45°, Ax is x + w / 2.
Basically, Ax grows in relation of the sin(alpha), giving us:
Having Ax, we can easily compute Cx:
In the same way we can compute By and then Dy:
Writing some code:
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// alpha is the rotation IN RADIANS
const vertices = (bounds, alpha) => {
const { x, y, w, h } = bounds,
A = { x: x + w * Math.sin(alpha), y },
B = { x, y: y + h * Math.sin(alpha) },
C = { x: x + w - w * Math.sin(alpha), y },
D = { x, y: y + h - h * Math.sin(alpha) }
return { A, B, C, D }
}
Finding the sides
Now that we have all the vertices, we can easily compute the inner rectangle sides, we need to define a couple more points E and F for clarity of explanation:
Its clearly visible that we can use the Pitagorean Theorem to compute W and H with:
where:
In code:
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// vertices is a POJO with shape: { A, B, C, D }, as returned by the `vertices` method
const sides = (bounds, vertices) => {
const { x, y, w, h } = bounds,
{ A, B, C, D } = vertices,
EA = A.x - x,
ED = D.y - y,
AF = w - EA,
FB = h - ED,
H = Math.sqrt(EA * EA + ED * ED),
W = Math.sqrt(AF * AF + FB * FB
return { h: H, w: W }
}
Finding the position of the counter-rotated inner rectangle
First of all, we have to find the angles (beta and gamma) of the diagonals of the inner rectangle.
Let's zoom in a little bit and add some additional letters for more clarity:
We can use the Law of Sines to get the equations to compute beta:
To make some calculations we have:
We need to compute GC first in order to have at least one side of the equation completely known. GC is the radius of the circumference the inner rectangle is inscribed in and also half of the inner rectangle diagonal.
Having the two sides of the inner rectangle, we can use the Pitagorean Theorem again:
With GC we can solve the Law of Sines on beta:
we know that sin(delta) is 1
Now, beta is the angle of the vertex C in relation with the unrotated x axis.
Looking again at this image, we can easily get the angles of all the other vertices:
Now that we have almost everything, we can compute the new coordinates of the A vertex:
From here, we need to translate both Ax and Ay because they are related to the center of the circumference, which is x + w / 2 and y + h / 2:
So, writing the last piece of code:
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// sides is a POJO with shape: { w, h }, as returned by the `sides` method
const origin = (bounds, sides) => {
const { x, y, w, h } = bounds
const { w: W, h: H } = sides
const GC = r = Math.sqrt(W * W + H * H) / 2,
IC = H / 2,
beta = Math.asin(IC / GC),
angleA = Math.PI + beta,
Ax = x + w / 2 + r * Math.cos(angleA),
Ay = y + h / 2 + r * Math.sin(angleA)
return { x: Ax, y: Ay }
}
Putting all together...
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// rotations is... the rotation of the inner rectangle IN RADIANS
const unrotate = (bounds, rotation) => {
const points = vertices(bounds, rotation),
dimensions = sides(bounds, points)
const { x, y } = origin(bounds, dimensions)
return { ...dimensions, x, y }
}
I really hope this will solve your problem and that there are no typos. This was a very, veeeery funny way to spend my weekend :D
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// alpha is the rotation IN RADIANS
const vertices = (bounds, alpha) => {
const { x, y, w, h } = bounds,
A = { x: x + w * Math.sin(alpha), y },
B = { x, y: y + h * Math.sin(alpha) },
C = { x: x + w - w * Math.sin(alpha), y },
D = { x, y: y + h - h * Math.sin(alpha) }
return { A, B, C, D }
}
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// vertices is a POJO with shape: { A, B, C, D }, as returned by the `vertices` method
const sides = (bounds, vertices) => {
const { x, y, w, h } = bounds,
{ A, B, C, D } = vertices,
EA = A.x - x,
ED = D.y - y,
AF = w - EA,
FB = h - ED,
H = Math.sqrt(EA * EA + ED * ED),
W = Math.sqrt(AF * AF + FB * FB)
return { h: H, w: W }
}
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// sides is a POJO with shape: { w, h }, as returned by the `sides` method
const originPoint = (bounds, sides) => {
const { x, y, w, h } = bounds
const { w: W, h: H } = sides
const GC = Math.sqrt(W * W + H * H) / 2,
r = Math.sqrt(W * W + H * H) / 2,
IC = H / 2,
beta = Math.asin(IC / GC),
angleA = Math.PI + beta,
Ax = x + w / 2 + r * Math.cos(angleA),
Ay = y + h / 2 + r * Math.sin(angleA)
return { x: Ax, y: Ay }
}
// bounds is a POJO with shape: { x, y, w, h }, update if needed
// rotations is... the rotation of the inner rectangle IN RADIANS
const unrotate = (bounds, rotation) => {
const points = vertices(bounds, rotation)
const dimensions = sides(bounds, points)
const { x, y } = originPoint(bounds, dimensions)
return { ...dimensions, x, y }
}
function shortNumber(value) {
var places = 2;
value = Math.round(value * Math.pow(10, places)) / Math.pow(10, places);
return value;
}
function getInputtedBounds() {
var rectangle = {};
rectangle.x = parseFloat(app.xInput.value);
rectangle.y = parseFloat(app.yInput.value);
rectangle.w = parseFloat(app.widthInput.value);
rectangle.h = parseFloat(app.heightInput.value);
return rectangle;
}
function rotationSliderHandler() {
var rotation = app.rotationSlider.value;
app.rotationOutput.value = rotation;
rotate(rotation);
}
function rotationInputHandler() {
var rotation = app.rotationInput.value;
app.rotationSlider.value = rotation;
app.rotationOutput.value = rotation;
rotate(rotation);
}
function unrotateButtonHandler() {
var rotation = app.rotationInput.value;
app.rotationSlider.value = 0;
app.rotationOutput.value = 0;
var outerBounds = getInputtedBounds();
var radians = Math.PI / 180 * rotation;
var unrotatedBounds = unrotate(outerBounds, radians);
updateOutput(unrotatedBounds);
}
function rotate(value) {
var outerBounds = getInputtedBounds();
var radians = Math.PI / 180 * value;
var bounds = unrotate(outerBounds, radians);
updateOutput(bounds);
}
function updateOutput(bounds) {
app.xOutput.value = shortNumber(bounds.x);
app.yOutput.value = shortNumber(bounds.y);
app.widthOutput.value = shortNumber(bounds.w);
app.heightOutput.value = shortNumber(bounds.h);
}
function onload() {
app.xInput = document.getElementById("x");
app.yInput = document.getElementById("y");
app.widthInput = document.getElementById("w");
app.heightInput = document.getElementById("h");
app.rotationInput = document.getElementById("r");
app.xOutput = document.getElementById("x2");
app.yOutput = document.getElementById("y2");
app.widthOutput = document.getElementById("w2");
app.heightOutput = document.getElementById("h2");
app.rotationOutput = document.getElementById("r2");
app.rotationSlider = document.getElementById("rotationSlider");
app.unrotateButton = document.getElementById("unrotateButton");
app.unrotateButton.addEventListener("click", unrotateButtonHandler);
app.rotationSlider.addEventListener("input", rotationSliderHandler);
app.rotationInput.addEventListener("change", rotationInputHandler);
app.rotationInput.addEventListener("input", rotationInputHandler);
app.rotationInput.addEventListener("keyup", (e) => {if (e.keyCode==13) rotationInputHandler() });
app.rotationSlider.value = app.rotationInput.value;
}
var app = {};
window.addEventListener("load", onload);
* {
font-family: sans-serif;
font-size: 12px;
outline: 0px dashed red;
}
granola {
display: flex;
align-items: top;
}
flan {
width: 90px;
display: inline-block;
}
hamburger {
display: flex:
align-items: center;
}
spagetti {
display: inline-block;
font-size: 11px;
font-weight: bold;
letter-spacing: 1.5px;
}
fish {
display: inline-block;
padding-right: 40px;
position: relative;
}
input[type=text] {
width: 50px;
}
input[type=range] {
padding-top: 10px;
width: 140px;
padding-left: 0;
margin-left: 0;
}
button {
padding-top: 3px;
padding-bottom:1px;
margin-top: 10px;
}
<granola>
<fish>
<spagetti>Bounds of Rectangle</spagetti><br><br>
<flan>x: </flan><input id="x" type="text" value="14.39"><br>
<flan>y: </flan><input id="y" type="text" value="14.39"><br>
<flan>width: </flan><input id="w" type="text" value="21.2"><br>
<flan>height: </flan><input id="h" type="text" value="21.2"><br>
<flan>rotation:</flan><input id="r" type="text" value="90"><br>
<button id="unrotateButton">Unrotate</button>
</fish>
<fish>
<spagetti>Computed Bounds</spagetti><br><br>
<flan>x: </flan><input id="x2" type="text" disabled="true"><br>
<flan>y: </flan><input id="y2" type="text"disabled="true"><br>
<flan>width: </flan><input id="w2" type="text" disabled="true"><br>
<flan>height: </flan><input id="h2" type="text" disabled="true"><br>
<flan>rotation:</flan><input id="r2" type="text" disabled="true"><br>
<input id="rotationSlider" type="range" min="-360" max="360" step="5"><br>
</fish>
</granola>
How does this work?
Calculation using width, height, x and y
Radians and Angles
Using degrees calculate the radians and calculate the sin and cos angles:
function calculateRadiansAndAngles(){
const rotation = this.value;
const dr = Math.PI / 180;
const s = Math.sin(rotation * dr);
const c = Math.cos(rotation * dr);
console.log(rotation, s, c);
}
document.getElementById("range").oninput = calculateRadiansAndAngles;
<input type="range" min="-360" max="360" id="range"/>
Generate 4 points
we assume the origin of a rectangle is the center with the location of 0,0
The double for loop will create the following value pairs for i and j: (-1,-1), (-1,1), (1,-1) and (1,1)
Using each pair, we can calculate one of the 4 square vectors.
(i.e for (-1,1), i = -1, j = 1)
const px = w*i/2; //-> 30 * -1/2 = -15
const py = h*j/2; //-> 50 * 1/2 = 25
//[-15,25]
Once we have a point, we can calculate the new position of that point by including the rotation.
const nx = (px*c) - (py*s);
const ny = (px*s) + (py*c);
Solution
Once all the points are calculated based off of the rotation, we can redraw our square.
Before the draw call, a translate is used to position the cursor at the x and y of the rectangle. This is the reason as to why I was able to assume the center and the origin of the rectangle was 0,0 for the calculations.
const canvas = document.getElementById("canvas");
const range = document.getElementById("range");
const rotat = document.getElementById("rotat");
range.addEventListener("input", function(e) {
rotat.innerText = this.value;
handleRotation(this.value);
})
const context = canvas.getContext("2d");
const container = document.getElementById("container");
const rect = {
x: 50,
y: 75,
w: 30,
h: 50
}
function handleRotation(rotation) {
const { w, h, x, y } = rect;
const dr = Math.PI / 180;
const s = Math.sin(rotation * dr);
const c = Math.cos(rotation * dr);
const points = [];
for(let i = -1; i < 2; i+=2){
for(let j = -1; j < 2; j+=2){
const px = w*i/2;
const py = h*j/2;
const nx = (px*c) - (py*s);
const ny = (px*s) + (py*c);
points.push([nx, ny]);
}
}
//console.log(points);
draw(points);
}
function draw(points) {
context.clearRect(0,0,canvas.width, canvas.height);
context.save();
context.translate(rect.x+(rect.w/2), rect.y + (rect.h/2))
context.beginPath();
context.moveTo(...points.shift());
[...points.splice(0,1), ...points.reverse()]
.forEach(p=>{
context.lineTo(...p);
})
context.fill();
context.restore();
}
window.onload = () => handleRotation(0);
div {
display: flex;
background-color: lightgrey;
padding: 0 5px;
}
div>p {
padding: 0px 10px;
}
div>input {
flex-grow: 1;
}
canvas {
border: 1px solid black;
}
<div>
<p id="rotat">0</p>
<input type="range" id="range" min="-360" max="360" value="0" step="5" />
</div>
<canvas id="canvas"></canvas>
This is the basic code for a rectangle rotating(Unrotating is the same thing only with a negative angle) around its center.
function getUnrotatedRectangleBounds(rect, currentRotation) {
//Convert deg to radians
var rot = currentRotation / 180 * Math.PI;
var hyp = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
return {
x: rect.x + rect.width / 2 - hyp * Math.abs(Math.cos(rot)) / 2,
y: rect.y + rect.height / 2 - hyp * Math.abs(Math.sin(rot)) / 2,
width: hyp * Math.abs(Math.cos(rot)),
height: hyp * Math.abs(Math.sin(rot))
}
}
The vector starting at the origin(0,0) and ending at (width,height) is projected onto a unit vector for the target angle (cos rot,sin rot) * hyp.
The absolute values guarantee the width and height are both positive.
The coordinates of the projection are the width and height, respectively, of the new rectangle.
For the x and y values, take the original values at the center(x + rect.x) and move it back out(- 1/2 * NewWidth) so it centers the new rectangle.
Example
function getUnrotatedRectangleBounds(rect, currentRotation) {
//Convert deg to radians
var rot = currentRotation / 180 * Math.PI;
var hyp = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
return {
x: rect.x + rect.width / 2 - hyp * Math.abs(Math.cos(rot)) / 2,
y: rect.y + rect.height / 2 - hyp * Math.abs(Math.sin(rot)) / 2,
width: hyp * Math.abs(Math.cos(rot)),
height: hyp * Math.abs(Math.sin(rot))
}
}
var originalRectangle = {x:10, y:25, width:30, height:0};
var rotatedRectangle = {x:14.39, y:14.39, width:21.2, height:21.2};
var rotation = 45;
var unrotatedRectangle = getUnrotatedRectangleBounds(rotatedRectangle, rotation);
var boundsLabel = document.getElementById("boundsLabel");
boundsLabel.innerHTML = JSON.stringify(unrotatedRectangle);
<span id="boundsLabel"></span>

HTML5 Canvas generate isometric tiles [duplicate]

This question already has answers here:
True Isometric Projection with HTML5 Canvas
(3 answers)
Closed 5 years ago.
I'm trying to generate basic tiles and stairs in HTML5 Canvas without using images.
Here's what I did until now:
but I'm trying to reproduce this:
and I have no idea how to.
Here's my current code:
class IsometricGraphics {
constructor(canvas, thickness) {
this.Canvas = canvas;
this.Context = canvas.getContext("2d");
if(thickness) {
this.thickness = thickness;
} else {
this.thickness = 2;
}
}
LeftPanelWide(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 16; i++) {
this.Context.fillRect(x + i * 2, y + i * 1, 2, this.thickness * 4);
}
}
RightPanelWide(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 16; i++) {
this.Context.fillRect(x + (i * 2), y + 15 - (i * 1), 2, this.thickness * 4);
}
}
UpperPanelWide(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 17; i++) {
this.Context.fillRect(x + 16 + 16 - (i * 2), y + i - 2, i * 4, 1);
}
for(var i = 0; i < 16; i++) {
this.Context.fillRect(x + i * 2, y + (32 / 2) - 1 + i, ((32 / 2) - i) * 4, 1);
}
}
UpperPanelWideBorder(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
var y = y + 2;
for(var i = 0; i < 17; i++) {
this.Context.fillRect(x + 17 + 16 - (i * 2) - 2, y + i - 2, (i == 17) ? 1 : 2, 1);
this.Context.fillRect(x + 17 + 16 + (i * 2) - 2, y + i - 2, (i == 17) ? 1 : 2, 1);
}
for(var i = 0; i < 32 / 2; i++) {
this.Context.fillRect(x + i * 2, y + 16 - 1 + i, 2, 1);
this.Context.fillRect(x + 62 - i * 2, y + 16 - 1 + i, 2, 1);
}
}
RightUpperPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 32 / 2 + 4; i++) {
this.Context.fillRect(x + (i * 2), (i >= 4) ? (i - 1) + y : 3 - i + 3 + y, 2, (i >= 4) ? (i <= 20 - 5) ? 8 : (20 - i) * 2 - 1 : 1 + (i * 2));
}
}
LeftUpperPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 32 / 2 + 4; i++) {
this.Context.fillRect(x + (i * 2), (i >= 16) ? y + (i - 16) : 16 + y - (i * 1) - 1, 2, (i >= 4) ? (i >= 16) ? 8 - (i - 16) - (i - 16) - 1 : 8 : 8 * i - (i * 6) + 1);
}
}
LeftPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 8 / 2; i++) {
this.Context.fillRect(x + i * 2, y + i * 1, 2, this.thickness * 4);
}
}
RightPanelSmall(x, y, fillStyle) {
this.Context.fillStyle = fillStyle;
for(var i = 0; i < 8 / 2; i++) {
this.Context.fillRect(x + (i * 2), y + 3 - (i * 1), 2, this.thickness * 4);
}
}
}
class IsoGenerator {
constructor() {
var Canvas = document.querySelector("canvas");
var Context = Canvas.getContext("2d");
//Context.scale(5, 5);
this.Context = Context;
this.IsometricGraphics = new IsometricGraphics(Canvas, 2);
}
StairLeft(x, y, Color1, Color2, Color3) {
for(var i = 0; i < 4; i++) {
this.IsometricGraphics.RightPanelWide((x + 8) + (i * 8), (y + 4) + (i * 12), Color1);
this.IsometricGraphics.LeftUpperPanelSmall(x + (i * 8), y + (i * 12), Color2);
this.IsometricGraphics.LeftPanelSmall((i * 8) + x, (16 + (i * 12)) + y, Color3);
}
}
StairRight(x, y, Color1, Color2, Color3) {
for(var i = 0; i < 4; i++) {
this.IsometricGraphics.LeftPanelWide(x + 24 - (i * 8), (4 + (i * 12)) + y, Color1);
this.IsometricGraphics.RightUpperPanelSmall(x + 24 - (i * 8), y + (i * 12) - 3, Color2);
this.IsometricGraphics.RightPanelSmall(x + 56 - (i * 8), (16 + (i * 12)) + y, Color3);
}
}
Tile(x, y, Color1, Color2, Color3, Border) {
this.IsometricGraphics.LeftPanelWide(x, 18 + y, Color1);
this.IsometricGraphics.RightPanelWide(x + 32, 18 + y, Color2);
this.IsometricGraphics.UpperPanelWide(x, 2 + y, Color3);
if(Border) {
this.IsometricGraphics.UpperPanelWideBorder(x, y, Border);
}
}
}
var Canvas = document.querySelector("canvas");
var Context = Canvas.getContext("2d");
Context.scale(3, 3);
new IsoGenerator().Tile(0, 0, "#B3E5FC", "#2196F3", "#03A9F4")
new IsoGenerator().StairLeft(70, 0, "#B3E5FC", "#2196F3", "#03A9F4")
new IsoGenerator().StairRight(70 * 2, 0, "#B3E5FC", "#2196F3", "#03A9F4")
// What I'm trying to reproduce: http://i.imgur.com/YF4xyz9.png
<canvas width="1000" height="1000"></canvas>
Fiddle: https://jsfiddle.net/xvak0jh1/2/
Axonometric rendering
The best way to handle axonometric (commonly called isometric) rendering is by modeling the object in 3D and then render the model in the particular axonometric projection you want.
3D object as a Mesh
The most simple object (in this case) is a box. The box has 6 sides and 8 vertices and can be described via its vertices and the polygons representing the sides as a set of indexes to the vertices.
Eg 3D box with x from left to right, y going top to bottom, and z as up.
First create the vertices that make up the box
UPDATE as requested in the comments I have changed the box into its x,y,z dimensions.
// function creates a 3D point (vertex)
function vertex(x,y,z){ return {x,y,z} };
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSizeX = 10; // size of the box x axis
const boxSizeY = 50; // size of the box y axis
const boxSizeZ = 8; // size of the box z axis
const hx = boxSizeX / 2; // half size shorthand for easier typing
const hy = boxSizeY / 2;
const hz = boxSizeZ / 2;
vertices.push(vertex(-hx,-hy,-hz)); // lower top left index 0
vertices.push(vertex( hx,-hy,-hz)); // lower top right
vertices.push(vertex( hx, hy,-hz)); // lower bottom right
vertices.push(vertex(-hx, hy,-hz)); // lower bottom left
vertices.push(vertex(-hx,-hy, hz)); // upper top left index 4
vertices.push(vertex( hx,-hy, hz)); // upper top right
vertices.push(vertex( hx, hy, hz)); // upper bottom right
vertices.push(vertex(-hx, hy, hz)); // upper bottom left index 7
Then create the polygons for each face on the box
const colours = {
dark : "#444",
shade : "#666",
light : "#aaa",
bright : "#eee",
}
function createPoly(indexes,colour){ return { indexes, colour} }
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
polygons.push(createPoly([3,2,1,0],colours.dark)); // bottom face
polygons.push(createPoly([0,1,5,4],colours.dark)); // back face
polygons.push(createPoly([1,2,6,5],colours.shade)); // right face
polygons.push(createPoly([2,3,7,6],colours.light)); // front face
polygons.push(createPoly([3,0,4,7],colours.dark)); // left face
polygons.push(createPoly([4,5,6,7],colours.bright)); // top face
Now you have a 3D model of a box with 6 polygons.
Projection
The projection describes how a 3D object is transformed into a 2D projection. This is done by providing a 2D axis for each of the 3D coordinates.
In this case you are using a modification of a bimetric projection
So lets define that 2D axis for each of the 3 3D coordinates.
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x=0, y=0, z=0) => ({x,y,z});
const P2 = (x=0, y=0) => ({x, y});
// an object to handle the projection
const isoProjMat = {
xAxis : P2(1 , 0.5) , // 3D x axis for every 1 pixel in x go down half a pixel in y
yAxis : P2(-1 , 0.5) , // 3D y axis for every -1 pixel in x go down half a pixel in y
zAxis : P2(0 , -1) , // 3D z axis go up 1 pixels
origin : P2(100,100), // where on the screen 3D coordinate (0,0,0) will be
Now define the function that does the projection by converting the x,y,z (3d) coordinate into a x,y (2d)
project (p, retP = P2()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
return retP;
}
}
Rendering
Now you can render the model. First you must project each vertices into the 2D screen coordinates.
// create a new array of 2D projected verts
const projVerts = vertices.map(vert => isoProjMat.project(vert));
Then it is just a matter of rendering each polygon via the indexes into the projVerts array
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.beginPath();
poly.indexs.forEach(index => ctx.lineTo(projVerts[index].x, projVerts[index].y) );
ctx.fill();
});
As a snippet
const ctx = canvas.getContext("2d");
// function creates a 3D point (vertex)
function vertex(x, y, z) { return { x, y, z}};
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSizeX = 10 * 4; // size of the box x axis
const boxSizeY = 50 * 4; // size of the box y axis
const boxSizeZ = 8 * 4; // size of the box z axis
const hx = boxSizeX / 2; // half size shorthand for easier typing
const hy = boxSizeY / 2;
const hz = boxSizeZ / 2;
vertices.push(vertex(-hx,-hy,-hz)); // lower top left index 0
vertices.push(vertex( hx,-hy,-hz)); // lower top right
vertices.push(vertex( hx, hy,-hz)); // lower bottom right
vertices.push(vertex(-hx, hy,-hz)); // lower bottom left
vertices.push(vertex(-hx,-hy, hz)); // upper top left index 4
vertices.push(vertex( hx,-hy, hz)); // upper top right
vertices.push(vertex( hx, hy, hz)); // upper bottom right
vertices.push(vertex(-hx, hy, hz)); // upper bottom left index 7
const colours = {
dark: "#444",
shade: "#666",
light: "#aaa",
bright: "#eee",
}
function createPoly(indexes, colour) {
return {
indexes,
colour
}
}
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
polygons.push(createPoly([3, 2, 1, 0], colours.dark)); // bottom face
polygons.push(createPoly([0, 1, 5, 4], colours.dark)); // back face
polygons.push(createPoly([3, 0, 4, 7], colours.dark)); // left face
polygons.push(createPoly([1, 2, 6, 5], colours.shade)); // right face
polygons.push(createPoly([2, 3, 7, 6], colours.light)); // front face
polygons.push(createPoly([4, 5, 6, 7], colours.bright)); // top face
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x = 0, y = 0, z = 0) => ({x,y,z});
const P2 = (x = 0, y = 0) => ({ x, y});
// an object to handle the projection
const isoProjMat = {
xAxis: P2(1, 0.5), // 3D x axis for every 1 pixel in x go down half a pixel in y
yAxis: P2(-1, 0.5), // 3D y axis for every -1 pixel in x go down half a pixel in y
zAxis: P2(0, -1), // 3D z axis go up 1 pixels
origin: P2(150, 75), // where on the screen 3D coordinate (0,0,0) will be
project(p, retP = P2()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
return retP;
}
}
// create a new array of 2D projected verts
const projVerts = vertices.map(vert => isoProjMat.project(vert));
// and render
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.beginPath();
poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x, projVerts[index].y));
ctx.fill();
});
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>
More
That is the basics, but by no means all. I have cheated by making sure that the order of the polygons is correct in terms of distance from the viewer. Ensuring that the further polygons are not drawn over the nearer. For more complex shapes you will need to add Depth sorting. You also want to optimise the rendering by not drawing faces (polygons) that face away from the viewer. This is called backface culling.
You will also want to add lighting models and much more.
Pixel Bimetric projection.
The above is in fact not what you want. In gaming the projection you use is often called a pixel art projection that does not fit the nice mathematical projection. The are many sets of rules concerning anti aliasing, where vertices are rendered depending on the direction of the face.
eg a vertex is drawn at a pixel top,left or top,right, or bottom,right, or bottom,left depending on the face direction, and alternating between odd and even x coordinates to name but a few of the rules
This pen Axonometric Text Render (AKA Isometric) is a slightly more complex example of Axonometric rendering that has options for 8 common axonometric projections and includes simple depth sorting, though not built for speed. This answer is what inspired writing the pen.
Your shape.
So after all that the next snippet draws the shape you are after by moving the basic box to each position and rendering it in order from back to front.
const ctx = canvas.getContext("2d");
// function creates a 3D point (vertex)
function vertex(x, y, z) { return { x, y, z}};
// an array of vertices
const vertices = []; // an array of vertices
// create the 8 vertices that make up a box
const boxSize = 20; // size of the box
const hs = boxSize / 2; // half size shorthand for easier typing
vertices.push(vertex(-hs, -hs, -hs)); // lower top left index 0
vertices.push(vertex(hs, -hs, -hs)); // lower top right
vertices.push(vertex(hs, hs, -hs)); // lower bottom right
vertices.push(vertex(-hs, hs, -hs)); // lower bottom left
vertices.push(vertex(-hs, -hs, hs)); // upper top left index 4
vertices.push(vertex(hs, -hs, hs)); // upper top right
vertices.push(vertex(hs, hs, hs)); // upper bottom right
vertices.push(vertex(-hs, hs, hs)); // upper bottom left index 7
const colours = {
dark: "#004",
shade: "#036",
light: "#0ad",
bright: "#0ee",
}
function createPoly(indexes, colour) {
return {
indexes,
colour
}
}
const polygons = [];
// always make the polygon vertices indexes in a clockwise direction
// when looking at the polygon from the outside of the object
//polygons.push(createPoly([3, 2, 1, 0], colours.dark)); // bottom face
//polygons.push(createPoly([0, 1, 5, 4], colours.dark)); // back face
//polygons.push(createPoly([3, 0, 4, 7], colours.dark)); // left face
polygons.push(createPoly([1, 2, 6, 5], colours.shade)); // right face
polygons.push(createPoly([2, 3, 7, 6], colours.light)); // front face
polygons.push(createPoly([4, 5, 6, 7], colours.bright)); // top face
// From here in I use P2,P3 to create 2D and 3D points
const P3 = (x = 0, y = 0, z = 0) => ({x,y,z});
const P2 = (x = 0, y = 0) => ({ x, y});
// an object to handle the projection
const isoProjMat = {
xAxis: P2(1, 0.5), // 3D x axis for every 1 pixel in x go down half a pixel in y
yAxis: P2(-1, 0.5), // 3D y axis for every -1 pixel in x go down half a pixel in y
zAxis: P2(0, -1), // 3D z axis go up 1 pixels
origin: P2(150, 55), // where on the screen 3D coordinate (0,0,0) will be
project(p, retP = P2()) {
retP.x = p.x * this.xAxis.x + p.y * this.yAxis.x + p.z * this.zAxis.x + this.origin.x;
retP.y = p.x * this.xAxis.y + p.y * this.yAxis.y + p.z * this.zAxis.y + this.origin.y;
return retP;
}
}
var x,y,z;
for(z = 0; z < 4; z++){
const hz = z/2;
for(y = hz; y < 4-hz; y++){
for(x = hz; x < 4-hz; x++){
// move the box
const translated = vertices.map(vert => {
return P3(
vert.x + x * boxSize,
vert.y + y * boxSize,
vert.z + z * boxSize,
);
});
// create a new array of 2D projected verts
const projVerts = translated.map(vert => isoProjMat.project(vert));
// and render
polygons.forEach(poly => {
ctx.fillStyle = poly.colour;
ctx.strokeStyle = poly.colour;
ctx.lineWidth = 1;
ctx.beginPath();
poly.indexes.forEach(index => ctx.lineTo(projVerts[index].x , projVerts[index].y));
ctx.stroke();
ctx.fill();
});
}
}
}
canvas {
border: 2px solid black;
}
<canvas id="canvas"></canvas>

Continuous gradient along a HTML5 canvas path

I am trying to draw a continous gradient along a path of points, where each point has a it's own color, using the HTML5 canvas API.
See http://bl.ocks.org/rveciana/10743959 for inspiration, where that effect is achieved with D3.
There doesn't seem to be a way to add multiple linear gradients for a single canvas path, so I resorted to something like this: http://jsfiddle.net/51toapv2/
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var pts = [[100, 100, "red"], [150, 150, "green"], [200, 100, "yellow"]];
ctx.lineWidth = 20;
ctx.lineJoin = "round";
ctx.lineCap = "round";
for (var i = 0; i < pts.length - 1; i++) {
var begin = pts[i];
var end = pts[i + 1];
ctx.beginPath();
var grad = ctx.createLinearGradient(begin[0], begin[1], end[0], end[1]);
grad.addColorStop(0, begin[2]);
grad.addColorStop(1, end[2]);
ctx.strokeStyle = grad;
ctx.moveTo(begin[0], begin[1]);
ctx.lineTo(end[0], end[1]);
ctx.stroke();
}
As you can see it produces a subpar effect as the paths aren't merged and the "line joins" are clearly visible.
Is it possible to achieve the effect I'm looking for with the canvas API?
Here's a slight modification of your original idea that makes the joins blend nicely.
Original: Draw a gradient line from the start to end of a line segment.
This causes the line joins to overlap and produces a noticeable & undesired transition.
Modification: Draw a gradient line that doesn't extend to the start / endpoints.
With this modification, the line joins will always be solid colors rather than be partially gradiented. As a result, the line joins will transition nicely between line segments.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var lines = [
{x:100, y:050,color:'red'},
{x:150, y:100,color:'green'},
{x:200, y:050,color:'gold'},
{x:275, y:150,color:'blue'}
];
var linewidth=20;
ctx.lineCap='round';
ctx.lineJoint='round';
for(var i=1;i<lines.length;i++){
// calculate the smaller part of the line segment over
// which the gradient will run
var p0=lines[i-1];
var p1=lines[i];
var dx=p1.x-p0.x;
var dy=p1.y-p0.y;
var angle=Math.atan2(dy,dx);
var p0x=p0.x+linewidth*Math.cos(angle);
var p0y=p0.y+linewidth*Math.sin(angle);
var p1x=p1.x+linewidth*Math.cos(angle+Math.PI);
var p1y=p1.y+linewidth*Math.sin(angle+Math.PI);
// determine where the gradient starts and ends
if(i==1){
var g=ctx.createLinearGradient(p0.x,p0.y,p1x,p1y);
}else if(i==lines.length-1){
var g=ctx.createLinearGradient(p0x,p0y,p1.x,p1.y);
}else{
var g=ctx.createLinearGradient(p0x,p0y,p1x,p1y);
}
// add the gradient color stops
// and draw the gradient line from p0 to p1
g.addColorStop(0,p0.color);
g.addColorStop(1,p1.color);
ctx.beginPath();
ctx.moveTo(p0.x,p0.y);
ctx.lineTo(p1.x,p1.y);
ctx.strokeStyle=g;
ctx.lineWidth=linewidth;
ctx.stroke();
}
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=350 height=200></canvas>
You can do a simple approach interpolating two colors along a line. If you need smooth/shared gradients where two lines joins at steeper angles, you would need to calculate and basically implement a line drawing algorithm from (almost) scratch. This would be out of scope for SO, so here is a simpler approach.
That being said - the example in the link is not actually a line but several plots of squares of different colors. The issues it would have too is "hidden" by its subtle variations.
Example
This approach requires two main functions:
Line interpolate function which draws each segment in a line from previous mouse position to current position
Color interpolate function which takes an array of colors and interpolate between two current colors depending on length, position and segment size.
Tweak parameters such as segment size, number of colors in the array etc. to get the optimal result.
Line interpolate function
function plotLine(ctx, x1, y1, x2, y2) {
var diffX = Math.abs(x2 - x1), // get line length
diffY = Math.abs(y2 - y1),
dist = Math.sqrt(diffX * diffX + diffY * diffY),
step = dist / 10, // define some resolution
i = 0, t, b, x, y;
while (i <= dist) { // render circles along the line
t = Math.min(1, i / dist);
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
ctx.fillStyle = getColor(); // get current color
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI*2);
ctx.fill();
i += step;
}
Color interpolate function
function getColor() {
var r, g, b, t, c1, c2;
c1 = colors[cIndex]; // get current color from array
c2 = colors[(cIndex + 1) % maxColors]; // get next color
t = Math.min(1, total / segment); // calculate t
if (++total > segment) { // rotate segment
total = 0;
if (++cIndex >= maxColors) cIndex = 0; // rotate color array
}
r = c1.r + (c2.r - c1.r) * t; // interpolate color
g = c1.g + (c2.g - c1.g) * t;
b = c1.b + (c2.b - c1.b) * t;
return "rgb(" + (r|0) + "," + (g|0) + "," + (b|0) + ")";
}
Demo
Putting it all together will allow you to draw gradient lines. If you don't want to draw them manually simply call the plotLine() function whenever needed.
// Some setup code
var c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
colors = [
{r: 255, g: 0, b: 0},
{r: 255, g: 255, b: 0},
{r: 0, g: 255, b: 0},
{r: 0, g: 255, b: 255},
{r: 0, g: 0, b: 255},
{r: 255, g: 0, b: 255},
{r: 0, g: 255, b: 255},
{r: 0, g: 255, b: 0},
{r: 255, g: 255, b: 0},
],
cIndex = 0, maxColors = colors.length,
total = 0, segment = 500,
isDown = false, px, py;
setSize();
c.onmousedown = c.ontouchstart = function(e) {
isDown = true;
var pos = getPos(e);
px = pos.x;
py = pos.y;
};
window.onmousemove = window.ontouchmove = function(e) {if (isDown) plot(e)};
window.onmouseup = window.ontouchend = function(e) {
e.preventDefault();
isDown = false
};
function getPos(e) {
e.preventDefault();
if (e.touches) e = e.touches[0];
var r = c.getBoundingClientRect();
return {
x: e.clientX - r.left,
y: e.clientY - r.top
}
}
function plot(e) {
var pos = getPos(e);
plotLine(ctx, px, py, pos.x, pos.y);
px = pos.x;
py = pos.y;
}
function plotLine(ctx, x1, y1, x2, y2) {
var diffX = Math.abs(x2 - x1),
diffY = Math.abs(y2 - y1),
dist = Math.sqrt(diffX * diffX + diffY * diffY),
step = dist / 50,
i = 0,
t, b, x, y;
while (i <= dist) {
t = Math.min(1, i / dist);
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
ctx.fillStyle = getColor();
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI*2);
ctx.fill();
i += step;
}
function getColor() {
var r, g, b, t, c1, c2;
c1 = colors[cIndex];
c2 = colors[(cIndex + 1) % maxColors];
t = Math.min(1, total / segment);
if (++total > segment) {
total = 0;
if (++cIndex >= maxColors) cIndex = 0;
}
r = c1.r + (c2.r - c1.r) * t;
g = c1.g + (c2.g - c1.g) * t;
b = c1.b + (c2.b - c1.b) * t;
return "rgb(" + (r|0) + "," + (g|0) + "," + (b|0) + ")";
}
}
window.onresize = setSize;
function setSize() {
c.width = window.innerWidth;
c.height = window.innerHeight;
}
document.querySelector("button").onclick = function() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
};
html, body {background:#777; margin:0; overflow:hidden}
canvas {position:fixed;left:0;top:0;background: #333}
button {position:fixed;left:10px;top:10px}
<canvas></canvas>
<button>Clear</button>
TIPS:
The gradient values can be pre-populated / cached beforehand
The step for position in gradient can be bound to length to get even spread independent of draw speed
You can easily replace the brush with other path/figures/shapes, even combine image based brushes which is composited with current color

Categories

Resources