Welcome to Bloom

Make art with code. No experience needed.

How This Works
1 Code goes on the left - this is your instructions
2 Canvas appears on the right - this shows your drawing
3 Run button executes your code and shows the result
4 Red text means an error - read it carefully, it tells you what went wrong

All distances in Bloom are measured in pixels (tiny dots on your screen). A typical phone screen is about 400 pixels wide.

Easiest

Your Very First Line of Code

Let's start with the simplest possible program. This just shows a message:

print("Hello, world!")

What happened? The word print tells the computer to show a message. The message goes inside quotes " " and parentheses ( ). Look at the console - the text area below the code editor!

What is the Console?

The console is a text area that shows messages from your program. It's below the code editor.

  • print("Hello") shows "Hello" in the console
  • print(x) shows the value of x - great for debugging!
  • print("x is: " + x) shows text and a value together (the + glues them)

Joining text: Use + to combine text and values: "Score: " + score becomes "Score: 42" if score is 42.

Tip: When something isn't working, add print() to see what values your variables have!

Try changing "Hello, world!" to your name, then tap Run.

Beginner

Now let's draw! Change any number and watch what happens:

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) // Move your mouse or finger over the canvas! fill(orange) circle(mouseX, mouseY, 40) // These circles follow with a delay fill(pink) circle(mouseX - 20, mouseY + 30, 25) circle(mouseX + 20, mouseY + 30, 25) }

You just drew with code!

  • The code on the left tells the computer what to draw
  • The canvas (drawing area) on the right shows the result in real-time
  • Move your mouse or finger over the canvas to see coordinates
  • Change any number and watch the result update instantly
New to coding?

Here's All You Need to Know

fn
Short for "function" - a saved recipe you can use over and over.
Why use functions? Instead of writing the same code 10 times, save it once and reuse it.
fn setup() { } runs once when your program starts. Always call size(width, height) here to create your canvas.
fn draw() { } runs 60 times per second (like a flipbook creating animation).
let
A labeled box - Store a value and give it a name. let size = 40 creates a box labeled "size" containing the number 40.
if
A question - Do something only when the answer is yes. if mousePressed { } means "if the mouse is pressed, do this."
for
Repeat - Do something multiple times. for i in 0..5 { } repeats 5 times, with i being 0, then 1, then 2, then 3, then 4.
frame
A frame counter - Starts at 0 when your program begins, increases by 1 every time draw() runs. After 1 second, frame equals 60. Use it to make things move!

Tap any keyword in the code editor to learn more about it.

Reading Code: A Quick Guide

What's a "value"? Any number, color, or text. 100, red, and "Hello" are all values.
What's a "function"? A command that does something. circle() draws a circle. The values inside the parentheses tell it where and how big.
Why quotes around text? Quotes like "Hello" tell the computer "this is text, not a command." Without quotes, Bloom would try to run it as code.

( ) Parentheses: hold inputs for a command circle(100, 100, 40) gives circle 3 inputs
{ } Braces: contain a block of instructions fn draw() { ...code here... }
[ ] Brackets: hold a list of values [10, 20, 30] is a list of 3 numbers
.. Range: shorthand for a sequence 0..5 means 0, 1, 2, 3, 4
// Comment: a note the computer ignores // remind yourself what code does
Suggested Learning Path

New to coding? Follow this order for the best experience:

  1. Start here: Your First Sketch - Draw your first shape
  2. Common pitfalls: Mistakes Everyone Makes - Avoid frustration
  3. Get creative: Shapes and Colors - More drawing tools
  4. Add movement: Animation - Make things move
  5. Go deeper: Variables and Loops - Real programming power

Or just explore! Click any topic in the sidebar that interests you.

Mistakes Everyone Makes

These are totally normal! Every programmer makes these mistakes at first. Here's how to spot and fix them.

Nothing appears on the canvas
Why: You drew something but forgot to set a color first
Fix: Add fill(color) before drawing
fn setup() { size(200, 200) } fn draw() { background(white) fill(coral) // Add this line! circle(100, 100, 40) }
Shapes leave trails (smearing)
Why: The canvas isn't being cleared each frame
Fix: Add background(color) as the first line inside draw()
Without background():
Frame 1: 1 circle Frame 2: 2 circles Frame 3: 3 circles...
Shapes pile up = messy trails
With background():
Frame 1: clear, draw 1 Frame 2: clear, draw 1 Frame 3: clear, draw 1...
Fresh start each frame = clean animation

Key rule: background() must be the first line inside draw(). If you put it at the end, it erases everything you just drew!

fn setup() { size(200, 200) } fn draw() { background(white) // Clear canvas each frame fill(coral) circle(mouseX, mouseY, 30) }
Animation not moving
Why: Variable is created inside draw() so it resets to the same value every frame
Fix: Create the variable outside of draw() so it remembers its value
let x = 0 // Outside draw = remembers! fn setup() { size(200, 200) } fn draw() { background(white) fill(coral) circle(x, 100, 30) x = x + 2 // Now x grows each frame }
Code runs but nothing happens
Why: Using = (assignment) instead of == (comparison) in if statements
Fix: Use == to compare values
fn setup() { size(200, 200) } fn draw() { background(white) // Wrong: x = 5 (assigns 5 to x) // Right: x == 5 (checks if x equals 5) if mousePressed == true { fill(coral) } else { fill(skyblue) } circle(100, 100, 60) }
Remember: Getting errors is normal! Every error message tries to help you fix the problem. Read them carefully - they often tell you exactly what to do.

How to Debug (Find and Fix Problems)

When your code isn't working, don't panic! Follow these steps:

1
Read the error message

Bloom shows errors in the console with hints. The error tells you what's wrong and often how to fix it.

2
Use print() to see values

Add print(x) to see what a variable actually is. This often reveals the problem instantly.

// Not sure why your circle is off-screen?
print("x is: " + x)  // See the actual value!
print("y is: " + y)
3
Simplify your code

Comment out lines (add // at the start) to find which line causes the problem. Remove complexity until it works, then add back one piece at a time.

4
Check the basics
  • Did you spell everything correctly?
  • Are your brackets { } and parentheses ( ) balanced?
  • Is your variable defined before you use it?
// Debugging Example: Why isn't my circle moving? let x = 100 fn setup() { size(200, 200) } fn draw() { background(white) // Step 2: Add print() to see what x is print("x = " + x) fill(coral) circle(x, 100, 30) // The circle moves because x changes each frame x = x + 1 }

Your First Sketch

Let's build your first program together, step by step. Don't worry about memorizing anything - just follow along and experiment!

1

The Setup

Every Bloom program starts with fn setup(). This runs once when your program starts. We use it to create the drawing area:

fn setup() {
  size(200, 200)  // Makes a 200x200 pixel canvas
}
What does size(200, 200) do? It creates your canvas - the rectangle where your art appears. The first number is width (200 pixels across), the second is height (200 pixels tall). Think of it like choosing the size of paper before you start drawing.
2

The Draw Loop

The fn draw() function runs 60 times per second - that's what makes animation possible! Each time it runs, we draw a fresh picture:

fn draw() {
  background(white)  // Clear with white
  // Your drawing code goes here
}
Why background()? Think of animation like a flipbook. Each page is a fresh drawing. Without background(), you're drawing on the same page over and over, leaving trails.
How It Works
setup()
Runs once at start
then
draw()
Frame 1
~16ms
draw()
Frame 2
~16ms
draw()
Frame 3...
60 frames per second = smooth animation
Where should I put my code?
Put in setup() if:
  • It only needs to happen once (canvas size, initial background)
  • You're setting up starting conditions
Put in draw() if:
  • It should update or animate over time
  • It responds to mouse, keyboard, or frame count
  • You want to see changes every frame
3

Draw Something!

Let's draw a blue circle. First we set the color with fill(), then we draw with circle(x, y, size):

fill(blue)           // Set the fill color
circle(100, 100, 40) // Draw circle at center
What Each Number Means
circle(100, 100, 40)
x position (distance from left edge)
y position (distance from top edge)
radius (distance from center to edge)

Here's the complete program. Try changing the numbers and colors!

fn setup() { size(200, 200) } fn draw() { background(white) fill(blue) circle(100, 100, 40) }
Try These Challenges
  1. Change blue to coral or purple
  2. Make the circle bigger by changing 40 to 80
  3. Move the circle to the bottom-right by changing 100, 100 to 150, 150
  4. Add a second circle with a different color!

The circle(100, 100, 40) draws a circle at position (100, 100) with a radius of 40. Try changing 100, 100 to move it around, or change 40 to make it bigger or smaller.

Understanding the Canvas

How Coordinates Work

The canvas is like a piece of graph paper. Every point has two numbers: x (how far right) and y (how far down).

(0, 0)
x increases
y increases
(100, 100)
(200, 200)

Unlike math class, y goes down (like reading a book from top to bottom). Computer screens draw from the top-left corner.

Move your mouse or finger over the canvas below to see the coordinates:

fn setup() { size(200, 200) } fn draw() { background(white) // Draw grid lines stroke(220) strokeWeight(1) line(100, 0, 100, 200) line(0, 100, 200, 100) // Mark corners with coordinates fill(150) textSize(10) text("(0,0)", 4, 12) text("(200,0)", 160, 12) text("(0,200)", 4, 195) text("(200,200)", 145, 195) // Draw crosshair at mouse position stroke(coral) strokeWeight(1) line(mouseX, 0, mouseX, 200) line(0, mouseY, 200, mouseY) // Draw a dot where your mouse is fill(coral) noStroke() circle(mouseX, mouseY, 12) // Show the coordinates prominently fill(black) textSize(14) text("x: " + floor(mouseX), 80, 95) text("y: " + floor(mouseY), 105, 115) }

Tip: If a shape disappears, it might be outside the canvas boundaries. Check that your x is between 0 and the canvas width, and y is between 0 and the height.

Beginner Shapes

Bloom gives you a handful of basic shapes to work with. Each shape needs different information to know where and how big to draw itself.

Understanding Coordinates (x, y)

Every position on the canvas is described by two numbers:

  • x = distance from the left edge (increases going right)
  • y = distance from the top edge (increases going down)
(0, 0)
x increases →
y increases ↓
(100, 50)
In math class:
y=50 is above y=0
vs
On screens:
y=50 is below y=0

Why the difference? Screens display text line-by-line from top to bottom, like reading a book. Line 1 is at the top (y=0), line 2 is below it (y=1), and so on. This became the standard for all computer graphics.

Circles and Ellipses

A circle needs a center point (x, y) and a radius. An ellipse is like a stretched circle - it needs a center point plus width and height.

fn setup() { size(200, 200) } fn draw() { background(white) fill(blue) circle(60, 100, 30) fill(purple) ellipse(140, 100, 60, 80) }
Important: Radius vs Diameter

The radius is the distance from the center to the edge - it's half the total width.

  • circle(100, 100, 30) = 30px radius = 60px wide circle
  • circle(100, 100, 50) = 50px radius = 100px wide circle

If your circle looks twice as big as expected, you probably wanted diameter (total width) instead of radius (half width).

Rectangles

A rect needs a top-left corner (x, y), then width and height.

fn setup() { size(200, 200) } fn draw() { background(white) fill(orange) rect(40, 40, 50, 80) fill(teal) rect(110, 60, 60, 60) }

Lines and Triangles

Lines go from one point to another. Triangles need three corner points.

fn setup() { size(200, 200) } fn draw() { background(white) stroke(red) strokeWeight(3) line(20, 20, 180, 180) fill(green) triangle(100, 30, 40, 170, 160, 170) }

Quadrilaterals

A quad is any four-sided shape. You give it four corner points (8 numbers total). Unlike rectangles, the corners don't have to form right angles.

fn setup() { size(200, 200) } fn draw() { background(white) fill(coral) // Parallelogram shape quad(30, 50, 90, 30, 110, 100, 50, 120) fill(teal) // Trapezoid shape quad(120, 60, 180, 60, 190, 140, 110, 140) }

Beginner Colors

Bloom comes with a set of named colors you can use right away. Just type the name!

red
blue
green
yellow
orange
purple
pink
cyan
coral
teal
gold
sky
white
black
gray
navy
mint
violet

More colors: rose, tomato, amber, honey, jade, sage, lime, azure, cobalt, indigo, lavender, plum, peach, cream, ivory, beige, and many more! See the Reference for the full list.

fn setup() { size(200, 200) } fn draw() { background(white) fill(red) circle(50, 50, 30) fill(green) circle(100, 100, 30) fill(blue) circle(150, 150, 30) }

Custom Colors with RGB

Want more control? Use rgb() to mix your own colors by combining Red, Green, and Blue light.

How RGB Works

Each color has a number from 0 (none) to 255 (maximum):

  • rgb(255, 0, 0) = full red, no green, no blue = red
  • rgb(0, 255, 0) = no red, full green, no blue = green
  • rgb(255, 255, 0) = full red + full green = yellow
  • rgb(255, 255, 255) = all colors full = white
  • rgb(0, 0, 0) = no colors = black

Why 0-255? Think of it like a volume knob with 256 positions (0 to 255). 0 means "off" and 255 means "full blast." The middle (128) is half strength. This range became standard because it gives enough shades to look smooth to our eyes.

Tip: If you type a number higher than 255 (like 300), Bloom just treats it as 255. Nothing breaks!

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) fill(rgb(255, 100, 100)) circle(60, 100, 40) fill(rgb(100, 200, 255)) circle(140, 100, 40) }

Transparency

Add a fourth number to rgba() for transparency. It goes from 0 (invisible) to 1 (solid).

fn setup() { size(200, 200) } fn draw() { background(white) fill(rgba(255, 0, 0, 0.7)) circle(80, 80, 50) fill(rgba(0, 0, 255, 0.7)) circle(120, 120, 50) }

HSB Colors (Hue, Saturation, Brightness)

Sometimes it's easier to think about colors in a different way. hsb() lets you describe colors using Hue (which color), Saturation (how vivid), and Brightness (how light/dark).

How HSB Works
  • Hue (0-360): The color on the rainbow wheel. 0 = red, 120 = green, 240 = blue, 360 = back to red
  • Saturation (0-100): How vivid. 0 = gray, 100 = pure color
  • Brightness (0-100): How light. 0 = black, 100 = full brightness

Why use HSB? It's great for color animations! Change the hue to cycle through rainbow colors, or the brightness to fade in/out.

fn setup() { size(200, 200) } fn draw() { background(white) // Rainbow colors by changing hue for i in 0..6 { fill(hsb(i * 60, 80, 90)) circle(35 + i * 30, 70, 25) } // Same hue, different saturation for i in 0..4 { fill(hsb(200, i * 30, 90)) circle(50 + i * 40, 140, 25) } }

The top row shows different hues (colors around the rainbow). The bottom row shows the same blue with increasing saturation (from gray to vivid blue).

Modifying Colors

Bloom provides functions to adjust any color. These work with named colors or custom colors:

lighten(color, amount) Make lighter (amount 0-100)
darken(color, amount) Make darker (amount 0-100)
saturate(color, amount) More vivid (amount 0-100)
desaturate(color, amount) More muted (amount 0-100)
mix(color1, color2) Blend two colors together
rotateHue(color, degrees) Shift around color wheel
fn setup() { size(200, 200) } fn draw() { background(white) // Original color fill(blue) circle(40, 100, 30) // Lighter version fill(lighten(blue, 30)) circle(100, 100, 30) // Darker version fill(darken(blue, 30)) circle(160, 100, 30) }

Color Harmony

Use color theory to find colors that look good together. These functions return colors that are harmonious with your starting color:

complement(color) Opposite on color wheel
analogous(color) Returns 3 similar colors
triadic(color) Returns 3 evenly spaced colors
splitComplement(color) Returns 3 balanced colors
fn setup() { size(200, 200) } fn draw() { background(white) let baseColor = coral // Original color fill(baseColor) circle(50, 100, 35) // Complementary (opposite) fill(complement(baseColor)) circle(110, 100, 35) // A triadic color let triad = triadic(baseColor) fill(triad[1]) circle(170, 100, 35) }

Color Palettes

Bloom includes pre-made color palettes - collections of colors that look good together. Perfect for quickly making beautiful art without picking colors yourself!

palette(name, index) Get a color from a named palette
paletteSize(name) How many colors in the palette
randomFromPalette(name) Pick a random color from palette
paletteNames() List all available palette names

Available palettes: sunset, ocean, forest, candy, neon, pastel, earth, jewel, autumn, winter, retro, midnight, tropical, nordic, desert, cherry, lavender, mint

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 40)) // Draw circles using the "sunset" palette for i in 0..paletteSize("sunset") { fill(palette("sunset", i)) circle(30 + i * 35, 60, 25) } // Draw with the "ocean" palette for i in 0..paletteSize("ocean") { fill(palette("ocean", i)) circle(30 + i * 35, 120, 25) } // Random colors from "neon" fill(randomFromPalette("neon")) circle(100, 170, 30) }

Styling

Shapes have two parts: the fill (inside color) and the stroke (outline). You can control both independently.

fn setup() { size(200, 200) } fn draw() { background(white) fill(yellow) stroke(orange) strokeWeight(4) circle(70, 100, 40) noFill() stroke(blue) strokeWeight(2) circle(130, 100, 40) }

Use noFill() to draw just the outline, or noStroke() to draw without an outline.

Remember: Styles Stay Until You Change Them

Once you set a style, it applies to ALL shapes drawn after it - until you change it again:

fill(red)
circle(50, 100, 20)   // Red
circle(100, 100, 20)  // Still red!

fill(blue)
circle(150, 100, 20)  // Now blue

noFill()
circle(200, 100, 20)  // No fill - just outline
circle(250, 100, 20)  // Still no fill!

Tip: If your shape colors seem wrong, check what style was set earlier in your code.

Text Styling

You can customize how text looks with a few functions:

textSize(pixels) Set font size (default is 12)
textFont(name) "serif", "sans-serif", or "monospace"
textAlign(align) "left", "center", or "right"
fn setup() { size(200, 200) } fn draw() { background(white) fill(navy) textSize(24) textFont("serif") text("Hello!", 100, 60) textSize(14) textFont("sans-serif") textAlign("center") text("Centered text", 100, 100) textSize(12) textFont("monospace") textAlign("right") text("Right aligned", 190, 140) }
Understanding Text Positioning

The y coordinate positions the baseline (bottom of letters like "a", not the top):

text("Hello", 10, 50)  // Baseline is at y=50, text appears ABOVE this line

textAlign() changes the x anchor point:

  • textAlign("left") - x is the left edge (default)
  • textAlign("center") - x is the center of the text
  • textAlign("right") - x is the right edge

Tip: If text seems too low, subtract from y. If centering on screen, use textAlign("center") with x = width / 2.

Beginner Variables

Variables let you store values and use them later. Create one with let and give it a name.

let x = 100 let radius = 60 fn setup() { size(200, 200) } fn draw() { background(white) fill(blue) circle(x, 100, radius) }

Try changing the values of x and radius at the top. Variables make it easy to adjust your sketch without hunting through all the code.

Where to Put Variables

Variables work differently depending on where you create them:

  • Outside functions - The variable keeps its value between frames. Perfect for animation (like tracking position).
  • Inside draw() - The variable starts fresh every frame. Good for calculations that depend on the current moment.
Common Mistake: Animation Not Moving?

If your animation is stuck, you probably put the variable inside draw():

Wrong - resets every frame
fn draw() {
  let x = 0  // Oops!
  x = x + 1
  circle(x, 100, 20)
}

Circle stays at x=1 forever

Correct - remembers value
let x = 0  // Outside!

fn draw() {
  x = x + 1
  circle(x, 100, 20)
}

Circle moves across screen

Why does this happen? Here's what the computer does:
With let x = 0 inside draw():
Frame 1: create x=0, add 1 makes x=1, draw at x=1
Frame 2: create x=0 again, add 1 makes x=1, draw at x=1
Frame 3: create x=0 again, add 1 makes x=1, draw at x=1
Result: stuck forever at x=1
With let x = 0 outside:
Start: create x=0 (once!)
Frame 1: add 1 makes x=1, draw at x=1
Frame 2: add 1 makes x=2, draw at x=2
Frame 3: add 1 makes x=3, draw at x=3
Result: x grows, circle moves!

Intermediate Animation

How Animation Works

Bloom creates animation like a flipbook. Your draw() function runs 60 times per second. Each time it runs is called a "frame":

Frame 1
x = 0
then
Frame 2
x = 2
then
Frame 3
x = 4
...

By changing values a little bit each frame, you create smooth motion!

Here's where it gets fun. Since draw() runs over and over, you can change values each frame to create motion.

let x = 0 fn setup() { size(200, 200) } fn draw() { background(white) fill(blue) circle(x, 100, 20) x = x + 2 if x > 200 { x = 0 } }

Each frame, we add 2 to x, making the circle move right. When it goes past the edge, we reset it to 0.

The Frame Counter

Bloom gives you a built-in variable called frame - a counter that starts at 0 and increases by 1 every time draw() runs.

How frame counts up over time:
0 sec
frame = 0
0.5 sec
frame = 30
1 sec
frame = 60
2 sec
frame = 120
...
keeps going
Since draw() runs 60 times per second, frame increases by 60 every second

Try adding text("Frame: " + frame, 10, 20) to any sketch to watch the counter grow!

Smooth Motion with sin() and cos()

Combined with sin() and cos(), the frame counter creates smooth, looping motion.

What are sin() and cos()?

Why use these instead of just counting? When you do x = x + 1, the value goes up forever and eventually leaves the screen. But sin() and cos() automatically loop back and forth - they never escape!

What sin() outputs over time:
+1 0 -1
time (frame)
The wave smoothly goes up to 1, back down to -1, and repeats forever
  • sin() and cos() output smooth waves that go back and forth between -1 and 1
  • They're perfect for making things swing, pulse, or orbit without writing reset logic
  • Multiply by a number to control how far it moves (e.g., * 50 means 50 pixels)
x = x + 1 Goes 0, 1, 2, 3... forever (escapes screen)
x = sin(frame * 0.05) * 50 Goes -50 to +50 and back, forever (stays on screen)
Why multiply by 0.05?

The frame counter increases by 1 every frame (60 times per second). That's too fast for smooth animation!

  • frame * 1 = very fast (completes a full cycle in ~6 frames)
  • frame * 0.1 = medium speed
  • frame * 0.05 = slow and smooth (completes a full cycle in ~125 frames, about 2 seconds)
  • frame * 0.01 = very slow

Try it: Change 0.05 to 0.2 in the example below to see faster motion!

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) // sin and cos together create circular motion let x = 100 + sin(frame * 0.05) * 50 let y = 100 + cos(frame * 0.05) * 50 fill(rgb(255, 150, 100)) circle(x, y, 20) }

Animation Helper Functions

Bloom provides shortcuts for common animation patterns. These are easier to use than raw sin() and cos() - just pick the one that matches the motion you want!

oscillate(min, max, speed) Smoothly swing between min and max
bounce(min, max, speed) Like oscillate but with bouncy easing
wave(min, max, speed) Smooth wave motion (similar to oscillate)
pulse(speed) Pulsing value from 0 to 1 and back
pingpong(t, length) Bounces between 0 and length
smoothstep(edge0, edge1, x) Smooth transition between two values
repeat(t, length) Loops value: 0 to length, then restarts
fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) // oscillate: smooth back and forth fill(coral) circle(oscillate(30, 170, 0.03), 40, 20) // bounce: snappy bouncing motion fill(cyan) circle(bounce(30, 170, 0.03), 80, 20) // pulse: size from 0-1 (multiply to scale) fill(lime) circle(100, 120, 10 + pulse(0.05) * 20) // pingpong: goes 0 to 100 and back fill(pink) circle(50 + pingpong(frame, 100), 160, 20) }

Easing Functions

Control how animations speed up and slow down. These take a value from 0 to 1 and return a modified value (also 0 to 1):

easeIn(t) Starts slow, speeds up (accelerate)
easeOut(t) Starts fast, slows down (decelerate)
easeInOut(t) Slow start and end, fast middle
spring(t, damping, stiffness) Bouncy spring motion
When to Use Easing

Easing makes animations feel more natural. In real life, things don't move at constant speed - they accelerate and decelerate.

  • easeIn - Use for objects "leaving" (zoom out, fade out)
  • easeOut - Use for objects "arriving" (zoom in, fade in, landing)
  • easeInOut - Use for continuous motion that feels organic
  • spring - Use for playful, bouncy UI interactions

Making Decisions

Programs can make choices using if. If a condition is true, the code inside the braces runs.

Comparing Values: Quick Reference
Math
+ add
- subtract
* multiply
/ divide
% remainder (makes numbers loop!)
Comparison
== equals
!= not equals
> greater than
< less than
>= greater or equal
<= less or equal
Logic
and both true
or either true
not opposite

Important: Use = to set a value, but == to compare. x = 5 sets x to 5. x == 5 asks "is x equal to 5?"

What's %? It gives the remainder after division. 7 % 3 = 1 (7 ÷ 3 = 2 remainder 1). Great for making values loop: frame % 60 goes 0, 1, 2... 59, 0, 1, 2... (restarts every 60).

Order of operations: * and / happen before + and -, just like in math class. 2 + 3 * 4 equals 14, not 20. Use parentheses to be clear: (2 + 3) * 4 = 20.

let x = 0 let speed = 2 fn setup() { size(200, 200) } fn draw() { background(white) if x > 100 { fill(red) } else { fill(blue) } circle(x, 100, 20) x = x + speed if x > 200 { x = 0 } }

The circle changes color when it passes the middle of the canvas.

Using else and else if

You can add more branches to your decisions:

  • else = "otherwise, do this instead"
  • else if = "otherwise, check this other condition"
if score > 90 {
  fill(green)      // Great score!
} else if score > 70 {
  fill(yellow)     // Good score
} else {
  fill(red)        // Needs improvement
}

How it works: The computer checks each condition from top to bottom. As soon as one is true, it runs that code and skips the rest. If none are true, it runs the else code (if you have one).

Intermediate Loops

Loops let you repeat things without writing the same code over and over. A for loop is perfect for drawing patterns.

Understanding Ranges

The .. symbol creates a range of numbers:

0..5
0 1 2 3 4 5
5 numbers total
Easy rule: Want 5 circles? Use 0..5. Want 10? Use 0..10.
The number after .. is exactly how many times it repeats!

Why start at 0? Computers count from 0, not 1. So 0..5 gives you: 0, 1, 2, 3, 4 (that's 5 numbers).
Watch Out: Range Edge Cases

Same start and end: 0..0 or 5..5 produces zero iterations (nothing happens).

for i in 5..5 { print(i) }  // Prints nothing - the range is empty!

Backwards range: 5..0 also produces zero iterations. Ranges only count up.

for i in 5..0 { print(i) }  // Prints nothing - can't count backwards!

// To count down, you can do math instead:
for i in 0..5 {
  let n = 4 - i  // n goes 4, 3, 2, 1, 0
  print(n)
}
fn setup() { size(200, 200) } fn draw() { background(white) for i in 0..5 { fill(blue) circle(40 + i * 30, 100, 20) } }

The loop runs 5 times. Each time, i takes the next value (0, then 1, then 2...). We use i to position each circle differently.

Nested Loops

Put a loop inside a loop to create grids. The inner loop runs completely for each step of the outer loop:

How nested loops run:
row=0: col=0, col=1, col=2, col=3, col=4
row=1: col=0, col=1, col=2, col=3, col=4
row=2: col=0, col=1, col=2, col=3, col=4
...and so on. This creates a 5x5 grid = 25 circles!
What is hsl()?

Another way to create colors using 3 numbers:

  • Hue (0-360) = which color on the rainbow: 0=red, 60=yellow, 120=green, 180=cyan, 240=blue, 300=purple, 360=red again
  • Saturation (0-100) = how colorful: 0=gray, 100=vivid
  • Lightness (0-100) = how bright: 0=black, 50=normal, 100=white

HSL is great for creating color gradients because changing just the hue cycles through the rainbow!

hsl() vs hsb() - What's the Difference?

Bloom also has hsb(h, s, b) which uses Brightness instead of Lightness:

  • hsl(0, 100, 50) = pure red (Lightness 50 = normal)
  • hsb(0, 100, 100) = pure red (Brightness 100 = full brightness)

When to use which? HSL is often easier because 50 lightness = normal color. HSB is common in design tools like Photoshop. Both work great - pick whichever feels more natural!

fn setup() { size(200, 200) } fn draw() { background(white) for row in 0..5 { for col in 0..5 { let x = 30 + col * 35 let y = 30 + row * 35 // Change hue based on position for rainbow effect fill(hsl(row * 40 + col * 20, 70, 60)) circle(x, y, 12) } } }

Looping Over Arrays

You can also loop directly over the items in an array using for item in array:

Two Ways to Loop Over Arrays

By index (when you need the position):

let colors = [red, green, blue]
for i in 0..len(colors) {
  fill(colors[i])  // i = 0, 1, 2
  circle(50 + i * 40, 100, 15)
}

By item (simpler when you just need the values):

let colors = [red, green, blue]
for color in colors {
  fill(color)  // color = red, then green, then blue
  circle(50, 100, 15)
}

Use for item in array when you just need each value. Use for i in 0..len(array) when you need the index number too.

The grid() Helper

For grids, Bloom provides a grid() helper function that handles the math for you:

grid(cols, rows, cellWidth, cellHeight, fn(col, row, x, y) {
  // col = column number (0, 1, 2...)
  // row = row number (0, 1, 2...)
  // x, y = center position of this cell
  circle(x, y, 10)
})
fn setup() { size(200, 200) } fn draw() { background(white) // Create a 5x4 grid with 40x50 pixel cells grid(5, 4, 40, 50, fn(col, row, x, y) { fill(hsl(col * 50 + row * 30, 70, 60)) circle(x, y, 15) }) }

grid() is a shortcut for nested loops. It calculates the center x,y position of each cell for you.

While Loops

A while loop keeps running as long as a condition is true. Use it when you don't know how many times you need to repeat:

let x = 0
while x < 200 {
  circle(x, 100, 10)
  x = x + 30  // Move right each time
}
// Draws circles until x reaches 200
for vs while - When to Use Each
  • for loop: When you know how many times to repeat. for i in 0..10
  • while loop: When you repeat until something happens. while x < 200

Most of the time, for loops are simpler. Use while when the number of repetitions depends on a condition.

Shortcut: x++ and x--

Instead of writing x = x + 1, you can use x++ as a shortcut:

let count = 0
count++      // Same as: count = count + 1 (now count is 1)
count++      // count is now 2
count--      // Same as: count = count - 1 (now count is 1)

Functions

When you find yourself writing the same code in multiple places, it's time to make a function. Functions bundle up code so you can reuse it.

fn setup() { size(200, 200) } fn draw() { background(white) flower(50, 50) flower(100, 120) flower(150, 70) } fn flower(x, y) { fill(pink) circle(x - 15, y, 12) circle(x + 15, y, 12) circle(x, y - 15, 12) circle(x, y + 15, 12) fill(yellow) circle(x, y, 10) }

Now you can draw flowers anywhere by just calling flower(x, y). Try adding more flowers or changing the flower function.

How Function Parameters Work

The names in parentheses (like x, y) are placeholders that take on different values each time you call the function:

flower(50, 50) Inside function: x=50, y=50
flower(100, 120) Inside function: x=100, y=120
flower(150, 70) Inside function: x=150, y=70

Think of it like a recipe: "Add [amount] of sugar" - the recipe works no matter what amount you use. x and y are like those blanks that get filled in when you call the function.

Returning Values

Functions can also send back (return) a value. Use return to give back the result:

fn setup() { size(200, 200) } fn draw() { background(white) // Our function calculates distance, then returns it let d = distance(0, 0, mouseX, mouseY) fill(black) text("Distance from corner: " + round(d), 20, 100) // Use the returned value to size a circle fill(coral) circle(100, 100, d / 3) } // This function RETURNS a number fn distance(x1, y1, x2, y2) { let dx = x2 - x1 let dy = y2 - y1 return sqrt(dx * dx + dy * dy) }
Return vs No Return

Functions that DO something (like flower) don't need return - they draw or change things.

Functions that CALCULATE something (like distance) use return to send back the answer.

Built-in functions like sqrt(), random(), and len() all return values. That's why you can write let x = random(100).

Arrays (Lists)

An array is a list that holds multiple values. Perfect for storing collections of things like positions, colors, or scores.

Creating Arrays

Use square brackets [ ] to create an array:

let colors = [red, blue, green]
let scores = [10, 25, 42, 8]
let empty = []  // An empty array

Accessing Items

Get items by their position (called "index"). Remember: counting starts at 0!

colors = [red, blue, green]
red colors[0]
blue colors[1]
green colors[2]
Watch Out: Invalid Index

If you try to access an index that doesn't exist, you'll get nil (nothing):

let colors = [red, blue, green]  // Has indices 0, 1, 2
colors[5]   // nil - index 5 doesn't exist!
colors[-1]  // nil - negative indices don't work

To get the last item: Use array[len(array) - 1]

let colors = [red, blue, green]
let last = colors[len(colors) - 1]  // green (the last item)

Array Functions

len(array) How many items? len(colors) → 3
push(array, item) Add to end: push(colors, yellow)
pop(array) Remove last item and return it
choose(array) Pick a random item
shuffle(array) Randomize the order
includes(array, item) Check if item exists (true/false)
indexOf(array, item) Find position of item (-1 if not found)
reverse(array) Flip the order
sort(array) Put in order (numbers or alphabetical)
Generating Number Arrays with range()

Create arrays of numbers automatically instead of typing them out:

range(5)        // [0, 1, 2, 3, 4] - 5 numbers starting at 0
range(2, 6)     // [2, 3, 4, 5] - from 2 up to (not including) 6
range(0, 10, 2) // [0, 2, 4, 6, 8] - count by 2s

Great for loops: for i in range(5) is the same as for i in 0..5

How push() and pop() Work

push() and pop() modify the original array - they don't create a new one:

let nums = [1, 2, 3]
push(nums, 4)       // nums is now [1, 2, 3, 4]

let last = pop(nums)  // last = 4, nums is now [1, 2, 3]

Note: push() returns nothing (nil). To add and get the new array, just use push then use the array: push(arr, item) then use arr.

let sizes = [10, 20, 30, 40, 50] fn setup() { size(200, 200) } fn draw() { background(white) // Loop through the array for i in 0..len(sizes) { fill(hsl(i * 60, 70, 60)) circle(40 + i * 35, 100, sizes[i]) } }

Building Arrays Dynamically

You can create an empty array and add items as your program runs:

let trail = [] fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 40)) // Add current mouse position to the end push(trail, {x: mouseX, y: mouseY}) // Keep only the last 20 positions // We trim from the start by rebuilding the array if len(trail) > 20 { let newTrail = [] for i in (len(trail) - 20)..len(trail) { push(newTrail, trail[i]) } trail = newTrail } // Draw trail - older positions are smaller/fainter for i in 0..len(trail) { let pos = trail[i] let progress = i / len(trail) // 0 = oldest, 1 = newest fill(rgba(255, 150, 100, progress)) circle(pos.x, pos.y, 3 + progress * 8) } }

Objects (Grouped Data)

An object lets you group related values together with labels. Think of it as a labeled container - instead of numbered slots (like arrays), each value has a name.

Arrays vs Objects

Array: Values accessed by number (position)

let colors = ["red", "blue", "green"]
colors[0]  // "red" - first item

Object: Values accessed by name (label)

let ball = {x: 100, y: 50, size: 20}
ball.x     // 100 - the x value

Creating Objects

Use curly braces { } with name: value pairs:

let player = {
  x: 100,
  y: 100,
  speed: 5,
  color: blue
}

Accessing Values

Use a dot . followed by the property name:

let ball = {x: 100, y: 100, size: 30} fn setup() { size(200, 200) } fn draw() { background(white) fill(coral) circle(ball.x, ball.y, ball.size) }

Changing Values

You can update object properties just like variables:

let ball = {x: 100, y: 100} fn setup() { size(200, 200) } fn draw() { background(white) // Move ball toward mouse ball.x = ball.x + (mouseX - ball.x) * 0.1 ball.y = ball.y + (mouseY - ball.y) * 0.1 fill(teal) circle(ball.x, ball.y, 25) }

Adding New Properties

You can add new properties to an object at any time:

let ball = {x: 100, y: 100}
ball.speed = 5    // adds a new "speed" property
ball.color = red  // adds a new "color" property

When to Use Objects

Objects are perfect when you have related data that belongs together:

  • Position: {x: 100, y: 50}
  • Circle: {x: 100, y: 50, radius: 20, color: red}
  • Player: {x: 0, y: 0, speed: 5, score: 0}

Strings (Text)

A string is text wrapped in quotes. You can combine, split, and transform strings.

Creating Strings

let name = "Bloom"
let greeting = "Hello, world!"

String Interpolation

Put variables inside strings using {curly braces}:

let name = "Artist" let score = 42 print("Hello, {name}!") print("Your score is {score} points")
How String Interpolation Works

When Bloom sees {something} inside quotes, it replaces it with the value of that variable:

let x = 100
let y = 50
print("Position: {x}, {y}")  // Prints: Position: 100, 50

You can even put expressions inside the braces:

let a = 5
print("Double: {a * 2}")  // Prints: Double: 10

Combining Strings

Use + to join strings together:

let first = "Hello"
let second = "World"
let message = first + " " + second  // "Hello World"
Which should I use: {braces} or + ?
Use {braces} when:
  • Putting a variable inside a sentence
  • Building messages with multiple values
"Score: {score} points"
Use + when:
  • Combining separate string variables
  • Building strings piece by piece
firstName + " " + lastName

Both work! Pick whichever feels clearer to you.

Mixing Strings and Numbers

When you use + with a string, numbers automatically become text:

"Score: " + 42       // "Score: 42"
"Position: " + x + ", " + y  // "Position: 100, 50"

Watch out: + means different things based on context:

5 + 3           // 8 (math - both are numbers)
"5" + "3"       // "53" (text - joins strings)
"Score: " + 5   // "Score: 5" (text - number becomes string)

Accessing Characters

Get individual characters using bracket notation, just like arrays:

let word = "BLOOM"
word[0]   // "B" - first character (index 0)
word[1]   // "L" - second character
word[4]   // "M" - fifth character (index 4)
Strings are Read-Only

You can read characters, but you cannot change them directly:

let word = "hello"
word[0]         // "h" - this works (reading)
word[0] = "H"   // ERROR - can't modify strings like this

Instead: Build a new string using concatenation or string functions.

String Functions

len(string) How many characters? len("hello") → 5
upper(string) UPPERCASE: upper("hi") → "HI"
lower(string) lowercase: lower("HI") → "hi"
trim(string) Remove spaces from ends
split(string, separator) Break into array: split("a,b,c", ",") → ["a", "b", "c"]
join(array, separator) Combine array: join(["a","b"], "-") → "a-b"
fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) let message = "BLOOM" let letters = split(message, "") // Draw each letter with different color for i in 0..len(letters) { fill(hsl(i * 60, 80, 60)) text(letters[i], 30 + i * 35, 110) } }

Mouse & Touch

Bloom tracks the mouse position in two variables: mouseX and mouseY. You can also check if the mouse button is pressed with mousePressed.

On Phones & Tablets

Your finger works just like a mouse:

  • mouseX and mouseY = where your finger is touching
  • mousePressed = true while your finger is on the screen
  • Drag your finger to "draw" or move things around
fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) if mousePressed { fill(red) } else { fill(blue) } circle(mouseX, mouseY, 30) }

Move your mouse (or finger) over the canvas. Tap or click to change the color.

Drawing App

Here's a simple drawing program. Notice we removed background() from draw, so each frame builds on the last.

fn setup() { size(200, 200) background(white) } fn draw() { if mousePressed { fill(blue) noStroke() circle(mouseX, mouseY, 8) } }

Multi-Touch (Advanced)

For tablets and phones, Bloom can track multiple fingers at once using the touches array. Each touch point has x, y, and id properties.

On desktop with a mouse, touches contains one point when clicking and is empty otherwise. Use mouseX/mouseY for desktop mouse tracking.

Touch Variables
touches Array of all current touch points: [{x: 100, y: 50, id: 0}, ...]
touchCount How many fingers are currently touching the screen
Understanding the touches Array

Each item in touches is an object with three properties you access with a dot:

for touch in touches {
  touch.x   // x position of this finger
  touch.y   // y position of this finger
  touch.id  // unique number for tracking this finger
}

The .id stays the same for a finger as it moves, useful for tracking individual fingers.

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) // Draw a circle for each finger for touch in touches { fill(rgb(255, 150, 100)) circle(touch.x, touch.y, 30) } // Show how many fingers are touching fill(white) text("Fingers: " + touchCount, 10, 20) }

Try this on a phone or tablet with multiple fingers. Each finger creates its own circle.

Keyboard

Use keyPressed to check if any key is down, and key to see which key it is.

let x = 100 let y = 100 fn setup() { size(200, 200) } fn draw() { background(white) if keyPressed { if key == "ArrowUp" { y = y - 3 } if key == "ArrowDown" { y = y + 3 } if key == "ArrowLeft" { x = x - 3 } if key == "ArrowRight" { x = x + 3 } } fill(green) circle(x, y, 20) }

Tap or click on the canvas first to give it focus, then use arrow keys to move. On mobile? Keyboard examples work best with a Bluetooth keyboard. Try the touch-friendly mouse examples above, or use the code keyboard button { } in the bottom-right to type code.

Advanced Noise

Perlin noise is a special kind of randomness that flows smoothly. Unlike random() which jumps around, noise() creates organic, natural-looking variation. It's perfect for terrain, clouds, textures, and organic movement.

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) for i in 0..40 { let x = i * 5 let n = noise(i * 0.1 + frame * 0.02) let y = 100 + n * 80 - 40 fill(hsl(200 + n * 60, 80, 60)) circle(x, y, 4) } }

The noise() function takes 1 to 3 numbers and returns a value between 0 and 1. Nearby inputs give similar outputs, which is what makes it flow smoothly.

fn setup() { size(200, 200) } fn draw() { background(white) for y in 0..20 { for x in 0..20 { let n = noise(x * 0.2, y * 0.2, frame * 0.01) fill(hsl(0, 0, n * 100)) rect(x * 10, y * 10, 10, 10) } } }

Color Palettes

Choosing colors that work well together is hard. Bloom includes several curated color palettes you can use right away with palette().

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) for i in 0..5 { fill(palette("sunset", i)) circle(30 + i * 35, 60, 15) } for i in 0..5 { fill(palette("ocean", i)) circle(30 + i * 35, 100, 15) } for i in 0..5 { fill(palette("forest", i)) circle(30 + i * 35, 140, 15) } }

Available palettes:

sunset
ocean
forest
fire
pastel
neon

Blending Colors

Use lerpColor() to smoothly blend between two colors.

fn setup() { size(200, 200) } fn draw() { background(white) for i in 0..10 { let t = i / 9 let c = lerpColor("#ff6b6b", "#4ecdc4", t) fill(c) rect(i * 20, 80, 20, 40) } }

Complex Shapes

Beyond basic shapes, Bloom provides polygon() and star() for more interesting geometry.

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) fill(rgb(100, 200, 255)) polygon(60, 100, 30, 6) fill(rgb(255, 200, 100)) polygon(140, 100, 30, 8) }

