Hello,
I’m a bit late on your question and I do not answer it completely since I am using squares instead of circles but I would still like to share what I have done because I had a lot of fun doing it and it can maybe gives you some ideas.
Also it is possible to change the fill shape.
The end result looks like this:
Here is the code. It is dynamic: you can drag some points to change the shadow’s direction, start and end point. It is also displaying construction lines.
I admit that my code is a real nightmare to go through so if you have any questions please do not hesitate =)
// PVector squareDir;
Quadrilateral q;
void setup() {
size(500, 500);
background(240);
//squareDir = new PVector(1/sqrt(2), 1/sqrt(2));
q = new Quadrilateral(350, 70, 110, 150, 153, 418, 348, 300);
//drawSquare(30, 30, 10, squareDir);
//q.Show();
}
void draw() {
background(230);
q.Update();
q.Show();
}
void mousePressed() {
q.OnMousePressed();
}
void mouseReleased() {
q.OnMouseReleased();
}
void drawSquare(float x, float y, float size, PVector dir) {
float diagSize = size / sqrt(2);
fill(20);
noStroke();
beginShape();
vertex(x + dir.x * diagSize, y + dir.y * diagSize);
vertex(x - dir.y * diagSize, y + dir.x * diagSize);
vertex(x - dir.x * diagSize, y - dir.y * diagSize);
vertex(x + dir.y * diagSize, y - dir.x * diagSize);
endShape();
}
PVector[] getSquarePt(float x, float y, float size, PVector dir) {
PVector[] result = new PVector[4];
float diagSize = size / sqrt(2);
result[0] = new PVector(x + dir.x * diagSize, y + dir.y * diagSize);
result[1] = new PVector(x - dir.y * diagSize, y + dir.x * diagSize);
result[2] = new PVector(x - dir.x * diagSize, y - dir.y * diagSize);
result[3] = new PVector(x + dir.y * diagSize, y - dir.x * diagSize);
return result;
}
void drawSquareInQuadrilateral(float x, float y, float size, PVector dir, Point[] quad) {
// Prepare the quadrilateral points
PVector[] quadPt = new PVector[4];
for (int i = 0; i < 4; i++) {
quadPt[i] = quad[i].getPos();
}
// Prepare the square points
PVector[] squarePt = getSquarePt(x, y, size, dir);
// Array to store all the points we'll need
ArrayList<PVector> finalPt = new ArrayList<PVector>();
// Add point of square that are in the quadrilateral
// If all points are inside => the square can be drawn
// If no points are inside => Nothing will need to be drawn
for (int i = 0; i<4; i++) {
if (ptInQuad(squarePt[i], quadPt)) {
finalPt.add(squarePt[i]);
}
}
if (finalPt.size() == 0) {
return;
}
if (finalPt.size() == 4) {
drawSquare(x, y, size, dir);
return;
}
// Add point of quadrilateral that are in the square
for (int i = 0; i<4; i++) {
if (ptInQuad(quadPt[i], squarePt)) {
finalPt.add(quadPt[i]);
}
}
// Add all intersecting points
for (int i = 0; i < 4; i++) {
int idx1 = i;
int idx2 = i + 1;
if (idx2 > 3) {
idx2 = 0;
}
for (int j = 0; j < 4; j++) {
int idx3 = j;
int idx4 = j + 1;
if (idx4 > 3) {
idx4 = 0;
}
intersectSolution sol = intersectingPt(squarePt[idx1], squarePt[idx2], quadPt[idx3], quadPt[idx4]);
if (sol.haveSolution()) {
finalPt.add(sol.getIntersection());
}
}
}
// Sort all points in correct order to draw them
// Start by computing center point
float mx = 0, my = 0;
for (int i = 0; i < finalPt.size(); i++) {
mx += finalPt.get(i).x;
my += finalPt.get(i).y;
}
mx /= finalPt.size();
my /= finalPt.size();
// Then get the atan2 value
float[] arcTanVal = new float[finalPt.size()];
int[] indices = new int[finalPt.size()]; // To track the sorting
for (int i = 0; i < finalPt.size(); i++) {
arcTanVal[i] = atan2(finalPt.get(i).y - my, finalPt.get(i).x - mx);
indices[i] = i;
}
// Sort from lower to higher atan2
int n = arcTanVal.length;
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (arcTanVal[j] > arcTanVal[j+1])
{
// swap arr[j+1] and arr[i]
float temp1 = arcTanVal[j];
int temp2 = indices[j];
arcTanVal[j] = arcTanVal[j+1];
arcTanVal[j+1] = temp1;
indices[j] = indices[j+1];
indices[j+1] = temp2;
}
}
}
// Draw new polygon
fill(20);
noStroke();
beginShape();
for (int i = 0; i < n; i++) {
vertex(finalPt.get(indices[i]).x, finalPt.get(indices[i]).y);
}
endShape();
}
intersectSolution intersectingPt(PVector A1, PVector A2, PVector B1, PVector B2) {
PVector result = new PVector(0, 0);
// first line go through A1 and A2 : y = a1 * x + b1
// second line go thourgh B1 and B2 : y = a2 * x + b2
float a1 = (A2.y - A1.y) / (A2.x - A1.x);
float a2 = (B2.y - B1.y) / (B2.x - B1.x);
if (abs(a1 - a2) < 0.000000000001) {
return new intersectSolution(false, result);
}
float b1 = A1.y - (a1 * A1.x);
float b2 = B1.y - (a2 * B1.x);
float x = (b2-b1) / (a1-a2);
float minAx, maxAx, minBx, maxBx;
minAx = min(A1.x, A2.x);
maxAx = max(A1.x, A2.x);
minBx = min(B1.x, B2.x);
maxBx = max(B1.x, B2.x);
if ( (x > minAx) && (x < maxAx) && (x > minBx) && (x < maxBx)) {
result.set(x, a1 * x + b1);
return new intersectSolution(true, result);
}
return new intersectSolution(false, result);
}
class intersectSolution {
boolean haveSolution;
PVector p;
intersectSolution(boolean b, PVector point) {
haveSolution = b;
p = point;
}
boolean haveSolution() {
return haveSolution;
}
PVector getIntersection() {
return p;
}
}
boolean ptInQuad(PVector pt, PVector[] quad) {
if (ptInTriangle(pt, quad[0], quad[1], quad[2]) || ptInTriangle(pt, quad[0], quad[2], quad[3])) {
return true;
}
return false;
}
boolean ptInTriangle(PVector p, PVector a, PVector b, PVector c) {
if (isSameSide(p, a, b, c) && isSameSide(p, b, a, c) && isSameSide(p, c, a, b)) {
return true;
}
return false;
}
boolean isSameSide(PVector p1, PVector p2, PVector a, PVector b) {
PVector v1 = b.copy().add(a.copy().mult(-1)); // b - a
PVector v2 = p1.copy().add(a.copy().mult(-1)); // p1 - a
PVector v3 = p2.copy().add(a.copy().mult(-1)); // p2 - a
PVector cp1 = v1.cross(v2);
PVector cp2 = v1.cross(v3);
if (cp1.dot(cp2) >= 0) {
return true;
}
return false;
}
class Quadrilateral {
Point[] points = new Point[4];
Shading shading;
boolean displayShading;
Quadrilateral(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) {
points[0] = new Point(x1, y1, color(40, 40, 40), 8);
points[1] = new Point(x2, y2, color(40, 40, 40), 8);
points[2] = new Point(x3, y3, color(40, 40, 40), 8);
points[3] = new Point(x4, y4, color(40, 40, 40), 8);
shading = new Shading(10, 10, 40, 40);
displayShading = true;
}
void DisplayShading(boolean b) {
displayShading = b;
}
void OnMousePressed() {
if (displayShading) {
shading.OnMousePressed();
}
}
void OnMouseReleased() {
shading.OnMouseReleased();
}
void Update() {
shading.Update();
}
void Show() {
shading.DrawShading(points);
noFill();
stroke(60, 60, 60);
strokeWeight(2);
for (int i = 0; i < 3; i++) {
line(points[i].getPos().x, points[i].getPos().y, points[i+1].getPos().x, points[i+1].getPos().y);
}
line(points[3].getPos().x, points[3].getPos().y, points[0].getPos().x, points[0].getPos().y);
fill(60, 60, 60);
noStroke();
for (int i = 0; i < 4; i++) {
points[i].Show();
}
if (displayShading) {
shading.Show();
}
}
void Shade() {
}
}
class Shading {
Point start, end;
PVector dir; // Direction of the shading
float patternSize;
Shading(int x1, int y1, int x2, int y2) {
start = new Point(x1, y1, color(230, 20, 20), 10);
start.setMovable(true);
end = new Point(x2, y2, color(20, 20, 230), 10);
end.setMovable(true);
dir = new PVector(x2 - x1, y2 - y1);
dir.normalize();
patternSize = 10;
}
void setPatternSize(float s) {
patternSize = s;
}
void OnMousePressed() {
if (end.isHoovered()) {
end.setSelect(true);
end.computeDeltaFromMouse();
return;
}
if (start.isHoovered()) {
start.setSelect(true);
start.computeDeltaFromMouse();
}
}
void OnMouseReleased() {
start.setSelect(false);
end.setSelect(false);
}
void Update() {
start.Update();
end.Update();
dir.set(end.getPos().x - start.getPos().x, end.getPos().y - start.getPos().y);
dir.normalize();
}
void Show() {
DrawArrow(start.getPos().x, start.getPos().y, end.getPos().x, end.getPos().y);
start.Show();
end.Show();
}
void DrawShading(Point[] points) {
// Chose a perpendicular direction to the shading direction to draw the shading
// dir = direction of the shading
// drawingDir = direction in which the shading is drawn. It is perpendicular to the shading dir
PVector drawingDir = new PVector(-dir.y, dir.x);
DrawArrow(start.getPos().x, start.getPos().y, start.getPos().x + 40 * drawingDir.x, start.getPos().y + 40 * drawingDir.y);
// Create the shading line
Line shadingLine = new Line(start.getPos().x, start.getPos().y, dir.x, dir.y);
shadingLine.Show();
// FIND TOP AND BOTTOM OF DRAWING BOX
// create the start line: line that go trough the start point and is perpendicular to the direction of the shading
Line startLine = new Line(start.getPos().x, start.getPos().y, drawingDir.x, drawingDir.y);
startLine.Show();
// Compute signed distance of each points of the quadrilateral to the start line
float[] sDist = new float[4];
for (int i = 0; i < 4; i++) {
sDist[i] = points[i].getSignedDistFrom(startLine, dir);
}
// Get the closest and furthest point of the quadrilateral to the start line to know bottom of the drawing box and the top of the drawing box
int idxBot = 0, idxTop = 0;
for (int i = 1; i < 4; i++) {
if (sDist[i] < sDist[idxBot]) {
idxBot = i;
}
if (sDist[i] > sDist[idxTop]) {
idxTop = i;
}
}
// Create the lower bounding line and higher bounding line
Line drawingBoxBot = new Line(points[idxBot].getPos().x, points[idxBot].getPos().y, drawingDir.x, drawingDir.y);
Line drawingBoxTop = new Line(points[idxTop].getPos().x, points[idxTop].getPos().y, drawingDir.x, drawingDir.y);
drawingBoxBot.setColor(color(230, 20, 20));
drawingBoxTop.setColor(color(20, 20, 230));
drawingBoxBot.Show();
drawingBoxTop.Show();
float drawingBoxHeight = abs(sDist[idxTop] - sDist[idxBot]);
// FIND LEFT AND RIGHT OF DRAWING BOX
// Compute signed distance of each points of the quadrilateral to the start line
for (int i = 0; i < 4; i++) {
sDist[i] = points[i].getSignedDistFrom(shadingLine, drawingDir);
}
// Get the closest and furthest point of the quadrilateral to the start line to know bottom of the drawing box and the top of the drawing box
int idxLeft = 0, idxRight = 0;
for (int i = 1; i < 4; i++) {
if (sDist[i] < sDist[idxLeft]) {
idxLeft = i;
}
if (sDist[i] > sDist[idxRight]) {
idxRight = i;
}
}
// Create the lower bounding line and higher bounding line
Line drawingBoxLeft = new Line(points[idxLeft].getPos().x, points[idxLeft].getPos().y, dir.x, dir.y);
Line drawingBoxRight = new Line(points[idxRight].getPos().x, points[idxRight].getPos().y, dir.x, dir.y);
drawingBoxLeft.setColor(color(230, 20, 230));
drawingBoxRight.setColor(color(20, 230, 230));
drawingBoxLeft.Show();
drawingBoxRight.Show();
float drawingBoxWidth = abs(sDist[idxRight] - sDist[idxLeft]);
// Get the start drawing point
PVector startDrawPt = drawingBoxLeft.intersect(drawingBoxBot);
// Fille in the shape
noStroke();
fill(255, 0, 0);
float maxDist = end.getSignedDistFrom(startLine, dir); // Get the distance between the start point and the end point
for (float j = -1; j < (drawingBoxHeight / patternSize) + 1; j+=0.5) {
Point firstRowPt = new Point(startDrawPt.x + j * patternSize * dir.x, startDrawPt.y + j * patternSize * dir.y);
float offset = 0;
if (floor(j) != j) {
offset = 0.5;
}
float currRowDist = firstRowPt.getSignedDistFrom(startLine, dir);
float sizeRatio = constrain(currRowDist / maxDist, 0, 1);
float fadeOut = -1;
float currPatternSize = (1 - (exp(fadeOut * sizeRatio) - 1) / (exp(fadeOut) - 1)) * patternSize;
for (int i = 0; i < (drawingBoxWidth / patternSize) + 1; i++) {
//ellipse(startDrawPt.x + i * patternSize * drawingDir.x + j * patternSize * dir.x, startDrawPt.y + i * patternSize * drawingDir.y + j * patternSize * dir.y, currPatternSize, currPatternSize);
//drawSquare(startDrawPt.x + (i + offset) * patternSize * drawingDir.x + j * patternSize * dir.x, startDrawPt.y + (i + offset) * patternSize * drawingDir.y + j * patternSize * dir.y, currPatternSize, dir);
drawSquareInQuadrilateral(startDrawPt.x + (i + offset) * patternSize * drawingDir.x + j * patternSize * dir.x, startDrawPt.y + (i + offset) * patternSize * drawingDir.y + j * patternSize * dir.y, currPatternSize, dir, points);
}
}
}
}
void DrawArrow(float x1, float y1, float x2, float y2) {
noFill();
stroke(50);
strokeWeight(1);
float arrowSize = 20;
line(x1, y1, x2, y2);
PVector dir = new PVector(x1 - x2, y1 - y2);
dir.normalize();
dir.rotate(PI / 6);
line(x2, y2, x2 + arrowSize * dir.x, y2 + arrowSize * dir.y);
dir.rotate(-PI / 3);
line(x2, y2, x2 + arrowSize * dir.x, y2 + arrowSize * dir.y);
}
class Point {
PVector pos, deltaFromMouse;
color col;
float size;
boolean isSelected;
boolean movable;
Point(float x, float y) {
pos = new PVector(x, y);
col = color(0);
size = 0;
isSelected = false;
movable = false;
deltaFromMouse = new PVector(0, 0);
}
Point(float x, float y, color c, float s) {
pos = new PVector(x, y);
col = c;
size = s;
isSelected = false;
movable = false;
deltaFromMouse = new PVector(0, 0);
}
PVector getPos() {
return pos;
}
void setMovable(boolean b) {
movable = b;
}
boolean isHoovered() {
return ((mouseX - pos.x) * (mouseX - pos.x) + (mouseY - pos.y) * (mouseY - pos.y) - (size/2) * (size/2) < 0);
}
void setSelect(boolean b) {
isSelected = b;
}
void computeDeltaFromMouse() {
deltaFromMouse.x = mouseX - pos.x;
deltaFromMouse.y = mouseY - pos.y;
}
void Update() {
if (isSelected == false || movable == false) {
return;
}
pos.x = mouseX - deltaFromMouse.x;
pos.y = mouseY - deltaFromMouse.y;
}
void Show() {
if (movable && isHoovered()) {
noStroke();
int r = (col >> 16) & 0xFF;
int g = (col >> 8) & 0xFF;
int b = col & 0xFF;
fill(color(r, g, b, 100));
ellipse(pos.x, pos.y, size * 1.4, size * 1.4);
}
noStroke();
fill(col);
ellipse(pos.x, pos.y, size, size);
}
float getSignedDistFrom(Line l, PVector signDir) {
PVector projectedPt = new PVector(0, 0);
PVector linePt = l.getPt();
PVector lineDir = l.getDir();
// find projected point on line
// Line dir vector is unit vector so sqrt(x² + y²) = 1 and can be omited in below formulas
projectedPt.set(linePt.x + ((pos.x - linePt.x) * lineDir.x + (pos.y - linePt.y) * lineDir.y) * lineDir.x,
linePt.y + ((pos.x - linePt.x) * lineDir.x + (pos.y - linePt.y) * lineDir.y) * lineDir.y);
// Get distance from line
float dist = (pos.x - projectedPt.x) * (pos.x - projectedPt.x) + (pos.y - projectedPt.y) * (pos.y - projectedPt.y);
// Get sign of distance with dot product
float sign = 1;
if ((pos.x - projectedPt.x) * signDir.x + (pos.y - projectedPt.y) * signDir.y < 0) {
sign = -1;
}
return sign * sqrt(dist);
}
}
class Line {
PVector p, dir; // p = point of the line, dir = unit vector
float a, b; // y = ax+b
color col;
Line(float x1, float y1, PVector d) {
p = new PVector(x1, y1);
dir = d;
init();
}
Line(float x1, float y1, float x2, float y2) {
p = new PVector(x1, y1);
dir = new PVector(x2, y2);
init();
}
void init() {
dir.normalize();
a = dir.y / dir.x;
b = p.y - a * p.x;
col = color(50, 50, 50);
}
float getA() {
return a;
}
float getB() {
return b;
}
void setColor(color c) {
col = c;
}
PVector getPt() {
return p;
}
PVector getDir() {
return dir;
}
void Show() {
PVector[] p = new PVector[2];
int i = 0;
float currValue;
p[0] = new PVector(0, 0);
p[1] = new PVector(0, 0);
if (b > 0 && b < height) {
p[i].set(0, b);
i++;
}
currValue = -b/a;
if (currValue > 0 && currValue < width) {
p[i].set(currValue, 0);
i++;
}
currValue = a * width + b;
if (i < 2 && currValue > 0 && currValue < height) {
p[i].set(width, currValue);
i++;
}
currValue = (height - b) / a;
if (i < 2 && currValue > 0 && currValue < width) {
p[i].set(currValue, height);
}
PVector dir = new PVector(p[1].x - p[0].x, p[1].y - p[0].y);
dir.normalize();
float dist = sqrt((p[1].x - p[0].x) * (p[1].x - p[0].x) + (p[1].y - p[0].y) * (p[1].y - p[0].y));
float dashSize = 10;
PVector currPoint = p[0].copy();
stroke(col);
strokeWeight(1);
for (int j = 0; j < dist / (dashSize * 2); j++) {
line(currPoint.x, currPoint.y, currPoint.x + dashSize * dir.x, currPoint.y + dashSize * dir.y);
currPoint.set(currPoint.x + 2 * dashSize * dir.x, currPoint.y + 2 * dashSize * dir.y);
}
}
PVector intersect(Line line) {
float x, y;
x = (line.getB() - b) / (a - line.getA());
y = a * x + b;
return new PVector(x, y);
}
}