Skip to content

Instantly share code, notes, and snippets.

@artemuzz
Created October 27, 2019 03:09
Show Gist options
  • Save artemuzz/4f2f65640ffca493cca1ab0f31bea5f0 to your computer and use it in GitHub Desktop.
Save artemuzz/4f2f65640ffca493cca1ab0f31bea5f0 to your computer and use it in GitHub Desktop.
simplex noise and color map
<div id = 'general'>
<div id = 'menu'>
<div class="group">
<p>width (200-2000 pixels) : <input type = 'text' size=3 id ='tWidth' value="400"></p>
<p>height (200-2000 pixels) : <input type = 'text' size=3 id ='tHeight' value="400"></p>
<hr>
<p>noise : </p>
<p>period (0.05 - 20) : <input type = 'text' size=5 id ='period' value="0.5"></p>
<p>octaves (0 - 5): <input type = 'text' size=3 id ='octaves' value="2"></p>
<p>coeff. / octave (0.01 - 1) : <input type = 'text' size=3 id ='attenuation' value="0.2"></p>
<hr>
<p style = "text-align:center"><button type="button" id="newPicture" class="ld">new picture</button></p>
</div>
<div class="group">
<p>color map :</p>
<canvas width = 330 height=60 id = "cMap"></canvas>
<p><button type="button" id="previous">&lt; previous</button>
Stop <span id="nStop">1</span> of <span id="nbStops">2</span>
<button type="button" style ="float:right" id="next">next &gt;</button></p>
<p>position : <input type = 'text' size = 5 id="position">
color : <input type = 'color' id="color"></p>
<p>
<button type="button" id="insert">insert after</button>
<button type="button" style ="float:right" id="delete">delete</button>
</p>
<p style = "text-align:center">
<button type="button" id="apply">Apply</button>
</p>
</div>
</div>
<div>
<canvas width = 100 height = 100 id = 'destCanvas'></canvas>
</div>
</div>
"use strict";
window.onload = function() {
/* values from user interface */
let tWidth,tHeight;
let period,pperiod, octaves, attenuation;
/* end of values from user interface */
let destCtx;
let srcData;
let dimx, dimy;
/* canvas for color map */
let ctxMap;
let hMap, wMap;
let x0Ruler, x1Ruler; // left & right position of rulers in color map
let yRuler1; // top position
const hRuler = 20; // width of ruler
const colorResol = 1024; // resolution of color map
let tbMap; // table for color mapping
let tbMap2; // tbMap 'exploded' in 'colorResol' levels
let tbMap3; // == tbMap2, with separate r,g,b
let nStop = 0; // number of current selected stop in UI
/* shortcuts for Math */
const mrandom = frandom(9578634);
// const mrandom = Math.random;
const mfloor = Math.floor;
const mround = Math.round;
const mceil = Math.ceil;
const mabs = Math.abs;
const mmin = Math.min;
const mmax = Math.max;
const mPI = Math.PI;
const mPIS2 = Math.PI / 2;
const m2PI = Math.PI * 2;
const msin = Math.sin;
const mcos = Math.cos;
const matan2 = Math.atan2;
const mexp = Math.exp;
const mhypot = Math.hypot;
const msqrt = Math.sqrt;
//-----------------------------------------------------------------------------
// miscellaneous functions
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/* returns a random number generator function
the number returned by the returned function is in the range [0..1[
without seed (or seed evaluating to false) the returned function is truly unpredictable
with the same integer value (10 figures typical) for seed, the returned function
will allways produce the same sequence.
*/
function frandom (seed) {
let m_w = 123456789;
let m_z = 987654321;
const mask = 0xffffffff;
// Takes any integer
m_w = seed || (4294967296 * Math.random());
if (! seed) m_z = (4294967296 * Math.random())
// Returns number between 0 (inclusive) and 1.0 (exclusive),
// just like Math.random().
return function() {
m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
let result = ((m_z << 16) | m_w) & mask;
result /= 4294967296;
return result + 0.5;
}
// much inspired by https://prograide.com/pregunta/5883/appro-aleatoire
} // frandom
//-----------------------------------------------------------------------------
/* rgbToHsl and hslToRgb : code from stackOverflow */
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param {number} r The red color value
* @param {number} g The green color value
* @param {number} b The blue color value
* @return {Array} The HSL representation
*/
function rgbToHsl(r, g, b){ // this function copied from somewhere on Stack Overflow
r /= 255, g /= 255, b /= 255;
var max = mmax(r, g, b), min = mmin(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
} // function rgbToHsl
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [mround(r * 255), mround(g * 255), mround(b * 255)];
} // function hslToRgb
//------------------------------------------------------------------------------
function toHex2(number) {
var s = number.toString(16).toUpperCase();
return (((s.length)<2) ?'0':'')+s;
} // toHex2
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function rgbToCssString(arRgb) {
return '#' + toHex2(mround(arRgb[0]))+
toHex2(mround(arRgb[1]))+
toHex2(mround(arRgb[2]));
} // rgbToCssString
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function cssStringToRgb (str) {
return [parseInt ('0x' + str.substring(1,3), 16),
parseInt ('0x' + str.substring(3,5), 16),
parseInt ('0x' + str.substring(5,7), 16)];
} // cssStringToRgb
//--------------------------------------------------------------------
function lerp (x, x0, x1, y0, y1) {
return (y0 * (x1 - x) + y1 * (x - x0)) / (x1 - x0)
} // lerp
//--------------------------------------------------------------------
function Interpolator (x0, x1, color0, color1) {
/* returns a function for color interpolation
supposed to make a smart choice between hsl and rgb interpolation
- x0 and x1 are numeric values. The returned function is supposed to be
called later with a parameter between x0 and x1
- color0 and color1 are given in [r, g, b] arrays format. The returned function
will return a value with the same format.
*/
let [r0, g0, b0] = color0;
let [r1, g1, b1] = color1;
let [h0, s0, l0] = rgbToHsl (r0, g0, b0);
let [h1, s1, l1] = rgbToHsl (r1, g1, b1);
let h, s, l;
let dx = x1 - x0;
// if x interval too short, just return color1
if (mabs(dx) < 0.001) return ()=> color1;
/* if hue is not very well defined (small saturation or too dark or too light)
at at least one end, interpolate in rgb
else interpolate in hsl
Threshold levels subject to change */
if (s0 < 0.1 || s1 < 0.1 || l0 < 0.1 || l1 < 0.1 || l0 > 0.9 || l1 > 0.9) {
return x => {
let dx0 = x - x0;
let dx1 = x1 - x;
return [(r0 * dx1 + r1 * dx0) / dx,
(g0 * dx1 + g1 * dx0) / dx,
(b0 * dx1 + b1 * dx0) / dx];
};
} // if rgb interpolation
/* hsl interpolation : find shorter path from h0 to h1 */
if (h1 > h0 + 0.5) h0 += 1;
if (h0 > h1 + 0.5) h1 += 1;
// here, h0 and h1 may be greater than 1
return x => {
let dx0 = x - x0;
let dx1 = x1 - x;
h = ((h0 * dx1 + h1 * dx0) / dx) % 1; // back to the 0-1 interval
s = (s0 * dx1 + s1 * dx0) / dx;
l = (l0 * dx1 + l1 * dx0) / dx;
return hslToRgb(h, s, l);
};
} // Interpolator
//--------------------------------------------------------------------
function createImage() {
let line, yNoise;
let x1, y2, ns;
let sn = (new SimplexNoise());
/* read user parameters */
readUI();
dimx = tWidth;
dimy = tHeight;
const decalx = 0.123456;
const decaly = 2.123456;
let noise;
{ // block to limit the scope to some functions
let ampl = 1;
let accAmpl = 0;
for (let k = 0; k <= octaves; ++ k) {
accAmpl += ampl;
ampl *= attenuation;
} // for k
noise = function(x, y) {
let ampl = 1;
let per = period * dimx;
let acc = sn.noise2D( x / per, y / per);
for (let k = 1; k <= octaves; ++k) {
ampl *= attenuation;
per /= 2;
acc += sn.noise2D( x / per + decalx, y / per + decaly) * ampl;
} // for k
return mround((acc / accAmpl + 1 ) * 511.5); // 0..1023
} // function noise
}
srcData = [];
for (let ky = 0; ky < dimy; ++ky) {
line = srcData[ky] = [];
for (let kx = 0; kx < dimx; ++kx) {
line[kx] = noise(kx, ky);
} // for kx
} // for ky
colorImage();
} // function createImage
//--------------------------------------------------------------------
function colorImage() {
let kx, ky;
let line, pix, offs;
let destCanvas = document.getElementById('destCanvas');
destCanvas.width = dimx;
destCanvas.height = dimy;
destCtx = destCanvas.getContext ('2d');
destCtx.fillStyle = '#000';
destCtx.fillRect (0, 0, dimx, dimy);
let nData = destCtx.createImageData(dimx, dimy);
offs = 0;
for (ky = 0 ; ky < dimy; ++ky) {
line = srcData[ky];
for (kx = 0 ; kx < dimx; ++kx) {
pix = tbMap3[line[kx]];
nData.data[offs] = pix[0]; // r
nData.data[offs+1] = pix[1]; // g
nData.data[offs+2] = pix[2]; // b
nData.data[offs+3] = 255; // opacity
offs += 4;
} // for kx
} // for ky
destCtx.putImageData(nData, 0, 0);
} // filterImage
//--------------------------------------------------------------------
//--------------------------------------------------------------------
function readUI() {
// read user interface - except color map
getTWidth();
getTHeight();
getPeriod();
getOctaves();
getAttenuation();
} // readUI
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function getTWidth() {
// width
let ctrl = document.getElementById('tWidth');
let x = parseInt(ctrl.value, 10);
if (isNaN (x)) { x = tWidth }
if (x < 200) x = 200;
if (x > 2000) x = 2000;
ctrl.value = tWidth = x;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function getTHeight() {
// height
let ctrl = document.getElementById('tHeight');
let x = parseInt(ctrl.value, 10);
if (isNaN (x)) { x = tHeight }
if (x < 200) x = 200;
if (x > 2000) x = 2000;
ctrl.value = tHeight = x;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function getPeriod() {
// period for noise
let ctrl = document.getElementById('period');
let x = parseFloat(ctrl.value, 10);
if (isNaN (x)) { x = period }
if (x < 0.05) x = 0.05;
if (x > 20) x = 20;
ctrl.value = period = x;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function getOctaves() {
// number of octaves
let ctrl = document.getElementById('octaves');
let x = parseInt(ctrl.value, 10);
if (isNaN (x)) { x = octaves }
if (x < 0) x = 0;
if (x > 5) x = 5;
ctrl.value = octaves = x;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function getAttenuation() {
// multiplying coeff for each octave
let ctrl = document.getElementById('attenuation');
let x = parseFloat(ctrl.value);
if (isNaN (x)) { x = attenuation }
if (x < 0.01) x = 0.01;
if (x > 1) x = 1;
ctrl.value = attenuation = x;
}
//--------------------------------------------------------------------
function drawRuler1 () {
ctxMap.clearRect(x0Ruler, yRuler1, x1Ruler - x0Ruler, hRuler);
ctxMap.lineWidth = 2;
for (let k = 0; k < (x1Ruler - x0Ruler); ++k) {
ctxMap.beginPath();
ctxMap.moveTo (x0Ruler + k, yRuler1);
ctxMap.lineTo (x0Ruler + k, yRuler1 + hRuler);
ctxMap.strokeStyle = tbMap2[mround(k * colorResol / (x1Ruler - x0Ruler) )];
ctxMap.stroke();
} // for k
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function explodeTbMap () {
/* re-orders the points of tbMap by level
pre-computes the (colorResol) shades to use for final drawing */
let level0, color0;
let level1, color1;
let level;
let interp, rInterp;
let ky = 0;
tbMap.sort((vala, valb) => {
if (vala.level < valb.level) return -1;
if (vala.level > valb.level) return 1;
return 0;
});
tbMap2 = [];
tbMap3 = [];
let p1 = tbMap[0];
for (let k = 1 ; k < tbMap.length; ++k) {
({level: level0, color: color0} = p1);
p1 = tbMap[k];
({level: level1, color:color1} = p1);
interp = Interpolator (level0, level1, color0, color1);
while (ky <= colorResol && (level = ky / colorResol) <= level1) {
tbMap3[ky] = rInterp = interp(level);
tbMap2[ky++] = rgbToCssString(rInterp);
} // while ky
} // for k
interp = null;
} //
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function updateUIMap() {
/* updates user interface for mapping of colors BUT explodeTbMap and drawRuler1 */
if (nStop < 0) nStop = 0;
if (nStop >= tbMap.length) nStop = tbMap.length - 1;
document.getElementById("nStop").innerHTML = nStop + 1; // humans begin to count at 1
document.getElementById("nbStops").innerHTML = tbMap.length;
document.getElementById("previous").style.visibility = ((nStop == 0) ? 'hidden' : 'visible');
document.getElementById("next").style.visibility = ((nStop >= tbMap.length - 1) ? 'hidden' : 'visible');
document.getElementById("insert").style.visibility = ((nStop >= tbMap.length - 1) ? 'hidden' : 'visible');
document.getElementById("delete").style.visibility = ((nStop == 0 || nStop >= tbMap.length - 1) ? 'hidden' : 'visible');
if (nStop == 0 || nStop >= tbMap.length - 1) {
document.getElementById("position").setAttribute("disabled","");
} else {
document.getElementById("position").removeAttribute("disabled");
}
document.getElementById("position").value = tbMap[nStop].level;
document.getElementById("color").value = rgbToCssString(tbMap[nStop].color);
// draw triangle to display current stop
ctxMap.clearRect(x0Ruler - 5 , yRuler1 + hRuler + 1 , x1Ruler - x0Ruler + 10, 12);
let x = x0Ruler + (x1Ruler - x0Ruler) * tbMap[nStop].level;
ctxMap.beginPath();
ctxMap.fillStyle = '#FFFFFF';
ctxMap.moveTo (x, yRuler1 + hRuler + 2);
ctxMap.lineTo (x + 4, yRuler1 + hRuler + 2 + 10);
ctxMap.lineTo (x - 4, yRuler1 + hRuler + 2 + 10);
ctxMap.fill();
} //
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function incNStop() {
++nStop;
updateUIMap();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function decNStop() {
--nStop;
updateUIMap();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function changePosition() {
let vs;
if (nStop == 0 || nStop >= tbMap.length - 1) return; // should not happen
let curStop = tbMap[nStop];
let curV = curStop.level;
let v = Number.parseFloat (this.value);
if (Number.isNaN(v)) v = tbMap[nStop].level;
if (v <= 0.0001) v = 0.0001;
if (v >= 0.9999) v = 0.9999;
vs = v.toFixed(4); // string representation
v = Number.parseFloat(vs); // adjust value to representation
this.value = v; // update display to exact retained value
if (curV == v) return; // no real change
curStop.level = v; // update position in map
explodeTbMap(); // re-order stops and explode tbMap
// nStop may have changed
nStop = tbMap.findIndex(elem => (elem==curStop));
updateUIMap();
drawRuler1();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function changeColor() {
tbMap[nStop].color = cssStringToRgb(this.value);
explodeTbMap(); // re-order stops and explode tbMap
drawRuler1();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function insertStop() {
if (nStop >= tbMap.length - 1) return; // should not happen
let {level: lev0, color: color0 } = tbMap[nStop];
let {level: lev1, color: color1 } = tbMap[nStop + 1];
let newStop = {};
newStop.level = (lev0 + lev1) / 2
newStop.level = Number.parseFloat(newStop.level.toFixed(4));
let interp = Interpolator (lev0, lev1, color0, color1);
newStop.color = interp(newStop.level);
tbMap.splice(++nStop, 0, newStop);
explodeTbMap();
updateUIMap();
drawRuler1();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function deleteStop() {
if (nStop == 0 || nStop >= tbMap.length - 1) return; // should not happen
tbMap.splice(nStop, 1);
--nStop;
explodeTbMap();
updateUIMap();
drawRuler1();
}
//--------------------------------------------------------------------
// beginning of execution
/* UI
/* initialize color map rules */
ctxMap = document.getElementById('cMap').getContext('2d');
hMap = hRuler + 30 ;
ctxMap.canvas.height = hMap;
wMap = ctxMap.canvas.width;
yRuler1 = 10;
x0Ruler = 10;
x1Ruler = wMap - x0Ruler;
// initial color map : shades of green
// 1st point MUST have level 0 and last point MUST have level 1
tbMap = [{level: 0, color: [0, 0, 0]},
{level: 0.25, color: [0, 0, 255]},
{level: 0.345, color: [255, 0, 255]},
{level: 0.346, color: [255, 255, 100]},
{level: 0.399, color: [255, 255, 100]},
{level: 0.4, color: [255, 0, 255]},
{level: 0.55, color: [255, 180, 0]},
{level: 0.80, color: [128, 90, 0]},
{level: 0.81, color: [0, 0, 0]},
{level: 1, color: [0, 0, 0]}];
explodeTbMap();
drawRuler1();
updateUIMap();
document.getElementById('newPicture').addEventListener ('click', createImage);
document.getElementById('tWidth').addEventListener ('change', getTWidth);
document.getElementById('tHeight').addEventListener ('change', getTHeight);
document.getElementById('period').addEventListener ('change', getPeriod);
document.getElementById('octaves').addEventListener ('change', getOctaves);
document.getElementById('attenuation').addEventListener ('change', getAttenuation);
document.getElementById("previous").addEventListener('click', decNStop);
document.getElementById("next").addEventListener('click', incNStop);
document.getElementById("position").addEventListener('change', changePosition);
document.getElementById("color").addEventListener('change', changeColor);
document.getElementById("insert").addEventListener('click', insertStop);
document.getElementById("delete").addEventListener('click', deleteStop);
document.getElementById("apply").addEventListener('click', colorImage);
createImage();
};
<script src="https://codepen.io/Dillo/pen/GRRNgVp"></script>

simplex noise and color map

Change the noise parameters.
Change the color mapping.
Enjoy !

A Pen by Monika on CodePen.

License.

body {
position:relative;
margin:0;
background-color: #444;
color: #FFF;
}
a, p, h1, h2, h3, h4, h5, h6, th, td, li {
font-family: Verdana, "Bitstream Vera Sans", "Lucida Grande", sans-serif;
}
#general {
display:flex;
}
#menu {
width: 340px;
}
#menu p, #menu button {
margin: 5px;
}
#destCanvas {
margin: 5px;
box-shadow: 5px 5px 5px #888;
background-color : #000;
}
#menu button.ld {
width:15ex;
}
div.group {
border: solid 1px #FFF;
width: 330px;
margin:5px;
}
.remark {
font-size : 80%;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment