Port of Jared Tarbell’s Substrate from processing to p5.js. Original source is also archived in this repository and care was taken to change as little as possible from the original.
Mouse press restarts the program.
port by @dribnet July 2021
<head>
<script language="javascript" type="text/javascript" src="p5_1.3.1.js"></script>
<script language="javascript" type="text/javascript" src="z_purview_helper.js"></script>
<script language="javascript" type="text/javascript" src="substrate.js"></script>
<style>
body { padding: 0; margin: 0; }
</style>
</head>
<body style="background-color:WhiteSmoke;">
<div class="outer">
<div class="inner">
<div id="canvasContainer"></div>
</div>
</div>
<br>
</body>
GIF89a�
� 0 8( (88(P X( `88@0XX8XPHHHXhXXXhXPX`Xhhhhp`pphpx�0�(�8�@�X �X�X�x �PH�hH�xH�`p�hp�px�� ��0��(�(��(��(��(��H��h��h��h��x��p��x��x��p��x�P��`Ȱ`Ƞx��xȰxаx�`�x��X��H��X��P��P��p��h��x��x��p��p��h��p��x��x��xxx�p�����������������������������������а����и�ఀ���ȸ�а��������������Ș�Ȁ����Ș�Ș�И�ؐ�И�ؘ����������������������Ƞ�Ƞ����Ȩ����ȸ����Ȱ�Ȱ�а�Ƞ�Ȩ�Р�ؠ�ب�ؠ�ب�а�ذ�ظ�ذ�и������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� , �
� }ʼn,ؙ3�d�(Q�PŊ����OAW�0#v�"D�ժ%L�%=z B�֛7�=J��M�#Gܸ�LJK�:uʔI��W�0C�b�1bL��%J�R
x�T��)R� �&MĈ�T��+V�.�(!CF�S4�A�',\���c�3fԨ!BD5j4iJ����!-j�Ȍ���
!b�ӧ'O0aj��˗V�����<y�xA�L�2�٣H�
t�2l�&m��.;v��K��M�����B�
8pH��3�
"T��!D�-�G�"�dJ*��L2�dN:��PBeRJ1�TRQeVZq�W`�E�Yg��V[o�5W]w�W_6Xa���b�=�d�]fJf�u�Y��V�i����k��V�m����o�
W�q�-��s�MWLu�e�]w߅7^y祷^{�
T�A -��CMT�Em��G!�T�I)���K1�T�M9���OA
U�QI��TSOE51U]��V]}�Xe�U��l��\tم�^|��`����b�9�d�Y��!�q�h��fj���l��fn���p�gr�1�t�C�u�iǝw��G�y�Ǟ{�9���ՙ��� ��j ���`�2�Jja�b����鈢���+��b�1�J����x+����k����*yl��B���Z9m��r���azKf�g���m�'}s�g�~���_�Z�0� �
.
��F:酖j�i���*�h�Uj���8��0�:��7֪#�=�ګ��9l��2����J�l��bY��z f�c�k�i��� h���H �E��C��2�� �P,�*��K ��Ql8��K��� C,���J+iE4��d��(���,�TRI$����A[lq�
N8!�$W
0@h�\P�
*��B��Q��e�@"�:�@B�E���8�mC�ƣ^��#����T$a!�J�(B�\��:�+C����D�J����l���BЄ&XAL}��&$ ��\�@ ���I�r�Ü�8�9Љ��3�T�:��Nv���t�;�O�#^0�w �)�y����aO{���J!>�}�c��,?_ȯ�����?�0!!�)� صD��hI$"�B��� ��]���EZ��� ���4�A�pe(C\���c#x@t���!�?0�Z��ΰ�V��Ї>�� � Z��"�8��].s����B7���t�[]�^����v��]�~�b�x�8�^��Eozջ�����}/|�+��ҷ�������w�T�o��_ e�HYt$- )IJ��� &5�IO�R�� �.L�JU����Eey Z����/��K`
�����ihCe2әДf�ɇkfs�E�\7�N&��Q<��i�vf�\��ÈO}���jhJ�8�� �#C��ЈNT�%dF��8(��"%)-&Y��t����'C9�R�2��� N_�Y��ܥ/z��`ӅI5&S���f>3�Ӥ�U|���#zS��l"9�hNtRq�Wt���E���c�g��F�1�o,�jDž�ѡ|��%
H����hG?R6<2�\�IQ��LF֥���ek��V�t���ih�:Z�����U�1��Z��v��L6i ;
// Substrate Watercolor
// j.tarbell June, 2004
// Albuquerque, New Mexico
// complexification.net
// p5.js port by @dribnet July 2021
const canvasWidth = 960;
const canvasHeight = 500;
let dimx = canvasWidth;
let dimy = canvasHeight;
let num = 0;
// maxnum value interpolated between "medium" and "large"
let maxnum = 175;
// grid of cracks
let cgrid = [];
let cracks = [];
// sand painters
let sands = [];
function setup() {
let can = createCanvas(canvasWidth, canvasHeight);
can.parent('canvasContainer');
cgrid = Array(dimx*dimy).fill(0);
cracks = [];
begin();
}
function draw() {
// crack all cracks
for (let n=0; n<num; n++) {
cracks[n].move();
}
}
function mousePressed() {
begin();
}
// added by dribnet for screen caps
function keyTyped() {
if (key == '!') {
saveBlocksImages();
}
else if (key == '@') {
saveBlocksImages(true);
}
}
// METHODS --------------------------------------------------
function makeCrack() {
if (num<maxnum) {
// make a new crack instance
cracks[num] = new Crack();
num++;
}
}
function begin() {
// erase crack grid
for (let y=0; y<dimy; y++) {
for (let x=0; x<dimx; x++) {
cgrid[y*dimx+x] = 10001;
}
}
// make random crack seeds
for (let k=0; k<16; k++) {
let i = int(random(dimx*dimy-1));
cgrid[i]=int(random(360));
}
// make just three cracks
num=0;
for (let k=0; k<3; k++) {
makeCrack();
}
background(255);
}
// COLOR METHODS ----------------------------------------------------------------
// note: original code file built a color-table from the
// pollockShimmering.gif when run. this version caches
// the result to remove that depedency. - @dribnet
const goodcolor = ["#fff0d0","#ffc828","#ffe898","#f0c898","#ffffd0","#a07800","#e8c898","#f8e070","#fff0c8","#d0b080","#fff8d0","#f8e8e0","#ffd8b0","#d0b078","#f0d8c0","#f0d8d0","#ffffd8","#c8c098","#d0c058","#e0c8a8","#d8d0b0","#f8f8d0","#b0a098","#fff0c0","#f0e8b8","#986870","#fff8d8","#ffe8c8","#f0e8c0","#f0e8d8","#e0d8b8","#e0d0a0","#e0d0b0","#d0c8a0","#e8e8d8","#f0d898","#f8f0d8","#fff0a0","#f8e878","#ffe878","#e8c848","#e8b878","#f8e050","#585048","#ffffc8","#a8a078","#e8e098","#f8f0c8","#e8d8a8","#fff8c8","#f0f8a8","#f0f0c0","#ffe078","#f0e8c8","#e8e080","#ffe868","#d0c0a0","#f0e080","#ffe890","#e0c858","#f8e0b8","#f0e0b8","#d8c898","#fff0b8","#c8b078","#f8e8b8","#e8d8c8","#f0c868","#a09078","#fff8c0","#f0f0c8","#ffe8b0","#e8e0b0","#ffd028","#b09030","#f0f0d0","#c0c090","#f8e0c0","#d0b890","#c0b078","#b0b098","#a8a880","#f0e0c0","#e0e0b8","#585838","#d0d0c0","#382810","#383828","#b8b8b0","#c0b090","#98a0b8","#e0b080","#c8c8b8","#f8e0b0","#d8c070","#f8e8c0","#e0d098","#e0d8b0","#586868","#e8f0c0","#906848","#b08868","#e8b828","#ffe8c0","#b0b078","#e0e0b0","#686870","#e0d8a0","#a0a0a8","#e0a060","#685858","#ff9828","#c0a060","#905818","#f8f0b8","#a89868","#a89070","#b0a870","#f0f0e0","#a89848","#586858","#e0b850","#c8b060","#b09020","#e0e0c0","#906070","#a07078","#b88868","#f8e0d8","#c09888","#ffe8d0","#d8c0a0","#c0c0b0","#e8c880","#c8b8a0","#d0c8b0","#fff0e8","#e8f0e0","#f8b828","#384030","#302008","#505860","#d8c0b0","#f0e0b0","#ffd0b8","#a05810","#501000","#e8c078","#f8b888","#e8d050","#fff0d8","#f0d870","#984008","#805800","#e8e0c8","#b8b8a8","#f0e8a0","#102028","#708080","#d8c8a0","#b0b8b0","#ffd8a0","#582800","#d8c8b0","#fff098","#d0c8a8","#fff8b0","#687078","#f0d098","#607070","#484858","#787880","#983010","#fff8e0","#905048","#a82818","#603810","#f8f8f8","#fff0f8"];
function somecolor() {
return color(random(goodcolor));
}
// OBJECTS -------------------------------------------------------
function Crack() {
this.x = null;
this.y = null;
this.t = null; // direction of travel in degrees
this.findStart = function() {
// pick random point
let px=0;
let py=0;
// shift until crack is found
let found=false;
let timeout = 0;
while ((!found) || (timeout++>1000)) {
px = int(random(dimx));
py = int(random(dimy));
if (cgrid[py*dimx+px]<10000) {
found=true;
}
}
if (found) {
// start crack
let a = cgrid[py*dimx+px];
if (random(100)<50) {
a-=90+int(random(-2, 2.1));
}
else {
a+=90+int(random(-2, 2.1));
}
this.startCrack(px, py, a);
}
else {
//println("timeout: "+timeout);
}
}
this.startCrack = function(X, Y, T) {
this.x=X;
this.y=Y;
this.t=T;//%360;
this.x+=0.61*cos(this.t*PI/180);
this.y+=0.61*sin(this.t*PI/180);
}
this.move = function() {
// continue cracking
this.x+=0.42*cos(this.t*PI/180);
this.y+=0.42*sin(this.t*PI/180);
// bound check
let z = 0.33;
let cx = int(this.x+random(-z, z)); // add fuzz
let cy = int(this.y+random(-z, z));
// draw sand painter
this.regionColor();
// draw black crack
stroke(0, 85);
point(this.x+random(-z, z), this.y+random(-z, z));
if ((cx>=0) && (cx<dimx) && (cy>=0) && (cy<dimy)) {
// safe to check
if ((cgrid[cy*dimx+cx]>10000) || (abs(cgrid[cy*dimx+cx]-this.t)<5)) {
// continue cracking
cgrid[cy*dimx+cx]=int(this.t);
}
else if (abs(cgrid[cy*dimx+cx]-this.t)>2) {
// crack encountered (not self), stop cracking
this.findStart();
makeCrack();
}
}
else {
// out of bounds, stop cracking
this.findStart();
makeCrack();
}
}
this.regionColor = function() {
// start checking one step away
let rx=this.x;
let ry=this.y;
let openspace=true;
// find extents of open space
while (openspace) {
// move perpendicular to crack
rx+=0.81*sin(this.t*PI/180);
ry-=0.81*cos(this.t*PI/180);
let cx = int(rx);
let cy = int(ry);
if ((cx>=0) && (cx<dimx) && (cy>=0) && (cy<dimy)) {
// safe to check
if (cgrid[cy*dimx+cx]>10000) {
// space is open
}
else {
openspace=false;
}
}
else {
openspace=false;
}
}
// draw sand painter
this.sp.render(rx, ry, this.x, this.y);
}
// sand painter
this.findStart();
this.sp = new SandPainter();
}
function SandPainter() {
this.c = somecolor();
this.g = random(0.01, 0.1);
this.render = function(x, y, ox, oy) {
// modulate gain
this.g+=random(-0.050, 0.050);
let maxg = 1.0;
if (this.g<0) {
this.g=0;
}
if (this.g>maxg) {
this.g=maxg;
}
// calculate grains by distance
// let grains = int(sqrt((ox-x)*(ox-x)+(oy-y)*(oy-y)));
let grains = 64;
// lay down grains of sand (transparent pixels)
let w = this.g/(grains-1);
for (let i=0; i<grains; i++) {
let a = 0.1-i/(grains*10.0);
stroke(red(this.c), green(this.c), blue(this.c), a*256);
point(ox+(x-ox)*sin(sin(i*w)), oy+(y-oy)*sin(sin(i*w)));
}
}
}
// Substrate Watercolor
// j.tarbell June, 2004
// Albuquerque, New Mexico
// complexification.net
// Processing 0085 Beta syntax update
// j.tarbell April, 2005
int dimx = 500;
int dimy = 500;
int num = 0;
int maxnum = 150;
// grid of cracks
int[] cgrid;
Crack[] cracks;
// color parameters
int maxpal = 512;
int numpal = 0;
color[] goodcolor = new color[maxpal];
// sand painters
SandPainter[] sands;
// MAIN METHODS ---------------------------------------------
void setup() {
size(500,500,P3D);
// size(dimx,dimy,P3D);
background(255);
takecolor("pollockShimmering.gif");
cgrid = new int[dimx*dimy];
cracks = new Crack[maxnum];
begin();
}
void draw() {
// crack all cracks
for (int n=0;n<num;n++) {
cracks[n].move();
}
}
void mousePressed() {
begin();
}
// METHODS --------------------------------------------------
void makeCrack() {
if (num<maxnum) {
// make a new crack instance
cracks[num] = new Crack();
num++;
}
}
void begin() {
// erase crack grid
for (int y=0;y<dimy;y++) {
for (int x=0;x<dimx;x++) {
cgrid[y*dimx+x] = 10001;
}
}
// make random crack seeds
for (int k=0;k<16;k++) {
int i = int(random(dimx*dimy-1));
cgrid[i]=int(random(360));
}
// make just three cracks
num=0;
for (int k=0;k<3;k++) {
makeCrack();
}
background(255);
}
// COLOR METHODS ----------------------------------------------------------------
color somecolor() {
// pick some random good color
return goodcolor[int(random(numpal))];
}
void takecolor(String fn) {
PImage b;
b = loadImage(fn);
image(b,0,0);
for (int x=0;x<b.width;x++){
for (int y=0;y<b.height;y++) {
color c = get(x,y);
boolean exists = false;
for (int n=0;n<numpal;n++) {
if (c==goodcolor[n]) {
exists = true;
break;
}
}
if (!exists) {
// add color to pal
if (numpal<maxpal) {
goodcolor[numpal] = c;
numpal++;
}
}
}
}
}
// OBJECTS -------------------------------------------------------
class Crack {
float x, y;
float t; // direction of travel in degrees
// sand painter
SandPainter sp;
Crack() {
// find placement along existing crack
findStart();
sp = new SandPainter();
}
void findStart() {
// pick random point
int px=0;
int py=0;
// shift until crack is found
boolean found=false;
int timeout = 0;
while ((!found) || (timeout++>1000)) {
px = int(random(dimx));
py = int(random(dimy));
if (cgrid[py*dimx+px]<10000) {
found=true;
}
}
if (found) {
// start crack
int a = cgrid[py*dimx+px];
if (random(100)<50) {
a-=90+int(random(-2,2.1));
} else {
a+=90+int(random(-2,2.1));
}
startCrack(px,py,a);
} else {
//println("timeout: "+timeout);
}
}
void startCrack(int X, int Y, int T) {
x=X;
y=Y;
t=T;//%360;
x+=0.61*cos(t*PI/180);
y+=0.61*sin(t*PI/180);
}
void move() {
// continue cracking
x+=0.42*cos(t*PI/180);
y+=0.42*sin(t*PI/180);
// bound check
float z = 0.33;
int cx = int(x+random(-z,z)); // add fuzz
int cy = int(y+random(-z,z));
// draw sand painter
regionColor();
// draw black crack
stroke(0,85);
point(x+random(-z,z),y+random(-z,z));
if ((cx>=0) && (cx<dimx) && (cy>=0) && (cy<dimy)) {
// safe to check
if ((cgrid[cy*dimx+cx]>10000) || (abs(cgrid[cy*dimx+cx]-t)<5)) {
// continue cracking
cgrid[cy*dimx+cx]=int(t);
} else if (abs(cgrid[cy*dimx+cx]-t)>2) {
// crack encountered (not self), stop cracking
findStart();
makeCrack();
}
} else {
// out of bounds, stop cracking
findStart();
makeCrack();
}
}
void regionColor() {
// start checking one step away
float rx=x;
float ry=y;
boolean openspace=true;
// find extents of open space
while (openspace) {
// move perpendicular to crack
rx+=0.81*sin(t*PI/180);
ry-=0.81*cos(t*PI/180);
int cx = int(rx);
int cy = int(ry);
if ((cx>=0) && (cx<dimx) && (cy>=0) && (cy<dimy)) {
// safe to check
if (cgrid[cy*dimx+cx]>10000) {
// space is open
} else {
openspace=false;
}
} else {
openspace=false;
}
}
// draw sand painter
sp.render(rx,ry,x,y);
}
}
class SandPainter {
color c;
float g;
SandPainter() {
c = somecolor();
g = random(0.01,0.1);
}
void render(float x, float y, float ox, float oy) {
// modulate gain
g+=random(-0.050,0.050);
float maxg = 1.0;
if (g<0) g=0;
if (g>maxg) g=maxg;
// calculate grains by distance
//int grains = int(sqrt((ox-x)*(ox-x)+(oy-y)*(oy-y)));
int grains = 64;
// lay down grains of sand (transparent pixels)
float w = g/(grains-1);
for (int i=0;i<grains;i++) {
float a = 0.1-i/(grains*10.0);
stroke(red(c),green(c),blue(c),a*256);
point(ox+(x-ox)*sin(sin(i*w)),oy+(y-oy)*sin(sin(i*w)));
}
}
}
// j.tarbell June, 2004
// Albuquerque, New Mexico
// complexification.net
/** ----------------- 8< ----------------
notes by dribnet july 2021
Full information about Jared Tarbell's substrate found here:
http://www.complexification.net/gallery/machines/substrate/
This pde file was originally published online here:
http://complexification.net/gallery/machines/substrate/appletm/substrate_m.pde
There are two other versions posted, but these three
differ only by resolution and maxnum:
* small: 250x250, maxnum=100
* medium: 500x500, maxnum=150
* large: 900x900, maxnum=200
Jared has no license but is very liberal with permissions,
as is stated here:
http://www.complexification.net/medium.html
"I believe all code is dead unless executing within the computer.
For this reason I distribute the source code of my programs in
modifiable form. Modifications and extensions of these algorithms
are encouraged."
----------------- 8< ---------------- **/
// helper functions below for supporting blocks/purview/screencap
function saveBlocksImages(doZoom) {
if(doZoom == null) {
doZoom = false;
}
// generate 960x500 preview.jpg of entire canvas
// TODO: should this be recycled?
var offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 960;
offscreenCanvas.height = 500;
var context = offscreenCanvas.getContext('2d');
// background is flat white
context.fillStyle="#FFFFFF";
context.fillRect(0, 0, 960, 500);
context.drawImage(this.canvas, 0, 0, 960, 500);
// save to browser
var downloadMime = 'image/octet-stream';
var imageData = offscreenCanvas.toDataURL('image/jpeg');
imageData = imageData.replace('image/jpeg', downloadMime);
p5.prototype.downloadFile(imageData, 'preview.jpg', 'jpg');
// generate 230x120 thumbnail.png centered on mouse
offscreenCanvas.width = 230;
offscreenCanvas.height = 120;
// background is flat white
context = offscreenCanvas.getContext('2d');
context.fillStyle="#FFFFFF";
context.fillRect(0, 0, 230, 120);
if(doZoom) {
// pixelDensity does the right thing on retina displays
var pd = this._pixelDensity;
var sx = pd * mouseX - pd * 230/2;
var sy = pd * mouseY - pd * 120/2;
var sw = pd * 230;
var sh = pd * 120;
// bounds checking - just displace if necessary
if (sx < 0) {
sx = 0;
}
if (sx > this.canvas.width - sw) {
sx = this.canvas.width - sw;
}
if (sy < 0) {
sy = 0;
}
if (sy > this.canvas.height - sh) {
sy = this.canvas.height - sh;
}
// save to browser
context.drawImage(this.canvas, sx, sy, sw, sh, 0, 0, 230, 120);
}
else {
// now scaledown
var full_width = this.canvas.width;
var full_height = this.canvas.height;
context.drawImage(this.canvas, 0, 0, full_width, full_height, 0, 0, 230, 120);
}
imageData = offscreenCanvas.toDataURL('image/png');
imageData = imageData.replace('image/png', downloadMime);
// call this function after 1 second
setTimeout(function(){
p5.prototype.downloadFile(imageData, 'thumbnail.png', 'png');
}, 1000);
}