/** * Reaction-diffusion system (Gray-Scott equation) * by Kazuki Maeda * Last-Modified: Apr 11, 2017 */ var WIDTH = 200, HEIGHT = 200; var maxNumCanvas = 2; var delta = 0.1, epsilon = 0.1; var timeInterval = 1000/60; var ncMinU = -0.1, ncMaxU = 1.1; var ncMinV = -0.1, ncMaxV = 1.1; var thread; var T; var Du, Dv, cf, ck; var canvasU, ctxU, imageU; var canvasV, ctxV, imageV; var u, v, pu, pv; var numCanvas var nullcline; var nullcline, ncDist, ncDistCtx, ncDistImage; var ncCurveU, ncCurveV; var ncDistArray; window.onload = function(){ document.getElementById('startstop').addEventListener('click', startstop); document.getElementById('random').addEventListener('click', resetRandom); document.getElementById('target').addEventListener('click', resetTarget); document.getElementById('I').addEventListener('click', resetI); document.getElementById('Y').addEventListener('click', resetY); document.getElementById('double').addEventListener('click', showDouble); for(var c = 0; c < maxNumCanvas; ++c){ document.getElementById('cf'+c).addEventListener('keypress', updateNullclineK); document.getElementById('ck'+c).addEventListener('keypress', updateNullclineK); document.getElementById('cf'+c).addEventListener('blur', updateNullcline); document.getElementById('ck'+c).addEventListener('blur', updateNullcline); } init(); } function init(){ Du = [], Dv = [], cf = [], ck = []; canvasU = [], canvasV = [], ctxU = [], ctxV = [], imageU = [], imageV = []; nullcline = [], ncDist = [], ncCurveU = [], ncCurveV = []; ncDistCtx = [], ncDistImage = []; var ncAxisU = [], ncAxisV = [], ncLabelU = [], ncLabelV = []; for(var c = 0; c < maxNumCanvas; ++c){ canvasU[c] = document.createElement('canvas'); canvasV[c] = document.createElement('canvas'); canvasU[c].width = WIDTH, canvasV[c].width = WIDTH; canvasU[c].height = HEIGHT, canvasV[c].height = HEIGHT; canvasU[c].style.verticalAlign = 'middle', canvasV[c].style.verticalAlign = 'middle'; nullcline[c] = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); ncDist[c] = document.createElement('canvas'); nullcline[c].style.verticalAlign = 'middle'; nullcline[c].style.position = 'absolute'; ncDist[c].style.verticalAlign = 'middle'; ncDist[c].style.position = 'absolute'; ncDist[c].width = WIDTH, ncDist[c].height = HEIGHT; document.getElementById('canvas'+c).removeChild(document.getElementById('canvas'+c).firstChild); document.getElementById('canvas'+c).appendChild(document.createTextNode('$u$: ')); document.getElementById('canvas'+c).appendChild(canvasU[c]); document.getElementById('canvas'+c).appendChild(document.createTextNode(' $v$: ')); document.getElementById('canvas'+c).appendChild(canvasV[c]); document.getElementById('canvas'+c).appendChild(document.createTextNode(' nullcline: ')); document.getElementById('canvas'+c).appendChild(nullcline[c]); document.getElementById('canvas'+c).appendChild(ncDist[c]); ctxU[c] = canvasU[c].getContext('2d'), ctxV[c] = canvasV[c].getContext('2d'); imageU[c] = ctxU[c].getImageData(0, 0, WIDTH, HEIGHT); imageV[c] = ctxV[c].getImageData(0, 0, WIDTH, HEIGHT); for(var i = 0; i < WIDTH*HEIGHT; ++i){ imageU[c].data[i*4+0] = 0; // R imageV[c].data[i*4+1] = 0; // G imageU[c].data[i*4+2] = 0; // B imageU[c].data[i*4+3] = 255, imageV[c].data[i*4+3] = 255; // A } nullcline[c].style.backgroundColor = 'black'; nullcline[c].setAttribute('width', WIDTH); nullcline[c].setAttribute('height', HEIGHT); ncAxisU[c] = document.createElementNS('http://www.w3.org/2000/svg', 'line'); ncAxisU[c].setAttribute('x1', ''+ncTrU(ncMinU)); ncAxisU[c].setAttribute('y1', ''+ncTrV(0)); ncAxisU[c].setAttribute('x2', ''+ncTrU(ncMaxU)); ncAxisU[c].setAttribute('y2', ''+ncTrV(0)); ncAxisU[c].setAttribute('stroke', 'white'); ncAxisU[c].setAttribute('stroke-width', '1'); nullcline[c].appendChild(ncAxisU[c]); ncAxisV[c] = document.createElementNS('http://www.w3.org/2000/svg', 'line'); ncAxisV[c].setAttribute('x1', ''+ncTrU(0)); ncAxisV[c].setAttribute('y1', ''+ncTrV(ncMinV)); ncAxisV[c].setAttribute('x2', ''+ncTrU(0)); ncAxisV[c].setAttribute('y2', ''+ncTrV(ncMaxV)); ncAxisV[c].setAttribute('stroke', 'white'); ncAxisV[c].setAttribute('stroke-width', '1'); nullcline[c].appendChild(ncAxisV[c]); ncCurveU[c] = []; for(var i = 0; i < 3; ++i){ ncCurveU[c][i] = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); ncCurveU[c][i].setAttribute('stroke', '#00FF00'); ncCurveU[c][i].setAttribute('stroke-width', '3'); ncCurveU[c][i].setAttribute('fill', 'none'); nullcline[c].appendChild(ncCurveU[c][i]); } ncCurveV[c] = []; for(var i = 0; i < 2; ++i){ ncCurveV[c][i] = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); ncCurveV[c][i].setAttribute('stroke', '#FF00FF'); ncCurveV[c][i].setAttribute('stroke-width', '3'); ncCurveV[c][i].setAttribute('fill', 'none'); nullcline[c].appendChild(ncCurveV[c][i]); } ncLabelU[c] = document.createElementNS('http://www.w3.org/2000/svg', 'text'); ncLabelU[c].setAttribute('x', ''+(ncTrU(ncMaxU)-10)); ncLabelU[c].setAttribute('y', ''+(ncTrV(0)+10*1.5)); ncLabelU[c].setAttribute('font-size', '10pt'); ncLabelU[c].setAttribute('font-family', 'serif'); ncLabelU[c].setAttribute('font-style', 'italic'); ncLabelU[c].setAttribute('fill', 'white'); ncLabelU[c].textContent = 'u'; nullcline[c].appendChild(ncLabelU[c]); ncLabelV[c] = document.createElementNS('http://www.w3.org/2000/svg', 'text'); ncLabelV[c].setAttribute('x', ''+(ncTrU(0)+10*0.5)); ncLabelV[c].setAttribute('y', ''+(ncTrV(ncMaxV)+10)); ncLabelV[c].setAttribute('font-size', '10pt'); ncLabelV[c].setAttribute('font-family', 'serif'); ncLabelV[c].setAttribute('font-style', 'italic'); ncLabelV[c].setAttribute('fill', 'white'); ncLabelV[c].textContent = 'v'; nullcline[c].appendChild(ncLabelV[c]); ncDistCtx[c] = ncDist[c].getContext('2d'); ncDistImage[c] = ncDistCtx[c].getImageData(0, 0, WIDTH, HEIGHT); for(var i = 0; i < WIDTH*HEIGHT; ++i){ ncDistImage[c].data[i*4+0] = 255; // R ncDistImage[c].data[i*4+1] = 255; // G ncDistImage[c].data[i*4+2] = 255; // B ncDistImage[c].data[i*4+3] = 255; // A } } ncDistArray = []; for(var i = 0; i < HEIGHT; ++i) ncDistArray[i] = []; numCanvas = 1; updateNullcline(null); resetRandom(null); } function resetRandom(e){ u = [], v = [], pu = [], pv = []; for(var c = 0; c < maxNumCanvas; ++c){ u[c] = [], v[c] = [], pu[c] = [], pv[c] = []; for(var i = 0; i < HEIGHT; ++i){ u[c][i] = [], v[c][i] = [], pu[c][i] = [], pv[c][i] = []; for(var j = 0; j < WIDTH; ++j){ if(c == 0){ u[c][i][j] = Math.random(); v[c][i][j] = Math.random(); } else { u[c][i][j] = u[0][i][j]; v[c][i][j] = v[0][i][j]; } } } } updateImage(); } function resetTarget(e){ u = [], v = [], pu = [], pv = []; for(var c = 0; c < maxNumCanvas; ++c){ u[c] = [], v[c] = [], pu[c] = [], pv[c] = []; for(var i = 0; i < HEIGHT; ++i){ u[c][i] = [], v[c][i] = [], pu[c][i] = [], pv[c][i] = []; for(var j = 0; j < WIDTH; ++j){ if(j > 0.49*WIDTH && j < 0.51*WIDTH && i > 0.49*HEIGHT && i < 0.51*HEIGHT){ u[c][i][j] = 1.0; v[c][i][j] = 0.0; } else { u[c][i][j] = 0.0; v[c][i][j] = 0.0; } } } } updateImage(); } function resetI(e){ u = [], v = [], pu = [], pv = []; for(var c = 0; c < maxNumCanvas; ++c){ u[c] = [], v[c] = [], pu[c] = [], pv[c] = []; for(var i = 0; i < HEIGHT; ++i){ u[c][i] = [], v[c][i] = [], pu[c][i] = [], pv[c][i] = []; for(var j = 0; j < WIDTH; ++j){ if(j > 0.49*WIDTH && j < 0.51*WIDTH){ u[c][i][j] = 1.0; v[c][i][j] = 0.0; } else { u[c][i][j] = 0.0; v[c][i][j] = 0.0; } } } } updateImage(); } function resetY(e){ u = [], v = [], pu = [], pv = []; for(var c = 0; c < maxNumCanvas; ++c){ u[c] = [], v[c] = [], pu[c] = [], pv[c] = []; for(var i = 0; i < HEIGHT; ++i){ u[c][i] = [], v[c][i] = [], pu[c][i] = [], pv[c][i] = []; for(var j = 0; j < WIDTH; ++j){ if(j > 0.49*WIDTH && j < 0.51*WIDTH && i > 0.49*HEIGHT){ u[c][i][j] = 1.0; v[c][i][j] = 0.0; } else { u[c][i][j] = 0.0; v[c][i][j] = 0.0; } } } for(var j = 0; j < 0.5*WIDTH; ++j){ for(var i = 0; i < 0.02*HEIGHT; ++i){ var tmpi = parseInt(j-0.01*HEIGHT+i); if(tmpi >= 0){ u[c][tmpi][j] = 1.0; u[c][tmpi][WIDTH-1-j] = 1.0; } } } } updateImage(); } function updateImage(){ for(var c = 0; c < numCanvas; ++c){ for(var i = 0; i < HEIGHT; ++i) for(var j = 0; j < WIDTH; ++j){ var val = parseInt(u[c][i][j]*255); if(val < 0) val = 0; if(val > 255) val = 255; imageU[c].data[4*(i*WIDTH+j)+1] = val; // G val = parseInt(v[c][i][j]*255); if(val < 0) val = 0; if(val > 255) val = 255; imageV[c].data[4*(i*WIDTH+j)+0] = val; // R imageV[c].data[4*(i*WIDTH+j)+2] = val; // B } ctxU[c].putImageData(imageU[c], 0, 0), ctxV[c].putImageData(imageV[c], 0, 0); } // distribution on nullcline for(var c = 0; c < numCanvas; ++c){ // init for(var i = 0; i < HEIGHT; ++i) for(var j = 0; j < WIDTH; ++j) ncDistArray[i][j] = 0; // construct for(var i = 0; i < HEIGHT; ++i) for(var j = 0; j < WIDTH; ++j){ var iU = parseInt(ncTrU(u[c][i][j])); var iV = parseInt(ncTrV(v[c][i][j])); if(iU >= 0 && iU < WIDTH && iV >= 0 && iV < HEIGHT) ++ncDistArray[iV][iU]; } // draw for(var i = 0; i < HEIGHT; ++i) for(var j = 0; j < WIDTH; ++j){ var val = parseInt(1.0*ncDistArray[i][j]/(WIDTH*HEIGHT/2000)*255); if(val > 255) val = 255; ncDistImage[c].data[4*(i*WIDTH+j)+3] = val; // A } ncDistCtx[c].putImageData(ncDistImage[c], 0, 0); } } function updateNullcline(e){ for(var c = 0; c < numCanvas; ++c){ cf[c] = parseFloat(document.getElementById('cf'+c).value); ck[c] = parseFloat(document.getElementById('ck'+c).value); var points, unit; /* u = 0 */ unit = parseFloat(ncMaxV-ncMinV)/HEIGHT; points = '' + ncTrU(0) + ',' + ncTrV(ncMinV); for(var r = ncMinV+unit; r <= ncMaxV; r += unit){ points += ' ' + ncTrU(0) + ',' + ncTrV(r); } ncCurveU[c][0].setAttribute('points', points); if(cf[c]+ck[c] == 0){ /* v = 0 */ unit = parseFloat(ncMaxU-ncMinU)/WIDTH; points = '' + ncTrU(ncMinU) + ',' + ncTrV(0); for(var r = ncMinU+unit; r <= ncMaxU+unit; r += unit){ points += ' ' + ncTrU(r) + ',' + ncTrV(0); } ncCurveU[c][1].setAttribute('points', points); ncCurveU[c][2].setAttribute('points', ''); } else { /* v = (F+k)/u */ unit = parseFloat(ncMaxU-ncMinU)/WIDTH; if(ncMinU*ncMaxU < 0){ points = '' + ncTrU(ncMinU) + ',' + ncTrV((cf[c]+ck[c])/ncMinU); for(var r = ncMinU+unit; r < 0; r += unit){ points += ' ' + ncTrU(r) + ',' + ncTrV((cf[c]+ck[c])/r); } ncCurveU[c][1].setAttribute('points', points); points = '' + ncTrU(unit) + ',' + ncTrV((cf[c]+ck[c])/unit); for(var r = unit+unit; r <= ncMaxU+unit; r += unit){ points += ' ' + ncTrU(r) + ',' + ncTrV((cf[c]+ck[c])/r); } ncCurveU[c][2].setAttribute('points', points); } else { var tmp = ncMinU; if(tmp == 0) tmp += unit; points = '' + ncTrU(tmp) + ',' + ncTrV((cf[c]+ck[c])/tmp); for(var r = tmp+unit; r <= ncMaxU+unit; r += unit){ if(r != 0){ points += ' ' + ncTrU(r) + ',' + ncTrV((cf[c]+ck[c])/r); } } ncCurveU[c][1].setAttribute('points', points); ncCurveU[c][2].setAttribute('points', ''); } } if(cf[c] == 0){ /* u = 0 */ unit = parseFloat(ncMaxV-ncMinV)/HEIGHT; points = '' + ncTrU(0) + ',' + ncTrV(ncMinV); for(var r = ncMinV+unit; r <= ncMaxV+unit; r += unit){ points += ' ' + ncTrU(0) + ',' + ncTrV(r); } ncCurveV[c][0].setAttribute('points', points); /* v = 0 */ unit = parseFloat(ncMaxU-ncMinU)/WIDTH; points = '' + ncTrU(ncMinU) + ',' + ncTrV(0); for(var r = ncMinU+unit; r <= ncMaxU+unit; r += unit){ points += ' ' + ncTrU(r) + ',' + ncTrV(0); } ncCurveV[c][1].setAttribute('points', points); } else { // assume F > 0 /* v = F/(u^2+F) */ unit = parseFloat(ncMaxU-ncMinU)/WIDTH; points = '' + ncTrU(ncMinU) + ',' + ncTrV(cf[c]/(ncMinU*ncMinU+cf[c])); for(var r = ncMinU+unit; r <= ncMaxU+unit; r += unit){ points += ' ' + ncTrU(r) + ',' + ncTrV(cf[c]/(r*r+cf[c])); } ncCurveV[c][0].setAttribute('points', points); ncCurveV[c][1].setAttribute('points', ''); } } } function updateNullclineK(e){ if(e.keyCode == 13) updateNullcline(null); } function ncTrU(x){ return (parseFloat(x)-ncMinU)/(ncMaxU-ncMinU)*WIDTH; } function ncTrV(y){ return HEIGHT-(parseFloat(y)-ncMinV)/(ncMaxV-ncMinV)*HEIGHT; } function startstop(e){ if(document.getElementById('startstop').value == 'Start') { document.getElementById('random').disabled = true; document.getElementById('target').disabled = true; document.getElementById('speed').disabled = true; document.getElementById('double').disabled = true; document.getElementById('I').disabled = true; document.getElementById('Y').disabled = true; T = parseFloat(document.getElementById('speed').value); for(var c = 0; c < numCanvas; ++c){ document.getElementById('Du'+c).disabled = true; document.getElementById('Dv'+c).disabled = true; document.getElementById('cf'+c).disabled = true; document.getElementById('ck'+c).disabled = true; Du[c] = parseFloat(document.getElementById('Du'+c).value); Dv[c] = parseFloat(document.getElementById('Dv'+c).value); cf[c] = parseFloat(document.getElementById('cf'+c).value); ck[c] = parseFloat(document.getElementById('ck'+c).value); } thread = setInterval('calc();', timeInterval); document.getElementById('startstop').value = 'Stop'; } else { document.getElementById('random').disabled = false; document.getElementById('target').disabled = false; document.getElementById('I').disabled = false; document.getElementById('Y').disabled = false; document.getElementById('speed').disabled = false; document.getElementById('double').disabled = false; for(var c = 0; c < numCanvas; ++c){ document.getElementById('Du'+c).disabled = false; document.getElementById('Dv'+c).disabled = false; document.getElementById('cf'+c).disabled = false; document.getElementById('ck'+c).disabled = false; } clearInterval(thread); document.getElementById('startstop').value = 'Start'; } } function showDouble(){ if(document.getElementById('double').checked){ document.getElementById('canvas1').style.display = 'block'; document.getElementById('control1').style.display = 'block'; numCanvas = 2; updateNullcline(null); updateImage(); } else { document.getElementById('canvas1').style.display = 'none'; document.getElementById('control1').style.display = 'none'; numCanvas = 1; } } function calc(){ updateImage(); for(var t = 0; t < T; ++t){ // swap var tmp; tmp = pu, pu = u, u = tmp; tmp = pv, pv = v, v = tmp; for(var c = 0; c < numCanvas; ++c){ // Gray-Scott for(var i = 0 ; i < HEIGHT; ++i) for(var j = 0; j < WIDTH; ++j){ u[c][i][j] = pu[c][i][j]+delta*(Du[c]*(pu[c][trH(i+1)][j]+pu[c][trH(i-1)][j]+pu[c][i][trW(j+1)]+pu[c][i][trW(j-1)]-4.0*pu[c][i][j])/epsilon/epsilon+pu[c][i][j]*pu[c][i][j]*pv[c][i][j]-(cf[c]+ck[c])*pu[c][i][j]); v[c][i][j] = pv[c][i][j]+delta*(Dv[c]*(pv[c][trH(i+1)][j]+pv[c][trH(i-1)][j]+pv[c][i][trW(j+1)]+pv[c][i][trW(j-1)]-4.0*pv[c][i][j])/epsilon/epsilon-pu[c][i][j]*pu[c][i][j]*pv[c][i][j]+cf[c]*(1.0-pv[c][i][j])); } } } } // periodic condition function trW(x){ return x == WIDTH ? 0 : x == -1 ? WIDTH-1 : x; } function trH(x){ return x == HEIGHT ? 0 : x == -1 ? HEIGHT-1 : x; }