Skip to content

Flow-field print

Flow-field print

Run it here, in your browser

Hundreds of tiny walkers cross the page, and each one leaves a thin line. A hidden map of noise tells every walker which way to turn, so the lines flow side by side like wind, water or grain in wood. Overlapping lines mix their colours. Use the sliders under the canvas to change the swirl, the number of lines and the colours. Press R for a new pattern. Press P (or the "save PDF" button) to write flow_field.pdf: a print for an A3 sheet, made of lines and not of dots, so it stays sharp at any size.

How it works

  • A noise field gives an angle at every point: f.noise(x * scale, y * scale) is a number from 0 to 1, and times 720 it becomes a direction. A small scale makes wide, slow swirls. A big scale makes tight ones.
  • Each walker holds its place in an f.Vector. Every step, f.Vector.from_angle() makes a short arrow that points the way the field says, and .add() moves the walker along it.
  • Each walker draws its short step as a line on the "trails" layer, using with f.layer("trails"):. The layer keeps what was drawn, so each frame only adds the new steps, and the canvas under it holds the paper and the frame.
  • f.blend_mode("screen") or f.blend_mode("multiply") makes overlapping lines mix their colours. Light lines on dark paper use "screen". Dark lines on light paper use "multiply".
  • Every walker also keeps its list of points. To print, the sketch makes a picture the size of an A3 page (f.page_size("A3", landscape=True)) and draws each walker once, as one line made with begin_shape() and vertex(). Joining the steps into one line keeps the PDF small: about one line for each walker, and not one for every step.
  • f.noise_seed() and f.random_seed() make each pattern repeatable, and R adds one to the seed.

Make it yours

  • Change the 720 in angle_at(): 360 makes gentler curves, 1440 makes busier ones.
  • Add a palette to PALETTES: a name, a paper colour, three line colours and "screen" or "multiply". Then raise the palette slider's top value.
  • Change STEP (the length of each step) and LENGTHS (how many steps a walker takes) for long, smooth lines or short, sharp ones.
  • Change the line colour's last number, the 90 in PALETTES, to make the lines fainter or stronger.
  • Make walkers start in a circle and not all over the page: use f.Vector.from_angle() in new_walkers().

The code

Copy it into a file and run it.

"""Flow-field print

Hundreds of tiny walkers cross the page, and each one leaves a thin line. A hidden map of noise tells
every walker which way to turn, so the lines flow side by side like wind, water or grain in wood.
Overlapping lines mix their colours. Use the sliders under the canvas to change the swirl, the number
of lines and the colours. Press R for a new pattern. Press P (or the "save PDF" button) to write
flow_field.pdf: a print for an A3 sheet, made of lines and not of dots, so it stays sharp at any size.

How it works:
- A noise field gives an angle at every point: f.noise(x * scale, y * scale) is a number from 0 to 1,
  and times 720 it becomes a direction. A small scale makes wide, slow swirls. A big scale makes
  tight ones.
- Each walker holds its place in an f.Vector. Every step, f.Vector.from_angle() makes a short arrow
  that points the way the field says, and .add() moves the walker along it.
- Each walker draws its short step as a line on the "trails" layer, using `with f.layer("trails"):`.
  The layer keeps what was drawn, so each frame only adds the new steps, and the canvas under it
  holds the paper and the frame.
- f.blend_mode("screen") or f.blend_mode("multiply") makes overlapping lines mix their colours.
  Light lines on dark paper use "screen". Dark lines on light paper use "multiply".
- Every walker also keeps its list of points. To print, the sketch makes a picture the size of an A3
  page (f.page_size("A3", landscape=True)) and draws each walker once, as one line made with
  begin_shape() and vertex(). Joining the steps into one line keeps the PDF small: about one line
  for each walker, and not one for every step.
- f.noise_seed() and f.random_seed() make each pattern repeatable, and R adds one to the seed.

Make it yours:
- Change the 720 in angle_at(): 360 makes gentler curves, 1440 makes busier ones.
- Add a palette to PALETTES: a name, a paper colour, three line colours and "screen" or "multiply".
  Then raise the palette slider's top value.
- Change STEP (the length of each step) and LENGTHS (how many steps a walker takes) for long, smooth
  lines or short, sharp ones.
- Change the line colour's last number, the 90 in PALETTES, to make the lines fainter or stronger.
- Make walkers start in a circle and not all over the page: use f.Vector.from_angle() in
  new_walkers().
"""
import funground as f

W, H = 720, 509                       # the screen is the shape of A3 on its side
MARGIN = 26                           # empty paper all round
STEP = 4                              # how far a walker moves in one step
STEPS_PER_FRAME = 3                   # each walker takes this many steps in every frame
LENGTHS = (60, 130)                   # a walker takes between this many steps
MAX_POINTS_PER_LINE = 140             # a safety cap, so a line never has more points than this

