Luna Portmann 0 %

Portfolio2026

Two spools stand facing each other, bound by a single continuous thread.

One grows as the other diminishes, the same thread that constructs also consumes.

To be attached is to be made and unmade by proximity.

Scroll to transfer the thread

Reciprocal Bodies

2026

Arduino powered spinning machine

Two thread-wound spools and a guide rod on a white plinth

Two spools stand facing each other, bound by a single continuous thread. They spin in unison, one grows as the other diminishes, the same thread that constructs also consumes.

Together, they create a mechanical duet of dependency - neither can exist without the other, and neither can be whole.

By giving the thread a mechanical metabolism, the work exposes attachment not as stability but as a cyclical exchange. To be attached is to be made and unmade by proximity.

This machine continuously transfers yarn between two motorized spools while a vertically actuated guide precisely controls its placement. Through a programmed motion profile that varies speed across the travel path, the machine generates sculptural accumulations.

The diminishing spool, detail
The growing spool, detail
The spinning machine, full view
The diminishing spool with the thread leaving it
Thread guide between the two spools
The growing spool with the thread arriving
Reciprocal Bodies · Arduino
#include <AccelStepper.h>
#include <math.h>

// ====== MIDDLE MOTOR PINS ======
#define STEP_MITTE 4
#define DIR_MITTE 5

AccelStepper stepperMitte(AccelStepper::DRIVER, STEP_MITTE, DIR_MITTE);

// ====== RIGHT MOTOR PINS ======
#define STEP_RECHTS 2
#define DIR_RECHTS 3
#define ENABLE_RECHTS 8

AccelStepper stepperRechts(AccelStepper::DRIVER, STEP_RECHTS, DIR_RECHTS);

// ====== LEFT MOTOR PINS ======
#define STEP_LINKS 6
#define DIR_LINKS 7
#define ENABLE_LINKS 9

AccelStepper stepperLinks(AccelStepper::DRIVER, STEP_LINKS, DIR_LINKS);

// ====== CONTROL VARIABLES ======
bool paused = true;               // start paused until 'r' is pressed
unsigned long lastMoveTime = 0;   // timing for 5-second middle motor
const long moveAmount = 500;      // middle motor steps per move
bool forward = true;              // middle motor direction flag
long segmentStart = 0;
long segmentTarget = 0;
bool midPauseDone = false;
bool midPausing = false;
unsigned long midPauseStart = 0;
long segmentCenter = 0;
long basePosition = 0;            // lowest allowed position
long baseOffset = 0;              // shifts base upward in steps

// ====== MIDDLE MOTOR GAUSSIAN PROFILE ======
const float MITTE_MIN_SPEED = 20.0f;
const float MITTE_MAX_SPEED = 200.0f;
const float MITTE_MIN_ACCEL = 10.0f;
const float MITTE_MAX_ACCEL = 80.0f;
const float MITTE_SIGMA = 120.0f;

float gaussianFactor(float distance, float sigma) {
  float x = distance / sigma;
  float dip = expf(-0.5f * x * x);
  return 1.0f - dip;
}

// ====== SIDE MOTOR CONSTANT SPEED ======
const int SPOOL_SPEED = 220; // SAME speed for left & right

int leftDir = -1;
int rightDir = 1;

void applySideMotorSpeeds() {
  stepperLinks.setSpeed(leftDir * SPOOL_SPEED);
  stepperRechts.setSpeed(rightDir * SPOOL_SPEED);
}

void setup() {
  Serial.begin(115200);
  while (!Serial) {}

  // ---- ENABLE MOTORS ----
  pinMode(ENABLE_LINKS, OUTPUT);
  digitalWrite(ENABLE_LINKS, LOW);

  pinMode(ENABLE_RECHTS, OUTPUT);
  digitalWrite(ENABLE_RECHTS, LOW);

  // ---- SET STEP/DIR LOW ----
  pinMode(2, OUTPUT); digitalWrite(2, LOW);
  pinMode(3, OUTPUT); digitalWrite(3, LOW);
  pinMode(6, OUTPUT); digitalWrite(6, LOW);
  pinMode(7, OUTPUT); digitalWrite(7, LOW);

  // ---- MIDDLE MOTOR ----
  stepperMitte.setMaxSpeed(MITTE_MAX_SPEED);
  stepperMitte.setAcceleration(MITTE_MAX_ACCEL);

  // ---- SIDE MOTORS ----
  stepperLinks.setMaxSpeed(SPOOL_SPEED);
  stepperLinks.setAcceleration(0);

  stepperRechts.setMaxSpeed(SPOOL_SPEED);
  stepperRechts.setAcceleration(0);

  applySideMotorSpeeds();

  basePosition = stepperMitte.currentPosition();

  Serial.println("Send 'r' to START, 's' to STOP, 'x' to SWAP, 'u'/'d' to shift base");
}

