P145 - Phase Pattern (Original Spline)

Golan Levin


Based on: Phase Pattern by Manfred Mohr, 1976

Category: experimental


Description:

This is the second of my ports of Manfred Mohr's two plotter drawings, "P145 - Phase Pattern" (1973). These were first published in Catalog Manfred Mohr, "Drawings Dessins Zeichnungen Dibujos", Galerie Weiller, Paris and Galerie Gilles Gheerbrant, Montreal, 1974, and Re-published in "Computer Graphics and Art" Vol.1 No.4, 1976. The originals, whose algorithm was described by Mohr at http://www.emohr.com/sc69-73/vfile_145.html, are ink on paper, 50cm x 50cm.

This ProcessingJS port uses Bezier curves to duplicate, to the best of my abilities, the exact 3rd-degree splines used in Mohr's originals. The randomization technique by which Mohr generated these curves is unknown, and in the absence of other information about his method I have not attempted to invent one. This sketch is running in the browser.






/*
Part of the ReCode Project (http://recodeproject.com)
A series of 2 plotter drawings, ink on paper, 50cm x 50cm each, (c) 1973 by Manfred Mohr
First published in Catalog Manfred Mohr, "Drawings Dessins Zeichnungen Dibujos",
Galerie Weiller, Paris and Galerie Gilles Gheerbrant, Montreal, 1974
Re-published in "Computer Graphics and Art" Vol.1 No.4, 1976
https://github.com/downloads/matthewepler/ReCode_Project/COMPUTER_GRAPHICS_AND_ART_Nov1976.pdf
Copyright (c) 2012 Golan Levin - OSI/MIT license (http://recodeproject/license).

This version reproduces the exact 3rd-order spline curves used in Mohr's original. 
See: http://www.emohr.com/sc69-73/vfile_145.html
*/


int   nLinesAcross  = 133;        // Number of lines across, counted per original
float marginPercent = 0.02857;    // Thickness of margin, measured per original
float strokePercent = 0.25;       // StrokeWeight as a % of line step, estimated per original
int   seriesMode    = 0;          // 0 or 1, depending which of the series we're rendering

float margin; 
float xLeft, xRight;
float yTop, yBottom; 

SplineSegmentCollection curveA;
SplineSegmentCollection curveB; 

FPoint storedCurve[];
class FPoint {
  float x; 
  float y;
}


//==========================================================
void setup() {
  size (560, 560, P2D);

  storedCurve = new FPoint[nLinesAcross];
  for (int i=0; i<nLinesAcross; i++) {
    storedCurve[i] = new FPoint();
  }

  margin  = marginPercent * width; 
  xLeft   = margin;
  xRight  = width - margin;
  yTop    = margin;
  yBottom = height - margin;

  initializeSplineSegmentCollections();
  
  // Compute and store first curve
  for (int i=0; i<nLinesAcross; i++) {
    float x = map(i, 0, (nLinesAcross-1), 0, 1);
    float y = curveA.cubicBezierLookup (x); 
    x = map(x, 0, 1, xLeft, xRight); 
    y = map(y, 0, 1, yTop, yBottom); 
    storedCurve[i].x = x; 
    storedCurve[i].y = y;
  }
}


//==========================================================
void initializeSplineSegmentCollections() {
  
  // These are cubic Bezier control points in the format {x,y,x,y,x,y,x,y}
  // which duplicate, to the best of my abilities, the cubic splines used in
  // the original by Manfred Mohr: http://www.emohr.com/sc69-73/vfile_145.html.
  // Mohr's original randomization technique for producing these is otherwise unknown.
  // The numbers have been normalized in the range 0...1. 

  float ctrlPtsA0[] = {-0.09318,0.65749, 0.00986,0.65749, 0.02457,0.46173, 0.08051,0.46173};
  float ctrlPtsA1[] = { 0.08051,0.46173, 0.10847,0.46173, 0.14233,0.60598, 0.17324,0.60598};
  float ctrlPtsA2[] = { 0.17324,0.60598, 0.22328,0.60598, 0.32043,0.39402, 0.43818,0.39402};
  float ctrlPtsA3[] = { 0.43818,0.39402, 0.47792,0.39402, 0.53533,0.41169, 0.57948,0.41169};
  float ctrlPtsA4[] = { 0.57948,0.41169, 0.64719,0.41169, 0.73992,0.39255, 0.81646,0.39255};
  float ctrlPtsA5[] = { 0.81646,0.39255, 0.95629,0.39255, 1.10201,0.57654, 1.13733,0.57654};

  float ctrlPtsB0[] = {-0.15500,0.34987,-0.10790,0.34987, 0.00544,0.52355, 0.13202,0.52355};
  float ctrlPtsB1[] = { 0.13202,0.52355, 0.24094,0.52355, 0.27333,0.38666, 0.31012,0.38666};
  float ctrlPtsB2[] = { 0.31012,0.38666, 0.34251,0.38666, 0.41905,0.57507, 0.47056,0.57507};
  float ctrlPtsB3[] = { 0.47056,0.57507, 0.50000,0.57507, 0.50883,0.52797, 0.53238,0.52797};
  float ctrlPtsB4[] = { 0.53238,0.52797, 0.55004,0.52797, 0.56624,0.53680, 0.58684,0.53680};
  float ctrlPtsB5[] = { 0.58684,0.53680, 0.66191,0.53680, 0.71048,0.45731, 0.81204,0.45731};
  float ctrlPtsB6[] = { 0.81204,0.45731, 0.94893,0.45731, 1.04166,0.55299, 1.10495,0.55299};

  // Init the first curve, using those control points. 
  float ctrlPtsA[][] = new float[6][];
  ctrlPtsA[0] = ctrlPtsA0;
  ctrlPtsA[1] = ctrlPtsA1;
  ctrlPtsA[2] = ctrlPtsA2;
  ctrlPtsA[3] = ctrlPtsA3;
  ctrlPtsA[4] = ctrlPtsA4;
  ctrlPtsA[5] = ctrlPtsA5;
  curveA = new SplineSegmentCollection(6, ctrlPtsA); 
 
  // Init the second curve, using those control points. 
  float ctrlPtsB[][] = new float[7][];
  ctrlPtsB[0] = ctrlPtsB0;
  ctrlPtsB[1] = ctrlPtsB1;
  ctrlPtsB[2] = ctrlPtsB2;
  ctrlPtsB[3] = ctrlPtsB3;
  ctrlPtsB[4] = ctrlPtsB4;
  ctrlPtsB[5] = ctrlPtsB5;
  ctrlPtsB[6] = ctrlPtsB6;
  curveB = new SplineSegmentCollection(7, ctrlPtsB);
}

//==========================================================
void keyPressed() {
  seriesMode = 1 - seriesMode;
}

//==========================================================
void mousePressed() {
  seriesMode = 1 - seriesMode;
}

//==========================================================
void draw() {
  background (25); 
  stroke (240); 
  strokeCap (ROUND);
  noFill(); 
  smooth(); 

  float stepSize = (xRight-xLeft) / (float)nLinesAcross;
  float lineThickness = strokePercent * stepSize;
  strokeWeight (lineThickness); 

  // Draw first set of vertical lines, down from the first curve
  for (int i=0; i<nLinesAcross; i++) {
    float x = storedCurve[i].x;
    float y = storedCurve[i].y;
    line (x, y, x, yBottom);
  }

  // Draw the outline of the first curve, itself
  beginShape(); 
  for (int i=0; i<nLinesAcross; i++) {
    vertex (storedCurve[i].x, storedCurve[i].y);
  }
  endShape(); 


  // Search for top and bottom of storedCurve
  float curveTop = yBottom; 
  float curveBottom = 0; 
  for (int i=0; i<nLinesAcross; i++) {
    float y = storedCurve[i].y;
    if (y < curveTop) {
      curveTop = y;
    }
    if (y >  curveBottom) {
      curveBottom = y;
    }
  }

  // Draw horizontal lines
  for (int i=0; i<nLinesAcross; i++) {
    float y = map(i, 0, (nLinesAcross-1), yTop, yBottom); 
    if (y <= curveTop) {
      line (xLeft+lineThickness, y, xRight, y);
    } 
    else if ( y > curveBottom) {
      ; // draw no line
    } 
    else {

      float px0 = xLeft + lineThickness;
      int UNDEFINED_EDGE = -1;
      int RISING_EDGE = 0;
      int FALLING_EDGE = 1;
      int mostRecentIntersection = UNDEFINED_EDGE;

      for (int j=0; j<(nLinesAcross-1); j++) {
        float xj = storedCurve[j].x;
        float yj = storedCurve[j].y;
        float xk = storedCurve[j+1].x;
        float yk = storedCurve[j+1].y;
        boolean bIntersectionOnRisingEdge  = ((y <= yj) && (y >= yk));
        boolean bIntersectionOnFallingEdge = ((y >= yj) && (y <= yk));
        boolean bOnLastLine = (j == (nLinesAcross-2));

        if (bIntersectionOnRisingEdge) {
          float px1 = xj + (yj - y)/(yj - yk)*(xk-xj);  
          line (px0, y, px1, y);
          mostRecentIntersection = RISING_EDGE;
          px0 = px1;
        } 
        else if (bIntersectionOnFallingEdge) {
          px0 = xj + (y - yj)/(yk - yj)*(xk-xj); 
          mostRecentIntersection = FALLING_EDGE;
        } 
        else if (bOnLastLine && (mostRecentIntersection == FALLING_EDGE)) {
          line (px0, y, xk, y);
        }
      }
    }
  }

  // Draw the second set of vertical lines.
  for (int i=0; i<nLinesAcross; i++) {
    float x = map(i, 0, (nLinesAcross-1), 0, 1); 
    float y = curveB.cubicBezierLookup (x); 
    x = map(x, 0, 1, xLeft, xRight); 
    y = map(y, 0, 1, yTop, yBottom); 

    if (seriesMode == 0) {
      line (x+lineThickness+1, yTop, x+lineThickness, y);
    } 
    else {
      line (x+lineThickness+1, y, x+lineThickness, yBottom);
    }
  }
}



