Galton board sim

Hi,

Trying to figure out how to make something remotely similar to a Galton Board:

My goal is to have a number of rectangles that can grow in size (vertical, horizontal, does not matter) depending on the notes a Pbind plays. Say a Pbind can play notes 1, 2 and 3, to make a very simplified version of this. That would be 3 rectangles that grow in size 1 pixel at a time for each one of those notes played.

It could be cool to see how different Pattern makers in the Pbind churn out different random distributions.

I have been looking into Pen and drawFunc but i am a total SC graphics noob so i’m struggling hard, even though I suspect it might be quite easily doable (?).

Thank you

Talk to @Dindoleon… he’s a champion of SC & graphics

You can use Pen.addRect to draw rectangles.
From the help file:

(
w = Window.new.front;
w.view.background_(Color.white);
w.drawFunc = {
    80.do{
        // set the Color
        Pen.color = Color.green(rrand(0.0, 1), rrand(0.0, 0.5));
        Pen.addRect(
            Rect(20, 20, (w.bounds.width-40).rand, (w.bounds.height-40).rand)
        );
        Pen.perform([\stroke, \fill].choose);
    }
};
w.refresh;
)

Best,
Paul

also these:

Pen.strokeRect(rect)
Strokes a Rect into the window.

Pen.fillRect(rect)
Draws a filled Rect into the window.

I had a go at making a simple version (only the balls are shown):

(
// Galton board
var totalBalls = 200;
var totalRows = 40;
var gridSize = 5;
var ball, arrBalls, arrCols, step;

ball = ();
ball.posX = totalRows;
ball.posY = 0;
ball.alive = false;
arrBalls = Array.fill(totalBalls, {ball.copy});
arrBalls[0].alive = true;
arrCols = 0 ! (totalRows * 2);
step = 0;

{w.close;}.try;
w = Window.new("Galton Board", Rect(10, 60, 800, 600)).front;
w.view.background_(Color.white);
w.drawFunc = {
	Pen.color = Color.blue;
	arrBalls.do({arg item, i;
        Pen.fillOval(
			Rect(150 + (item.posX * gridSize), item.posY * gridSize,
				gridSize, gridSize)
		);
	});
};
w.refresh;

{
	while ({arrBalls.detect({arg item; item.alive == true;}).notNil},
		{
			if (step < (totalBalls - 1), {
				arrBalls[step + 1].alive = true;
			});
			arrBalls.do({arg item, i;
				if (item.alive == true, {
					if (item.posY >= 4 and: {item.posY < (totalRows + 4)}, {
						item.posX = item.posX + [-1, 1][2.rand];
					});
					item.posY = item.posY + 1;
					if (item.posY > (100 - arrCols[item.posX]), {
						item.alive = false;
						arrCols[item.posX] = arrCols[item.posX] + 1;
					});
				});
			});
			{w.refresh}.defer;
			step = step + 1;
			0.05.wait;
	});
}.fork;
)

Best,
Paul

1 Like

So I usually use UserViews to control Pbinds, but here the Pbind is supposed to control the UserView !

My concern was that both are kind of blackboxes, doing there own thing without communicating much with other parts of the software :

  • UserView’s drawFunc do not take custom arguments, but can access a variable that has been declared within it’s scope.
  • Patterns internally deal with their job, but won’t do more.

Fortunately, there’s a special pattern, Pfunc, which allows to execute a Function every time the Pbind plays. More importantly, it takes the current Event the Pbind is playing as argument ! In other words, it allows the Pbind to introspect itself, seeing it’s current parameters (i.e. freq, amp, etc), and to communicate with other parts of the software that way.

So the following code shows how to control an UserView from within a Pbind :


(
// Create a main window :
var win = Window();

// Create our custom view that will display the played note :
var display = UserView();

// The notes the Pbind can play :
var availableNotes = [58, 60, 61, 63, 65, 66, 68, 70].midicps;

// We need a variable that the drawFunc can access,
// which will hold the played note :
var playedNote = -1;

// This is our pbind !
// Here, I can create a Pfunc related to a non-existent symbol name (\foo),
// which is passed the current event the pbind has played.
// This in turn, allows me to reference the note inside the variable and to call the view refreshing.

var pbind = Pbind(
	\instrument, \default,
	\amp, 0.5,
	\freq, Prand(availableNotes, inf),

	// Here :
	\foo, Pfunc({ |event|
		// Pass the Pbind note to the variable
		playedNote = event.freq;

		// And call for the view refresh, we must use {}.defer because we don't have access to UI yet.
		{ display.refresh }.defer;

		0 // We must return a value so that the Pfunc keeps running
	})
).play;

// Now, we'll just setup our display drawFunc, pretty simple for now,
// just write down the note that has been played :
display.drawFunc = { |view|
	Pen.stringCenteredIn(
		playedNote.asString,
		Rect(
			0, 0,
			view.bounds.width, view.bounds.height
		)
	)
};

// Add the display to the window's view layout
win.layout_(
	VLayout(display)
);

// And go !
win.front;

// This can be very handy, comment if needed :
CmdPeriod.doOnce({ win.close });
)

With this, if you’re in the DIY mood, I think it’s enough to figure out how to simulate the Galton box.


However, here is the simple version of the simulation (fully commented if you’d like to understand how it works) :