void loop() {

  // ---- SERIAL CONTROL ----
  if (Serial.available()) {
    char c = Serial.read();

    if (c == 's') {
      paused = true;
      stepperMitte.stop();
      stepperLinks.setSpeed(0);
      stepperRechts.setSpeed(0);
      Serial.println("Motors STOPPED");
    }

    if (c == 'r') {
      paused = false;
      applySideMotorSpeeds();
      Serial.println("Motors RESUMED");
    }

    if (c == 'x') {
      leftDir = -leftDir;
      rightDir = -rightDir;
      applySideMotorSpeeds();
      Serial.println("Left/Right directions swapped");
    }

    if (c == 'u' || c == 'd') {
      long delta = (c == 'u') ? 2000 : -2000;
      baseOffset += delta;
      if (baseOffset < 0) baseOffset = 0;
      long newBase = basePosition + baseOffset;
      stepperMitte.moveTo(newBase);
      segmentStart = stepperMitte.currentPosition();
      segmentTarget = newBase;
      segmentCenter = newBase;
      midPauseDone = true;
      midPausing = false;
      lastMoveTime = millis();
      Serial.println("Base position shifted");
    }
  }

  if (paused) return;

  // ---- MIDDLE MOTOR EVERY 5 SECONDS ----
  if (stepperMitte.distanceToGo() == 0 && millis() - lastMoveTime >= 5000) {
    long target = forward
      ? basePosition + baseOffset + moveAmount
      : basePosition + baseOffset;

    stepperMitte.moveTo(target);
    segmentStart = stepperMitte.currentPosition();
    segmentTarget = target;
    segmentCenter = basePosition + baseOffset + (moveAmount / 2);
    midPauseDone = false;
    midPausing = false;
    forward = !forward;
    lastMoveTime = millis();
  }

  // ---- MID-SEGMENT PAUSE ----
  if (stepperMitte.distanceToGo() != 0 && !midPauseDone && !midPausing) {
    long traveled = labs(stepperMitte.currentPosition() - segmentStart);
    long total = labs(segmentTarget - segmentStart);
    if (total > 0 && traveled >= total / 2) {
      midPausing = true;
      midPauseStart = millis();
    }
  }

  if (midPausing) {
    if (millis() - midPauseStart >= 3000) {
      midPausing = false;
      midPauseDone = true;
    }
  } else {
    float distance = fabsf(stepperMitte.currentPosition() - segmentCenter);
    float factor = gaussianFactor(distance, MITTE_SIGMA);
    float targetSpeed = MITTE_MIN_SPEED + (MITTE_MAX_SPEED - MITTE_MIN_SPEED) * factor;
    float targetAccel = MITTE_MIN_ACCEL + (MITTE_MAX_ACCEL - MITTE_MIN_ACCEL) * factor;
    stepperMitte.setMaxSpeed(targetSpeed);
    stepperMitte.setAcceleration(targetAccel);
    stepperMitte.run();
  }

  // ---- RUN SIDE MOTORS ----
  stepperLinks.runSpeed();
  stepperRechts.runSpeed();
}

Bound

2026

Materials and text to be supplied by the artist

Bound, deflated Bound, beginning to inflate Bound, half inflated Bound, nearly inflated Bound, inflated Bound, fully inflated
Bound, detail: knotted black rope on the inflated body
Bound, detail: black tassels

What Would My Chair Wear?

2025

Clothing for furniture

The undressed welded steel chair
Naked

