Morphing String using Geomerative library, bug on the iterations of the program

Hi,
I wrote a program that uses Geomerative library in order to morph between different words that ends with “ire” and form a short poem, but when the program loops instead of repeating perfectly the animation, it bugs!?
Instead of the first word “fire me” morphing immediatly to form the second “hire me” its stays still ?! How can I repeat perfectly the first loop please? I don’t find the bug if you can help me please. Sorry for the long code below, but I think you need it all in order to debug! Thanks a lot!!

import geomerative.*;
import processing.sound.*;

SoundFile[] file;

Clock[][] clocks;
String[] words = {
  "fire me", "hire me", "desire me", "disattire me",
  "inspire me", "exquire me", "admire me", "dewire me"
};
String[][] tokens;
RFont text;
RShape[][] shapes;
int splitGlyph = 500;
int inc = 200;

RPoint[][][] points;
float[][][] shiftPntX;
float[][][] shiftPntY;

float timeFactor = 6000;
float morphDuration;
PGraphics pg;

int morph = 0;
boolean run = false;
boolean forward = true;
float diam = 0;
int currentMorph = 0;
long pauseStartTime = 0;
boolean isPaused = false;
long morphStartTime = 0;

// Store positions for the fixed phrases
float[][][] posStaticX;
float[][][] posStaticY;
boolean[] showStatic;
float[] staticAlpha;
boolean fadingOut = false;

void setup() {
  fullScreen();
  smooth(4);

  tokens = new String[words.length][];
  shapes = new RShape[words.length][];
  points = new RPoint[words.length][][];

  for (int i = 0; i < words.length; i++) {
    tokens[i] = splitTokens(words[i]);
    shapes[i] = new RShape[tokens[i].length];
    points[i] = new RPoint[tokens[i].length][splitGlyph];
  }

  clocks = new Clock[tokens[0].length][splitGlyph];
  shiftPntX = new float[tokens[0].length][inc * words.length][splitGlyph];
  shiftPntY = new float[tokens[0].length][inc * words.length][splitGlyph];

  posStaticX = new float[words.length][tokens[0].length][splitGlyph];
  posStaticY = new float[words.length][tokens[0].length][splitGlyph];
  showStatic = new boolean[words.length];
  staticAlpha = new float[words.length];

  RG.init(this);
  text = new RFont("FreeSans.ttf", 150, RIGHT);

  float frac = 1.0 / splitGlyph;

  for (int w = 0; w < words.length; w++) {
    for (int i = 0; i < tokens[w].length; i++) {
      shapes[w][i] = text.toShape(tokens[w][i]);
      for (int j = 0; j < splitGlyph; j++) {
        points[w][i][j] = shapes[w][i].getPoint(j * frac);
        if (w == 0) {
          float offsetMinutes = 7.5;
          float offsetHours = 24;
          float startHours = offsetHours * j * 1 / 8;
          float startMinutes = j * offsetMinutes;
          clocks[i][j] = new Clock(points[w][i][j].x, points[w][i][j].y, startHours, startMinutes, color(200, 200, 0, 10));
        }
      }
    }
  }

  for (int i = 0; i < tokens[0].length; i++) {
    for (int k = 0; k < inc; k++) {
      for (int j = 0; j < splitGlyph; j++) {
        for (int w = 0; w < words.length - 1; w++) {
          if (points[w][i][j] != null && points[w + 1][i][j] != null) {
            shiftPntX[i][k + w * inc][j] = lerp(points[w][i][j].x, points[w + 1][i][j].x, k * (1.0 / inc));
            shiftPntY[i][k + w * inc][j] = lerp(points[w][i][j].y, points[w + 1][i][j].y, k * (1.0 / inc));
          }
        }
        if (points[words.length - 1][i][j] != null && points[0][i][j] != null) {
          shiftPntX[i][k + (words.length - 1) * inc][j] = lerp(points[words.length - 1][i][j].x, points[0][i][j].x, k * (1.0 / inc));
          shiftPntY[i][k + (words.length - 1) * inc][j] = lerp(points[words.length - 1][i][j].y, points[0][i][j].y, k * (1.0 / inc));
        }
      }
    }
  }

  morph = 0;
  morphStartTime = millis();

  // Calculate morph duration to ensure equal timing
  morphDuration = (float) (12000.0 / words.length);

  file = new SoundFile[5];
  for (int i = 0; i < 5; i++) {
    file[i] = new SoundFile(this, "Essais_piano_01.wav");
    file[i].jump(i * file[i].duration() / 25);
    file[i].amp(0.0001);
    file[i].play();
  }
}

