// This is an example Chiasm plugin based on HTML5 Canvas.
// This displays a wave simulation.
// Clicking on the canvas interacts with the simulation.
// Note that D3.js is not used at all here.
function Wave (){
var my = ChiasmComponent({
waterColor: "red",
skyColor: Model.None,
gridSize: 50,
pullStrength: 0.01,
dampeningFactor: 0.99,
initialHeight: 0.5
});
// These functions will be populated later when the canvas is available.
var drawCells = function (){};
var clearCanvas = function (){};
var executeMouseInteraction = function (){};
var iterateSimulation = function (){};
var canvas = document.createElement("canvas");
var c = canvas.getContext("2d");
var cells = [];
// Poor man's RAF polyfill
var nextFrame = requestAnimationFrame || setTimeout;
// Expose the canvas as the element that Chiasm will append to its container.
my.el = canvas;
// This function executes once per animation frame
function executeFrame(){
nextFrame(executeFrame);
clearCanvas();
drawCells();
iterateSimulation();
executeMouseInteraction();
}
// Initialize the cells.
my.when(["gridSize", "initialHeight"], function (gridSize, initialHeight) {
cells = [];
// Initialize the water height
for(var i = 0; i < gridSize; i++){
cells.push({
// for a still initial surface
//height: 0.5,
// for an initial wave:
height: i === Math.floor(gridSize*1/4) ? 2 : initialHeight,
velocity: 0
});
}
});
my.when(["box", "gridSize", "waterColor", "skyColor"],
function (box, gridSize, waterColor, skyColor){
clearCanvas = function (){
canvas.width = box.width;
canvas.height = box.height;
if(skyColor !== Model.None){
c.fillStyle = skyColor;
c.fillRect(0, 0, box.width, box.height);
}
}
drawCells = function (){
c.beginPath();
c.moveTo(box.width, box.height);
c.lineTo(0, box.height);
for(var i = 0; i < gridSize; i++){
var cell = cells[i];
var x = i / (gridSize-1) * box.width;
var y = box.height - cell.height * box.height;
c.lineTo(x,y);
}
c.closePath();
c.fillStyle = waterColor;
c.fill();
}
});
my.when(["gridSize", "pullStrength", "dampeningFactor", "initialHeight"],
function (gridSize, pullStrength, dampeningFactor, initialHeight){
var conservationOfMassCorrection = 0;
// Increment the wave simulation:
// Neighboring cells pull on one another.
iterateSimulation = function(){
var avgHeight = 0;
for(var i = 0; i < gridSize; i++){
// center cell
var c = cells[i];
// left neighbor
var l = cells[((i - 1) + gridSize) % gridSize];
// right neighbor
var r = cells[(i + 1) % gridSize];
// pull toward neighbors
c.velocity += pullStrength * (l.height - c.height);
c.velocity += pullStrength * (r.height - c.height);
// increment velocity
c.height += c.velocity;
// ensure conservation of mass
c.height += conservationOfMassCorrection;
// apply dampening
c.velocity *= dampeningFactor;
avgHeight += c.height;
}
avgHeight /= (gridSize - 1);
conservationOfMassCorrection = initialHeight - avgHeight;
}
});
my.when(["mouseDown", "mouseX", "mouseY", "box"], function (mouseDown, mouseX, mouseY, box){
var cellWidth = 1 / (cells.length - 1) * box.width;
// Pull the wave cell closest to the mouse
executeMouseInteraction = function(){
if(mouseDown){
for(var i = 0; i < cells.length; i++){
var x = i / (cells.length - 1) * box.width;
if(Math.abs(x - mouseX) < cellWidth){
var cell = cells[i];
cell.height = 1 - mouseY / box.height;
cell.velocity = 0;
}
}
}
}
});
// Record when the mouse is pressed
canvas.addEventListener("mousedown", function (e){
my.mouseDown = true;
// This line prevents unintended drag behavior, like selecting page elements.
e.preventDefault();
});
// Record when the mouse is moved
canvas.addEventListener("mousemove", function (e){
var rect = canvas.getBoundingClientRect();
my.mouseX = e.clientX - rect.left;
my.mouseY = e.clientY - rect.top;
});
// Record when the mouse is released
canvas.addEventListener("mouseup", function (e){
my.mouseDown = false;
});
// Draw the first frame
executeFrame();
return my;
}