//==========================================================
class SplineSegmentCollection {
  int nSplineSegments;
  SplineSegment segments[];

  SplineSegmentCollection (int nSegs, float[][] controlPoints) {
    nSplineSegments = nSegs;
    segments = new SplineSegment[nSplineSegments]; 
    for (int i=0; i<nSplineSegments; i++) {
      segments[i] = new SplineSegment(); 

      segments[i].x0 = controlPoints[i][0];
      segments[i].y0 = controlPoints[i][1];
      segments[i].x1 = controlPoints[i][2];
      segments[i].y1 = controlPoints[i][3];
      segments[i].x2 = controlPoints[i][4];
      segments[i].y2 = controlPoints[i][5];
      segments[i].x3 = controlPoints[i][6];
      segments[i].y3 = controlPoints[i][7];
    }
  }
  
  //---------------------------------------
  float cubicBezierLookup (float x) {
    float y = 0; 
    int whichSegment = inWhichSegmentIsXValueContained (x); 
    if (whichSegment != -1) {
      y = segments[whichSegment].cubicBezierLookup (x);
    }
    return y;
  }

  //---------------------------------------
  int inWhichSegmentIsXValueContained(float x) {
    int whichSegment = -1; 
    for (int s=0; s<nSplineSegments; s++) {
      if ((x >= segments[s].x0) && (x <= segments[s].x3)) {
        whichSegment = s;
      }
    }
    return whichSegment;
  }

}


//==========================================================
class SplineSegment {
  float x0;  // initial x 
  float y0;  // initial y
  float x1;  // 1st influence x 
  float y1;  // 1st influence y   
  float x2;  // 2nd influence x
  float y2;  // 2nd influence y
  float x3;  // final x 
  float y3;  // final y 

  //---------------------------------------
  float cubicBezierLookup (float queryx) {

    // Assume control points of (0,0), (a,b), (c,d), and (1,1); 
    // Given horizontal query point x between 0...1 
    // Return function value y (also in the range 0...1).

    // Adapted from BEZMATH.PS (1993) by Don Lancaster, Synergetics Inc. 
    // http://www.tinaja.com/text/bezmath.html

    float A =   x3 - 3*x2 + 3*x1 - x0;
    float B = 3*x2 - 6*x1 + 3*x0;
    float C = 3*x1 - 3*x0;   
    float D =   x0;

    float E =   y3 - 3*y2 + 3*y1 - y0;    
    float F = 3*y2 - 6*y1 + 3*y0;             
    float G = 3*y1 - 3*y0;             
    float H =   y0;

    // Solve for t given x (using Newton-Raphelson), then solve for y given t.
    // Assume for the first guess that t = 0.5. (Could also use queryx if in 0...1)
    float currentt = 0.5; 
    int nRefinementIterations = 5;
    for (int i=0; i<nRefinementIterations; i++) {
      float currentx = xFromT (currentt, A, B, C, D); 
      float currentslope = slopeFromT (currentt, A, B, C);
      currentt -= (currentx - queryx)*(currentslope);
      currentt = constrain(currentt, 0, 1);
    } 

    float y = yFromT (currentt, E, F, G, H);
    return y;
  }

  //----------------------------------------------------------
  float slopeFromT (float t, float A, float B, float C) {
    float dtdx = 1.0/(3.0*A*t*t + 2.0*B*t + C); 
    return dtdx;
  }
  //----------------------------------------------------------
  float xFromT (float t, float A, float B, float C, float D) {
    float x = A*(t*t*t) + B*(t*t) + C*t + D;
    return x;
  }
  //----------------------------------------------------------
  float yFromT (float t, float E, float F, float G, float H) {
    float y = E*(t*t*t) + F*(t*t) + G*t + H;
    return y;
  }
}