Experimenting simplex-noise.js.
xxxxxxxxxx
<meta charset="utf-8">
<title></title>
<style>
body {
background: black;
font: 10px sans-serif;
width: 960px;
margin: 0 auto;
}
svg {
shape-rendering: optimizeSpeed;
width: 960px;
height: 500px;
}
.drawing-area {
width: 960px;
height: 500px;
}
.obfuscator {
fill: black;
stroke: none;
}
.ground {
fill: none;
stroke-width: 1;
}
.land {
stroke: grey;
}
.water {
stroke: lightblue;
}
</style>
<body>
<svg width= 960 height: 500;>
<g class="drawing-area" transform="translate(480,0)"></g>
</svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="simplex-noise.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js"></script>
<script>
var width = 960,
height = 500;
var CHAOTIC = 12;
var SMOOTH = 1;
var noiseDensityScale = d3.scale.linear().domain([0,100]).range([SMOOTH, CHAOTIC]);
var config = {
noise: {
main: {
amplitude: height/4,
density: 0,
scaledDensity: noiseDensityScale(0)
},
details: {
amplitude: 20,
density: 55,
scaledDensity: noiseDensityScale(55)
}
},
water: {
draw: "water level",
waterLevel: 0
},
//3D-like aspect
speed: 5,
scale: 5
};
insertControls();
var drawingArea = d3.select(".drawing-area");
var simplex = new SimplexNoise()
d3.timer(function(t){
//Simplex is used in 'computeNewHorizon()'
drawNewHorizon(computeNewHorizon(t));
})
function insertControls () {
var ctrls = new dat.GUI({width: 200});
ctrls.close();
var noiseCtrls = ctrls.addFolder("Landscape/Noise");
noiseCtrls.open();
var mainNoiseCtrl = noiseCtrls.addFolder("Main landscape");
var mainAmplitudeCtrl = mainNoiseCtrl.add(config.noise.main, "amplitude", 1, height/2);
mainAmplitudeCtrl.onChange(function(value) {
updateWaterLevelValue(ctrls);
})
var mainDensityCtrl = mainNoiseCtrl.add(config.noise.main, "density", 0, 25);
mainDensityCtrl.onChange(function(value) {
config.noise.main.scaledDensity = noiseDensityScale(value);
})
var detailedNoiseCtrl = noiseCtrls.addFolder("Additionnal details/bumps");
detailedNoiseCtrl.open();
var detailedAmplitudeCtrl = detailedNoiseCtrl.add(config.noise.details, "amplitude", 1, height/4);
detailedAmplitudeCtrl.onChange(function(value) {
updateWaterLevelValue(ctrls);
})
var densityCtrl = detailedNoiseCtrl.add(config.noise.details, "density", 0, 100);
densityCtrl.onChange(function(value) {
config.noise.details.scaledDensity = noiseDensityScale(value);
})
var waterCtrls = ctrls.addFolder("Water");
waterCtrls.close();
waterCtrls.add(config.water, "draw", ["no water", "water level", "throught water"]);
waterCtrls.add(config.water, "waterLevel", -(height/4+20), (height/4+20)).listen();
}
function updateWaterLevelValue (ctrls) {
//waterLevel should be between [-maxNoiseAmplitude, +minNoiseAmplitude]
maxAmplitude = config.noise.main.amplitude + config.noise.details.amplitude;
ctrls.__folders["Water"].__controllers.forEach(function(c){
if(c.property==="waterLevel"){
c.__min = -maxAmplitude;
c.__max = maxAmplitude
}
})
if (config.water.waterLevel < -0.9*maxAmplitude) {
config.water.waterLevel = -0.9*maxAmplitude;
};
if (config.water.waterLevel > 0.9*maxAmplitude) {
config.water.waterLevel = 0.9*maxAmplitude;
};
}
function computeNewHorizon (t) {
//computed ground, land and water pathes
var precision = 5;
var mainAmplitude = config.noise.main.amplitude;
var mainXDensity = width/config.noise.main.scaledDensity;
var mainYDensity = height/config.noise.main.scaledDensity*10;
var detailedAmplitude = config.noise.details.amplitude;
var detailedXDensity = width/config.noise.details.scaledDensity;
var detailedYDensity = height/config.noise.details.scaledDensity*10;
var newHorizon = {groundPath: "M", landPath: "M", waterPath: "M"};
var waterLevel = height/4-config.water.waterLevel;
var mainPointAltitude, newPointAltitude, newPointUnderWater, prevPointAltitude, prevPointUnderWater;
mainPointAltitude = height/4 + simplex.noise2D(-width/2/mainXDensity, t/mainYDensity, 1) * mainAmplitude;
newPointAltitude = mainPointAltitude + simplex.noise2D(-width/2/detailedXDensity, t/detailedYDensity, 1) * detailedAmplitude;
prevPointUnderWater = newPointAltitude>waterLevel; //SVG's y-axis goes down
prevPointAltitude = newPointAltitude;
for (x = -width/2; x <= width/2; x+=precision) {
//Simplex is used twice: 1 for main lanscape's shape, one for detailed 'bumps'
mainPointAltitude = height/4 + simplex.noise2D(x/mainXDensity, t/mainYDensity, 1) * mainAmplitude;
newPointAltitude = mainPointAltitude + simplex.noise2D(x/detailedXDensity, t/detailedYDensity, 1) * detailedAmplitude;
//compute ground path
newHorizon.groundPath += " " + [x, newPointAltitude];
//if required, compute land and water pathes
if (config.water.draw!=="no water") {
newPointUnderWater = newPointAltitude>waterLevel; //SVG's y-axis goes down
if (prevPointUnderWater != newPointUnderWater) {
//ground intersects water level
//compute intersection's x using linear interpolation
xIntersec = x - (precision*(newPointAltitude-waterLevel)/(newPointAltitude-prevPointAltitude));
if (newPointUnderWater) {
newHorizon.landPath += " " + [xIntersec, waterLevel] + " M";
newHorizon.waterPath += " " + [xIntersec, waterLevel];
} else {
newHorizon.landPath += " " + [xIntersec, waterLevel];
newHorizon.waterPath += " " + [xIntersec, waterLevel] + " M";
}
}
if (newPointUnderWater) {
if (config.water.draw==="water level") { newPointAltitude = waterLevel; }
newHorizon.waterPath += " " + [x, newPointAltitude];
} else {
newHorizon.landPath += " " + [x, newPointAltitude];
}
prevPointUnderWater = newPointUnderWater;
prevPointAltitude = newPointAltitude;
}
}
if (newHorizon.landPath.endsWith("M")) {
newHorizon.landPath = newHorizon.landPath.slice(0, -1);
}
if (newHorizon.waterPath.endsWith("M")) {
newHorizon.waterPath = newHorizon.waterPath.slice(0, -1);
}
return newHorizon;
}
function drawNewHorizon (newHorizon) {
//add a new ground slice to the svg, behind previous ones
var newDrawnHorizon = drawingArea.insert("g", "g")
.classed("ground-slice", true);
//Obfuscator prevent seeing ground/lines hidden behind previous upper ones
var newObfuscator = newDrawnHorizon.append("path")
.classed("obfuscator", true)
.attr("d", newHorizon.groundPath+" "+[width/2, height]+" "+[-width/2, height]+"z");
var newLandSlice = newDrawnHorizon.append("path")
.classed("ground land", true)
.attr("d", (config.water.draw==="no water")? newHorizon.groundPath : newHorizon.landPath)
.style("stroke-opacity", 0);
var newWaterSlice = newDrawnHorizon.append("path")
if (config.water!="no water") {
newWaterSlice.classed("ground water", true)
.attr("d", newHorizon.waterPath)
.style("stroke-opacity", 0);
}
//3D-like aspect
newDrawnHorizon.transition("hovering")
.duration(config.speed*1000)
.ease("cubic") //kind of curvature of planescape
.attr("transform", "scale("+config.scale+"),translate(0,"+height/4+")")
.remove();
//fog-like aspect; far horizons are less visibles
newLandSlice.transition("opacity")
.duration(config.speed/2*1000)
.style("stroke-opacity", 1);
if (config.water!="no water") {
newWaterSlice.transition("opacity")
.duration(config.speed/2*1000)
.style("stroke-opacity", 1);
}
}
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js