Tone Matrix of Life

For general discussion about Conway's Game of Life.
Post Reply
Skolnick1
Posts: 1
Joined: January 22nd, 2015, 2:11 am

Tone Matrix of Life

Post by Skolnick1 »

When I was introduced to Conway's, it sort of brought to mind another matrix-based app I've worked with, and I'd love to find some way to integrate the two. I imagine it might be hard to scale appropriately, but we could get creative with binning. Does this sound interesting enough to anyone to be worth pursuing?
User avatar
simsim314
Posts: 1828
Joined: February 10th, 2014, 1:27 pm

Re: Tone Matrix of Life

Post by simsim314 »

There is such project already.

It has an option to switch between default and game of life modes.

EDIT I modified it a bit -now you can also edit in default and switch to GOL.

Code: Select all

// http://paulirish.com/2011/requestanimationframe-for-smart-animating
// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
	return  window.requestAnimationFrame     || 
		  window.webkitRequestAnimationFrame || 
		  window.mozRequestAnimationFrame    || 
		  window.oRequestAnimationFrame      || 
		  window.msRequestAnimationFrame     || 
		  function( callback ){
				window.setTimeout(callback, 1000 / 60);
		  };
})();

var KGN = {
	WIDTH: 	0,
	HEIGHT: 0,	

	CELL_SIZE: 24,
	CELL_RADIUS: 2,
	CELL_GAP: 8,
	CELL_NUMBER: 16,
	INTERVAL: 128,
	WAVE_INTERVAL: 25,
	WAVE_FORCE: 80,
	WAVE_DAMP: 0.1,
	
	MODE_DEFAULT:	0,
	MODE_GAME_OF_LIFE: 1,
	
	BLACK: 	"#000000",	
	SAVE_BTN_IDLE: "#dadada",
	SAVE_BTN_PRESSED: "#24E33B",
	OFF: 0x2a,	
	ON: 0xda,
	
	VOLUME: 0.5,
	START_NOTE: 369.99,
	NOTE_RATIOS: [1, 9/8, 5/4, 3/2, 5/3, 2],
	ENVELOPE: new Float32Array([0,0.1,0,0]), //Attack Decay Sustain Release
		
	canvas: 			null,
	ctx: 				null,			
	state:				null,

	init: function() {				
		KGN.canvas = document.getElementById('game_world');
		KGN.ctx = KGN.canvas.getContext('2d');
		
		
				
		KGN.WIDTH = KGN.CELL_NUMBER * KGN.CELL_SIZE + (KGN.CELL_NUMBER-1)*KGN.CELL_GAP;
		KGN.HEIGHT = KGN.WIDTH + KGN.CELL_SIZE + KGN.CELL_GAP;
		
		
		KGN.canvas.width = KGN.WIDTH;
		KGN.canvas.height = KGN.HEIGHT;		
		
		var iOS = ( navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false );
		
		KGN.WaveMap.init();
		if (!iOS){
			KGN.Input.init();	
			KGN.Synth.init();
		}
		else {
			KGN.Start.init();
		}
		KGN.InGame.init();
		KGN.state = (iOS) ? KGN.Start : KGN.InGame;				        
		
		KGN.loop();		
	},		
	
	update: function() {
		KGN.state.update();
	},
	
	render: function() {
		KGN.ctx.fillStyle = KGN.BLACK;
		KGN.ctx.fillRect(0, 0, KGN.WIDTH, KGN.HEIGHT);		
		KGN.state.render();
	},
	
	loop: function() {
		requestAnimFrame( KGN.loop );

       KGN.update();
       KGN.render();
	},
	
	random: function(n){
		return ~~(Math.random()*n);
	}
};

KGN.Start = {

	init: function(){
		window.addEventListener("mousedown", this.touchstart_callback, false);
		window.addEventListener("touchstart", this.touchstart_callback, false);
	},
	
	touchstart_callback: function(e){
		e.preventDefault();    		
		KGN.Synth.init();
		window.removeEventListener("mousedown", KGN.Start.touchstart_callback, false);
		window.removeEventListener("touchstart", KGN.Start.touchstart_callback, false);
	},
	
	update: function(){
		if (KGN.Synth.ready){
			KGN.Input.init();
			KGN.state = KGN.InGame;
		}
	},
	
	render: function(){
		KGN.InGame.render();
	}
};

KGN.Input = {
	x: 0,
	y: 0,
	UNSET: -1,
	SET: 2,
	ON: 1,
	OFF: 0,
	state: -1,
	moved: false,
	
	init: function(){
		window.addEventListener('mousedown', function(e) {
			e.preventDefault();			
			KGN.Input.set(e);
		}, false);

		window.addEventListener('mousemove', function(e) {
			e.preventDefault();			
			KGN.Input.modify(e);
		}, false);

		window.addEventListener('mouseup', function(e) {
			e.preventDefault();			
			KGN.Input.unset(e);
		}, false);

		window.addEventListener('touchstart', function(e) {
			e.preventDefault();    		
			KGN.Input.set(e.touches[0]);			
		}, false);

		window.addEventListener('touchmove', function(e) {       
			e.preventDefault();
			KGN.Input.modify(e);
		}, false);

		window.addEventListener('touchend', function(e) {            
			e.preventDefault();
			KGN.Input.unset(e);
		}, false);
	},

	set: function(event) {
		event.button = event.button || 0;
		if (event.button == 0){
			this.x = event.pageX - KGN.canvas.offsetLeft;
			this.y = event.pageY - KGN.canvas.offsetTop;
			this.state = KGN.Input.SET;
			this.moved = false;
		}
	},
	
	unset: function(event) {
		event.button = event.button || 0;
		if (event.button == 0){
			this.state = KGN.Input.UNSET;
		}
	},
	
	modify: function(event) {
		this.x = event.pageX - KGN.canvas.offsetLeft;
       	this.y = event.pageY - KGN.canvas.offsetTop;  
       	this.moved = true;     
	},
   
   in_rect_area: function(x, y, width, height){
		var result = false;	
		
		if (this.x >= x && this.x <= x + width && this.y >= y && this.y <= y + height){
			result = true;
		}
		return result;
	}
};

KGN.InGame = {	
	mode: KGN.MODE_DEFAULT,
	cells: null,	
	column: 0,
	prev_column: 0,	
	previous_ms: new Date().getTime(),
	neighbour: [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]],	
	save_btn: null,
	clear_btn: null,
	gol_btn: null,
	
	init: function(){
		this.cells = new Array(KGN.CELL_NUMBER);
		var codes = location.search.substr(1).split('.');
		
		for (var i = 0; i < this.cells.length; i++){
			this.cells[i] = new Array(KGN.CELL_NUMBER);
			var code = decodeURIComponent(codes[i]||0);
			for (var j = 0; j < this.cells[i].length; j++){	
				var mask = 1<<j;
				this.cells[i][j] = new KGN.Cell(i, j, (code&mask)>>j);
			}
		}
		
		this.clear_btn = new KGN.Button(
			(this.cells.length/2-3) * (KGN.CELL_SIZE+KGN.CELL_GAP) - 1, 
			(this.cells.length) * (KGN.CELL_SIZE+KGN.CELL_GAP) - 1, 
			KGN.CELL_SIZE*2+KGN.CELL_GAP,
			KGN.CELL_SIZE,
			'clear'
		);
		
		this.save_btn = new KGN.Button(
			(this.cells.length/2-1) * (KGN.CELL_SIZE+KGN.CELL_GAP) - 1, 
			(this.cells.length) * (KGN.CELL_SIZE+KGN.CELL_GAP) - 1, 
			KGN.CELL_SIZE*2+KGN.CELL_GAP,
			KGN.CELL_SIZE,
			'url'
		);
		
		this.gol_btn = new KGN.Button(
			(this.cells.length/2+1) * (KGN.CELL_SIZE+KGN.CELL_GAP) - 1, 
			(this.cells.length) * (KGN.CELL_SIZE+KGN.CELL_GAP) - 1, 
			KGN.CELL_SIZE*3+KGN.CELL_GAP*2,
			KGN.CELL_SIZE,
			'default'
		);
		
	},
	
	update: function(){		
		if (this.save_btn.update()){
			this.make_url();
		}
		else if (this.clear_btn.update()){
			this.clear_map();
		}	
		else if (this.gol_btn.update()){
			this.toggle_gol();
		}
		if (KGN.Input.state != KGN.Input.UNSET){	
			var broke = false;
			for (var i = 0; i < this.cells.length; i++){
				for (var j = 0; j < this.cells[0].length; j++){
					var c = this.cells[i][j];
					if (KGN.Input.in_rect_area(c.x, c.y, KGN.CELL_SIZE, KGN.CELL_SIZE)){
						if (KGN.Input.state == KGN.Input.SET){
							c.status = 1 - c.status;
							KGN.Input.state = c.status;	
							KGN.WaveMap.drop(i, j);							
						}
						else {
							if (c.status != KGN.Input.state){
								c.status = KGN.Input.state;
								KGN.WaveMap.drop(i, j);
							}
						}
						
						broke = true;
						break;
					}
				}
				if (broke) break;
			}
		}
		
		var this_ms = new Date().getTime();
		
		if (this_ms - this.previous_ms >= KGN.INTERVAL){
			this.previous_ms = this_ms;
			this.column = (this.column + 1) % KGN.CELL_NUMBER;
			for (var i = 0; i < this.cells.length; i++){
				this.cells[i][this.prev_column].playing = false;
				if (this.cells[i][this.column].status){
					this.cells[i][this.column].playing = true;
					KGN.Synth.play(i);
					KGN.WaveMap.drop(i, this.column);
				}
			}
			this.prev_column = this.column;
			
			if (this.mode == KGN.MODE_GAME_OF_LIFE && this.column == KGN.CELL_NUMBER - 1){
				for (var i = 0; i < this.cells.length; i++){
					for (var j = 0; j < this.cells[i].length; j++){
						var alive = 0;	
						for (var k = 0; k < this.neighbour.length; k++){
							var ni = i + this.neighbour[k][0];
							var nj = j + this.neighbour[k][1];
							ni = (ni < 0) ? ni + this.cells.length : (ni >= this.cells.length) ? ni - this.cells.length : ni;
							nj = (nj < 0) ? nj + this.cells[i].length : (nj >= this.cells[i].length) ? nj - this.cells[i].length : nj;
							alive += this.cells[ni][nj].status;
						}
						if (alive < 2 || alive > 3){
							this.cells[i][j].next_status = 0;
						}					
						else if (alive == 3){
							this.cells[i][j].next_status = 1;
						}
						else {
							this.cells[i][j].next_status = this.cells[i][j].status;
						}
					}
				}
				for (var i = 0; i < this.cells.length; i++){
					for (var j = 0; j < this.cells[i].length; j++){
						this.cells[i][j].status = this.cells[i][j].next_status;
					}
				}
			}
		}
		
		KGN.WaveMap.update();		
	},
	
	render: function(){		
		for (var i = 0; i < this.cells.length; i++){
			for (var j = 0; j < this.cells[i].length; j++){
				this.cells[i][j].render();
			}
		}
		this.save_btn.render();
		this.clear_btn.render();
		this.gol_btn.render();
	},
	
	make_url: function(){
		var codes = "?";
		for (var i = 0; i < this.cells.length; i++){
			var code = 0;
			for (var j = 0; j < this.cells[i].length; j++){
				code |= this.cells[i][j].status << j;
			}
			codes += code + ".";
		}
		window.history.replaceState('foobar', 'Tonematrix clone', 'index.html'+codes);
	},
	
	clear_map: function(){
		this.mode = KGN.MODE_DEFAULT;
		this.gol_btn.change_label( "default" );
		for (var i = 0; i < this.cells.length; i++){
			for (var j = 0; j < this.cells[i].length; j++){
				this.cells[i][j].pause();
				this.cells[i][j].status = 0;
			}
		}
		window.history.replaceState('foobar', 'Tonematrix clone', 'index.html');
	},
	
	toggle_gol: function(){
		this.mode = 1 - this.mode;
		
		if (this.mode == KGN.MODE_DEFAULT){
			this.gol_btn.change_label( "default" );
		}
		else{
			this.gol_btn.change_label( "game of life" );
			for (var i = 0; i < this.cells.length; i++){
				for (var j = 0; j < this.cells[i].length; j++){	
					this.cells[i][j].pause();
					//this.cells[i][j].status = KGN.random(2);
				}
			}
		}
	}
}

KGN.Button = function(x, y, width, height, label){
	this.x = x;
	this.y = y;
	this.width = width;
	this.height = height;
	KGN.ctx.font = "12px sans-serif";
	this.label_x = x + (width - KGN.ctx.measureText(label).width)/2;
	this.label_y = y + height/2;
	this.label = label;
	this.pressed = false;
	
	this.render = function(){
		if (this.pressed){
			KGN.ctx.strokeStyle = KGN.SAVE_BTN_PRESSED;
			KGN.ctx.fillStyle = KGN.SAVE_BTN_PRESSED;
		}
		else{
			KGN.ctx.strokeStyle = KGN.SAVE_BTN_IDLE;
			KGN.ctx.fillStyle = KGN.SAVE_BTN_IDLE;
		}
		KGN.ctx.strokeRect(this.x, this.y, this.width, this.height);
		KGN.ctx.font="12px sans-serif";
		KGN.ctx.textBaseline="middle";
		KGN.ctx.fillText(this.label, this.label_x, this.label_y);
	}
	
	this.update = function(){
		var result = false;
		if (KGN.Input.state == KGN.Input.SET && KGN.Input.moved == false && KGN.Input.in_rect_area(this.x, this.y, this.width, this.height)){
			if (!this.pressed){
				this.pressed = true;
				result = true;
			}
		}	
		else{
			this.pressed = false;
		}	
		return result;
	}
	
	this.change_label = function(new_label){
		KGN.ctx.font = "12px sans-serif";
		this.label_x = x + (width - KGN.ctx.measureText(new_label).width)/2;
		this.label_y = y + height/2;
		this.label = new_label;
	}
	
}

KGN.Cell = function(i, j, status){
	this.i = i;
	this.j = j;
	this.x = j * (KGN.CELL_SIZE+KGN.CELL_GAP);
	this.y = i * (KGN.CELL_SIZE+KGN.CELL_GAP);			
	this.status = status;
	this.next_status = status;
	this.color = KGN.ON;
	this.palying = false;
	
	this.render = function(){
		if (this.playing){
			this.color = "#ffffff";
		}
		else{		
			this.color = ~~(((this.status) ? KGN.ON : KGN.OFF) + 0xff*(KGN.WaveMap.curr_map[this.i][this.j] / KGN.WAVE_FORCE));
			this.color = (this.color > 220) ? 220 : this.color
			var temp = this.color.toString(16);
			this.color = "#" + temp + "" + temp + "" + temp;
		}
		KGN.ctx.fillStyle = this.color;						
		
		KGN.ctx.beginPath();
		KGN.ctx.moveTo(this.x, this.y + KGN.CELL_RADIUS);
		KGN.ctx.lineTo(this.x, this.y + KGN.CELL_SIZE - KGN.CELL_RADIUS);
		KGN.ctx.quadraticCurveTo(this.x, this.y + KGN.CELL_SIZE, this.x + KGN.CELL_RADIUS, this.y + KGN.CELL_SIZE);
		KGN.ctx.lineTo(this.x + KGN.CELL_SIZE - KGN.CELL_RADIUS, this.y + KGN.CELL_SIZE);
		KGN.ctx.quadraticCurveTo(this.x + KGN.CELL_SIZE, this.y + KGN.CELL_SIZE, this.x + KGN.CELL_SIZE, this.y + KGN.CELL_SIZE - KGN.CELL_RADIUS);
		KGN.ctx.lineTo(this.x + KGN.CELL_SIZE, this.y + KGN.CELL_RADIUS);
		KGN.ctx.quadraticCurveTo(this.x + KGN.CELL_SIZE, this.y, this.x + KGN.CELL_SIZE - KGN.CELL_RADIUS, this.y);
		KGN.ctx.lineTo(this.x + KGN.CELL_RADIUS, this.y);
		KGN.ctx.quadraticCurveTo(this.x, this.y, this.x, this.y + KGN.CELL_RADIUS);
		KGN.ctx.fill();		
		
	};
	
	this.play = function(){
		this.playing = true;
	};
	
	this.pause = function(){
		this.playing = false;	
	};
}