(
// Create a main window :
var win = Window();

// Create the view that will contain every counter :
var display = UserView().layout_(HLayout());

// The notes the Pbind can play :
var availableNotes = [58, 60, 61, 63, 65, 66, 68, 70].midicps;

// Create an array that will hold each note's occurence count :
var timesNotesWerePlayed = Array.fill(availableNotes.size, { 0 });

var myFavoriteColor = Color(1, 0.333, 0.333);

// Serialisation time !
// For each note that can be played, we'll create an UserView()
// that will display a rectangle,
// which height equals the number of time the note has been played

availableNotes.size.do({ |note, index|
	var view = UserView();

	view.drawFunc_({ |view|
		Pen.fillColor_(myFavoriteColor);

		Pen.fillRect(
			Rect(
				0,
				view.bounds.height - timesNotesWerePlayed[index],
				view.bounds.width,
				timesNotesWerePlayed[index] // <- /!\ Here we access the variable /!\
			)
		);
	});

	// Then, we had it to the display layout :
	display.layout.add(view);
});
// End of serialisation

// The pbind :
Pbind(
	\instrument, \default,
	\dur, 0.25,

	\amp, 0.5,
	\freq, Prand(availableNotes, inf),

	\foo, Pfunc({ |event|
		// Use .indexOf to find which note was played
		var playedNote = availableNotes.indexOf(event.freq);

		// Tell the software the note has been played once more
		timesNotesWerePlayed[playedNote] = timesNotesWerePlayed[playedNote] + 1;

		// We could only refresh the associated view
		{ display.refresh }.defer;
		0
	})
).play;

// Add the display to the window's view layout
win.layout_(
	VLayout(display)
);

// And go !
win.front;

// This can be very handy, comment if needed :
CmdPeriod.doOnce({ win.close });
)

It’s honest work, but it ain’t much. The following code is much more advanced (and evilly uncommented), and is a polish version of the previous code. It lacks a few optimizations, but I don’t think it matters that much :

(
var win = Window();
var display = UserView().layout_(HLayout().margins_(30));

var availableNotes = [58, 60, 61, 63, 65, 66, 68, 70].midicps;
var timesNotesWerePlayed = Array.fill(availableNotes.size, { 0 });
var timers = Array.fill(availableNotes.size, { 0 });

var myFavoriteColor = Color(1, 0.333, 0.333);
var mySecondFavoriteColor = Color(1, 1, 1);
var blinkTime = 0.25;

var displayRatio = 0.1;

var playedNote;

availableNotes.size.do({ |note, index|
	var view = UserView();
	var counterView = UserView().animate_(true).frameRate_(24);
	var numberView = UserView().animate_(true);
	var barSize = 0;

	counterView.drawFunc_({ |view|
		barSize = view.bounds.height * displayRatio * timesNotesWerePlayed[index];
		Pen.fillColor_(
			Color(
				myFavoriteColor.red +
				(mySecondFavoriteColor.red - myFavoriteColor.red * timers[index]),
				myFavoriteColor.green +
				(mySecondFavoriteColor.green - myFavoriteColor.green * timers[index]),
				myFavoriteColor.blue +
				(mySecondFavoriteColor.blue - myFavoriteColor.blue * timers[index]),
			)
		);

		Pen.fillRect(
			Rect(
				0, view.bounds.height - barSize,
				view.bounds.width, barSize
			)
		);

		if(timers[index] > 0)
		{ timers[index] = timers[index] - (blinkTime.reciprocal / view.frameRate) }
		{ timers[index] = 0 };
	});

	numberView.drawFunc = { |view|
		Pen.stringCenteredIn(
			timesNotesWerePlayed[index].asString,
			Rect(
				0, 0,
				view.bounds.width, view.bounds.height
			),
			Font.default.deepCopy.size_(view.bounds.height * 0.5),
			Color.white
		)
	};

	view.layout_(
		VLayout(
			[counterView, stretch: 6],
			[numberView, stretch: 1]
		).margins_(3)
	);

	display.layout.add(view);
});

Pbind(
	\instrument, \default,
	\dur, 0.25,

	\amp, 0.5,
	\freq, Prand(availableNotes, inf),

	\foo, Pfunc({ |event|
		playedNote = availableNotes.indexOf(event.freq);

		timesNotesWerePlayed[playedNote] = timesNotesWerePlayed[playedNote] + 1;
		if((timesNotesWerePlayed[playedNote] * displayRatio) >= 1)
		{ displayRatio = displayRatio / 2 };

		timers[playedNote] = 1;

		0
	})
).play;

display.drawFunc_({ |view|
	Pen.fillColor_(myFavoriteColor);
	Pen.fillRect(
		Rect(
			0, 0,
			view.bounds.width, view.bounds.height
		)
	);
	Pen.fillColor_(Color.black);
	Pen.fillRect(
		Rect(
			5, 5,
			view.bounds.width - 10, view.bounds.height - 10
		)
	);
});

win.layout_(
		VLayout(display).margins_(0)
);

win.front;
CmdPeriod.doOnce({ win.close });
)

Do not hesitate if you have questions about the code,

<3 ,
Simon

Also, for simple drawings, the Plotter is quite quick!

var r = { { 1.0.sum3rand }.dup(1000).histo(1000) };
var p = r.value.plot(maxval: 250);
{
	100.do({
		p.setValue(arrays: p.value + r.value, findSpecs: false);
		0.05.wait
	})
}.fork(AppClock)

True indeed !

In this case, we could also assign the array containing the note’s occurrences to a MultiSliderView,and set it up to display rects instead of lines.