# Paper colour, three line colours (red, green, blue, strength) and how overlapping lines mix.
PALETTES = [
    ("Ink", (244, 238, 224), [(24, 36, 64, 90), (150, 40, 50, 90), (30, 110, 120, 90)], "multiply"),
    ("Night", (14, 18, 34), [(255, 190, 90, 80), (240, 90, 120, 80), (110, 190, 255, 80)], "screen"),
    ("Forest", (232, 236, 222), [(30, 90, 60, 90), (190, 120, 30, 90), (60, 60, 40, 90)], "multiply"),
    ("Sunset", (30, 14, 40), [(255, 120, 60, 80), (255, 210, 120, 80), (200, 70, 160, 80)], "screen"),
]

seed = 3
walkers = []                          # each one: {"pos": Vector, "points": [...], "colour": ..., "left": steps}
message = ""
settings_now = None                   # what the sliders said when the pattern was made


def angle_at(x, y, scale):
    """The way the field points at (x, y), in degrees. noise() is 0 to 1, so this is 0 to 720."""
    return f.noise(x * scale, y * scale) * 720


def setup():
    global noise_scale, line_count, palette, save_button
    f.size(W, H)
    noise_scale = f.create_slider(1, 8, 3, step=0.5, label="noise scale")
    line_count = f.create_slider(100, 1200, 500, step=50, label="lines")
    palette = f.create_slider(1, 4, 1, step=1, label="palette")
    save_button = f.create_button("save PDF")
    new_walkers()


def current_settings():
    return (noise_scale.value(), line_count.value(), palette.value())


def new_walkers():
    """Start again: new walkers, an empty trails layer, and the same noise for the same seed."""
    global walkers, settings_now
    settings_now = current_settings()
    f.noise_seed(seed)
    f.random_seed(seed)
    colours = PALETTES[palette.value() - 1][2]
    walkers = []
    for _ in range(line_count.value()):
        start = f.Vector(f.random(MARGIN, W - MARGIN), f.random(MARGIN, H - MARGIN))
        walkers.append({
            "pos": start,
            "points": [(start.x, start.y)],
            "colour": f.random_choice(colours),
            "left": int(f.random(*LENGTHS)),
        })
    with f.layer("trails"):
        f.clear()                     # wipe the layer; the walkers draw it again


def inside(p):
    return MARGIN <= p.x <= W - MARGIN and MARGIN <= p.y <= H - MARGIN


def move_walkers():
    """Every walker takes a few steps and draws each one as a short line on the trails layer."""
    scale = noise_scale.value() * 0.001
    mode = PALETTES[palette.value() - 1][3]
    with f.layer("trails"):
        f.blend_mode(mode)
        f.stroke_width(1.1)
        f.stroke_cap("butt")
        for walker in walkers:
            f.stroke(*walker["colour"])
            for _ in range(STEPS_PER_FRAME):
                if walker["left"] <= 0 or len(walker["points"]) >= MAX_POINTS_PER_LINE:
                    break
                here = walker["pos"]
                arrow = f.Vector.from_angle(angle_at(here.x, here.y, scale), STEP)
                there = here + arrow
                if not inside(there):
                    walker["left"] = 0
                    break
                f.line(here.x, here.y, there.x, there.y)
                walker["pos"] = there
                walker["points"].append((there.x, there.y))
                walker["left"] -= 1


def draw_frame_and_paper(target, scale=1.0):
    """The paper and a thin frame, on the canvas or on the print."""
    name, paper, colours, mode = PALETTES[palette.value() - 1]
    target.background(*paper)
    target.no_fill()
    target.stroke(*colours[0][:3], 140)
    target.stroke_width(max(1, scale))
    target.rect(MARGIN * 0.5 * scale, MARGIN * 0.5 * scale, (W - MARGIN) * scale, (H - MARGIN) * scale)


def save_print():
    """Draw every walker again, as one line each, on a picture the size of A3, and save it as a PDF."""
    global message
    page_w, page_h = f.page_size("A3", landscape=True)
    k = page_w / W                                  # how much bigger the print is than the screen
    sheet = f.create_graphics(page_w, page_h)
    draw_frame_and_paper(sheet, k)
    sheet.scale(k)
    mode = PALETTES[palette.value() - 1][3]
    sheet.blend_mode(mode)
    sheet.no_fill()
    sheet.stroke_width(1.1)
    sheet.stroke_cap("round")
    sheet.stroke_join("round")
    for walker in walkers:
        if len(walker["points"]) < 2:
            continue
        sheet.stroke(*walker["colour"])
        sheet.begin_shape()
        for x, y in walker["points"]:
            sheet.vertex(x, y)
        sheet.end_shape()
    sheet.save("flow_field.pdf")
    message = f"Saved flow_field.pdf: A3, {len(walkers)} lines"


def draw():
    if current_settings() != settings_now:
        new_walkers()
    if save_button.clicked():
        save_print()
    draw_frame_and_paper(f)
    move_walkers()
    f.fill(120)
    f.no_stroke()
    f.text_size(12)
    f.text_align("left", "center")
    f.text(message or "R: new pattern    P: save flow_field.pdf for A3", MARGIN, H - MARGIN / 2 + 1)


def key_pressed():
    global seed
    if f.key in ("r", "R"):
        seed += 1
        new_walkers()
    if f.key in ("p", "P"):
        save_print()


f.run()

View on GitHub ยท Back to the gallery