KGN.WaveMap = {
	next_map: null,
	curr_map: null,	
	buffer: null,
	previous_ms: new Date().getTime(),		
	neighbours: [[-1, -1], [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-2, 0], [0, -2], [2, 0], [0, 2]],
	
	init: function(){
		this.next_map = new Array(KGN.CELL_NUMBER);	
		
		for (var i = 0; i < KGN.CELL_NUMBER; i++){
			this.next_map[i] = new Array(KGN.CELL_NUMBER);
			for (var j = 0; j < KGN.CELL_NUMBER; j++){
				this.next_map[i][j] = 0;
			}			
		}
		this.curr_map = this.copy(this.next_map);
		this.buffer = this.copy(this.next_map);
	},
	
	drop: function(i, j){
		this.curr_map[i][j] = KGN.WAVE_FORCE;		
	},
	
	update: function(){
		var this_ms = new Date().getTime();
		if (this_ms - this.previous_ms >= KGN.WAVE_INTERVAL){
			this.previous_ms = this_ms;
			for (var i = 0; i < this.curr_map.length; i++){
				for (var j = 0; j < this.curr_map.length; j++){								
					var neighbours_number = 0;
					var neighbours_sum = 0;
					for (var k = 0; k < this.neighbours.length; k++){
						var ni = i + this.neighbours[k][0];
						var nj = j + this.neighbours[k][1];
						if (ni >= 0 && ni < KGN.CELL_NUMBER && nj >= 0 && nj < KGN.CELL_NUMBER){
							neighbours_number++;
							neighbours_sum += this.curr_map[ni][nj];
						}
					}
					var new_value = ~~(neighbours_sum/~~(neighbours_number/2)) - this.next_map[i][j];
					new_value -= new_value*KGN.WAVE_DAMP;
					this.buffer[i][j] = (new_value < 0.1) ? 0 : new_value;					
				}
			}
			this.next_map = this.copy(this.curr_map);
			this.curr_map = this.copy(this.buffer);
		}		
				
	},
	
	copy: function(original){
		var copy = new Array(original.length);
		for (var i = 0; i < copy.length; i++){
			copy[i] = original[i].slice(0);
		}
		return copy;
	}
	
}

KGN.Synth = {
	ready: false,
	context: null, 
	voices: null,
	filter: null,
	reverb: null,
	compressor: null,
	
	init: function(){
		window.AudioContext = window.AudioContext || window.webkitAudioContext;
		this.context = new AudioContext();
		this.reverb = this.context.createConvolver();
		var rate = this.context.sampleRate
		, length = rate * 2
		, decay = 15
		, impulse = this.context.createBuffer(2, length, rate)
		, impulseL = impulse.getChannelData(0)
		, impulseR = impulse.getChannelData(1);

		for (var i = 0; i < length; i++) {
			impulseL[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
			impulseR[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
			if (i > length/3){
				decay = 8;
			}
		}
		this.reverb.buffer = impulse;
		
		this.compressor = this.context.createDynamicsCompressor();
		
		this.filter = this.context.createBiquadFilter();
		this.filter.type = this.filter.LOWPASS;
		this.filter.frequency.value = KGN.START_NOTE*3;
		
		this.filter.connect(this.reverb);
		this.reverb.connect(this.compressor);
		this.compressor.connect(this.context.destination);
		
		this.voices = new Array(KGN.CELL_NUMBER);
		var base_freq = KGN.START_NOTE;
		var ratio_index = 0;
		for (var i = this.voices.length-1; i >=0; i--){
			this.voices[i] = new KGN.Voice(this.context, base_freq*KGN.NOTE_RATIOS[ratio_index++]);
			if (ratio_index == 5){
				base_freq = base_freq*KGN.NOTE_RATIOS[ratio_index];
				ratio_index = 0;
			}
		}
		this.ready = true;
	},
	
	play: function(n){
		this.voices[n].play();
	}
	
}

KGN.Voice = function(context, frequency){
	
	var sine_osc = KGN.Synth.context.createOscillator();
	sine_osc.frequency.value = frequency;
	sine_osc.type = sine_osc.SINE;
	if (!sine_osc.start){
		sine_osc.start = sine_osc.noteOn;
	}
	
	var gain = KGN.Synth.context.createGain();
	gain.gain.value = 0;
	
	sine_osc.connect(gain);
	gain.connect(KGN.Synth.filter);
	sine_osc.start(0);
	
	this.sine_osc = sine_osc;
	this.gain = gain;
	
	this.play = function(){
		var now = KGN.Synth.context.currentTime;
  		
  		this.gain.gain.cancelScheduledValues(now);
		this.gain.gain.setValueAtTime(this.gain.gain.value, now);
		this.gain.gain.linearRampToValueAtTime(KGN.VOLUME, now + KGN.ENVELOPE[0]);
		this.gain.gain.linearRampToValueAtTime(KGN.ENVELOPE[2], now + KGN.ENVELOPE[0] + KGN.ENVELOPE[1]);
	};
}
User avatar
simsim314
Posts: 1828
Joined: February 10th, 2014, 1:27 pm

Re: Tone Matrix of Life

Post by simsim314 »

GOL is a bit too dynamic for music... maybe we should use some B4+ rule that has only oscillators and SLs
Post Reply