Draw a line of varying width in p5.js - line

I wish to draw lines on my canvas with width that gradually vary from start to end. That is, say the line starts at (0, 0) with width = 1 (equivalently, strokeWeight) and ends at (50, 50) with width = 3, and the width must (linearly) increase from 1 to 3 from start to end.
Any ideas on how to achieve this? Can't scrape it out of the web.

The key is to separate the line into for example 30 segments. You draw each segment with increasing strokeWeight(). The more segments you have, the smoother the line will look.
You can use lerp() to find x,y coordinates for points between two ends.
You can use lerp() to find strokeWeight() for lines between two ends.
function setup() {
createCanvas(200, 200);
background("black");
stroke("white");
gradientLine(0, 0, 50, 50, 1, 3, 30);
noLoop();
}
function gradientLine(
start_x,
start_y,
end_x,
end_y,
start_weight,
end_weight,
segments
) {
let prev_loc_x = start_x;
let prev_loc_y = start_y;
for (let i = 1; i <= segments; i++) {
let cur_loc_x = lerp(start_x, end_x, i / segments);
let cur_loc_y = lerp(start_y, end_y, i / segments);
push();
strokeWeight(lerp(start_weight, end_weight, i / segments));
line(prev_loc_x, prev_loc_y, cur_loc_x, cur_loc_y);
pop();
prev_loc_x = cur_loc_x;
prev_loc_y = cur_loc_y;
}
}
<script src="https://cdn.jsdelivr.net/npm/p5#1.0.0/lib/p5.min.js"></script>
EDIT:
Also, in case one is working with colours with alpha < 255, such artifacts can appear:
This happens because the default capping for strokes is set to ROUND. Setting strokeCap(SQUARE) will fix this problem:
This needs to be set in the push()...pop() block in gradientLine(...) function. (Note: This will make the ends of the line look flat, and that needs more refined work.)

