Wolfram Physics in Vanilla Processing

@paulgoux Speculatively tried to translate a @solub Wolfram Physics Sketch to run in vanilla processing. Here I offer you the CircleGrowth sketch (that I had previously translated to JRubyArt). To do this I created a small library class to help with java array operations (not all functions are used in the sketch):-

package monkstone.utils;

import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 *
 * @author tux
 */
public class ArrayOps {

    /**
     * Return true if number num is appeared only once in the array num is
     * unique.
     *
     * @param array
     * @param num
     * @return
     */
    public static boolean isUnique(int[] array, int num) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == num) {
                return false;
            }
        }
        return true;
    }

    /**
     * Convert the given array to an array with unique values without duplicates
     * and returns it.
     *
     * @param array
     * @return
     */
    public static int[] toUniqueArray(int[] array) {
        int[] temp = new int[array.length];
        for (int i = 0; i < temp.length; i++) {
            temp[i] = -1; // in case u have value of 0 in the array
        }

        int counter = 0;
        for (int i = 0; i < array.length; i++) {
            if (isUnique(temp, array[i])) {
                temp[counter++] = array[i];
            }
        }

        int[] uniqueArray = new int[counter];
        System.arraycopy(temp, 0, uniqueArray, 0, uniqueArray.length);
        return uniqueArray;
    }

    /**
     *
     * @param array
     * @return
     */
    public static int numberOfUniques(int[] array) {
        int count = 1;
        Arrays.sort(array);
        for (int i = 1; i < array.length - 1; i++) {
            if (array[i] != array[i + 1]) {
                count++;
            }
        }
        return count;
    }

    /**
     *
     * @param array
     * @return
     */
    public static int[] flatten(int[][] array) {
        IntStream stream = Stream.of(array) // returns Stream<int[]>
                .flatMapToInt(Arrays::stream);
        return stream.toArray();
    }

    /**
     *
     * @param input
     * @param index
     * @return
     */
    public static int[][] removeElementAt(int[][] input, int index) {
        if (input == null || index < 0 || index > input.length) {
            return input;
        }
        int[][] result = new int[input.length - 1][2];
        int count = 0;
        for (int i = 0; i < input.length; i++) {
            if (i != index) {
                result[count] = input[i];
                count++;
            }
        }
        return result;
    }

    /**
     *
     * @param input
     * @param index
     * @return
     */
    public static int[] removeElementAt(int[] input, int index) {
        if (input == null || index < 0 || index > input.length) {
            return input;
        }
        return IntStream.range(0, input.length)
                .filter(i -> i != index)
                .map(i -> input[i]).toArray();
    }

    /**
     *
     * @param origin array of nodes
     * @param other array of nodes
     * @return array of nodes
     */
    public static int[][] mergeArray(int[][] origin, int[][] other) {
        int first = origin.length;
        int second = other.length;
        int[][] result = new int[first + second][2];
        System.arraycopy(origin, 0, result, 0, first);
        System.arraycopy(other, 0, result, first, second);
        return result;
    }
}

Here’s the sketch (for processing 4.0, only because I use a lambda expression, rather than a for loop)

import processing.core.*; 
import toxi.geom.*;
import toxi.physics2d.*;
import toxi.physics2d.behaviors.*;
import monkstone.utils.ArrayOps;

int[][] axiom = {{0, 1}, {0, 2}, {1, 2}};
int[] flata;
int z;
VerletPhysics2D physics;
int uniqueCount;

void setup() {
  size(1000, 600);
  flata = ArrayOps.flatten(axiom);
  z = max(flata) + 1;
  physics = new VerletPhysics2D();
  physics.setDrag(0.2f);
  uniqueCount = ArrayOps.numberOfUniques(flata);
  for (int i = 0; i < uniqueCount; i++) {
    VerletParticle2D p = new VerletParticle2D(
      Vec2D.randomVector().add(new Vec2D(width / 2, height / 2))
      );
    physics.addParticle(p);
    physics.addBehavior(new AttractionBehavior2D(p, 10, -0.5f));
  }
  for (int[] node : axiom) {
    VerletParticle2D p1 = physics.particles.get(node[0]);
    VerletParticle2D p2 = physics.particles.get(node[1]);
    VerletSpring2D s = new VerletSpring2D(p1, p2, 10, -0.5f);
    physics.addSpring(s);
  }
}

