By Jules Kris, Tristan Bunn
Introduction
Welcome! This Drawing chapter provides an introduction to drawing custom shapes constructed from straight and curved line segments using vertex()
and bezierVertex()
functions. You’ll use these techniques in combination with beginShape()
and endShape()
to create sparkle stickers for the sticker-based photo decorating app from the Color Gradients tutorial.
Prerequisites
To follow along with this tutorial, you should have completed:
It is required for you to have finished and understood the previous chapter “Introduction to p5.js”. This will give you a basic understanding of the p5.js library.
Creating Color Gradients tutorial:
This chapter is a continuation of the Creating Color Gradients, which provides the starting code for this sketch and introduces linear gradients, radial gradients, and some blending mode techniques.
Step 1 – Open your Creating Color Gradients sketch
To prepare the sketch for adding sparkles, let’s set up a few things.
- Open your code from the Creating Color Gradients tutorial, and if you haven’t done that or need a fresh copy, you can start with this template code.
- Let’s adjust the values of the linear gradient to emulate a dreamy night sky by changing
startColor
tocolor(200, 100, 100)
for a bright and saturated blue andendColor
tocolor(300, 100, 100)
for a bright and saturated magenta. - For now, comment out where we call the
lensFlare()
function insidemousePressed()
. We’ll come back to it later.
Your code should look something like this:
let video;
let snapped = false;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100);
//instantiate the VIDEO object, and draw it on the screen at 0, 0
video = createCapture(VIDEO);
video.position(0, 0);
//When we click the snap button, run the takeSnap function
let snapButton = createButton('snap');
snapButton.mouseClicked(takeSnap);
blendMode(LIGHTEST);
noStroke();
}
function mousePressed() {
if (snapped === true) {
gradientFilter();
// lensFlare();
}
}
//If we haven’t snapped a photo yet, we see our webcam video feed
//Once we run the takeSnap function, set snapped to true and remove the video feed, leaving only the still photo we took
function takeSnap() {
if (snapped === false) {
image(video, 0, 0);
snapped = true;
video.remove();
}
}
//Draws circles filled with radial gradients when we click the screen
//Each circle’s size and color are a random value contained in the diameter and h variables
function lensFlare() {
let diameter = random(50, 200);
let h = random(150, 360);
for (let d = diameter; d > 0; d -= 1) {
fill(h, 90, 90);
circle(mouseX, mouseY, d);
h = (h + 1) % 360;
}
}
//Draws a linear gradient on the screen using a for loop and lerpColor
function gradientFilter() {
let startColor = color(200, 100, 100);
let endColor = color(300, 100, 100);
for (let y = 0; y < height; y += 1) {
let amt = map(y, 0, height, 0, 1);
let gradColor = lerpColor(startColor, endColor, amt);
stroke(gradColor);
line(0, y, width, y);
}
}
Now run the code. You may have to allow permission for the p5.js editor to access your webcam.
Click the “snap” button, then click on the new photo to apply the color gradient effect. The result should look something like this:
Try this!
Think about the mood you want to set. A night sky is chosen here, but you might want something else! Try creating a variable with a third lerpColor, so our gradient goes between three colors instead of two.
Step 2 – Create sparkles using the vertex()
function
We’ll create angular sparkles made of vertices connected by straight lines using vertex()
functions. A vertex (plural: vertices) is a point at an x-y coordinate where lines meet to form an angle that we can use to define different polygonal shapes such as triangles, quadrilaterals, and, of course, stars.
- Define a new function and name it
sparkle()
- Inside the
sparkle()
function:- We’ll want to be able to paste our sparkles as stickers, just like we pasted the lens flares in the previous chapter. Add a
push()
to the beginning of the sparkle function, and apop()
to the end of the sparkle function. Then, writetranslate(mouseX, mouseY)
. The translate function will shift our shape’s vertex values to move to wherever we click the mouse. Thepush()
andpop()
functions ensure that ourtranslate
function only runs inside oursparkle()
function and does not accidentally translate other elements of our sketch. - We’ll start by drawing a white line to demonstrate how the
vertex()
function works. - Write
stroke(255)
to make the line white. - For shapes defined using
vertex()
functions, start with abeginShape()
function. - Add a
vertex(100, 100)
to create your first point at an x-y coordinate of (100, 100). The vertex function takes an x value, a y value, and an optional z value. We don’t need the z value because that is for 3-dimensional coordinates, and this is a 2D sketch. - Add a second vertex to form a straight line by writing
vertex(200, 200)
. - As you continue to create more vertex points, they’ll connect to each other by straight lines. For now, end the shape with an
endShape(CLOSE)
function.
- We’ll want to be able to paste our sparkles as stickers, just like we pasted the lens flares in the previous chapter. Add a
If we add sparkle()
inside the if statement within the mousePressed()
function, we’ll see the line we just made when we hit play on our sketch and click on the canvas:
We’ll now add some randomness to our vertex()
functions to vary their size.
Create a variable called
innerRadius
and set it equal torandom(3, 5),
then create a second variable calledouterRadius
and set it to equalrandom(10, 50)
. This will allow us to add variation to the size and shape of each sparkle we create.Delete
vertex(100, 100)
and replacevertex(200, 200)
withvertex(-innerRadius, innerRadius);
To draw a 4-pointed sparkle, we’ll need to create 8 symmetrical vertex points. We’re making this easier for ourselves by using our
innerRadius
andouterRadius
variables. Here is how we’ll draw our sparkle:vertex(-innerRadius, innerRadius); vertex(0, outerRadius); vertex(innerRadius, innerRadius); vertex(outerRadius, 0); vertex(innerRadius, -innerRadius); vertex(0, -outerRadius); vertex(-innerRadius, -innerRadius); vertex(-outerRadius, 0);
Here, we can see how this inversion works to create a 4-pointed sparkle in this diagram, where we see the coordinates of each vertex:
- In this example, innerRadius is 5 and outerRadius is 50.
- Notice how every second vertex is an inversion of the other. The outer points mirror each other vertically/horizontally, and the inner points mirror each other diagonally. This maintains the symmetry of our sparkle shape.
- Call the
sparkle()
function within themousePressed()
function, on the line just belowgradientFilter()
.
Your code should look like this:
function sparkle() {
push();
// Translate the coordinate space so that (0, 0) matches mouse coordinates.
translate(mouseX, mouseY);
// Define a random inner and outer radius for each star.
let innerRadius = random(3, 5);
let outerRadius = random(10, 50);
// Draw the star shape.
beginShape();
vertex(-innerRadius, innerRadius);
vertex(0, outerRadius);
vertex(innerRadius, innerRadius);
vertex(outerRadius, 0);
vertex(innerRadius, -innerRadius);
vertex(0, -outerRadius);
vertex(-innerRadius, -innerRadius);
vertex(-outerRadius, 0);
endShape(CLOSE);
pop();
}
Hit play, and you’ll see your modified blue-to-pink gradient filter and 4-sided sparkle stickers of varying sizes that appear wherever you click the canvas.
Try this!
We’ve started with four points. Use what we’ve learned about vertices to add more points to these sparkles! Pay attention to how even and odd-numbered sparkles require different calculations to achieve symmetry.
Step 3 – The Bézier Curve Interlude
We’ve now successfully drawn angular sparkles using the vertex()
function! You can draw more interesting shapes using curved lines, and Bézier curves are one good way to do that. They define curves using a pair of anchor points (the grey circles below) and a pair of control points (the red points below.) Here’s what they look like. The curve itself is in black, with the grey lines showing how the points relate to the final curve.
Here’s how you can draw that Bézier curve using the bezier()
function. This curve is drawn by calling bezier(100, 150, 50, 50, 250, 50, 200, 150)
:
Next up, we’ll use bezierVertex()
to form a shape by connecting Bézier curves.
Try this!
Change the numbers in the third, fourth, fifth, and sixth parameters within bezier to change the control points and see how that affects the shape!
Step 4 – Create rounded sparkles using the bezierVertex()
function
A bezierVertex()
function begins with an anchor point at the very top, which we call using the vertex()
function, like we did for the pointed sparkle. This time, however, instead of calling another vertex()
function to create our shape, we’ll call the bezierVertex()
function. If you’ve ever used Adobe Illustrator, or similar vector-based illustration software, you’ve probably used bezier curves! bezierVertex()
works by creating invisible control points, similar to the handles used to manipulate bezier curves in vector graphics software.
- For now, comment out where we call the
sparkle()
function insidemousePressed()
. We’ll come back to it later. - Define a new function called
curvedSparkle()
- Add
push()
andpop()
functions, and between those, atranslate(mouseX, mouseY)
, just like you did in thesparkle()
function before.
To create a curved line, we’ll need to use the bezierVertex()
function.
- Just like we did for the pointed sparkles, we’ll begin our shape with
beginShape()
, and end it withendShape()
. - Any shape with
bezierVertex()
functions must still begin with avertex()
function. We’ll callvertex(x, y)
first. This acts as the first point in our shape.x
andy
represent the x-y coordinates for the first vertex in our star.
- Next, we’ll call the
bezierVertex()
function. Since we’re working on a 2D sketch, the function takes six parameters, ordered as follows:- the x-coordinate for the first control point
- the y-coordinate for the first control point
- the x-coordinate for the second control point
- the y-coordinate for the second control point
- the x-coordinate for the anchor point
- the y-coordinate for the anchor point
Now that you have some context, let’s begin our shape!
// Draw the curved star shape.
beginShape();
// Original anchor at top.
vertex(0, -100); // anchor point 1
// Top-right curve.
bezierVertex(0, -50, // control point 1 x-y coord
50, 0, // control point 2 x-y coord
100, 0 // anchor point 2
);
endShape();
You may be wondering where the control points are since it looks like there are only two points and a line between them. The control points are invisibly controlling the curve of the line! Here is how that would work:
stroke('orange');
line(0, -100, 0, -50);
line(50, 0, 100, 0);
circle(0, -50, 5);
Here, we can see the control points visualized as circles, with lines connecting them to the anchor points:
Let’s go back to our main sketch and continue to build out our curved sparkle shape! We’ll use the same variables as before, but we’ll alter them by making them negative and adding or subtracting proportional numbers.
Let’s complete our shape:
// Draw the curved star shape.
beginShape();
// Original anchor at top.
vertex(0, -100);
// Top-right curve.
bezierVertex(0, -50, 50, 0, 100, 0);
// Bottom-right curve.
bezierVertex(50, 0, 0, 50, 0, 100);
// Bottom-left curve.
bezierVertex( 0, 50, -50, 0, -100, 0);
// Top-left curve.
bezierVertex(-50, 0, 0,-50, 0,-100);
endShape();
Let’s also add a
scale()
function with arandom()
function as a parameter so that each time we click, the curved sparkle randomly scales between 0.1 and 0.5 (between 10% and 50% of its original size).scale()
is another transformation function, much liketranslate()
, but instead of moving the coordinates written below it, it resizes the shape to make it larger or smaller.let starScale = random(0.1, 0.5); scale(starScale);
We’ll set the fill for our curved sparkle to
fill(0, 0, 100)
to make it a bright white.
Your curvedSparkle()
function looks like this:
function curvedSparkle() {
push();
// Translate to the mouse's position.
translate(mouseX, mouseY);
// Scale the coordinate system.
let starScale = random(0.1, 0.5);
scale(starScale);
// Set fill color.
fill(0, 0, 100);
// Draw the curved star shape.
beginShape();
// Original anchor at top.
vertex(0, -100);
// Top-right curve.
bezierVertex(0, -50, 50, 0, 100, 0);
// Bottom-right curve.
bezierVertex(50, 0, 0, 50, 0, 100);
// Bottom-left curve.
bezierVertex( 0, 50, -50, 0, -100, 0);
// Top-left curve.
bezierVertex(-50, 0, 0,-50, 0,-100);
endShape();
pop();
}
- Call the
curvedSparkle()
function insidemousePressed
, beneath where we call thegradientFilter()
function.
Hit play, and you’ll see your modified blue-to-pink gradient filter and 4-pointed curved sparkle stickers of varying sizes that appear wherever you click the canvas.
Try this!
Try changing the values of the control points to see how it changes the curves of the sparkles!
Hint: You can achieve this by changing the values of the first four parameters in the bezierVertex()
function.
Step 5 – Switch between the three sticker types with each click
Let’s create conditional statements to switch between the lens flare, the angular sparkle, and the curved sparkle stickers.
- Create a global variable called
sparkleCounter
and set it to equal 0. This counter will increment each time we place a sticker, helping track which of the three stickers we’ll paste on the next click. - Inside the mousePressed() function, within the
snapped === true
block, create a set of conditional statements:
if (sparkleCounter % 3 === 0) {
sparkle();
} else if (sparkleCounter % 3 === 1) {
curvedSparkle();
} else {
lensFlare();
}
sparkleCounter += 1;
- Let’s break down what this if statement does! It runs each time we click on the canvas. We use modulo (
%
) to divide sparkleCounter by 3 (the number of sticker functions we have) and calculate its remainder. We run a function depending on whether the remainder equals 0, 1, or something else. Each time we do that calculation, we add 1 to our sparkleCounter. This allows us to cycle through our three sticker functions each time we click on the canvas. - Make sure there is a
push()
at the beginning of each of your sticker functions and apop()
at the end of each of your sticker functions. This will ensure that the various translations we apply within those functions don’t affect anything outside of the functions.
Your final code can look like this:
let video;
let snapped = false;
let sparkleCounter = 0;
function setup() {
createCanvas(640, 480);
colorMode(HSB, 360, 100, 100);
noStroke();
//instantiate the VIDEO object, and draw it on the screen at 0, 0
video = createCapture(VIDEO);
video.position(0, 0);
//When we click the snap button, run the takeSnap function
let snapButton = createButton("snap");
snapButton.mouseClicked(takeSnap);
blendMode(LIGHTEST);
}
//Cycle through our three stickers each time we press the mouse
function mousePressed() {
if (snapped === true) {
gradientFilter();
if (sparkleCounter % 3 === 0) {
sparkle();
} else if (sparkleCounter % 3 === 1) {
curvedSparkle();
} else {
lensFlare();
}
sparkleCounter += 1;
}
}
//If we haven’t snapped a photo yet, we see our webcam video feed
//Once we run the takeSnap function, set snapped to true and remove the video feed, leaving only the still photo we took
function takeSnap() {
if (snapped === false) {
image(video, 0, 0);
snapped = true;
video.remove();
}
}
function sparkle() {
push();
// Translate to the mouse's position.
translate(mouseX, mouseY);
// Set the shape's vertices.
let vertex1 = random(3, 5);
let vertex2 = random(10, 50);
// Draw the star shape.
beginShape();
vertex(-vertex1, vertex1);
vertex(0, vertex2);
vertex(vertex1, vertex1);
vertex(vertex2, 0);
vertex(vertex1, -vertex1);
vertex(0, -vertex2);
vertex(-vertex1, -vertex1);
vertex(-vertex2, 0);
endShape(CLOSE);
pop();
}
function curvedSparkle() {
push();
// Translate to the mouse's position.
translate(mouseX, mouseY);
// Scale the coordinate system.
let starScale = random(0.1, 0.5);
scale(starScale);
// Set fill color.
fill(0, 0, 100);
// Draw the curved star shape.
beginShape();
// Original anchor at top.
vertex(0, -100);
// Top-right curve.
bezierVertex(0, -50, 50, 0, 100, 0);
// Bottom-right curve.
bezierVertex(50, 0, 0, 50, 0, 100);
// Bottom-left curve.
bezierVertex( 0, 50, -50, 0, -100, 0);
// Top-left curve.
bezierVertex(-50, 0, 0,-50, 0,-100);
endShape();
pop();
}
//Draws circles filled with radial gradients when we click the screen
//Each circle’s size and color are a random value contained in the diameter and h variables
function lensFlare() {
push();
let diameter = random(50, 200);
let h = random(150, 360);
for (let d = diameter; d > 0; d -= 1) {
fill(h, 90, 90);
circle(mouseX, mouseY, d);
h = (h + 1) % 360;
}
pop();
}
//Draws a linear gradient on the screen using a for loop and lerpColor
function gradientFilter() {
push();
let startColor = color(200, 100, 100);
let endColor = color(300, 100, 100);
for (let y = 0; y < height; y += 1) {
let amt = map(y, 0, height, 0, 1);
let gradColor = lerpColor(startColor, endColor, amt);
stroke(gradColor);
line(0, y, width, y);
}
pop();
}
Hit play, snap your photo, click on the canvas, and you’ll be able to see the final result!
Keep adding stickers, and you’ll be able to cover the entire screen in stickers, and see how the LIGHTEST
blendMode interacts with all of the layers of the canvas.
Here is the completed sketch for reference.
Try this!
Use what you’ve learned so far to create a fourth sticker function, containing anything from the previous lessons. Then, add it to our existing conditional statement within mousePressed so the sketch cycles through all four shape functions each time you click the canvas.
Conclusion
In this tutorial, you learned how to use vertex()
and bezierVertex()
to create angular and curved sparkle stickers. You also learned how to use translate()
to place custom shapes beneath your mouse pointer. Finally, you learned how to create a counter to iterate through three different functions each time you click on the canvas. Congrats! Try changing the coordinates, sizes, and color values within this sketch or even introducing your own custom shapes using what you’ve learned!
Next Steps
Try one of our other tutorials:
- Creating and Styling HTML (Web Design Chapter)
- Abracadabra: Speak With Your Hands (ml5.js and p5.js)
- Melody App (Node.js and p5.js)
相关参考
vertex
向自定义形状添加一个顶点。 vertex() 设置在 beginShape() 和 endShape() 函数之间绘制的顶点的坐标。 前两个参数 x 和 y 设置顶点的 x 和 y 坐标。 第三个参数 z 是可选的。在 WebGL 模式下,它设置顶点的 z 坐标。默认情况下,z 为 0。 第四和第五个参数 u 和 v 也是可选的。它们在与 endShape() 配合使用时,设置顶点的纹理的 u 和 v 坐标。 默认情况下,u 和 v 都为 0。 .
bezierVertex
向自定义形状添加贝塞尔曲线片段。 bezierVertex() 向自定义形状添加曲线片段。它创建的贝塞尔曲线与 bezier() 函数创建的相似。 bezierVertex() 必须在 beginShape() 和 endShape() 函数之间调用。曲线段 使用前一个顶点作为第一个锚点,因此必须至少在 bezierVertex() 之前之前调用一次 vertex()。 前四个参数 x2,y2, x3,和 y3 设置曲线的两个控制点,曲线朝着控制点的方向被拉拽。 第五个和第六个参数 x4,和 y4 设置最后一个锚点。 最后一个锚点是曲线结束的地方。 贝塞尔曲线也可以在 WebGL 模式下绘制 3D。 bezierVertex() 的 3D 版本有八个参数,因为每个点都有 x、y 和 z 坐标。 注意:当向 beginShape() 传递参数时, bezierVertex() 不起作用。 .
beginShape
开始向自定义形状添加顶点。 beginShape() 和 endShape() 函数 可以用来创建自定义的 2D 或者 3D 形状。beginShape() 开始添加顶点到自定义形状,而 endShape() 则停止添加。 参数 kind 设置要创建的形状的种类。默认情况下,可以绘制任何 不规则多边形。kind 可用的模式有: POINTS 绘制一系列点。 LINES 绘制一系列不相连的线段。 TRIANGLES 绘制一系列单独的三角形。 TRIANGLE_FAN 以扇形方式绘制一系列连接的三角形,共享第一个顶点。 TRIANGLE_STRIP 以条带方式绘制一系列连接的三角形。 QUADS 绘制一系列单独的四边形(四边形)。 QUAD_STRIP 使用相邻边绘制四边形条带形式。 TESS 通过显式细分创建填充曲线(仅限 WebGL)。 调用 beginShape() 后,可以通过用 vertex(), bezierVertex(), quadraticVertex(), 和/或 curveVertex() 来构建形状。调用 endShape() 将停止向 形状添加顶点。每个形状都将用当前的描边颜色勾勒出轮廓,并填充当前的填充颜色。 translate(), rotate(),和 scale() 等变换图形的函数不适用于 beginShape() 和 endShape() 之间。也不能在 beginShape() 和 endShape() 之间使用其他形状,比如 ellipse() 或 rect()。 .
endShape
开始向自定义形状添加顶点。 beginShape() 和 endShape() 函数允许在 2D 或 3D 中创建自定义形状。 beginShape() 开始添加顶点到一个自定义形状,而 endShape() 则停止添加。 第一个参数 mode 是可选的。默认情况下,形状的第一个和最后一个顶点不相连。如果传递常量 CLOSE, 如 endShape(CLOSE) 那么第一个和最后一个顶点将会连接起来。 第二个参数 count 也是可选的。 在 WebGL 模式下,如果要绘制许多相同形状的副本,更高效的方法是使用 instancing 的技术。 count 参数告诉 WebGL 模式要绘制多少个副本。例如,在绘制自定义形状后调用 endShape(CLOSE, 400) 会使绘制 400 个副本变得高效。 此功能需要 writing a custom shader。 调用 beginShape() 后,可以通过调用 vertex(), bezierVertex(), quadraticVertex(),和/或 curveVertex() 来构建形状。调用 endShape() 将停止向形状添加顶点。每个形状都将用当前的描边颜色勾勒出轮廓,并填充当前的填充颜色。 一些变换图形的函数,如 translate(), rotate(),和 scale() 在 beginShape() 和 endShape() 之间不起作用。在 beginShape() 和 endShape() 之间也不能使用其他形状,比如 ellipse() 或 rect()。 .
translate
转换坐标系统。 默认情况下,在 2D 模式和 WebGL 模式中心,原点 (0, 0) 位于画布的左上角。 translate() 函数将原点移动到不同的位置。在调用 translate() 后绘制的所有内容将会被移动。有两种方式可以调用 translate() 来设置原点的位置参数。 第一种调用 translate() 的方式是使用数字设置平移的量。前两个参数 x 和 y 设置沿正 x 轴和正 y 轴的平移量。例如,调用 translate(20, 30) 将原点沿 x 轴移动 20 个像素,沿 y 轴移动 30 个像素。第三个参数 z 是可选的。它设置沿正 z 轴的平移量。例如,调用 translate(20, 30, 40) 将原点沿 x 轴移动 20 个像素,沿 y 轴移动 30 个像素,沿 z 轴移动 40 个像素。 第二种调用 translate() 的方式是使用 p5.Vector 对象来设置平移的量。例如,调用 translate(myVector) 使用 myVector 的 x、y 和 z 分量来设置沿 x、y 和 z 轴的平移量。这样做与调用 translate(myVector.x, myVector.y, myVector.z) 是相同的。 默认情况下,变换会累积。例如,调用 translate(10, 0) 两次与调用 translate(20, 0) 一次具有相同的效果。可以使用 push() 和 pop() 函数来隔离不同绘制组内的变换。 注意:变换会在绘制循环的开头被重置。在 draw() 函数内调用 translate(10, 0) 不会导致形状持续移动。 .