You could also just draw the line "as a trapezoid" using beginShape() / endShape() and transforming each end of the line into two separate points. See the code snippet below:
function setup() {
createCanvas(400, 400);
noStroke();
fill(220);
}
function draw() {
background(20);
drawVaryingWidthLine(0, 0, 50, 50, 1, 3);
drawVaryingWidthLine(80, 20, 200, 140, 1, 5);
drawVaryingWidthLine(30, 60, 230, 260, 10, 3);
drawVaryingWidthLine(210, 180, 360, 330, 40, 20);
}
function drawVaryingWidthLine(x1, y1, x2, y2, startWidth, endWidth) {
const halfStartWidth = startWidth / 2
const halfEndwidth = endWidth / 2
beginShape();
vertex(x1 + halfStartWidth, y1 - halfStartWidth);
vertex(x2 + halfEndwidth, y2 - halfEndwidth);
vertex(x2 - halfEndwidth, y2 + halfEndwidth);
vertex(x1 - halfStartWidth, y1 + halfStartWidth);
vertex(x1 + halfStartWidth, y1 - halfStartWidth);
endShape();
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
Compared to #ffmaer's solution, this one doesn't have any smoothness / transparency artifacts.
I assume that mine is somewhat faster because you only draw one shape per line (though I haven't tested it). However, this version here is currently limited to lines of the angle asked for in the original post (45 degrees downward right). It could be quite easily adapted to arbitrary angles by adjusting the corner positions of the trapezoid.
Edit: now using trapezoids instead of triangles, based on #ffmaer's comment.

Related

Is an AlphaMaskFilter correct in this case?

I am trying to use an alppha mask filter to apply a texture to a canvas element but cannot seem to get things to work. I have a base image which is a flat white color and to which I want to apply a color filter at runtime based on a users selection for example:
bitmap = new createjs.Bitmap(image);
bitmap.filters = [
new createjs.ColorFilter(0,0, 0.5, 1, 0, 0, 120, 0)
];
bitmap.cache(0, 0, 500, 500, 2);
I then want to use a second image which is a texture png that will add various shading texture to that first one. Looking over the docs it would seem that I need to use an AlphaMaskFilter but that does not seem to work and nothing is rendered onto the canvas. For example:
//filterImage contains the transparent image which has a shaded texture
var bitmap2 = new createjs.Bitmap(filterImage);
bitmap2.cache(0, 0, 500, 500, 2);
var bitmap = new createjs.Bitmap(image);
bitmap.filters = [
new createjs.ColorFilter(0,0, 0.5, 1, 0, 0, 120, 0),
new createjs.AlphaMaskFilter(bitmap2.cacheCanvas)
];
bitmap.cache(0, 0, 500, 500, 2);
Can someone help point me in the right direction here or if I am trying to do something which is just not possible using that filter.

How to calculate the length in mm of a string in a PDF document created with jsPDF library?

I use jsPDF library to create and print a PDF document. This library exposes low level methods which are ok, but i have tons of fields to create, many of which are similar, and i need to create higher level abstractions.
For example i have a createLabel function that i want to call instead of this low level stuff.
var doc = new jsPDF('portrait', 'mm', 'a4');
doc.addFont('Arial', "sans-serif", "normal");
// name
doc.setFontSize(14);
doc.text(10, 19, "name:");
doc.setLineWidth(0.1);
doc.line(25, 19, 100, 19); // meaning x1, y1, x2, y2
// CUI
doc.setFontSize(14);
doc.text(10, 29, "CUI:");
doc.setLineWidth(0.1);
doc.line(21, 29, 100, 29);
// same stuff but use functions instead.
createLabel("name: ", 10,50, 100); // meaning (labelName, x, y, totalWidth)
createLabel("CUI: ", 10,60, 100);
As you can see, the lines for the second group of labels are not placed in the right position. They are too much on the left. Their starting postion is generated based on the length of the labelName, and this length calculation fails. How can i make this work properly? The code so far is:
function createLabel(name, x, y, totalWidth) {
//draw name
doc.setFontSize(14);
doc.text(x, y, name);
// draw line
const nameLength = (measureLength(name)) + 2;
doc.setLineWidth(0.1);
// i want to start the line after the name ends + 2 mm.
// and end the line in such a way that nameLength + lineLength == totalWidth of the compoenent.
doc.line(x + nameLength, y, x + totalWidth, y);
}
function measureLength(str) {
let canvas = document.createElement('canvas'); // in memory canvas.. not rendered anywere..
let ctx = canvas.getContext("2d")
ctx.font = "14px Arial";
let width = ctx.measureText(str).width;
let mm = ( width * 25.4 ) / 149 // meaning (px * 25.4) / screen DPI
console.log (mm);
return mm; // of course, this calculation turns out wrong..
}
How to make this measureLength function work correctly? Most solutions i found involve DOM but this is PDF.
Notice: I use the same font ('14px Arial') for the PDF document and for the canvas. jsPDF live demo.
Any insight is appreciated, thanks :)
This might resolve your problem:
createLabel(name, x, y, totalWidth) {
doc.setFontSize(14);
doc.text(x, y, name);
// draw line
const nameLength = (doc.getTextDimensions(name).w / (72 / 25.6) ) + 2;
console.log('nameLength', nameLength); // todo remove
doc.setLineWidth(0.1);
// i want to start the line after the name ends + 2 mm.
// and end the line in such a way that nameLength + lineLength == totalWidth of the compoenent.
doc.line(x + nameLength, y, x + totalWidth, y);
}
Check how I calculate nameLength - using build in jsPDF function and converting to mm.
Helpful links:
how to calculate text size
why sometimes calculation might be wrong by few pixels
This is the result:
Remember that you use x + totalWidth for line width, so lines are longer by x compared to manual example at the top

How do I get the frame of visible content from SKCropNode?

It appears that, in SpriteKit, when I use a mask in a SKCropNode to hide some content, it fails to change the frame calculated by calculateAccumulatedFrame. I'm wondering if there's any way to calculate the visible frame.
A quick example:
import SpriteKit
let par = SKCropNode()
let bigShape = SKShapeNode(rect: CGRect(x: 0, y: 0, width: 100, height: 100))
bigShape.fillColor = UIColor.redColor()
bigShape.strokeColor = UIColor.clearColor()
par.addChild(bigShape)
let smallShape = SKShapeNode(rect: CGRect(x: 0, y: 0, width: 20, height: 20))
smallShape.fillColor = UIColor.greenColor()
smallShape.strokeColor = UIColor.clearColor()
par.maskNode = smallShape
par.calculateAccumulatedFrame() // returns (x=0, y=0, width=100, height=100)
I expected par.calculateAccumulatedFrame() to return (x=0, y=0, width=20, height=20) based on the crop node mask.
I thought maybe I could code the function myself as an extension that basically reimplements calculateAccumulatedFrame with support for checking for SKCropNodes and their masks, but it occurred to me that I would need to consider the alpha of that mask to determine if there's actual content that grows the frame. Sounds difficult.
Is there an easy way to calculate this?

How do I resize a drawing in a processing applet?

I imported the processing language into Java and I drew a simple house.
Like:
rect (75, 175, 350, 175);
// door
rect (225, 275, 50, 75);
// roof
triangle (250, 100, 75, 175, 425, 175);
// windows
rect (125, 200, 50, 50);
rect (325, 200, 50, 50);
I want to be able to resize the image, or scale that image when I change the window size. Right now the window size is just 500 by 500, but if I expand the window it doesn't expand the drawing with it.
I tried using scale(), but for some reason it scaled the image for like a millisecond and then reverted back to the unscaled version. My java teacher told me the draw method in processing refreshes at something like 60 times per second, but I don't see why it would be different from the first time.
If you want your drawing to scale with the window, you're going to need to base all your coordinates off the width and height variables.
For example, let's say I'm drawing a 200x200 circle inside a 200x200 window:
size(200, 200);
ellipse(100, 100, 200, 200);
If I then change the size of the window to 400x400, then I have to change the parameters I'm passing into the ellipse() function:
size(400, 400);
ellipse(200, 200, 400, 400);
This can be annoying (as you're experiencing), so it's better to just base your parameters off the width and height variables. That way whenever your window size changes, Processing does the calculation for you and the drawing scaled with your window.
size(600, 600);
ellipse(width/2, height/2, width, height);
This is just an example, but the process for you would be the same: go through and change every value to be based off the width and height variables instead of hard-coded numbers.
More info can be found here: Using Variables - HappyCoding.io
(full disclosure: I wrote that tutorial, but I think it explains exactly what you're asking about)
Edit: You asked about the scale() method, so I'll explain that below. But if you're trying to base the size of your drawing off the size of your window, then you should use the above approach! But since this is a homework assignment, you're restricted to doing it the way the teacher says. Anyway...
Let's look at a simple program:
size(100, 100);
ellipse(50, 50, 10, 10);
This code simply creates a 100x100 sized window and then draws a small ellipse in the center:
If we wanted to make the circle bigger, we could use the scale() function. You can think of the scale() function as automatically multiplying every parameter you pass into a drawing function (like ellipse() or rect()) by whatever number(s) you pass into the scale() function.
For example, this code multiplies every number by 2:
size(100, 100);
scale(2);
ellipse(50, 50, 10, 10);
The circle is now twice as large, but it's also no longer in the center. That's because we're also multiplying 50,50 (the center of the window) by 2 to get 100,100 (the bottom-right corner of the window). To fix that, we either need to change the 50,50:
size(100, 100);
scale(2);
ellipse(25, 25, 10, 10);
Or we could use the translate() function to move the coordinates before we do the scale:
size(100, 100);
translate(50, 50);
scale(2);
ellipse(0, 0, 10, 10);
Either approach is fine, so it's really up to which one makes more sense to you. Now we have a big centered circle, even though we're still sizing it to 10,10.

Fabricjs line coordinates after (moved, scaled, rotated) - canvas.on('object:modified'…

I need to find the Line coordinates(x1,y1,x2,y2) after the object has been modified. (moved, scaled, rotated)
I thought to use the oCoords information and based on angle and flip information to decide which corners are the line ends, but it seems that it will not be too accurate…
Any help?
Example:
x1: 164,
y1: 295.78334045410156,
x2: 451,
y2: 162.78334045410156
x: 163, y: 161.78334045410156 - top left corner
x: 452, y: 161.78334045410156 - top right corner
x: 163, y: 296.78334045410156 - bottom left corner
x: 452, y: 296.78334045410156 - bottom right corner
When Fabric.js calculates oCoords - i.e. object's corners' coordinates - it takes into account the object's strokeWidth:
// fabric.Object.prototype
_getNonTransformedDimensions: function() {
var strokeWidth = this.strokeWidth,
w = this.width + strokeWidth,
h = this.height + strokeWidth;
return { x: w, y: h };
},
For most objects, stroke is kind of a border that outlines the outer edges, so it makes perfect sense to account for strokeWidth it when calculating corner coordinates.
In fabric.Line, though, stroke is used to draw the body of the line. There is no example in the question but I assume this is the reason behind discrepancies between the real end-point coordinates and those in oCoords.
So, if you really want to use oCoords to detect the coordinates of the end points, you'll have to adjust for strokeWidth / 2, e.g.
const realx1 = line.oCoords.tl.x + line.strokeWidth / 2
const realy1 = line.oCoords.tl.y + line.strokeWidth / 2
Keep in mind that fabric.Line's own _getNonTransformedDimensions() does adjust for strokeWidth, but only when the line's width or height equal 0:
// fabric.Line.prototype
_getNonTransformedDimensions: function() {
var dim = this.callSuper('_getNonTransformedDimensions');
if (this.strokeLineCap === 'butt') {
if (this.width === 0) {
dim.y -= this.strokeWidth;
}
if (this.height === 0) {
dim.x -= this.strokeWidth;
}
}
return dim;
},