polygon(x, y, radius, sides) draws a regular polygon. Try different numbers of sides: 3 for triangle, 5 for pentagon, 6 for hexagon.

Stars

fn setup() { size(200, 200) } fn draw() { background(rgb(20, 20, 40)) fill(rgb(255, 220, 100)) star(60, 100, 35, 15, 5) fill(rgb(255, 150, 200)) star(140, 100, 35, 20, 8) }

star(x, y, outerRadius, innerRadius, points) draws a star shape. The inner radius determines how pointy the star is.

Arcs and Curves

For more complex shapes, Bloom provides arcs and bezier curves:

arc(x, y, w, h, start, stop) Draw part of an ellipse. Angles in radians (0 to TWO_PI)
bezier(x1, y1, cx1, cy1, cx2, cy2, x2, y2) Smooth curve from (x1,y1) to (x2,y2) with control points
fn setup() { size(200, 200) } fn draw() { background(white) // Arc: partial circle (like a pie slice) fill(coral) arc(60, 100, 80, 80, 0, PI) // Half circle // Bezier curve: smooth S-shape noFill() stroke(blue) strokeWeight(3) bezier(120, 50, 180, 50, 120, 150, 180, 150) }

Arcs use radians: PI = half circle, TWO_PI = full circle. Bezier curves need practice - the control points (cx1, cy1, cx2, cy2) shape the curve.

Advanced Easing and Motion

Real things don't move at constant speed. They accelerate, decelerate, and bounce. Bloom provides easing functions and oscillators to create natural-feeling motion.

Animation Functions Quick Reference
oscillate(min, max) Smooth back-and-forth between two values
bounce(min, max) Like oscillate but with a bouncy feel
pulse(speed) Returns 0-1 pulsing value for effects
wave(freq, amp) Sine wave for wavy motion effects
spring(target) Bouncy motion toward a target value
pingpong(t, length) Value bounces between 0 and length

Oscillators: Automatic Movement

These functions create automatic movement that changes over time (based on the frame count). You don't need to manage any state - just call them and they animate!

oscillate(min, max, speed?, offset?)

Smoothly moves between min and max. The optional speed controls how fast (default: 1), and offset shifts the starting point.

fn setup() { size(200, 200) } fn draw() { background(white) let size1 = oscillate(20, 50) let size2 = oscillate(20, 50, 1.5) let size3 = oscillate(20, 50, 2, 1) fill(red) circle(50, 100, size1) fill(green) circle(100, 100, size2) fill(blue) circle(150, 100, size3) }

bounce(min, max, speed?)

Like oscillate but uses absolute sine for a bouncy, elastic feel:

fn setup() { size(200, 200) } fn draw() { background(white) // Bouncing ball effect let y = bounce(50, 150, 2) fill(orange) circle(100, y, 25) }

pulse(speed?)

Returns a value from 0 to 1 that pulses over time. Perfect for fading effects:

fn setup() { size(200, 200) } fn draw() { background(white) // Pulsing opacity let p = pulse(1) let alpha = p * 255 fill(rgba(100, 150, 255, alpha)) circle(100, 100, 60) }

wave(frequency?, amplitude?, offset?)

A sine wave returning values from -amplitude to +amplitude. Great for wavy motion:

fn setup() { size(200, 200) } fn draw() { background(white) noStroke() // Draw wavy dots for i in 0..10 { let x = 20 + i * 18 let y = 100 + wave(1, 30, i * 0.5) fill(coral) circle(x, y, 12) } }

spring(target, stiffness?, damping?)

Creates a bouncy spring motion toward a target value. Higher stiffness = faster spring, higher damping = less bounce:

fn setup() { size(200, 200) } fn draw() { background(white) // Spring toward 100 let x = spring(100, 0.15, 0.9) fill(purple) circle(100, x, 30) }

Utility Functions

pingpong(t, length)

Takes a value t and bounces it between 0 and length. Useful with frame count:

fn setup() { size(200, 200) } fn draw() { background(white) // pingpong makes frame bounce 0->100->0->100... let x = pingpong(frame, 100) fill(teal) circle(50 + x, 100, 25) }

repeat(t, length)

Wraps a value between 0 and length (like modulo but always positive):

fn setup() { size(200, 200) } fn draw() { background(white) // Repeating position let x = repeat(frame * 2, 200) fill(coral) circle(x, 100, 20) }

smoothstep(edge0, edge1, x)

Returns 0 if x < edge0, 1 if x > edge1, and a smooth S-curve in between. Perfect for smooth transitions:

fn setup() { size(200, 200) } fn draw() { background(white) // Smooth transition based on mouseX let t = smoothstep(50, 150, mouseX) // Interpolate color and size let r = 50 + t * 200 let size = 20 + t * 40 fill(rgb(r, 100, 150)) circle(100, 100, size) }

Easing Functions

Easing functions transform linear motion (0 to 1) into more interesting curves. Pass a value from 0-1 and get back a curved version.

easeIn(t) Starts slow, ends fast (accelerates)
easeOut(t) Starts fast, ends slow (decelerates)
easeInOut(t) Smooth start and end (S-curve)
fn setup() { size(200, 200) } fn draw() { background(white) let t = (frame % 120) / 120 let linear = t * 160 let easeI = easeIn(t) * 160 let easeO = easeOut(t) * 160 let easeIO = easeInOut(t) * 160 stroke(gray) strokeWeight(1) line(20, 40, 20, 160) line(180, 40, 180, 160) noStroke() fill(red) circle(20 + linear, 50, 8) fill(green) circle(20 + easeI, 90, 8) fill(blue) circle(20 + easeO, 130, 8) fill(purple) circle(20 + easeIO, 170, 8) }

Advanced Transforms

Transforms let you move, rotate, and scale your entire drawing system. Instead of calculating complex positions, you can move the "paper" itself.

How Transforms Work

Imagine your canvas is a piece of paper:

  • translate(x, y) = slide the paper to a new position
  • rotate(angle) = spin the paper around
  • scale(x, y) = zoom in (values > 1) or out (values < 1). Use scale(2, 2) to double size

Important: After a transform, all drawing happens relative to the new position/rotation!

Translate (Move)

translate() moves the origin point. This is useful for drawing complex shapes around a center point:

fn setup() { size(200, 200) } fn draw() { background(white) // Move origin to center of canvas translate(100, 100) // Now (0, 0) is at the center! fill(blue) circle(0, 0, 30) // These draw relative to center fill(red) circle(50, 0, 15) circle(-50, 0, 15) }

Rotate

rotate(angle) spins everything around the current origin. Angles are in radians (PI = 180 degrees, TWO_PI = 360 degrees):

Watch Out: Radians, Not Degrees

rotate(45) does NOT rotate 45 degrees - it rotates 45 radians (about 7 full circles)!

  • rotate(PI / 4) = 45 degrees
  • rotate(PI / 2) = 90 degrees
  • rotate(PI) = 180 degrees
  • rotate(TWO_PI) = 360 degrees (full circle)

Quick formula: degrees * PI / 180 converts to radians.

fn setup() { size(200, 200) } fn draw() { background(white) // Move to center, then rotate translate(100, 100) rotate(frame * 0.02) // This rectangle spins around the center fill(coral) rect(-40, -10, 80, 20) }

Push and Pop (Saving Transform State)

Use pushMatrix() and popMatrix() to save and restore your transform state. Think of it like a stack of saved positions:

Transform State Stack
Origin at (100, 100)
Origin at (50, 50)
Original: Origin at (0, 0)
pushMatrix() saves current state to stack
popMatrix() restores most recent saved state

Each pushMatrix() adds to the stack. Each popMatrix() removes the top item and restores that saved position/rotation/scale.

fn setup() { size(200, 200) } fn draw() { background(white) // Draw first rotating shape pushMatrix() translate(60, 100) rotate(frame * 0.03) fill(blue) rect(-20, -20, 40, 40) popMatrix() // Back to normal! Draw second shape pushMatrix() translate(140, 100) rotate(-frame * 0.02) fill(coral) rect(-25, -25, 50, 50) popMatrix() }
Common Mistake: Forgetting push/pop

Without push/pop, transforms "stack up" and affect everything after:

// Without push/pop (PROBLEM!)
translate(50, 50)   // Move right and down
circle(0, 0, 20)    // Circle at (50, 50) - OK!

translate(50, 50)   // Move MORE right and down
circle(0, 0, 20)    // Circle at (100, 100) - Wait, why so far?

With push/pop (FIXED!):

pushMatrix()
translate(50, 50)
circle(0, 0, 20)    // Circle at (50, 50)
popMatrix()         // Reset! Back to (0, 0)

pushMatrix()
translate(50, 50)
circle(0, 0, 20)    // Circle at (50, 50) - Correct!
popMatrix()

Think of it like bookmarks: pushMatrix() = save my place, popMatrix() = go back to saved place.

Grids

Grids are essential for patterns, tiles, and organized layouts. Bloom offers two ways to create grids: nested loops (flexible) or the grid() function (simpler).

The grid() Function

The easiest way to draw a grid! Just tell it how many columns and rows, the cell size, and what to draw in each cell:

grid(cols, rows, cellWidth, cellHeight, callback)

Your callback function receives 4 values:

  • col = which column (0, 1, 2...)
  • row = which row (0, 1, 2...)
  • x = center x position for this cell
  • y = center y position for this cell
fn setup() { size(200, 200) } fn draw() { background(white) // 5 columns, 5 rows, 40px wide, 40px tall grid(5, 5, 40, 40, fn(col, row, x, y) { let hue = (col + row) * 30 fill(hsl(hue, 70, 60)) circle(x, y, 15) }) }

Notice how you don't need to calculate positions - x and y are automatically set to the center of each cell!

Interactive Grid

Use the callback parameters to create interactive effects:

fn setup() { size(200, 200) } fn draw() { background(rgb(30, 30, 50)) grid(8, 8, 25, 25, fn(col, row, x, y) { // Distance from mouse let d = dist(x, y, mouseX, mouseY) let size = max(3, 20 - d * 0.15) fill(coral) circle(x, y, size) }) }

Using Nested Loops

For more control, use nested for loops. You calculate positions yourself, but get complete flexibility:

fn setup() { size(200, 200) } fn draw() { background(white) for row in 0..5 { for col in 0..5 { let hue = (col + row) * 30 fill(hsl(hue, 70, 60)) circle(20 + col * 40, 20 + row * 40, 15) } } }

Animated Grid

Combined with noise and animation, grids can create mesmerizing effects:

fn setup() { size(200, 200) } fn draw() { background(rgb(20, 20, 40)) grid(8, 8, 25, 25, fn(col, row, x, y) { let n = noise(col * 0.3, row * 0.3, frame * 0.02) let r = 5 + n * 15 fill(palette("ocean", floor(n * 5))) circle(x, y, r) }) }

Debugging Your Code

When something isn't working, here's how to figure out what's wrong:

Use print() to See Values

The most powerful debugging tool is print(). It shows values in the console (bottom panel).

Debugging Strategy

Step 1: Add print() to see what values your variables actually have:

let x = 100
print("x is: " + x)       // Shows: x is: 100
print("mouseX: " + mouseX) // Shows mouse position

Step 2: Check if your code is running at all:

fn draw() {
  print("draw is running!")  // If you see this, draw() is working
  // ... rest of code
}

Step 3: Print inside conditions to see which branch runs:

if x > 100 {
  print("x is big")
} else {
  print("x is small")
}

Common Debugging Checks

print(x) Show a variable's value
print("At line 5") Check if code reaches this point
print(len(array)) Check array length
print(mouseX + ", " + mouseY) Check multiple values at once
Tip: Too Many Prints?

Since draw() runs 60 times per second, you might flood the console with messages. To print less often, use the frame counter:

// Only print every 60 frames (once per second)
if frame % 60 == 0 {
  print("x is: " + x)
}

Troubleshooting

Stuck? Don't worry - everyone makes mistakes when learning to code! Here are solutions to common problems:

Nothing appears on the canvas
  • Make sure you have both fn setup() and fn draw()
  • Check that size() is called inside setup
  • Your shape might be off-screen - try position (100, 100)
  • Add background(white) at the start of draw
Error: "Undefined variable"
  • Check for typos in the variable name
  • Make sure you declared it with let name = value
  • Variables are case-sensitive: myVar is different from myvar
Error: "Expected something"
  • Check for missing closing brackets } or parentheses )
  • Make sure strings have matching quotes: "text"
  • Count your opening and closing braces - they should match!
Animation isn't working
  • Use frame in your calculations - it increases each frame
  • Don't set variables to fixed values inside draw()
  • Make sure you're using draw() not just setup()
Colors aren't showing
  • Call fill(color) before drawing the shape
  • Check for noFill() - it disables fill color
  • Try a named color like coral or blue
Program runs too slowly
  • Reduce the number of shapes being drawn
  • Use smaller loop ranges (e.g., 0..50 instead of 0..500)
  • Avoid nested loops with large ranges
Shape is off-screen (invisible)
  • Check x is between 0 and canvas width (e.g., 0-200)
  • Check y is between 0 and canvas height (e.g., 0-200)
  • Use print(x) to see where your shape actually is
  • Remember: (0, 0) is top-left, not center!
Error: "Unknown function"
  • Check spelling: cricle should be circle
  • Function names are case-sensitive: Circle won't work
  • Make sure custom functions are defined with fn name() { }

Still stuck? Tap or hover over keywords in the editor to see explanations, check the Glossary below, or try simplifying your code to find where the problem is.

Using Bloom on Different Devices

Bloom works on phones, tablets, and computers. Here are some tips for each:

On a Phone or Tablet
  • Use the code keyboard: Tap the { } button at the bottom right to open a special keyboard with programming symbols like parentheses, brackets, and operators
  • Tap to position cursor: Tap exactly where you want to type in the code editor
  • Scroll horizontally: Some code examples are wider than the screen - swipe left/right to see everything
  • Use landscape mode: Rotating your device gives you more space to see both code and canvas
  • Prev/Next navigation: Use the section links at the bottom of each section to move through the docs
On a Computer
  • Hover for hints: Move your mouse over keywords to see what they do
  • Resize the canvas: Drag the divider between code and canvas to adjust the view
Keyboard Shortcuts
Shift + Enter Run your code
Ctrl/Cmd + Z Undo
Ctrl/Cmd + S Save (code auto-saves, but runs too)
Tab Indent code
Works Everywhere
  • Auto-save: Your code is saved automatically - come back anytime to continue
  • Dark/Light mode: Click the theme button in the corner to switch
  • Works offline: Once loaded, Bloom works without an internet connection

Glossary

New to programming? Here's what these words mean:

Variable
A named container that stores a value. Like a labeled box. let x = 10
Function
A reusable recipe of code. fn myRecipe() { ... } defines it, myRecipe() uses it.
Loop
Code that runs multiple times. for i in 0..5 repeats 5 times.
Condition
A yes/no question. x > 100 is true when x is bigger than 100.
Parameter
A value you give to a function. In circle(100, 50, 20), the numbers are parameters.
Frame
One picture in an animation. Bloom draws 60 frames per second.
Canvas
The drawing area where your shapes appear. Set its size with size(w, h).
Coordinates
Two numbers (x, y) that describe a position. (0, 0) is the top-left corner.
Array
A list of values accessed by position (0, 1, 2...). [red, blue, green]
Object
A container with labeled values. {x: 100, y: 50} - access with .x or .y
Property
A labeled value inside an object. In ball.x, the x is the property.
Return Value
The answer a function gives back. return x + y sends the sum back to where the function was called.
nil
Means "nothing" or "no value yet". Different from "" (empty string - still a string, just with no characters) and 0 (a number, not nothing). You get nil when accessing an array index that doesn't exist, or using a variable that hasn't been set.
String
Text in quotes. "Hello" is a string. Use + to join strings together.
Boolean
A yes/no value: either true or false. Used in conditions like if x > 10.
Expression
Any code that produces a value. 5 + 3, x * 2, and sin(angle) are all expressions.
Operator
A symbol that does something with values. +, -, *, /, ==, and, or are operators.
Index
A number that picks an item from an array. colors[0] gets the first item (indexes start at 0).
Syntax
The rules for how code must be written. Like grammar for programming. Wrong syntax = error message.

Understanding Error Messages

When something goes wrong, Bloom tries to tell you what happened. Here's what those messages mean:

"Undefined variable"
You used a name that Bloom doesn't recognize. Check spelling, or make sure you declared it with let first.
"Expected ')'"
You opened a parenthesis ( but forgot to close it. Count your opening and closing parentheses - they should match.
"Expected '}'"
You opened a curly brace { but forgot to close it. Every { needs a matching }.
"Unexpected token"
Bloom found something it didn't expect. Often a typo, missing comma, or symbol in the wrong place.
"Wrong number of arguments"
You gave a function too many or too few values. For example, circle() needs 3 values: x, y, and radius.
"Cannot call ... as a function"
You're trying to use something as a function that isn't one. Maybe you put () after a variable name by mistake.
Example: Fixing a Missing Brace
Problem:
fn draw() {
  circle(100, 100, 40)
// Missing closing }
Fixed:
fn draw() {
  circle(100, 100, 40)
}  // Added closing brace

Tip: Every { needs a matching }. If you're lost, try adding } at the end of your code and see if the error changes.

Reference

Here's a quick reference of everything Bloom offers.

Canvas

  • size(w, h) - Set canvas size
  • background(color) - Clear with color

Shapes

  • circle(x, y, r) - Circle at x,y with radius r
  • ellipse(x, y, w, h) - Oval at x,y with width/height
  • rect(x, y, w, h) - Rectangle from top-left corner
  • line(x1, y1, x2, y2) - Line between two points
  • triangle(x1, y1, x2, y2, x3, y3) - Triangle with 3 corners
  • quad(x1, y1, x2, y2, x3, y3, x4, y4) - Quadrilateral with 4 corners
  • point(x, y) - Single pixel point
  • text(str, x, y) - Draw text at position

Text Styling

  • textSize(size) - Set font size in pixels
  • textFont(name) - Set font ("serif", "sans-serif", "monospace")
  • textAlign(align) - Alignment ("left", "center", "right")

Styling

  • fill(color), stroke(color), strokeWeight(w), noFill(), noStroke()

Colors

  • rgb(r, g, b) - RGB color (0-255 each)
  • rgba(r, g, b, a) - RGB + alpha (a: 0-1)
  • hsb(h, s, b) - Hue (0-360), Saturation (0-100), Brightness (0-100)
  • hsl(h, s, l) - Hue, Saturation, Lightness
  • Named: white, black, red, green, blue, yellow, orange, purple, pink, cyan, gray, navy, teal, coral, gold, mint, sky, violet, salmon, crimson, emerald, midnight

Variables

  • width, height - Canvas size
  • frame - Current frame number (starts at 0)
  • mouseX, mouseY - Mouse/touch position
  • mousePressed - True when mouse button or finger is down
  • key, keyPressed - Current key and whether any key is pressed
  • touches, touchCount - Multi-touch points array and count

Math

  • sin, cos, tan, atan2(y, x), sqrt, pow, abs, floor, ceil, round, min, max
  • random(), random(max), random(min, max), noise(x, y?, z?)
  • map(val, s1, e1, s2, e2), lerp(a, b, t), dist(x1, y1, x2, y2), clamp(val, min, max)
  • Constants: PI, TWO_PI, HALF_PI

Transforms

  • translate(x, y), rotate(angle), scale(x, y), pushMatrix(), popMatrix()

Language

  • let x = value, fn name(args) { }, if cond { } else { }
  • for i in 0..n { }, while cond { }, [1, 2, 3], return val
  • x++, x-- - Increment/decrement
  • "Hello {name}!" - String interpolation

Advanced Shapes

  • polygon(x, y, r, sides), star(x, y, r1, r2, pts), quad(x1,y1,x2,y2,x3,y3,x4,y4)
  • arc(x, y, w, h, start, stop), bezier(...)

Animation

  • oscillate(min, max, speed?, offset?), bounce(min, max, speed?), pulse(speed?)
  • easeIn(t), easeOut(t), easeInOut(t), smoothstep(e0, e1, x)

Grid Helper

  • grid(cols, rows, cellW, cellH, fn(col, row, x, y) { }) - Call function for each cell

Color Helpers

  • palette(name, i) - Get color i from named palette
  • lerpColor(c1, c2, t) - Blend between colors (t: 0-1)
  • complement(c) - Opposite color on wheel
  • analogous(c) - Array of 3 similar colors
  • triadic(c) - Array of 3 evenly spaced colors
  • splitComplement(c) - Array of 3 balanced colors
  • lighten(c, amt), darken(c, amt) - Adjust brightness (0-100)
  • saturate(c, amt), desaturate(c, amt) - Adjust vividness (0-100)
  • rotateHue(c, deg) - Shift color on wheel
  • mix(c1, c2) - Blend two colors equally

Palettes

  • sunset, ocean, forest, fire, pastel, mono, neon, earth, nordic, japan, miami, cyberpunk, vintage

More Colors

  • rose, tomato, amber, honey, jade, sage, moss, lime, azure, cobalt, indigo, sapphire, turquoise, aqua, seafoam, lavender, plum, grape, orchid, fuchsia, blush, peach, slate, charcoal, silver, cream, ivory, beige, sand, taupe, brown, coffee, snow, cloud, mist, fog, shadow

Utilities

  • print(val), len(arr), push(arr, val), pop(arr), constrain(val, min, max)

Array Functions

  • choose(arr) - Pick random element
  • shuffle(arr) - Randomly reorder
  • range(end), range(start, end), range(start, end, step) - Generate sequence
  • includes(arr, val), indexOf(arr, val), reverse(arr), sort(arr)
  • join(arr, sep) - Join into string

String Functions

  • split(str, sep) - Split into array
  • upper(str), lower(str), trim(str)

Angle Conversion

  • radians(deg) - Convert degrees to radians
  • degrees(rad) - Convert radians to degrees

Coming Soon

Features we're planning to add:

  • IDE Support - VS Code extension with syntax highlighting, autocomplete, and inline error hints
  • LSP Server - Language Server Protocol for editor integration
  • Image Loading - loadImage() and image() functions
  • Sound - Audio synthesis and playback
  • Particle Systems - Built-in particle emitters
  • Vector Math - vec2 and vec3 types with operators
  • Gradients - Linear and radial gradient fills
  • Sprites - Sprite sheets and animation
  • Video Export - Save animations as video (WebM/MP4)
  • Libraries - Import community-created libraries