void draw() {
  background(0);
  translate(width / 2, 150);

  float lineHeight = 130; // Height of each line
  float offsetX = 0;
  float firstWordWidth = 0;

  // Calculate morphIndex safely
  int morphIndex = currentMorph * inc + morph;
  morphIndex = min(morphIndex, inc * words.length - 1);

  // Compute the width of the first word for alignment
  for (int j = 0; j < splitGlyph; j++) {
    firstWordWidth = max(firstWordWidth, shiftPntX[0][morphIndex][j]);
  }
  float fixedOffset = firstWordWidth + 220;

  // Morphing and updating clocks
  for (int i = 0; i < tokens[0].length; i++) {
    for (int j = 0; j < splitGlyph; j++) {
      pushStyle();

      if (morphIndex < inc * words.length) {
        float yOffset = currentMorph * lineHeight + (lineHeight * morph / (float) inc);
        clocks[i][j].setPosition(shiftPntX[i][morphIndex][j] + offsetX, shiftPntY[i][morphIndex][j] + yOffset);
        clocks[i][j].update();
        clocks[i][j].display();
      }
      popStyle();
    }

    if (i == 0) {
      offsetX += fixedOffset;
    }
  }

  // Handle pauses and transitions
  if (isPaused) {
    if (!fadingOut && millis() - morphStartTime >= morphDuration) {
      isPaused = false;
      currentMorph = (currentMorph + 1) % words.length;
      morph = 0; // Reset morph to start the next transition
      morphStartTime = millis();

      // Ensure the cycle restarts correctly
      if (currentMorph == 0) {
        fadingOut = true; // Start fading out when transitioning from last to first phrase
      }
    }

    // Update positions for static phrases
    for (int i = 0; i < tokens[0].length; i++) {
      for (int j = 0; j < splitGlyph; j++) {
        if (currentMorph > 0) {
          posStaticX[currentMorph][i][j] = shiftPntX[i][currentMorph * inc + inc - 1][j];
          posStaticY[currentMorph][i][j] = shiftPntY[i][currentMorph * inc + inc - 1][j];
        } else {
          posStaticX[currentMorph][i][j] = shiftPntX[i][(words.length - 1) * inc + inc - 1][j];
          posStaticY[currentMorph][i][j] = shiftPntY[i][(words.length - 1) * inc + inc - 1][j];
        }
      }
    }
  } else {
    morph = (int) ((millis() - morphStartTime) / morphDuration * inc);
    if (morph >= inc) {
      isPaused = true;
      pauseStartTime = millis();

      // Store positions for the current static phrase
      for (int i = 0; i < tokens[0].length; i++) {
        for (int j = 0; j < splitGlyph; j++) {
          posStaticX[currentMorph][i][j] = shiftPntX[i][morphIndex][j];
          posStaticY[currentMorph][i][j] = shiftPntY[i][morphIndex][j];
        }
      }
      showStatic[currentMorph] = true;

      // Check if we are transitioning from "dewire me" to "fire me"
      if (currentMorph == words.length - 1) {
        fadingOut = true;
      }
    }
  }

  // Draw static phrases with fading effects
  for (int w = 0; w < words.length; w++) {
    if (showStatic[w]) {
      for (int i = 0; i < tokens[0].length; i++) {
        for (int j = 0; j < splitGlyph; j++) {
          pushStyle();
          fill(255, staticAlpha[w]);
          clocks[i][j].setPosition(posStaticX[w][i][j] + i * offsetX, posStaticY[w][i][j] + 130 * w + 130);
          clocks[i][j].update();
          clocks[i][j].display();
          popStyle();
        }
      }
    }
  }

  // Global fade-out logic when transitioning from last to first phrase
  if (fadingOut) {
    boolean allFaded = true;
    for (int w = 0; w < words.length; w++) {
      staticAlpha[w] = max(0, staticAlpha[w] - 10);
      showStatic[w] = staticAlpha[w] > 0;
      if (staticAlpha[w] > 0) {
        allFaded = false;
      }
    }

    if (allFaded) {
      // Once all phrases are faded out, reset everything for the next loop
      fadingOut = false;
      for (int w = 0; w < words.length; w++) {
        staticAlpha[w] = 255;
        showStatic[w] = false;
      }
      morph = 0;
      currentMorph = 0;
      morphStartTime = millis();
    }
  }

  // Ensure sounds play correctly
  for (int i = 0; i < 5; i++) {
    if (!file[i].isPlaying()) {
      file[i].jump(i * file[i].duration() / 25);
      file[i].amp(0.0001);
      file[i].play();
    }
  }
}

void mousePressed() {
  run = !run;
}

class Clock {
  float cx, cy;
  float radius;
  float clockDiameter;
  float hourRadius;
  float minuteRadius;
  float startHours;
  float startMinutes;
  float h, m;
  color col;
  long startMillis;
  float currentMinutes;

  Clock(float _cx, float _cy, float _startHours, float _startMinutes, color _col) {
    radius = random(10, 25);
    minuteRadius = radius * 0.3;
    hourRadius = radius * 0.15;
    clockDiameter = radius * 1;
    col = _col;
    cx = _cx;
    cy = _cy;
    startHours = _startHours;
    startMinutes = _startMinutes;
    startMillis = millis();
  }

  void setPosition(float _cx, float _cy) {
    cx = _cx;
    cy = _cy;
  }

  void setTime(float _startHours, float _startMinutes) {
    startHours = _startHours;
    startMinutes = _startMinutes;
  }

  void update() {
    long elapsedMillis = millis() - startMillis;
    float elapsedSeconds = (elapsedMillis / 1000.0) * timeFactor;
    float elapsedMinutes = elapsedSeconds / 60.0;
    float elapsedHours = elapsedSeconds / 3600.0;

    float currentHours = startHours + elapsedHours;
    float currentMinutes = startMinutes + elapsedMinutes;

    currentHours = currentHours % 24;
    currentMinutes = currentMinutes % 60;

    h = map(currentHours, 0, 12, 0, TWO_PI) - HALF_PI;
    m = map(currentMinutes, 0, 60, 0, TWO_PI) - HALF_PI;
  }

  void display() {
    col = color(250, 200, 0);
    noStroke();
    fill(col, map(m, 0, TWO_PI, 50, 255));

    arc(cx, cy, minuteRadius * 2, minuteRadius * 2, -HALF_PI, m);

    stroke(255, 150);
    strokeWeight(1.5);

    line(cx, cy, cx + cos(h) * hourRadius, cy + sin(h) * hourRadius);
    strokeWeight(1);
    line(cx, cy, cx + cos(m) * minuteRadius, cy + sin(m) * minuteRadius);
  }
}
1 Like

In my version, “fire me” disappears and all other stay.

Does it have to do with showStatic[w] ?

NICE Sketch!

2 Likes