/*
* waterpipe.js - v1.0 * jQuery plugin. Smoky backgrounds generator * http://www.dragdropsite.com * * Made by dragdropsite.com * * Under MIT License * * Credits: rectangleworld.com */
- (function ( $, window, document, undefined ) {
var pluginName = "waterpipe", defaults = { //Smoke gradientStart: '#000000', gradientEnd: '#222222', smokeOpacity: 0.1, numCircles: 1, maxMaxRad: 'auto', minMaxRad: 'auto', minRadFactor: 0, iterations: 8, drawsPerFrame: 10, lineWidth: 2, speed: 1, //Background bgColorInner: "#ffffff", bgColorOuter: "#666666", };
var TWO_PI = 2*Math.PI; var timer; var inst; function Smoke ( element, options ) { this.element = element; this.$element = $(element); inst = this; this.settings = $.extend( {}, defaults, options ); this._defaults = defaults; this._name = pluginName; this.init(); }
Smoke.prototype = { init: function () { this.initSettings(); this.initCanvas(); this.generate(); }, initSettings: function () { var radius = this.$element.height()*0.8/2; if(this.settings.maxMaxRad==='auto') this.settings.maxMaxRad = radius; if(this.settings.minMaxRad==='auto') this.settings.minMaxRad = radius; }, initCanvas: function () { this.displayCanvas = this.$element.find('canvas'); this.displayWidth = this.$element[0].clientWidth; this.displayHeight = this.$element[0].clientHeight; this.displayCanvas[0].width = this.displayWidth; this.displayCanvas[0].height = this.displayHeight; this.context = this.displayCanvas[0].getContext("2d");
//off screen canvas used only when exporting image this.exportCanvas = document.createElement('canvas'); this.exportCanvas.width = this.displayWidth; this.exportCanvas.height = this.displayHeight; this.exportContext = this.exportCanvas.getContext("2d"); }, generate: function () { this.drawCount = 0; this.context.setTransform(1,0,0,1,0,0); this.context.clearRect(0,0,this.displayWidth,this.displayHeight); this.fillBackground(); this.setCircles(); if(timer) {clearInterval(timer);} timer = setInterval(function(){inst.onTimer()},inst.settings.speed); }, fillBackground: function () { var outerRad = Math.sqrt(this.displayWidth*this.displayWidth + this.displayHeight*this.displayHeight)/2; this.niceGradient = new SmokeNiceBG(this.displayWidth*0.75,this.displayHeight/2*0.75,0,this.displayWidth/2,this.displayHeight/4,outerRad);
var hex = this.settings.bgColorInner.replace('#',);
var r0 = parseInt(hex.substring(0,2), 16), g0 = parseInt(hex.substring(2,4), 16), b0 = parseInt(hex.substring(4,6), 16);
hex = this.settings.bgColorOuter.replace('#',); var r1 = parseInt(hex.substring(0,2), 16), g1 = parseInt(hex.substring(2,4), 16), b1 = parseInt(hex.substring(4,6), 16);
this.niceGradient.addColorStop(0,r0,g0,b0); this.niceGradient.addColorStop(1,r1,g1,b1); this.niceGradient.fillRect(this.context,0,0,this.displayWidth,this.displayHeight); }, setCircles: function () { var i; var r,g,b,a; var maxR, minR; var grad; this.circles = []; for (i = 0; i < this.settings.numCircles; i++) { maxR = this.settings.minMaxRad+Math.random()*(this.settings.maxMaxRad-this.settings.minMaxRad); minR = this.settings.minRadFactor*maxR; //define gradient grad = this.context.createRadialGradient(0,0,minR,0,0,maxR); var gradientStart = this.hexToRGBA(this.settings.gradientStart, this.settings.smokeOpacity), gradientEnd = this.hexToRGBA(this.settings.gradientEnd, this.settings.smokeOpacity);
grad.addColorStop(1,gradientStart); grad.addColorStop(0,gradientEnd); var newCircle = { centerX: -maxR, centerY: this.displayHeight/2-50, maxRad : maxR, minRad : minR, color: grad, //can set a gradient or solid color here. //fillColor: "rgba(0,0,0,1)", param : 0, changeSpeed : 1/250, phase : Math.random()*TWO_PI, //the phase to use for a single fractal curve. globalPhase: Math.random()*TWO_PI //the curve as a whole will rise and fall by a sinusoid. }; this.circles.push(newCircle); newCircle.pointList1 = this.setLinePoints(this.settings.iterations); newCircle.pointList2 = this.setLinePoints(this.settings.iterations); } }, onTimer: function () { var i,j; var c; var rad; var point1,point2; var x0,y0; var cosParam; var xSqueeze = 0.75; //cheap 3D effect by shortening in x direction. var yOffset; //draw circles for (j = 0; j < this.settings.drawsPerFrame; j++) { this.drawCount++; for (i = 0; i < this.settings.numCircles; i++) { c = this.circles[i]; c.param += c.changeSpeed; if (c.param >= 1) { c.param = 0; c.pointList1 = c.pointList2; c.pointList2 = this.setLinePoints(this.settings.iterations); } cosParam = 0.5-0.5*Math.cos(Math.PI*c.param); this.context.strokeStyle = c.color; this.context.lineWidth = this.settings.lineWidth; //context.fillStyle = c.fillColor; this.context.beginPath(); point1 = c.pointList1.first; point2 = c.pointList2.first; //slowly rotate c.phase += 0.0002; theta = c.phase; rad = c.minRad + (point1.y + cosParam*(point2.y-point1.y))*(c.maxRad - c.minRad); //move center c.centerX += 0.5; c.centerY += 0.04; yOffset = 40*Math.sin(c.globalPhase + this.drawCount/1000*TWO_PI); //stop when off screen if (c.centerX > this.displayWidth + this.settings.maxMaxRad) { clearInterval(timer); timer = null; } //we are drawing in new position by applying a transform. We are doing this so the gradient will move with the drawing. this.context.setTransform(xSqueeze,0,0,1,c.centerX,c.centerY+yOffset) //Drawing the curve involves stepping through a linked list of points defined by a fractal subdivision process. //It is like drawing a circle, except with varying radius. x0 = xSqueeze*rad*Math.cos(theta); y0 = rad*Math.sin(theta); this.context.lineTo(x0, y0); while (point1.next != null) { point1 = point1.next; point2 = point2.next; theta = TWO_PI*(point1.x + cosParam*(point2.x-point1.x)) + c.phase; rad = c.minRad + (point1.y + cosParam*(point2.y-point1.y))*(c.maxRad - c.minRad); x0 = xSqueeze*rad*Math.cos(theta); y0 = rad*Math.sin(theta); this.context.lineTo(x0, y0); } this.context.closePath(); this.context.stroke(); //context.fill(); } } }, setLinePoints: function (iterations) { var pointList = {}; pointList.first = {x:0, y:1}; var lastPoint = {x:1, y:1} var minY = 1; var maxY = 1; var point; var nextPoint; var dx, newX, newY; var ratio; var minRatio = 0.5; pointList.first.next = lastPoint; for (var i = 0; i < iterations; i++) { point = pointList.first; while (point.next != null) { nextPoint = point.next; dx = nextPoint.x - point.x; newX = 0.5*(point.x + nextPoint.x); newY = 0.5*(point.y + nextPoint.y); newY += dx*(Math.random()*2 - 1); var newPoint = {x:newX, y:newY}; //min, max if (newY < minY) { minY = newY; } else if (newY > maxY) { maxY = newY; } //put between points newPoint.next = nextPoint; point.next = newPoint; point = nextPoint; } } //normalize to values between 0 and 1 if (maxY != minY) { var normalizeRate = 1/(maxY - minY); point = pointList.first; while (point != null) { point.y = normalizeRate*(point.y - minY); point = point.next; } } //unlikely that max = min, but could happen if using zero iterations. In this case, set all points equal to 1. else { point = pointList.first; while (point != null) { point.y = 1; point = point.next; } } return pointList; }, setOption: function (optionName, optionValue) { this.settings[optionName] = optionValue; }, hexToRGBA: function (hex, opacity) { hex = hex.replace('#',); r = parseInt(hex.substring(0,2), 16); g = parseInt(hex.substring(2,4), 16); b = parseInt(hex.substring(4,6), 16);
result = 'rgba('+r+','+g+','+b+','+opacity+')'; return result; }, download: function(width, height){ this.exportContext.drawImage(this.displayCanvas[0], 0, 0, width, height, 0, 0, width, height); //we will open a new window with the image contained within: //retrieve canvas image as data URL: var dataURL = this.exportCanvas.toDataURL("image/png"); //open a new window of appropriate size to hold the image: var imageWindow = window.open("", "fractalLineImage", "left=0,top=0,width="+width+",height="+height+",toolbar=0,resizable=0"); //write some html into the new window, creating an empty image: imageWindow.document.write("<title>Export Image</title>") imageWindow.document.write("<img id='exportImage'" + " alt=" + " height='" + height + "'" + " width='" + width + "'" + " style='position:absolute;left:0;top:0'/>"); imageWindow.document.close(); //copy the image into the empty img in the newly opened window: var exportImage = imageWindow.document.getElementById("exportImage"); exportImage.src = dataURL; } };
function SmokeNiceBG(_x0,_y0,_rad0,_x1,_y1,_rad1) { this.x0 = _x0; this.y0 = _y0; this.x1 = _x1; this.y1 = _y1; this.rad0 = _rad0; this.rad1 = _rad1; this.colorStops = []; }
SmokeNiceBG.prototype.addColorStop = function(ratio,r,g,b) { if ((ratio < 0) || (ratio > 1)) { return; } var n; var newStop = {ratio:ratio, r:r, g:g, b:b}; if ((ratio >= 0) && (ratio <= 1)) { if (this.colorStops.length == 0) { this.colorStops.push(newStop); } else { var i = 0; var found = false; var len = this.colorStops.length; //search for proper place to put stop in order. while ((!found) && (i<len)) { found = (ratio <= this.colorStops[i].ratio); if (!found) { i++; } } //add stop - remove next one if duplicate ratio if (!found) { //place at end this.colorStops.push(newStop); } else { if (ratio == this.colorStops[i].ratio) { //replace this.colorStops.splice(i, 1, newStop); } else { this.colorStops.splice(i, 0, newStop); } } } } }
SmokeNiceBG.prototype.fillRect = function(ctx, rectX0, rectY0, rectW, rectH) { if (this.colorStops.length == 0) { return; } var image = ctx.getImageData(rectX0, rectY0, rectW, rectH); var pixelData = image.data; var len = pixelData.length; var oldpixel, newpixel, nearestValue; var quantError; var x; var y; var vx = this.x1 - this.x0; var vy = this.y1 - this.y0; var vMagSquareRecip = 1/(vx*vx+vy*vy); var ratio; var r,g,b; var r0,g0,b0,r1,g1,b1; var ratio0,ratio1; var f; var stopNumber; var found; var q; var rBuffer = []; var gBuffer = []; var bBuffer = []; var aBuffer = []; var a,b,c,discrim; var dx,dy; var xDiff = this.x1 - this.x0; var yDiff = this.y1 - this.y0; var rDiff = this.rad1 - this.rad0; a = rDiff*rDiff - xDiff*xDiff - yDiff*yDiff; var rConst1 = 2*this.rad0*(this.rad1-this.rad0); var r0Square = this.rad0*this.rad0;
//first complete color stops with 0 and 1 ratios if not already present if (this.colorStops[0].ratio != 0) { var newStop = { ratio:0, r: this.colorStops[0].r, g: this.colorStops[0].g, b: this.colorStops[0].b} this.colorStops.splice(0,0,newStop); } if (this.colorStops[this.colorStops.length-1].ratio != 1) { var newStop = { ratio:1, r: this.colorStops[this.colorStops.length-1].r, g: this.colorStops[this.colorStops.length-1].g, b: this.colorStops[this.colorStops.length-1].b} this.colorStops.push(newStop); }
//create float valued gradient for (i = 0; i<len/4; i++) { x = rectX0 + (i % rectW); y = rectY0 + Math.floor(i/rectW); dx = x - this.x0; dy = y - this.y0; b = rConst1 + 2*(dx*xDiff + dy*yDiff); c = r0Square - dx*dx - dy*dy; discrim = b*b-4*a*c; if (discrim >= 0) { ratio = (-b + Math.sqrt(discrim))/(2*a); if (ratio < 0) { ratio = 0; } else if (ratio > 1) { ratio = 1; } //find out what two stops this is between if (ratio == 1) { stopNumber = this.colorStops.length-1; } else { stopNumber = 0; found = false; while (!found) { found = (ratio < this.colorStops[stopNumber].ratio); if (!found) { stopNumber++; } } } //calculate color. r0 = this.colorStops[stopNumber-1].r; g0 = this.colorStops[stopNumber-1].g; b0 = this.colorStops[stopNumber-1].b; r1 = this.colorStops[stopNumber].r; g1 = this.colorStops[stopNumber].g; b1 = this.colorStops[stopNumber].b; ratio0 = this.colorStops[stopNumber-1].ratio; ratio1 = this.colorStops[stopNumber].ratio; f = (ratio-ratio0)/(ratio1-ratio0); r = r0 + (r1 - r0)*f; g = g0 + (g1 - g0)*f; b = b0 + (b1 - b0)*f; } else { r = r0; g = g0; b = b0; } //set color as float values in buffer arrays rBuffer.push(r); gBuffer.push(g); bBuffer.push(b); } //While converting floats to integer valued color values, apply Floyd-Steinberg dither. for (i = 0; i<len/4; i++) { nearestValue = ~~(rBuffer[i]); quantError =rBuffer[i] - nearestValue; rBuffer[i+1] += 7/16*quantError; rBuffer[i-1+rectW] += 3/16*quantError; rBuffer[i + rectW] += 5/16*quantError; rBuffer[i+1 + rectW] += 1/16*quantError; nearestValue = ~~(gBuffer[i]); quantError =gBuffer[i] - nearestValue; gBuffer[i+1] += 7/16*quantError; gBuffer[i-1+rectW] += 3/16*quantError; gBuffer[i + rectW] += 5/16*quantError; gBuffer[i+1 + rectW] += 1/16*quantError; nearestValue = ~~(bBuffer[i]); quantError =bBuffer[i] - nearestValue; bBuffer[i+1] += 7/16*quantError; bBuffer[i-1+rectW] += 3/16*quantError; bBuffer[i + rectW] += 5/16*quantError; bBuffer[i+1 + rectW] += 1/16*quantError; } //copy to pixel data for (i=0; i<len; i += 4) { q = i/4; pixelData[i] = ~~rBuffer[q]; pixelData[i+1] = ~~gBuffer[q]; pixelData[i+2] = ~~bBuffer[q]; pixelData[i+3] = 255; } ctx.putImageData(image,rectX0,rectY0); }
// A really lightweight plugin wrapper around the constructor, // preventing against multiple instantiations $.fn[ pluginName ] = function ( options ) { this.each(function() { if ( !$.data( this, pluginName ) ) { $.data( this, pluginName, new Smoke( this, options ) ); } });
// chain jQuery functions return this; };
})( jQuery, window, document );