void draw() {
  background(0xffffffff);
  stroke(0);
  physics.update();
  int id = PApplet.parseInt(random(z));
  int[] node = axiom[id];
  axiom = ArrayOps.removeElementAt(axiom, id);
  int[][] temp = {{node[1], z}, {z, node[0]}};
  axiom = ArrayOps.mergeArray(axiom, temp);
  // Manage physics accordingly
  VerletParticle2D px = physics.particles.get(node[0]); //coordinate of node x
  VerletParticle2D py = physics.particles.get(node[1]); //coordinate of node y
  VerletParticle2D pz = new VerletParticle2D(px.add(py).scale(0.5f)); //create a new particle in between
  VerletSpring2D s = physics.getSpring(px, py); // find spring between the deleted edge
  physics.removeSpring(s); // remove that spring
  physics.addParticle(pz); // add particle
  physics.addBehavior(new AttractionBehavior2D(pz, 10, -0.5f)); // attach a repulsion behavior to it
  VerletSpring2D s1 = new VerletSpring2D(py, pz, 4, 0.5f); // create spring between 1st new edge
  VerletSpring2D s2 = new VerletSpring2D(pz, px, 4, 0.5f); // create spring between 2nd new edge
  physics.addSpring(s1); // add them to physics
  physics.addSpring(s2);
  z += 1; //increment 'z'
  physics.springs.forEach(sp -> {
      line(sp.a.x(), sp.a.y(), sp.b.x(), sp.b.y());
    });
}
2 Likes

unique= existing only once

this Function is more like „num doesn‘t exist“

The function actually only makes sense when used in toUniqueArray, I guess it should be private (and have a better name?).
Revised version:-

package monkstone.utils;

import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 *
 * @author tux
 */
public class ArrayOps {

    /**
     * Helper function
     * Only returns true if number num occurs only once in the array 
     * @param array
     * @param num
     * @return
     */
    private static boolean isNewValue(int[] array, int num) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == num) {
                return false;
            }
        }
        return true;
    }

    /**
     * Convert the given array to an array with unique values without duplicates
     * and returns it.
     *
     * @param array
     * @return
     */
    public static int[] toUniqueArray(int[] array) {
        int[] temp = new int[array.length];
        for (int i = 0; i < temp.length; i++) {
            temp[i] = -1; // in case u have value of 0 in the array
        }

        int counter = 0;
        for (int i = 0; i < array.length; i++) {
            if (isNewValue(temp, array[i])) {
                temp[counter++] = array[i];
            } 
        }

        int[] uniqueArray = new int[counter];
        System.arraycopy(temp, 0, uniqueArray, 0, uniqueArray.length);
        return uniqueArray;
    }

    /**
     *
     * @param array
     * @return
     */
    public static int numberOfUniques(int[] array) {
        int count = 1;
        Arrays.sort(array);
        for (int i = 1; i < array.length - 1; i++) {
            if (array[i] != array[i + 1]) {
                count++;
            }
        }
        return count;
    }

    /**
     *
     * @param array
     * @return
     */
    public static int[] flatten(int[][] array) {
        IntStream stream = Stream.of(array) // returns Stream<int[]>
                .flatMapToInt(Arrays::stream);
        return stream.toArray();
    }

    /**
     * For nodes with two elements
     * @param input
     * @param index
     * @return
     */
    public static int[][] removeElementAt(int[][] input, int index) {
        if (input == null || index < 0 || index > input.length) {
            return input;
        }
        int[][] result = new int[input.length - 1][2];
        int count = 0;
        for (int i = 0; i < input.length; i++) {
            if (i != index) {
                result[count] = input[i];
                count++;
            }
        }
        return result;
    }

    /**
     * For flat array
     * @param input
     * @param index
     * @return
     */
    public static int[] removeElementAt(int[] input, int index) {
        if (input == null || index < 0 || index > input.length) {
            return input;
        }
        return IntStream.range(0, input.length)
                .filter(i -> i != index)
                .map(i -> input[i]).toArray();
    }

    /**
     * For nodes with two elements
     * @param origin array of nodes
     * @param other array of nodes
     * @return array of nodes
     */
    public static int[][] appendArray(int[][] origin, int[][] other) {
        int first = origin.length;
        int second = other.length;
        int[][] result = new int[first + second][2];
        System.arraycopy(origin, 0, result, 0, first);
        System.arraycopy(other, 0, result, first, second);
        return result;
    }
}

1 Like

Steps to make it compatible w/ Processing 3 as well: :foot:

Step 1:

Replace the forEach() loop on the main “.pde” tab:

  physics.springs.forEach(sp -> {
    line(sp.a.x(), sp.a.y(), sp.b.x(), sp.b.y());
  });

w/ this simpler enhanced loop:

  for (final VerletSpring2D sp : physics.springs)
    line(sp.a.x, sp.a.y, sp.b.x, sp.b.y);

Step 2:

Add this extra import statement below on the 2nd tab “ArrayOps.java”:
import java.util.function.Function;

