Last active
January 12, 2019 14:02
-
-
Save ctlusto/839081f2cd9cf5217bb538f01bada2d5 to your computer and use it in GitHub Desktop.
An interactive exponential population model using Desmos
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="stylesheet" href="//cdn.jsdelivr.net/semantic-ui/2.2.6/semantic.min.css"> | |
<link rel="stylesheet" href="scrubber.css"> | |
<link rel="stylesheet" href="main.css"> | |
<script src="//www.desmos.com/api/v0.8/calculator.js?apiKey=dcb31709b452b1cf9dc26972add0fda6"></script> | |
<!-- Load jQuery from Desmos instead of pulling in another copy --> | |
<script>window.jQuery = window.$ = Desmos.$</script> | |
<script src="//cdn.jsdelivr.net/semantic-ui/2.2.6/semantic.min.js"></script> | |
<script src="scrubber.js"></script> | |
<script src="index.js"></script> | |
<title>Exponential Population Growth</title> | |
</head> | |
<body> | |
<div class="wrapper"> | |
<!-- Outer grid layout --> | |
<div class="ui two column stackable grid container"> | |
<!-- Calculator column --> | |
<div class="column"> | |
<div id="calculator" class="calculator"></div> | |
</div> | |
<!-- End calculator column --> | |
<!-- Parameters column --> | |
<div class="column"> | |
<h1>Parameters</h1> | |
<i id="settings" class="icon setting"></i> | |
<table class="ui table"> | |
<tr> | |
<td class="mq-label"><span id="n-label">N_0 = </span></td> | |
<td class="input-cell"> | |
<div class="ui input"> | |
<input type="text" id="n-input"></td> | |
</div> | |
<td id="n-slider"></td> | |
<td></td> | |
</tr> | |
<tr> | |
<td class="mq-label"><span id="r-label">r = </span></td> | |
<td class="input-cell"> | |
<div class="ui input"> | |
<input type="text" id="r-input"> | |
</div> | |
</td> | |
<td id="r-slider"></td> | |
<td><button id="calculate-r" class="ui button">Calculate</button></td> | |
</tr> | |
<tr> | |
<td class="mq-label"><span id="t-label">t = </span></td> | |
<td class="input-cell"> | |
<div class="ui input"> | |
<input type="text" id="t-input"> | |
</div> | |
</td> | |
<td id="t-slider"></td> | |
<td><button class="ui button" id="animation"><i></i>Play</button></td> | |
</tr> | |
</table> | |
<!-- Inner grid --> | |
<div class="ui two column grid"> | |
<div class="column"> | |
<table class="ui table"> | |
<tr> | |
<td colspan="2" class="t-cell">Population <span id="nt-label">N(t) = </span><span id="nt-value"></span></td> | |
</tr> | |
<tr> | |
<td class="t-cell">slope = <span id="slope-value"></span></td> | |
<td><button class="ui button" id="show-slope">Show</button></td> | |
</tr> | |
</table> | |
</div> | |
<div class="column"> | |
<canvas id="population-sketch" class="sketch" width=260 height=190></canvas> | |
</div> | |
</div> | |
<!-- End inner grid --> | |
</div> | |
<!-- End parameters column --> | |
</div> | |
<!-- End outer grid layout --> | |
<!-- Popup form for calculating r --> | |
<div id="r-calculation" class="ui popup"> | |
<div class="ui labeled input popup-input"> | |
<div class="ui label popup-label">Births</div> | |
<input type="text" id="births" class="r-input"> | |
</div> | |
<br/> | |
<div class="ui labeled input popup-input"> | |
<div class="ui label popup-label">Deaths</div> | |
<input type="text" id="deaths" class="r-input"> | |
</div> | |
<br/> | |
<div class="ui labeled input popup-input"> | |
<div class="ui label popup-label">Immigration</div> | |
<input type="text" id="immigration" class="r-input"> | |
</div> | |
<br/> | |
<div class="ui labeled input popup-input"> | |
<div class="ui label popup-label">Emigration</div> | |
<input type="text" id="emigration" class="r-input"> | |
</div> | |
<button class="ui button" id="clear-r">Clear</button> | |
</div> | |
<!-- End of popup form --> | |
<!-- Popup form for setting min and max values --> | |
<div id="bounds" class="ui popup settings"> | |
<table class="ui table"> | |
<thead> | |
<tr class="center aligned"> | |
<th>Parameter</th> | |
<th>Min</th> | |
<th>Max</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr> | |
<td>Pop. (<em>N<sub>0</sub></em>)</td> | |
<td> | |
<div class="ui input"> | |
<input type="text" id='n-min'> | |
</div> | |
</td> | |
<td> | |
<div class="ui input"> | |
<input type="text" id='n-max'> | |
</div> | |
</td> | |
</tr> | |
<tr> | |
<td>Rate (<em>r</em>)</td> | |
<td> | |
<div class="ui input"> | |
<input type="text" id='r-min'> | |
</div> | |
</td> | |
<td> | |
<div class="ui input"> | |
<input type="text" id='r-max'> | |
</div> | |
</td> | |
</tr> | |
<tr> | |
<td>Time (<em>t</em>)</td> | |
<td> | |
<div class="ui disabled input"> | |
<input type="text" id='t-min'> | |
</div> | |
</td> | |
<td> | |
<div class="ui input"> | |
<input type="text" id='t-max'> | |
</div> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<!-- End of popup form --> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$(function() { | |
// Some useful globals | |
var showSlopeLine = false; | |
var timeIsAnimating = false; | |
var viewportBuffer = 1.1; | |
// Set up the canvas | |
var canvas = $('#population-sketch')[0]; | |
var ctx = canvas.getContext('2d'); | |
var w = canvas.width; | |
var h = canvas.height; | |
// Initialize the r-calculation popup | |
$('#calculate-r').popup({ | |
popup: $('#r-calculation'), | |
on: 'click', | |
position: 'left center' | |
}); | |
// Initialize the settings menu | |
$('#settings').popup({ | |
popup: $('#bounds'), | |
on: 'click', | |
position: 'bottom right' | |
}); | |
// Cache some DOM collections | |
var $nInput = $('#n-input'), | |
$rInput = $('#r-input'), | |
$tInput = $('#t-input'), | |
$nMin = $('#n-min'), | |
$nMax = $('#n-max'), | |
$rMin = $('#r-min'), | |
$rMax = $('#r-max'), | |
$tMin = $('#t-min'), | |
$tMax = $('#t-max'), | |
$showCapacity = $('#show-capacity'), | |
$showSlope = $('#show-slope'), | |
$slopeValue = $('#slope-value'), | |
$births = $('#births'), | |
$deaths = $('#deaths'), | |
$immigration = $('#immigration'), | |
$emigration = $('#emigration'), | |
$popValue = $('#nt-value') | |
$animation = $('#animation'); | |
// Initialize the r calculator | |
$births.val(0); | |
$deaths.val(0); | |
$immigration.val(0); | |
$emigration.val(0); | |
// Initialize the bounds settings menu | |
$nMin.val(0); | |
$nMax.val(1500); | |
$rMin.val(0); | |
$rMax.val(2); | |
$tMin.val(0); | |
$tMax.val(10); | |
// set up the calculator | |
var elt = $('#calculator')[0]; | |
var opts = { | |
expressions: false, | |
settingsMenu: false, | |
zoomButtons: false, | |
border: false, | |
lockViewport: true | |
}; | |
var calc = Desmos.GraphingCalculator(elt, opts); | |
// MathQuill setup | |
var MQ = Desmos.MathQuill; | |
MQ.StaticMath($('#n-label')[0]); | |
MQ.StaticMath($('#r-label')[0]); | |
MQ.StaticMath($('#t-label')[0]); | |
MQ.StaticMath($('#nt-label')[0]); | |
// Create some sliders | |
var rScrubber = new ScrubberView(); | |
var nScrubber = new ScrubberView(); | |
var tScrubber = new ScrubberView(); | |
// Helpers | |
function getStep(min, max) { | |
return (max - min) / 150; // slider is 150px wide | |
} | |
function clampValue(slider) { | |
if (slider.value() < slider.min()) slider.value(slider.min()); | |
if (slider.value() > slider.max()) slider.value(slider.max()); | |
} | |
// A member of the population. I mean, a dot. | |
function Member(x, y) { | |
this.x = Math.random() * w; | |
this.y = Math.random() * h; | |
this.r = w / 40; | |
this.color = '#2d70b3'; | |
} | |
Member.prototype.render = function() { | |
ctx.beginPath(); | |
ctx.arc(this.x, this.y, this.r, this.r, 0, 2*Math.PI); | |
ctx.fillStyle = this.color; | |
ctx.globalAlpha = 0.5; | |
ctx.fill(); | |
}; | |
// Keep track of the population and rendering | |
function Population(size) { | |
this.members = []; | |
for (var i=0; i<size; i++) { | |
this.members.push(new Member()); | |
} | |
this.draw(); | |
} | |
Population.prototype.draw = function() { | |
ctx.clearRect(0, 0, w, h); | |
this.members.forEach(function(elt) { | |
elt.render(); | |
}); | |
}; | |
Population.prototype.setSize = function(size) { | |
// Don't try to create too many dots | |
size = Math.min(size, 5000); | |
var currentSize = this.members.length; | |
if (size === currentSize) return; | |
if (size < currentSize) { | |
this.members = this.members.slice(0, size); | |
} | |
if (size > currentSize) { | |
while (this.members.length < size) this.members.push(new Member()); | |
} | |
window.requestAnimationFrame(this.draw.bind(this)); | |
}; | |
// The overall model | |
function Model(opts) { | |
this.initialPopulation = opts.initialPopulation; | |
this.rate = opts.rate; | |
this.minPopulation = opts.minPopulation; | |
this.maxPopulation = opts.maxPopulation; | |
this.minRate = opts.minRate; | |
this.maxRate = opts.maxRate; | |
this.maxTime = opts.maxTime; | |
this.init(); | |
} | |
Model.prototype.init = function() { | |
this.population = new Population(this.initialPopulation); | |
this.computeSteps(); | |
// Set up the initial calculator state | |
calc.setExpressions([ | |
{ id: 'N_0', latex: 'N_0=' + this.initialPopulation }, | |
{ id: 'r', latex: 'r=' + this.rate, hidden: true }, | |
{ id: 'T', latex: 'T=0' }, | |
{ id: 'P', latex: 'P=N(T)', hidden: true }, | |
{ id: 'm', latex: 'm=N\'(T)' }, | |
{ id: 'time-point', latex: '(T, N(T))', color: Desmos.Colors.RED }, | |
{ | |
id: 'curve', | |
latex: 'N\\left(t\\right)=\\N_0e^{rt}\\left\\{t\\ge 0\\right\\}', | |
color: Desmos.Colors.RED | |
}, | |
{ | |
id: 'slope-line', | |
latex: 'y-N(T) = m(x-T)', | |
color: Desmos.Colors.BLUE, | |
hidden: true | |
} | |
]); | |
this.updateBounds(); | |
}; | |
Model.prototype.computeSteps = function() { | |
this.populationStep = getStep(this.minPopulation, this.maxPopulation); | |
this.rateStep = getStep(this.minRate, this.maxRate); | |
this.timeStep = getStep(0, this.maxTime); | |
}; | |
Model.prototype.setInitialPopulation = function(newPop) { | |
this.initialPopulation = newPop; | |
calc.setExpression({ id: 'N_0', latex: 'N_0=' + newPop }); | |
}; | |
Model.prototype.setRate = function(newRate) { | |
if (newRate < this.minRate) { | |
this.minRate = newRate; | |
rScrubber.min(newRate.toFixed(2)); | |
clampValue(rScrubber); | |
this.computeSteps(); | |
} | |
if (newRate > this.maxRate) { | |
this.maxRate = newRate; | |
rScrubber.max(newRate.toFixed(2)); | |
clampValue(rScrubber); | |
this.computeSteps(); | |
} | |
this.rate = newRate; | |
calc.setExpression({ id: 'r', latex: 'r=' + newRate }); | |
}; | |
Model.prototype.updateBounds = function() { | |
var xmax = this.maxTime * viewportBuffer; | |
var xmin = this.maxTime * (1 - viewportBuffer) | |
var ymax = this.maxPopulation * viewportBuffer; | |
var ymin = this.maxPopulation * (1 - viewportBuffer) | |
calc.setMathBounds({ left: xmin, right: xmax, bottom: ymin, top: ymax }); | |
} | |
// Set the model's initialPopulation and rate. | |
// For instance, if you wanted to set from a saved state. | |
Model.prototype.setParameters = function(newParams) { | |
if ( | |
newParams === undefined || | |
newParams.initialPopulation === undefined || | |
newParams.rate === undefined || | |
newparams.minPopulation === undefined || | |
newparams.maxPopulation === undefined || | |
newparams.minRate === undefined || | |
newparams.maxRate === undefined || | |
newparams.maxTime === undefined | |
) { | |
throw new Error( | |
'\nYou must pass in an object with the following properties:\n' + | |
'initialPopulation\n' + | |
'rate\n' + | |
'minPopulation\n' + | |
'maxPopulation\n' + | |
'minRate\n' + | |
'maxRate\n' + | |
'maxTime' | |
); | |
} | |
this.setInitialPopulation(newParams.initialPopulation); | |
this.setRate(newParams.rate); | |
this.minPopulation = newParams.minPopulation; | |
this.maxPopulation = newParams.maxPopulation; | |
this.minRate = newParams.minRate; | |
this.maxRate = newParams.maxRate; | |
this.maxTime = newParams.maxTime; | |
this.computeSteps(); | |
this.udpateBounds(); | |
}; | |
// Get the current model parameters. For instance, if you want to persist them. | |
Model.prototype.getParameters = function() { | |
return { | |
initialPopulation: this.initialPopulation, | |
rate: this.rate, | |
minPopulation: this.minPopulation, | |
maxPopulation: this.maxPopulation, | |
minRate: this.minRate, | |
maxRate: this.maxRate, | |
maxTime: this.maxTime | |
}; | |
}; | |
// Attach the model to the window object so that you can, e.g. get and set | |
// the model parameters from another script | |
window.model = new Model({ | |
initialPopulation: 100, | |
minPopulation: 0, | |
maxPopulation: 1500, | |
rate: 0.6, | |
minRate: 0, | |
maxRate: 2, | |
maxTime: 10 | |
}); | |
// Listen to some important calculator values | |
var P = calc.HelperExpression({ latex: 'P' }); | |
P.observe('numericValue', function() { | |
var currentPop = Math.round(P.numericValue); | |
model.population.setSize(currentPop); | |
$popValue.text(currentPop); | |
}); | |
var T = calc.HelperExpression({ latex: 'T' }); | |
function animationTimeout() { | |
clearTimeout(animationTimeout); | |
if (!timeIsAnimating) return; | |
var newTime = T.numericValue + model.timeStep; | |
if (newTime > model.maxTime) newTime = 0; | |
calc.setExpression({id: 'T', latex: 'T=' + newTime }); | |
setTimeout(animationTimeout, 1000/60); | |
} | |
$animation.click(function() { | |
$animation.toggleClass('play').toggleClass('pause'); | |
timeIsAnimating = $animation.hasClass('pause'); | |
$animation.text(timeIsAnimating ? 'Pause' : 'Play'); | |
animationTimeout(); | |
}); | |
var m = calc.HelperExpression({ latex: 'm' }); | |
m.observe('numericValue', function() { | |
$slopeValue.text(m.numericValue.toFixed(2)); | |
}); | |
var r = calc.HelperExpression({ latex: 'r' }); | |
var n = calc.HelperExpression({ latex: 'N_0' }); | |
// Show the slope line | |
$showSlope.click(function() { | |
showSlopeLine = !showSlopeLine; | |
calc.setExpression({ id: 'slope-line', hidden: !showSlopeLine }); | |
$showSlope.text(showSlopeLine ? 'Hide' : 'Show'); | |
}); | |
// Calculate and set r based on birth/death and immigration/emigration info | |
function setRateFromData() { | |
var b = $births.val(), | |
d = $deaths.val(), | |
i = $immigration.val(), | |
e = $emigration.val(); | |
var rate = ( (b-d) + (i-e) ); | |
model.setRate(rate); | |
} | |
[$births, $deaths, $immigration, $emigration].forEach(function(elt) { | |
elt.on('change', function() { | |
if (isNaN(elt.val())) elt.val(0); | |
setRateFromData(); | |
}); | |
}); | |
// Clear the r-calculator inputs | |
$('#clear-r').click(function() { | |
$('.r-input').val(0); | |
}); | |
// Set up sliders and keep them in sync with the inputs/calculator | |
nScrubber.min(0).max(1500).value(model.initialPopulation).step(10); | |
nScrubber.onValueChanged = function(val) { | |
$nInput.val(val); | |
model.setInitialPopulation(val); | |
}; | |
nScrubber.elt.style.width = '150px'; | |
$('#n-slider').append(nScrubber.elt); | |
n.observe('numericValue', function() { | |
nScrubber.value(n.numericValue); | |
}); | |
rScrubber.min(0.1).max(2).value(model.rate).step(0.01); | |
rScrubber.onValueChanged = function(val) { | |
$rInput.val(val); | |
model.setRate(val); | |
}; | |
rScrubber.elt.style.width = '150px'; | |
$('#r-slider').append(rScrubber.elt); | |
r.observe('numericValue', function() { | |
rScrubber.value(r.numericValue); | |
}); | |
tScrubber.min(0).max(10).step(0.01); | |
tScrubber.onValueChanged = function(val) { | |
$tInput.val(val); | |
calc.setExpression({ id: 'T', latex: 'T=' + val }); | |
}; | |
tScrubber.onScrubStart = function(val) { | |
// Kill animation if you grab the scrubber | |
if (timeIsAnimating) { | |
timeIsAnimating = false; | |
$animation.removeClass('pause').addClass('play'); | |
$animation.text('Play'); | |
} | |
}; | |
tScrubber.elt.style.width = '150px'; | |
$('#t-slider').append(tScrubber.elt); | |
T.observe('numericValue', function() { | |
tScrubber.value(T.numericValue); | |
}); | |
function sanitizeInput(input) { | |
return isNaN(input) ? 0 : input; | |
} | |
// Initialize inputs | |
$nInput.val(model.initialPopulation); | |
$nInput.on('change', function() { | |
nScrubber.value(sanitizeInput($nInput.val())); | |
}); | |
$rInput.val(model.rate); | |
$rInput.on('change', function() { | |
rScrubber.value(sanitizeInput($rInput.val())); | |
}); | |
$tInput.val(0); | |
$tInput.on('change', function() { | |
tScrubber.value(sanitizeInput($tInput.val())); | |
}); | |
// The settings inputs | |
$nMin.on('change', function() { | |
var newVal = sanitizeInput($nMin.val()); | |
model.minPopulation = newVal; | |
nScrubber.min(newVal); | |
clampValue(nScrubber); | |
model.computeSteps(); | |
model.updateBounds(); | |
}); | |
$nMax.on('change', function() { | |
var newVal = sanitizeInput($nMax.val()); | |
model.maxPopulation = newVal; | |
nScrubber.max(newVal); | |
clampValue(nScrubber); | |
model.computeSteps(); | |
model.updateBounds(); | |
}); | |
$rMin.on('change', function() { | |
var newVal = sanitizeInput($rMin.val()); | |
model.minRate = newVal; | |
rScrubber.min(newVal); | |
clampValue(rScrubber); | |
model.computeSteps(); | |
}); | |
$rMax.on('change', function() { | |
var newVal = sanitizeInput($rMax.val()); | |
model.maxRate = newVal; | |
rScrubber.max(newVal); | |
clampValue(rScrubber); | |
model.computeSteps(); | |
}); | |
$tMax.on('change', function() { | |
var newVal = sanitizeInput($tMax.val()); | |
model.maxTime = newVal; | |
tScrubber.max(newVal); | |
clampValue(tScrubber); | |
model.computeSteps(); | |
model.updateBounds(); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.calculator { | |
border: 1px solid #ddd; | |
width: 100%; | |
height: 500px; | |
} | |
.ui.container { | |
background: #ddd; | |
} | |
.wrapper { | |
margin-top: 50px; | |
} | |
.ui.button { | |
width: 100%; | |
} | |
.ui.input { | |
width: 100px; | |
} | |
.scrubber { | |
margin-bottom: 10px; | |
} | |
.mq-label { | |
text-align: right !important; | |
} | |
.input-cell { | |
text-align: left !important; | |
} | |
.t-cell { | |
width: 50%; | |
} | |
.sketch { | |
background: #fff; | |
} | |
#r-calculation { | |
width: 300px; | |
} | |
.popup-label { | |
width: 100px; | |
} | |
.popup-input { | |
margin: 0 5px 5px 10px; | |
} | |
#animation { | |
cursor: pointer; | |
} | |
#settings { | |
position: absolute; | |
top: 20px; | |
right: 15px; | |
cursor: pointer; | |
font-size: 30px; | |
color: #696969; | |
} | |
#settings:hover { | |
cursor: pointer; | |
font-size: 30px; | |
color: #4f81bd; | |
} | |
.settings { | |
width: 500px; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.scrubber { | |
margin-top: 10px; | |
width: 200px; | |
height: 40px; | |
position: relative; | |
} | |
.scrubber-vert { | |
margin-left: 10px; | |
width: 40px; | |
height: 200px; | |
position: relative; | |
} | |
.scrubber .track { | |
position: absolute; | |
top: 50%; | |
left: 0px; | |
width: 100%; | |
height: 6px; | |
background: #DDD; | |
border-radius: 3px; | |
margin-top: -3px; | |
} | |
.scrubber-vert .track { | |
position: absolute; | |
top: 0px; | |
height: 100%; | |
left: 50%; | |
width: 6px; | |
background: #DDD; | |
border-radius: 3px; | |
margin-left: -3px; | |
} | |
.scrubber .thumb { | |
-moz-box-sizing: border-box; | |
box-sizing: border-box; | |
position: absolute; | |
top: 50%; | |
left: 0px; | |
width: 22px; | |
height: 22px; | |
margin-left: -11px; | |
margin-top: -11px; | |
cursor: pointer; | |
opacity: 0.7; | |
border: 8px solid #BECFE4; | |
border-radius: 100%; | |
background: #4F81BD; | |
transition: border-width 0.2s ease 0s; | |
} | |
.scrubber-vert .thumb { | |
-moz-box-sizing: border-box; | |
box-sizing: border-box; | |
position: absolute; | |
top: 100%; | |
left: 50%; | |
width: 22px; | |
height: 22px; | |
margin-top: -11px; | |
margin-left: -11px; | |
cursor: pointer; | |
opacity: 0.7; | |
border: 8px solid #BECFE4; | |
border-radius: 100%; | |
background: #4F81BD; | |
transition: border-width 0.2s ease 0s; | |
} | |
.scrubber .thumb:hover, | |
.scrubber-vert .thumb:hover, | |
.thumb.dragging { | |
border-width: 0px; | |
opacity: 1; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function ScrubberView() { | |
this.makeAccessors(); | |
this.createDOM(); | |
this.attachListeners(); | |
this.onValueChanged = function () {}; | |
this.onScrubStart = function () {}; | |
this.onScrubEnd = function () {}; | |
} | |
ScrubberView.prototype.makeAccessors = function () { | |
var value = 0; | |
var min = 0; | |
var max = 1; | |
var step = 0; | |
var orientation = 'horizontal'; | |
this.value = function (_value) { | |
if (_value === undefined) return value; | |
if (value === _value) return this; | |
_value = Math.max(min, Math.min(max, _value)); | |
if (step > 0) { | |
var nsteps = Math.round((_value - min)/step); | |
var invStep = 1/step; | |
if (invStep === Math.round(invStep)) { | |
_value = (min*invStep + nsteps)/invStep; | |
} else { | |
_value = (min/step + nsteps)*step; | |
} | |
value = Math.max(min, Math.min(max, _value)); | |
} else { | |
value = _value; | |
} | |
this.redraw(); | |
this.onValueChanged(value); | |
return this; | |
}; | |
this.min = function (_min) { | |
if (_min === undefined) return min; | |
if (min === _min) return this; | |
min = _min; | |
this.redraw(); | |
return this; | |
}; | |
this.max = function (_max) { | |
if (_max === undefined) return max; | |
if (max === _max) return this; | |
max = _max; | |
this.redraw(); | |
return this; | |
}; | |
this.step = function (_step) { | |
if (_step === undefined) return step; | |
if (step === _step) return this; | |
step = _step; | |
this.redraw(); | |
return this; | |
}; | |
this.orientation = function(_orientation) { | |
if (_orientation === undefined) return orientation; | |
if (_orientation === orientation) return this; | |
orientation = _orientation; | |
this.redraw(); | |
return this; | |
}; | |
}; | |
ScrubberView.prototype.createDOM = function () { | |
this.elt = document.createElement('div'); | |
this.track = document.createElement('div'); | |
this.thumb = document.createElement('div'); | |
this.elt.className = this.orientation() === 'horizontal' ? 'scrubber' : 'scrubber-vert'; | |
this.track.className = 'track'; | |
this.thumb.className = 'thumb'; | |
this.elt.appendChild(this.track); | |
this.elt.appendChild(this.thumb); | |
}; | |
ScrubberView.prototype.redraw = function () { | |
var frac = (this.value() - this.min())/(this.max() - this.min()); | |
if (this.orientation() === 'horizontal') { | |
this.elt.className = 'scrubber'; | |
this.thumb.style.top = '50%'; | |
this.thumb.style.left = frac*100 + '%'; | |
} | |
else { | |
this.elt.className = 'scrubber-vert'; | |
this.thumb.style.left = '50%'; | |
this.thumb.style.top = 100 - (frac*100) + '%'; | |
} | |
}; | |
ScrubberView.prototype.attachListeners = function () { | |
var self = this; | |
var mousedown = false; | |
var cachedLeft; | |
var cachedWidth; | |
var cachedTop; | |
var cachedHeight; | |
var start = function (evt) { | |
evt.preventDefault(); | |
self.onScrubStart(self.value()); | |
mousedown = true; | |
var rect = self.elt.getBoundingClientRect(); | |
// NOTE: page[X|Y]Offset and the width and height | |
// properties of getBoundingClientRect are not | |
// supported in IE8 and below. | |
// | |
// Scrubber doesn't attempt to support IE<9. | |
var xOffset = window.pageXOffset; | |
var yOffset = window.pageYOffset; | |
cachedLeft = rect.left + xOffset; | |
cachedWidth = rect.width; | |
cachedTop = rect.top + yOffset; | |
cachedHeight = rect.height; | |
self.thumb.className += ' dragging'; | |
}; | |
var stop = function () { | |
mousedown = false; | |
cachedLeft = undefined; | |
cachedWidth = undefined; | |
cachedTop = undefined; | |
cachedHeight = undefined; | |
self.thumb.className = 'thumb'; | |
self.onScrubEnd(self.value()); | |
}; | |
var setValueFromPageX = function (pageX) { | |
var frac = Math.min(1, Math.max((pageX - cachedLeft)/cachedWidth, 0)); | |
self.value((1-frac)*self.min() + frac*self.max()); | |
}; | |
var setValueFromPageY = function (pageY) { | |
var frac = Math.min(1, Math.max(1 - (pageY - cachedTop)/cachedHeight, 0)); | |
self.value((1-frac)*self.min() + frac*self.max()); | |
}; | |
this.elt.addEventListener('mousedown', start); | |
this.elt.addEventListener('touchstart', start); | |
document.addEventListener('mousemove', function (evt) { | |
if (!mousedown) return; | |
evt.preventDefault(); | |
if (self.orientation() === 'horizontal') | |
setValueFromPageX(evt.pageX); | |
else | |
setValueFromPageY(evt.pageY); | |
}); | |
document.addEventListener('touchmove', function (evt) { | |
if (!mousedown) return; | |
evt.preventDefault(); | |
if (self.orientation() === 'horizontal') | |
setValueFromPageX(evt.changedTouches[0].pageX); | |
else | |
setValueFromPageY(evt.changedTouches[0].pageY); | |
}); | |
this.elt.addEventListener('mouseup', function (evt) { | |
if (!mousedown) return; | |
evt.preventDefault(); | |
if (self.orientation() === 'horizontal') | |
setValueFromPageX(evt.pageX); | |
else | |
setValueFromPageY(evt.pageY); | |
}); | |
this.elt.addEventListener('touchend', function (evt) { | |
if (!mousedown) return; | |
evt.preventDefault(); | |
if (self.orientation() === 'horizontal') | |
setValueFromPageX(evt.changedTouches[0].pageX); | |
else | |
setValueFromPageY(evt.changedTouches[0].pageY); | |
}); | |
document.addEventListener('mouseup', stop); | |
document.addEventListener('touchend', stop); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment