Link To Project: Taffy
This is a mini project about drawing a self-portrait using p5.js. Instead of using my real-life image, I use a meme of a Vtuber named Taffy (because some of my friends said we are alike). The meme for reference is on the left side.
My initial design 👇
I described what the meme looks like to ChatGPT as instructed. Here is my prompt:
Can you generate a self-portrait using p5.js? Please follow my instructions: it's a girl, and the canvas should be in the skin color. She has pink hair, but only the fringe should be shown in the canvas. Her eyes are closed, you can denote them in dark brown. She is crying, so there should be light-blue tears under her eyes. Moreover, her mouth is open with dark pink, and you can see a light pink tongue inside. There is also a small nose (I mean as small as only 2-4 pixels) between her closed eyes.
Add ChatGPT generated some code for me (attached at the end of this page). On the left is what the visual outcome looks like.
Before prompting the AI tool, I had imagined a few possible shapes to form the final outcome, which tends to be more irregular and humanized. What surprised me is that AI tends to utilize regular and repeated shapes, such as an array of pink triangles for the fringe, to construct the whole painting.
However, the final outcome of AI still seems less imaginative to me. It is also not as good as my initial design. I suppose it might be ascribed to my descriptive prompt above, which portrayed the expressions and the colors in too specific a way.
function setup() {
createCanvas(400, 400);
}
function draw() {
noStroke();
background(255,200,199); // pink
fill(153,51,51);
// ellipse(100, 100, 80, 15); // eye left
// ellipse(width - 100, 100, 80, 15); // eye right
arc(100, 100 + 10, 80 ,15, PI, 0, CHORD); // eye left
arc(width-100, 100 + 10 , 80 ,15, PI, 0, CHORD); // eye left
rectMode(CENTER);
fill(153,255,255); // tear blue
rect(100, 100 + 30 * 2 + 15, 30 * 1.5, 30 * 4.2); //left tear
rect(width-100,100 + 30 * 2 + 15,30 * 1.5,30 * 4.2); // right tear
fill(194,71,71); // mouth red dark
rect(width/2, height/2 - 100/2, 4 * 30, 1.5 * 30) // mouth (out)
fill(255,170,199); // mouth pink
rect(width/2, height/2 - 1.2 * 30, 3 * 30, 0.6 * 30) // mouth (in)
}
Then I started drawing with code. The first version for the eyes used ellipse() while the second version used arc(). I used a lot of arithmetic operators to adjust the position and size of different shapes meticulously.
AI's design of the fringe made me think of creating something irregular: The Vtuber is thought to be of a pink theme because of the hair. How about directly taking pink as the background theme color without explicitly painting the shape of her face? I also make the mouth of my self-portrait a rectangle instead of curved shapes to distinguish it from the eyes. Moreover, my friends compared me to Taffy because of her streaming styles. Consequently, I would like to emphasize the existence of the mouth because it articulates our similarities in my self-portrait.
I learned extensive use of the arc() function in this mini project. During Thursday's class, we used arc() to generate pies, but I did not want the endpoints to be connected to the center point. Instead, I need them to be connected together. Thus, I used the arc() reference in p5.js to explore another usage. I got to know an optional [MODE] parameter for this function and used arc(width-100, 100 + 10 , 80 ,15, PI, 0, CHORD); to draw the eyes.
What I found really reflective is the way AI uses its code. It can be seen that AI defines colors as variables at the very beginning, and they are reused later. However, when I was refining my project, I found different lines of shapes or colors meaningless only one hour after my initial declaration, because I forgot to write instructions for myself. It forced me to comment out almost every line to figure out what different combinations of numbers stand for. Therefore, I made comments after each line to make my code more systematic and structured.
What large language model lacks is the imaginative courage to jump out of routines. Since LLM is based on large datasets and extracts the outcome with higher possibilities, it might rigidly follow all instructions that human provides and depict something “boring” to some extent. We would not look at an AI-generated self-portrait for a long time because we are not really looking into the "self" of the artist, but only a general portrait without individual imagination.
// Self-portrait (girl) per instructions
// - Skin-colored canvas
// - Pink fringe (bangs) only
// - Closed eyes (dark brown) with light-blue tears
// - Small 2–4 px nose between eyes
// - Open mouth (dark pink) with a light pink tongue
// All geometry uses relative sizing so it scales if you change the canvas.
function setup() {
createCanvas(600, 800); // Feel free to adjust
noLoop(); // No animation
}
function draw() {
// --- Palette ---
const SKIN = color('#F6D1C1'); // canvas background
const HAIR = color('#FF7FBF'); // pink hair
const EYE = color('#3C2317'); // dark brown
const TEAR = color('#BFE8FF'); // light blue
const MOUTH = color('#C2185B'); // dark pink (inside mouth)
const TONGUE = color('#FFB3C7'); // light pink
// --- Canvas is skin color ---
background(SKIN);
// --- Draw pink fringe (bangs) only ---
drawFringe(HAIR);
// --- Face features (no head outline; canvas is skin-toned) ---
const eyeY = height * 0.45; // vertical position of the eyes
const eyeDX = width * 0.16; // half-distance between eye centers
const eyeW = width * 0.20; // eye arc width
const eyeH = height * 0.06; // eye arc height
// Closed eyes as simple curved arcs
stroke(EYE);
strokeWeight(6);
noFill();
arc(width/2 - eyeDX, eyeY, eyeW, eyeH, 0, PI);
arc(width/2 + eyeDX, eyeY, eyeW, eyeH, 0, PI);
// Small nose (2–4 px) between closed eyes
noStroke();
fill(lerpColor(SKIN, EYE, 0.25)); // subtle darker-than-skin
const noseSize = 3; // 2–4 pixels; choose 3 px
ellipse(width/2, eyeY + 6, noseSize, noseSize);
// Light-blue tears under each eye (simple droplets)
fill(TEAR);
// Left side tears
ellipse(width/2 - eyeDX - 22, eyeY + 52, 16, 24);
ellipse(width/2 - eyeDX + 26, eyeY + 60, 14, 22);
// Right side tears
ellipse(width/2 + eyeDX + 22, eyeY + 52, 16, 24);
ellipse(width/2 + eyeDX - 26, eyeY + 60, 14, 22);
// Open mouth: dark pink with a light pink tongue inside
const mouthY = eyeY + height * 0.18;
noStroke();
fill(MOUTH);
ellipse(width/2, mouthY, width * 0.30, height * 0.14); // mouth opening
// Tongue (light pink) sitting inside the mouth (lower half)
fill(TONGUE);
arc(width/2, mouthY + 6, width * 0.18, height * 0.08, 0, PI, CHORD);
}
// Draws only the fringe as a bar plus downward spikes (bangs)
function drawFringe(hairColor) {
const baseY = height * 0.18; // depth of the fringe
const spikes = 7; // number of triangular spikes
const spikeH = height * 0.08; // spike length
const marginX = width * 0.08; // leave a little margin
noStroke();
fill(hairColor);
// Top band of fringe
rect(0, 0, width, baseY);
// Evenly spaced spikes across the top band
for (let i = 0; i < spikes; i++) {
const x = map(i, 0, spikes - 1, marginX, width - marginX);
const w = width * 0.11; // width of each spike base
triangle(
x - w * 0.5, baseY, // left base
x, baseY + spikeH, // tip
x + w * 0.5, baseY // right base
);
}
}