Humans get dressed. Every day, we choose what to wear based on where we are going, what we are doing, and how we feel. Our clothing helps us adapt to environments, meet expectations, and express identity.

What if furniture could do the same?

A simple welded steel chair is getting dressed:

Drag an outfit onto the chair, or tap it.

And sometimes, it just wants to be naked.

Outfits tried: 0 of 3

Each outfit can be slipped on or off with ease and be stored on a regular hanger, right alongside human clothes. All garments are made from discarded clothing and deadstock fabrics - materials originally intended for human bodies, carrying memories of dressing, utility, and style, now reimagined for a chair. By changing its clothes, the chair shifts its role, its mood, and the atmosphere of the room it inhabits.

The undressed welded steel chair
The undressed welded steel chair, second view
Hustle Culture Chair
Hustle Culture Chair, seen from behind
Soirée Chair
Soirée Chair, seen from behind
Bedrotting Chair
Bedrotting Chair, seen from behind

Feeling Blue-ish

2024

Double layered lenticular weaving with dyed weft threads

Feeling Blue-ish seen from the left: muted, grey Feeling Blue-ish seen from the right: colour returns

Depression can change you in many ways. It affects various aspects of the senses, including the perception of colour. Research has shown that individuals suffering from depression often perceive colours as less vibrant and more muted. Luckily this change in perception is not irreversible, people who won their battle with the illness often report seeing the world shine in its „true colours“ again.

„Feeling Blue-ish“ aims to visualise this phenomenon and encourage people to view the world through different eyes. The double-layered textile is woven in stripes of two alternating images. The pleats create a lenticular effect, changing the visibility of the images depending on the viewer‘s perspective - signifying the shift in perception that is imperative to recovery.

Feeling Blue-ish, frontal view on the wall
Seen along the wall from the left, the grey image dominates
Seen along the wall, pleats in profile
Seen along the wall from the right, the coloured image dominates
The weaving lying flat, grey side towards the camera
The weaving lying flat, coloured side towards the camera
Detail of the grey weave
Detail of the dyed weft threads
Detail of the pleats and dyed weft threads

glitching_stitches

2024
Navy knitted textile with distorted, looping structures
Knitted top with dropped stitches on a mannequin
Grey knitted textile dissolving into loops

Inspired by knitting machine programming glitches, the collection of knitted textiles blends the organic softness of yarns with fragmented aesthetics of distortion. Each piece creates a sense of movement and visual disruption. Together, they aim to evoke a contrast between the traditional usage of knitting and modern interferance.

Sample Development

Sketch of a hand with a knitted sleeve
Knit sample with long dropped loops over pink
Fashion sketch of a figure in a knitted dress
Small ribbed knit sample
Draped grey knit sample
Knit sample with columns over pink
Chunky knit sample with open cells
Lace-like knit sample over pink

Protective Patterns

2023

Textile Mechanisms

Two textile mechanisms, each shown closed and opened by gloved hands

Defence is a primal instinct that connects all living beings. It manifests itself in strategies developed through evolution, that are essential for survival. These strategies vary greatly, ranging from passive camouflage to visual deterrence, escape or active combat.

Humans, too, are constantly faced with the challenge of finding an appropriate defence strategy in the face of conflicts or attacks. It is crucial for success to be able to react both passively and actively.

“Protective Patterns" depicts this duality. Textile mechanisms that can switch ad hoc between two different states. In the drastic changes of their surface structure, they can embody both defensive and offensive capabilities. Thus, they become extensions of the body, communicating on a textile basis.

Four further textile mechanisms in their two states

Traces

2024

Handmade Paper out of recycled cotton and jeans

Four sheets of handmade paper with blue denim pulp
Two embossed sheets of handmade paper with traces of garments and gloves

Weaving Archive

2024
Weaving Archive: eighteen documented weave structures, each with draft, sample and data sheet
Cleanse: a translucent blue crystalline digital sculpture with a glowing core

Cleanse

2022

digital sculpture for the global AR Pavillion at “wrong Biennale”

Clairvoyance: a symmetrical silver drawing on crumpled grey organza

Clairvoyance

2021

Silicone and silver foil on organza

Circular blue etching from the portfolio cover

About

Biography, education, exhibitions and CV: to be supplied by the artist.