function loopArray(array, callback) {
for (let i = array.length - 1; i >= 0; i -= 1) {
callback(array[i], i, array);
return this.array.length;
return this.array[index];
this.array.push(element);
loopArray(this.array, callback);
class SteppableArray extends ArrayWrapper {
static stepFunction(value) {
this.loop(SteppableArray.stepFunction);
class DrawableArray extends ArrayWrapper {
static drawFunction(value) {
this.loop(DrawableArray.drawFunction);
class SpriteArray extends ArrayWrapper {
this.draw = DrawableArray.prototype.draw;
this.step = SteppableArray.prototype.step;
this.shapeSize = params.shapeSize;
this.noiseMagnitudeFactor = params.noiseMagnitudeFactor;
this.centerPosition = createVector();
this.vertexCount = params.vertexCount || Math.floor(0.75 * params.shapeSize);
this.noiseDistanceScale = params.noiseDistanceScale || params.shapeSize / 320;
this.noiseTimeScale = params.noiseTimeScale || 0.005;
this.xNoiseParameterOffset
= createVector(Math.random(), Math.random()).mult(1024);
this.yNoiseParameterOffset
= createVector(Math.random(), Math.random()).mult(1024);
this.reachedEndOfScreen = false;
if (RorschachShape.isNotInitialized)
RorschachShape.initializeStatic();
static initializeStatic() {
this.temporalVector = createVector();
this.isNotInitialized = false;
this.noiseTime += this.noiseTimeScale;
if (this.reachedEndOfScreen)
translate(this.centerPosition.x, this.centerPosition.y);
rotate(this.rotationAngle);
rotate(-this.rotationAngle);
translate(-this.centerPosition.x, -this.centerPosition.y);
drawVertices(yScaleFactor) {
const noiseMagnitude = this.noiseMagnitudeFactor * 0.5 * this.shapeSize;
let currentBasePositionX = -0.5 * this.shapeSize;
const basePositionIntervalDistance = this.shapeSize / this.vertexCount;
const progressRatio = frameCounter.getProgressRatio();
for (let i = 0; i < this.vertexCount; i += 1) {
const distanceFactor = progressRatio * sq(sin((i / this.vertexCount) * PI));
const noiseX = (2 * noise(this.xNoiseParameterOffset.x + this.noiseDistanceScale * currentBasePositionX, this.xNoiseParameterOffset.y + this.noiseTime) - 1) * noiseMagnitude;
const noiseY = (2 * noise(this.yNoiseParameterOffset.x + this.noiseDistanceScale * currentBasePositionX, this.yNoiseParameterOffset.y + this.noiseTime) - 1) * noiseMagnitude;
const vertexPositionX = currentBasePositionX + distanceFactor * noiseX;
const vertexPositionY = yScaleFactor * distanceFactor * (0.3 * this.shapeSize + noiseY);
vertex(vertexPositionX, vertexPositionY);
const rotatedVertexPosition = RorschachShape.temporalVector;
rotatedVertexPosition.set(vertexPositionX, vertexPositionY);
rotatedVertexPosition.rotate(this.rotationAngle);
this.checkScreen(this.centerPosition.x + rotatedVertexPosition.x, this.centerPosition.y + rotatedVertexPosition.y);
currentBasePositionX += basePositionIntervalDistance;
checkScreen(absolutePositionX, absolutePositionY) {
const xMargin = 0.01 * width;
const yMargin = 0.05 * height;
if (absolutePositionX < xMargin || absolutePositionX > width - xMargin ||
absolutePositionY < yMargin || absolutePositionY > height - yMargin) {
this.reachedEndOfScreen = true;
RorschachShape.isNotInitialized = true;
class AbstractShapeColor {
static createAlphaColorArray(c) {
for (let alphaValue = 0; alphaValue <= 255; alphaValue += 1) {
array.push(color(red(c), green(c), blue(c), alpha(c) * alphaValue / 255));
class ShapeColor extends AbstractShapeColor {
constructor(strokeColor, fillColor) {
this.strokeColorArray = AbstractShapeColor.createAlphaColorArray(strokeColor);
this.fillColorArray = AbstractShapeColor.createAlphaColorArray(fillColor);
apply(alphaValue = 255) {
const index = Math.floor(constrain(alphaValue, 0, 255));
stroke(this.strokeColorArray[index]);
fill(this.fillColorArray[index]);
class NoStrokeShapeColor extends AbstractShapeColor {
this.fillColorArray = AbstractShapeColor.createAlphaColorArray(fillColor);
apply(alphaValue = 255) {
const index = Math.floor(constrain(alphaValue, 0, 255));
fill(this.fillColorArray[index]);
class NoFillShapeColor extends AbstractShapeColor {
constructor(strokeColor) {
this.strokeColorArray = AbstractShapeColor.createAlphaColorArray(strokeColor);
apply(alphaValue = 255) {
const index = Math.floor(constrain(alphaValue, 0, 255));
stroke(this.strokeColorArray[index]);
class NullShapeColor extends AbstractShapeColor {
if (FrameCounter.isNotInitialized)
console.log('FrameCounter is not initialized.');
static initializeStatic(frameRate) {
this.frameRate = frameRate;
this.isNotInitialized = false;
return this.count % divisor;
getCycleProgressRatio(frequency) {
return ((frequency * this.count) % FrameCounter.frameRate) / FrameCounter.frameRate;
return Math.sin(this.getCycleProgressRatio(frequency) * TWO_PI);
FrameCounter.isNotInitialized = true;
class TimedFrameCounter extends FrameCounter {
constructor(on, duration = 0, completeBehavior = () => { }) {
this.isCompleted = false;
this.completeBehavior = completeBehavior;
this.durationFrameCount = duration;
this.durationFrameCount = duration;
if (this.count > this.durationFrameCount) {
if (this.durationFrameCount)
return constrain(this.count / this.durationFrameCount, 0, 1);
function createSignFunction(xMargin, yMargin, textSize, textColor, textBackgroundColor, titleText) {
const textAreaWidth = xMargin;
const textAreaHeight = yMargin + textSize * 1.1;
const leftX = width - textAreaWidth;
const topY = height - textAreaHeight;
const baseLineY = height - yMargin;
const getTitleText = (typeof titleText === 'string') ? () => { return titleText; } : titleText;
textBackgroundColor.apply();
rect(leftX, topY, textAreaWidth, textAreaHeight);
text(getTitleText(), leftX, baseLineY);
p5.disableFriendlyErrors = true;
const IDEAL_FRAME_RATE = 60;
const fontPath = 'Bellefair-Regular.ttf';
function getCurrentISODate() {
const dateTime = new Date().toISOString();
return dateTime.substring(0, dateTime.indexOf('T'));
currentFont = loadFont(fontPath);
const canvasSideLength = Math.min(windowWidth, windowHeight);
createCanvas(canvasSideLength, canvasSideLength);
frameRate(IDEAL_FRAME_RATE);
unitLength = Math.min(width, height) / 640;
unitSpeed = unitLength / IDEAL_FRAME_RATE;
strokeWeight(Math.max(1, 1 * unitLength));
FrameCounter.initializeStatic(IDEAL_FRAME_RATE);
backgroundColor = color(252);
frameCounter = new TimedFrameCounter(true, 13 * IDEAL_FRAME_RATE, () => { noLoop(); });
currentFontSize = 14 * unitLength;
textFont(currentFont, currentFontSize);
sign = createSignFunction(200 * unitLength, 20 * unitLength, currentFontSize, new NoStrokeShapeColor(color(0)), new NoStrokeShapeColor(backgroundColor), () => { return 'Rorschach ' + getCurrentISODate() + ' - FAL'; });
rorschachShapeColor.apply();
background(backgroundColor);
const rorschachShapeSize = 480 * unitLength;
rorschachShape = new RorschachShape({
shapeSize: rorschachShapeSize,
vertexCount: Math.floor(1.5 * rorschachShapeSize),
noiseDistanceScale: random(0.005, 0.04),
noiseMagnitudeFactor: random(1, 4),
rorschachShape.centerPosition.set(0.5 * width, 0.48 * height);
rorschachShape.rotationAngle = PI + HALF_PI;
rorschachShapeColor = new NoFillShapeColor(color(0, random(8, 48)));
frameCounter.resetCount();
function mousePressed() {