// canvas settings var viewWidth = 768, viewHeight = 512, drawingCanvas = $("#drawing_canvas")[0], particleCanvas = $("#particle_canvas")[0], canvasRect = drawingCanvas.getBoundingClientRect(), ctx, particleCtx, timeStep = (1/60), time = 0; var particles = [], spawnIID; var startHandle, ctrl1Handle, ctrl2Handle, endHandle; ///////////// // GUI ///////////// var playing = true; var drawPaths = false; var drawParticles = true; var drawCentralPath = false; var handlesShown = true; var spawnInterval = 16; var spawnAmount = 1; var movementDuration = 3; var movementDurationVariance = 0.0; var $handles = $('.handle'); var debugToggle; var playToggle; var particleColor = '#00A388'; var canvasColor = '#ddd'; var pathColor = '#aaa'; function toggleHandles(){ if (handlesShown) { $handles.hide(); debugToggle.name('Show handles'); } else { $handles.show(); debugToggle.name('Hide handles'); } handlesShown = !handlesShown; } function togglePlay() { if (playing) { clearInterval(spawnIID); playToggle.name('Resume'); } else { spawnIID = setInterval(spawnParticles, spawnInterval); playToggle.name('Pause'); } playing = !playing; } function removeAll() { particles = []; } function initGui() { var gui = new dat.GUI(); var speed = 180; gui.add(window, 'spawnInterval', 16, 1000).onChange(function() { clearInterval(spawnIID); spawnIID = setInterval(spawnParticles, spawnInterval); }); gui.add(window, 'spawnAmount', 1, 32).step(1); gui.add(window, 'movementDuration', 1, 5).step(0.1).name('duration'); gui.add(window, 'movementDurationVariance', 0.0, 2.0).step(0.1).name('dur variance'); gui.addColor(window, 'particleColor').onChange(makeParticleImage); gui.addColor(window, 'pathColor'); gui.addColor(window, 'canvasColor'); gui.add(window, 'drawPaths'); gui.add(window, 'drawCentralPath'); gui.add(window, 'drawParticles'); debugToggle = gui.add(window, 'toggleHandles').name('Hide handles'); playToggle = gui.add(window, 'togglePlay').name('Pause'); gui.add(window, 'removeAll').name('Remove all particles'); gui.close(); } ///////////// // INITS ///////////// function initJq() { startHandle = new Handle($('#start_handle')); startHandle.radius = 64; startHandle.x = 0; startHandle.y = viewHeight; ctrl1Handle = new Handle($('#ctrl1_handle')); ctrl1Handle.radius = 64; ctrl1Handle.x = 0; ctrl1Handle.y = 0; ctrl2Handle = new Handle($('#ctrl2_handle')); ctrl2Handle.radius = 64; ctrl2Handle.x = viewWidth; ctrl2Handle.y = 0; endHandle = new Handle($('#end_handle')); endHandle.radius = 64; endHandle.x = viewWidth; endHandle.y = viewHeight; } function initCanvas() { drawingCanvas.width = viewWidth; drawingCanvas.height = viewHeight; ctx = drawingCanvas.getContext('2d'); particleCtx = particleCanvas.getContext('2d'); makeParticleImage(); spawnIID = setInterval(spawnParticles, spawnInterval); spawnParticles(); } function makeParticleImage() { var c = particleCanvas.getContext('2d'); c.canvas.width = 8; c.canvas.height = 8; c.fillStyle = particleColor; c.beginPath(); c.arc(4, 4, 4, 0, Math.PI * 2); c.fill(); } function spawnParticles() { for (var i = 0; i < spawnAmount; i++) { var p0 = randomPointInCircle(startHandle.radius).add(startHandle.position); var p1 = randomPointInCircle(ctrl1Handle.radius).add(ctrl1Handle.position); var p2 = randomPointInCircle(ctrl2Handle.radius).add(ctrl2Handle.position); var p3 = randomPointInCircle(endHandle.radius).add(endHandle.position); particles.push(new Particle(p0, p1, p2, p3)); } } function update() { particles.forEach(function(p) { if (p.update() === false) { particles.splice(particles.indexOf(p), 1); } }) } function draw() { ctx.fillStyle = canvasColor; ctx.fillRect(0, 0, viewWidth, viewHeight); if (drawPaths) { particles.forEach(function(p) { p.debugDraw(); }); } if (drawCentralPath) { ctx.fillStyle = '#000'; drawBezier(startHandle, ctrl1Handle, ctrl2Handle, endHandle); } if (drawParticles) { particles.forEach(function(p) { p.draw(); }); } } window.onload = function() { initJq(); initCanvas(); initGui(); requestAnimationFrame(loop); }; $(window).resize(function() { canvasRect = drawingCanvas.getBoundingClientRect(); }); function loop() { if (playing) { update(); draw(); time += timeStep; } requestAnimationFrame(loop); } ///////////// // CLASSES ///////////// Point = function(x, y) { this.x = x || 0; this.y = y || 0; }; Point.prototype = { add:function(p) { this.x += p.x; this.y += p.y; return this; }, mul:function(s) { this.x *= s; this.y *= s; return this; }, div:function(s) { this.x /= s; this.y /= s; return this; }, nrm:function() { this.div(this.length()); return this; }, length:function() { return Math.sqrt(this.x * this.x + this.y * this.y); } } Particle = function(p0, p1, p2, p3) { this.p0 = p0; this.p1 = p1; this.p2 = p2; this.p3 = p3; this.t = 0; this.duration = movementDuration + Math.random() * movementDurationVariance - movementDurationVariance; }; Particle.prototype = { update:function() { this.t = Math.min(this.duration, this.t + timeStep); return this.t !== this.duration; }, draw:function() { var p = cubeBezier(this.p0, this.p1, this.p2, this.p3, this.t / this.duration); ctx.drawImage(particleCanvas, p.x - 4, p.y - 4); }, debugDraw:function() { ctx.strokeStyle = pathColor; ctx.lineWidth = 0.1; drawBezier(this.p0, this.p1, this.p2, this.p3); ctx.lineWidth = 1; } }; Handle = function($element) { this.$element = $element; this.canvasPosition = new Point(); this._radius = 0; this.$element.draggable({ drag:$.proxy(this.dragHandler, this) }); this.$element.resizable({ aspectRatio:true, minWidth:16, minHeight:16, maxWidth:512, maxHeight:512, handles:{se:'.ui-resizable-handle'}, resize:$.proxy(this.resizeHandler, this) }); }; Handle.prototype = { dragHandler:function(event, ui) { this.canvasPosition.x = ui.position.left + this.radius - canvasRect.left; this.canvasPosition.y = ui.position.top + this.radius - canvasRect.top; }, resizeHandler:function(event, ui) { this.$element.css({ 'top':ui.position.top + (ui.originalSize.height - ui.size.height) * 0.5, 'left':ui.position.left + (ui.originalSize.width - ui.size.width) * 0.5 }); this._radius = ui.size.width * 0.5; }, // radius get radius() { return this._radius; }, set radius(r) { this.$element.width(r * 2); this.$element.height(r * 2); this._radius = r; }, // position (relative to canvas) set x(v) { this.$element.css('left', v + canvasRect.left - this.radius); this.canvasPosition.x = v; }, get x() { return this.canvasPosition.x; }, set y(v) { this.$element.css('top', v + canvasRect.top - this.radius); this.canvasPosition.y = v; }, get y() { return this.canvasPosition.y; }, get position() { return this.canvasPosition; } }; ///////////// // UTILS ///////////// function drawBezier(p0, p1, p2, p3) { ctx.beginPath(); ctx.moveTo(p0.x, p0.y); ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); ctx.stroke(); } function cubeBezier(p0, c0, c1, p1, t) { var p = new Point(); var nt = (1 - t); p.x = nt * nt * nt * p0.x + 3 * nt * nt * t * c0.x + 3 * nt * t * t * c1.x + t * t * t * p1.x; p.y = nt * nt * nt * p0.y + 3 * nt * nt * t * c0.y + 3 * nt * t * t * c1.y + t * t * t * p1.y; return p; } function randomPointInCircle(r) { var p = new Point(); p.x = Math.sin(Math.PI * 2 * Math.random()) * r * Math.random(); p.y = Math.cos(Math.PI * 2 * Math.random()) * r * Math.random(); return p; }