Step 3:

Add this extra field inside class ArrayOps which is a Function callback wrapper for Arrays.stream():

  protected static final Function<int[], IntStream> INT_ARR_TO_INT_STREAM = 
    new Function<int[], IntStream>() {
    @Override public final IntStream apply(final int[] arr1d) {
      return Arrays.stream(arr1d);
    }
  };

Step 4:

Replace the original method ArrayOps.flatten():

  public static int[] flatten(int[][] array) {
    IntStream stream = Stream.of(array) // returns Stream<int[]>
                       .flatMapToInt(Arrays::stream);
    return stream.toArray();
  }

w/ this more compatible 1 which relies on previous field callback INT_ARR_TO_INT_STREAM:

  @SafeVarargs public static final int[] flatten(final int[]... arr2d) {
    return Arrays.stream(arr2d).flatMapToInt(INT_ARR_TO_INT_STREAM).toArray();
  }

Step 5:

Comment out the 2nd overloaded method ArrayOps.removeElementAt() w/ the following signature (int input, int index).

It’s not used on the main “.pde” sketch after all. :stuck_out_tongue:

1 Like

@GoToLoop I did mention not all functions are used and this is because java code is in a library, and I can use java 8 syntax, this is a limitation of the antlr included in processing. Also instead of the for loop you suggested, I could have used an anonymous class:-

    physics.springs.forEach(new Consumer<VerletSpring2D>() {
      public void accept(VerletSpring2D sp) {
          line(sp.a.x(), sp.a.y(), sp.b.x(), sp.b.y());
      }
  });

Well, Processing’s IDE (PDE) can compile “.java” files along w/ “.pde” 1s.

As I stated at “Step 2”:

I’m assuming your 1st posted file should be named “ArrayOps.java” as the 2nd tab.

Even though Processing 3 uses Java 8, compilation is forced on Java 7 syntax.

So my “step” changes are workarounds to make Java 7 syntax capable to access the Java 8 API.

1 Like

I’ve made my own conversion to PDE Java 7 syntax from the original Python Mode sketch below: :snake:

Not only it runs on Processing 3 but also on browsers via Processing.js (Pjs) + Toxiclibs.js libraries. :partying_face:

There are some added small features like left-click pause/resume, right-click restart, middle-click toggle rendering, number keys for changing background color and key S for a snapshot! :art:

Click the button “View App” at the right bottom side in order to run the sketch: :running_man:

4 Likes

I am curious about who created the support.

It’s a very interesting read about Glitch’s backend (server-side) containerized language features.

However my hosted sketch “Blob Growth” runs 100% on the frontend (client-side).

In order to run it on the Processing’s IDE (PDE) only file “Blob_Growth.pde” is needed:
Blob-Growth-Java.Glitch.me/Blob_Growth.pde

For browser-running we need 3 more files though.

The “index.html” as its entry point:

<script defer 
  src=https://cdn.JsDelivr.net/gh/hapticdata/toxiclibsjs/build/toxiclibs.min.js>
</script>

<script defer src=ToxicLib_Imports.js></script>
<script defer src=ArrayList_Shims.js></script>

<script defer src=https://Unpkg.com/processing-js></script>
<canvas data-processing-sources=Blob_Growth.pde></canvas>

“ToxicLib_Imports.js” which copies the required “ToxicLibs.js” classes to the global context, so they’re available to direct usage by the sketch “Blob_Growth.pde”:
Blob-Growth-Java.Glitch.me/ToxicLib_Imports.js

And the shim hack file “ArrayList_Shims.js”, which makes it possible for JS arrays to use most of the Java ArrayList’s methods:

That is necessary b/c the Java version of “ToxicLibs” store particles on ArrayList containers, while the JS version goes w/ vanilla JS Array containers.

And neither versions feature getters & setters to access their containers via indices.

The reason why we’re able to deploy “.pde” files on the web as 100% client-side apps is b/c “Processing.js” (Pjs) is not only a JS library but also a simple Java Mode to JS transpiler in 1 file!

We can find more about its inception on the article below:
https://JohnResig.com/blog/processingjs/

1 Like

That’s interesting. So if someday OpenProcessing would abandon Pjs, I could still load Processing.js there as a library and run the pde files?

Normally in order to load a JS script we use the tag <script> inside an “.html” file.

AFAIK OpenProcessing doesn’t allow us to edit its HTML file.

We could still load JS scripts dynamically inside a “.js” file, but it’s not as practical as doing that on an “.html” file.

1 Like

Made “Blob Growth” sketch for p5js as well! :smile_cat:
1 JS file w/o any separated import & shim hack files: :sunglasses:

1 Like