- zoesch.de - blitzkiste.net - gruene-hassberge (norbert.zoesch.de) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1040 lines
45 KiB
JavaScript
1040 lines
45 KiB
JavaScript
/*!
|
|
jQuery.awesomeCloud v0.2 indyarmy.com
|
|
by Russ Porosky
|
|
IndyArmy Network, Inc.
|
|
|
|
Usage:
|
|
$( "#myContainer" ).awesomeCloud( settings );
|
|
|
|
Your container must contain words in the following format:
|
|
<element data-weight="12">Word</element>
|
|
The <element> can be any valid HTML element (for example, <span>), and
|
|
the weight must be a decimal or integer contained in the "data-weight"
|
|
attribute. The content of the <element> is the word that will be
|
|
displayed. The original element is removed from the page (but not the DOM).
|
|
|
|
Settings:
|
|
"size" {
|
|
"grid" : 8, // word spacing; smaller is more tightly packed but takes longer
|
|
"factor" : 0, // font resizing factor; default "0" means automatically fill the container
|
|
"normalize" : true // reduces outlier weights for a more attractive output
|
|
},
|
|
"color" {
|
|
"background" : "rgba(255,255,255,0)", // default is transparent
|
|
"start" : "#20f", // color of the smallest font
|
|
"end" : "#e00" // color of the largest font
|
|
},
|
|
"options" {
|
|
"color" : "gradient", // if set to "random-light" or "random-dark", color.start and color.end are ignored
|
|
"rotationRatio" : 0.3, // 0 is all horizontal words, 1 is all vertical words
|
|
"printMultiplier" : 1 // 1 will look best on screen and is fastest; setting to 3.5 gives nice 300dpi printer output but takes longer
|
|
},
|
|
"font" : "Futura, Helvetica, sans-serif", // font family, identical to CSS font-family attribute
|
|
"shape" : "circle", // one of "circle", "square", "diamond", "triangle", "triangle-forward", "x", "pentagon" or "star"; this can also be a function with the following prototype - function( theta ) {}
|
|
|
|
Notes:
|
|
AwesomeCloud uses the HTML5 canvas element to create word clouds
|
|
similar to http://wordle.net/. It may or may not work for you.
|
|
|
|
If your words are all fairly evenly weighted and are large compared to
|
|
the containing element, you may need to adjust the size.grid setting
|
|
to make the output more attractive. Conversely, you can adjust the
|
|
size.factor setting instead.
|
|
|
|
It should be noted that the more words you have, the smaller the size.grid,
|
|
and the larger the options.printMultiplier, the longer it will take to
|
|
generate and display the word cloud.
|
|
|
|
Extra Thanks:
|
|
Without Timothy Chien's work (https://github.com/timdream/wordcloud),
|
|
this plugin would have taken much longer and been much uglier. Fate
|
|
smiled and I found his version while I was searching out the equations
|
|
I needed to make a circle-shaped cloud. I've simplified and, in places,
|
|
dumbified his code for this plugin, and even outright copied chunks of
|
|
it since those parts just worked far better than what I had originally
|
|
written. Many thanks, Timothy, for saving some of my wits, sanity and
|
|
hair over the past week.
|
|
|
|
Thanks to http://www.websanova.com/tutorials/jquery/jquery-plugin-development-boilerplate
|
|
for providing a great boilerplate I could use for my first jQuery plugin.
|
|
My original layout worked, but this one was much better.
|
|
*/
|
|
|
|
|
|
(function ($) {
|
|
"use strict";
|
|
var pluginName = "awesomeCloud", // name of the plugin, mainly for canvas IDs
|
|
defaultSettings = {
|
|
"size": {
|
|
"grid": 8, // word spacing, smaller is more tightly packed
|
|
"factor": 0, // font resize factor, 0 means automatic
|
|
"normalize": true // reduces outliers for more attractive output
|
|
},
|
|
"color": {
|
|
"background": "rgba(255,255,255,0)", // background color, transparent by default
|
|
"start": "#20f", // color of the smallest font, if options.color = "gradient""
|
|
"end": "#e00" // color of the largest font, if options.color = "gradient"
|
|
},
|
|
"options": {
|
|
"color": "gradient", // if "random-light" or "random-dark", color.start and color.end are ignored
|
|
"rotationRatio": 0.3, // 0 is all horizontal, 1 is all vertical
|
|
"printMultiplier": 1, // set to 3 for nice printer output; higher numbers take longer
|
|
"sort": "highest" // "highest" to show big words first, "lowest" to do small words first, "random" to not care
|
|
},
|
|
"font": "Helvetica, Arial, sans-serif", // the CSS font-family string
|
|
"shape": "circle" // the selected shape keyword, or a theta function describing a shape
|
|
};
|
|
|
|
$.fn.awesomeCloud = function (option, settings) {
|
|
if (typeof option === "object") {
|
|
settings = option;
|
|
} else if (typeof option === "string") {
|
|
var data = this.data("_" + pluginName);
|
|
if (data) {
|
|
if (defaultSettings[ option ] !== undefined) {
|
|
if (settings !== undefined) {
|
|
data.settings[ option ] = settings;
|
|
return true;
|
|
} else {
|
|
return data.settings[ option ];
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
settings = $.extend(true, {}, defaultSettings, settings || {});
|
|
|
|
return this.each(function () {
|
|
var elem = $(this),
|
|
$settings = jQuery.extend(true, {}, settings),
|
|
cloud = new AwesomeCloud($settings, elem);
|
|
|
|
cloud.create();
|
|
|
|
elem.data("_" + pluginName, cloud);
|
|
});
|
|
};
|
|
|
|
function AwesomeCloud(settings, elem) {
|
|
this.bctx = null;
|
|
this.bgPixel = null;
|
|
this.ctx = null;
|
|
this.diffChannel = null;
|
|
this.container = elem;
|
|
this.grid = [];
|
|
this.ngx = null;
|
|
this.ngy = null;
|
|
this.settings = settings;
|
|
this.size = null;
|
|
this.words = [];
|
|
this.linkTable = [];
|
|
this.match = null;
|
|
return this;
|
|
}
|
|
|
|
AwesomeCloud.prototype = {
|
|
create: function () {
|
|
var $this = this,
|
|
i = 0,
|
|
ctxID = null,
|
|
runningTotal = 0,
|
|
currentWeight = 0,
|
|
tempCtxID = pluginName + "TempCheck",
|
|
lastWeight = null,
|
|
foundMax = false,
|
|
fontSize = 0,
|
|
jump = 0.1,
|
|
maxWidth = 0,
|
|
maxWord = 0,
|
|
counter = 0,
|
|
width = 0,
|
|
link = null,
|
|
target = null,
|
|
tag = null,
|
|
dimension,
|
|
fctx,
|
|
wdPixel,
|
|
x,
|
|
y;
|
|
|
|
this.settings.weightFactor = function (pt) {
|
|
return pt * $this.settings.size.factor;
|
|
};
|
|
|
|
this.settings.gridSize = Math.max(this.settings.size.grid, 4) * this.settings.options.printMultiplier;
|
|
|
|
this.settings.color.start = this.colorToRGBA(this.settings.color.start);
|
|
this.settings.color.end = this.colorToRGBA(this.settings.color.end);
|
|
this.settings.color.background = this.colorToRGBA(this.settings.color.background);
|
|
|
|
this.settings.minSize = this.minimumFontSize();
|
|
|
|
this.settings.ellipticity = 1;
|
|
|
|
switch (this.settings.shape) {
|
|
case "square":
|
|
this.settings.shape = function (theta) {
|
|
// A square
|
|
var thetaDelta = ( theta + Math.PI / 4 ) % ( 2 * Math.PI / 4 );
|
|
return 1 / ( Math.cos(thetaDelta) + Math.sin(thetaDelta) );
|
|
};
|
|
break;
|
|
case "diamond":
|
|
this.settings.shape = function (theta) {
|
|
// A diamond
|
|
var thetaDelta = theta % ( 2 * Math.PI / 4 );
|
|
return 1 / ( Math.cos(thetaDelta) + Math.sin(thetaDelta) );
|
|
};
|
|
break;
|
|
case "x":
|
|
this.settings.shape = function (theta) {
|
|
// An X
|
|
var thetaDelta = theta % ( 2 * Math.PI / 4 );
|
|
return 1 / ( Math.cos(thetaDelta) + Math.sin(thetaDelta) - ( 2 * Math.PI / 4 ) );
|
|
};
|
|
break;
|
|
case "triangle":
|
|
this.settings.shape = function (theta) {
|
|
// A triangle
|
|
var thetaDelta = ( theta + Math.PI * 3 / 2 ) % ( 2 * Math.PI / 3 );
|
|
return 1 / ( Math.cos(thetaDelta) + Math.sqrt(3) * Math.sin(thetaDelta) );
|
|
};
|
|
break;
|
|
case "triangle-forward":
|
|
this.settings.shape = function (theta) {
|
|
// A triangle pointing to the right
|
|
var thetaDelta = theta % ( 2 * Math.PI / 3 );
|
|
return 1 / ( Math.cos(thetaDelta) + Math.sqrt(3) * Math.sin(thetaDelta) );
|
|
};
|
|
break;
|
|
case "pentagon":
|
|
this.settings.shape = function (theta) {
|
|
// A pentagon
|
|
var thetaDelta = ( theta + 0.955 ) % ( 2 * Math.PI / 5 );
|
|
return 1 / ( Math.cos(thetaDelta) + 0.726543 * Math.sin(thetaDelta) );
|
|
};
|
|
break;
|
|
case "star":
|
|
this.settings.shape = function (theta) {
|
|
// A 5-pointed star
|
|
var thetaDelta = ( theta + 0.955 ) % ( 2 * Math.PI / 10 );
|
|
if (( theta + 0.955 ) % ( 2 * Math.PI / 5 ) - ( 2 * Math.PI / 10 ) >= 0) {
|
|
return 1 / ( Math.cos(( 2 * Math.PI / 10 ) - thetaDelta) + 3.07768 * Math.sin(( 2 * Math.PI / 10 ) - thetaDelta) );
|
|
} else {
|
|
return 1 / ( Math.cos(thetaDelta) + 3.07768 * Math.sin(thetaDelta) );
|
|
}
|
|
};
|
|
break;
|
|
case "circle":
|
|
this.settings.shape = function (theta) {
|
|
return 1;
|
|
};
|
|
break;
|
|
default:
|
|
this.settings.shape = function (theta) {
|
|
return 1;
|
|
};
|
|
break;
|
|
}
|
|
|
|
this.size = {
|
|
"left": this.container.offset().left,
|
|
"top": this.container.offset().top,
|
|
"height": ( this.container.height() * this.settings.options.printMultiplier ),
|
|
"width": ( this.container.width() * this.settings.options.printMultiplier ),
|
|
"screenHeight": this.container.height(),
|
|
"screenWidth": this.container.width()
|
|
};
|
|
|
|
this.settings.ellipticity = this.size.height / this.size.width;
|
|
if (this.settings.ellipticity > 2) {
|
|
this.settings.ellipticity = 2;
|
|
}
|
|
if (this.settings.ellipticity < 0.2) {
|
|
this.settings.ellipticity = 0.2;
|
|
}
|
|
|
|
this.settings.weight = {
|
|
"lowest": null,
|
|
"highest": null,
|
|
"average": null
|
|
};
|
|
this.container.children().each(function (i, e) {
|
|
link = null;
|
|
tag = null;
|
|
currentWeight = parseInt($(this).attr("data-weight"), 10);
|
|
runningTotal += currentWeight;
|
|
if (!$this.settings.weight.lowest) {
|
|
$this.settings.weight.lowest = currentWeight;
|
|
}
|
|
if (!$this.settings.weight.highest) {
|
|
$this.settings.weight.highest = currentWeight;
|
|
}
|
|
if (currentWeight < $this.settings.weight.lowest) {
|
|
$this.settings.weight.lowest = currentWeight;
|
|
}
|
|
if (currentWeight > $this.settings.weight.highest) {
|
|
$this.settings.weight.highest = currentWeight;
|
|
}
|
|
$this.settings.weight.average = runningTotal / ( i + 1 );
|
|
// While we're here looping anyways, let's hide the originals.
|
|
$(this).css("display", "none");
|
|
// Check whether there is a link
|
|
if ($(this).has("a").length === 0) {
|
|
tag = $(this).html();
|
|
} else {
|
|
var aElement = $(this).children(":first");
|
|
link = aElement.attr("href");
|
|
target = aElement.attr("target");
|
|
tag = aElement.html();
|
|
}
|
|
$this.words.push([ tag , currentWeight, link, target]);
|
|
});
|
|
this.settings.range = this.settings.weight.highest - this.settings.weight.lowest;
|
|
|
|
// Normalize the weight distribution if required.
|
|
if (this.settings.size.normalize === true) {
|
|
// Sort the words by weight, ascending.
|
|
this.words.sort(function (a, b) {
|
|
return ( a[ 1 ] - b[ 1 ]);
|
|
});
|
|
|
|
for (i = 0; i < this.words.length; i++) {
|
|
if (lastWeight === null) {
|
|
lastWeight = this.words[ i ][ 1 ];
|
|
} else {
|
|
if (this.words[ i ][ 1 ] - lastWeight > this.settings.weight.average) {
|
|
this.words[ i ][ 1 ] -= ( this.words[ i ][ 1 ] - lastWeight ) / ( this.settings.weight.average * 0.38 ) + lastWeight;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Either randomize the words, or sort 'em
|
|
this.words.sort(function (a, b) {
|
|
if ($this.settings.options.sort === "random") {
|
|
return 0.5 - Math.random();
|
|
} else if ($this.settings.options.sort === "lowest") {
|
|
return ( a[ 1 ] - b[ 1 ]);
|
|
} else { // "highest"
|
|
return ( b[ 1 ] - a[ 1 ]);
|
|
}
|
|
});
|
|
|
|
// Set the font scaling factor if factor is 0.
|
|
if (this.settings.size.factor === parseInt(0, 10)) {
|
|
this.settings.size.factor = 1;
|
|
ctxID = pluginName + "SizeTest";
|
|
foundMax = false;
|
|
fontSize = 0;
|
|
jump = 0.1;
|
|
maxWidth = 0;
|
|
maxWord = 0;
|
|
counter = 0;
|
|
width = 0;
|
|
dimension = Math.min(this.size.width, this.size.height);
|
|
fctx = this.createCanvas({
|
|
"id": ctxID,
|
|
"width": dimension,
|
|
"height": dimension,
|
|
"left": 0,
|
|
"top": 0
|
|
});
|
|
// Find the widest word at normal resolution.
|
|
for (i = 0; i < this.words.length; i++) {
|
|
fctx.font = this.settings.weightFactor(this.words[ i ][ 1 ]) + "px " + this.settings.font;
|
|
width = fctx.measureText(this.words[ i ][ 0 ]).width;
|
|
if (width > maxWidth) {
|
|
maxWidth = width;
|
|
maxWord = this.words[ i ];
|
|
}
|
|
}
|
|
// Keep increasing the font size until we find the largest that will fit in the smallest dimension of the container.
|
|
while (!foundMax) {
|
|
fontSize = this.settings.weightFactor(maxWord[ 1 ]);
|
|
fctx.font = fontSize.toString(10) + "px " + this.settings.font;
|
|
width = fctx.measureText(maxWord[ 0 ]).width;
|
|
if (width > ( dimension * 0.95 )) {
|
|
this.settings.size.factor -= jump;
|
|
} else if (width < ( dimension * 0.9 )) {
|
|
this.settings.size.factor += jump;
|
|
} else {
|
|
foundMax = true;
|
|
}
|
|
counter += 1;
|
|
if (counter > 10000) {
|
|
// Dude, if it takes this many tries to max out the font, set it yourself :)
|
|
foundMax = true;
|
|
}
|
|
}
|
|
this.destroyCanvas(ctxID);
|
|
this.settings.size.factor -= jump;
|
|
}
|
|
|
|
// Figure out the color stepping if options.color is "gradient".
|
|
this.settings.color.increment = {
|
|
"r": ( this.settings.color.end.r - this.settings.color.start.r ) / this.settings.range,
|
|
"g": ( this.settings.color.end.g - this.settings.color.start.g ) / this.settings.range,
|
|
"b": ( this.settings.color.end.b - this.settings.color.start.b ) / this.settings.range,
|
|
"a": ( this.settings.color.end.a - this.settings.color.start.a ) / this.settings.range
|
|
};
|
|
|
|
// Start drawing words!
|
|
this.ngx = Math.floor(this.size.width / this.settings.gridSize);
|
|
this.ngy = Math.floor(this.size.height / this.settings.gridSize);
|
|
this.grid = [];
|
|
ctxID = pluginName + this.container.attr("id");
|
|
this.ctx = this.createCanvas({
|
|
"parent": this.container,
|
|
"id": ctxID,
|
|
"width": this.size.width,
|
|
"height": this.size.height,
|
|
"left": "0px",
|
|
"top": "0px"
|
|
});
|
|
this.bctx = this.createCanvas({
|
|
"id": tempCtxID,
|
|
"width": 1,
|
|
"height": 1,
|
|
"left": 0,
|
|
"top": 0
|
|
});
|
|
this.bctx.fillStyle = this.settings.color.background.rgba;
|
|
this.bctx.clearRect(0, 0, 1, 1);
|
|
this.bctx.fillStyle = this.settings.color.background.rgba;
|
|
this.bctx.fillRect(0, 0, 1, 1);
|
|
this.bgPixel = this.bctx.getImageData(0, 0, 1, 1).data;
|
|
if (typeof this.settings.options.color !== "function" && this.settings.options.color.substr(0, 6) !== "random" && this.settings.options.color.substr(0, 8) !== "gradient") {
|
|
this.bctx.fillStyle = this.colorToRGBA(this.settings.options.color).rgba;
|
|
this.bctx.fillRect(0, 0, 1, 1);
|
|
wdPixel = this.bctx.getImageData(0, 0, 1, 1).data;
|
|
i = 4;
|
|
while (i--) {
|
|
if (Math.abs(wdPixel[ i ] - this.bgPixel[ i ]) > 10) {
|
|
this.diffChannel = i;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
this.diffChannel = NaN;
|
|
}
|
|
this.destroyCanvas(tempCtxID);
|
|
x = this.ngx;
|
|
while (x--) {
|
|
this.grid[ x ] = [];
|
|
y = this.ngy;
|
|
while (y--) {
|
|
this.grid[ x ][ y ] = true;
|
|
}
|
|
}
|
|
this.ctx.fillStyle = this.settings.color.background.rgba;
|
|
this.ctx.clearRect(0, 0, this.ngx * ( this.settings.gridSize + 1 ), this.ngy * ( this.settings.gridSize + 1 ));
|
|
this.ctx.fillRect(0, 0, this.ngx * ( this.settings.gridSize + 1 ), this.ngy * ( this.settings.gridSize + 1 ));
|
|
this.ctx.textBaseline = "top";
|
|
i = 0;
|
|
window.setImmediate(function loop() {
|
|
if (i >= $this.words.length) {
|
|
return;
|
|
}
|
|
$this.putWord($this.words[ i ][ 0 ], $this.words[ i ][ 1 ], $this.words[ i ][ 2 ], $this.words[ i ][ 3 ]);
|
|
i += 1;
|
|
window.setImmediate(loop);
|
|
});
|
|
$this.allDone(ctxID);
|
|
return true;
|
|
},
|
|
allDone: function (canvasID) {
|
|
// $this.linkTable
|
|
var $this = this,
|
|
canvasDOM = document.getElementById(canvasID);
|
|
$( "#" + canvasID ).width( this.size.screenWidth );
|
|
$( "#" + canvasID ).height( this.size.screenHeight );
|
|
$( "#" + canvasID ).css( "display", "block" );
|
|
$( "#" + canvasID ).css( "visibility", "visible" );
|
|
|
|
canvasDOM.addEventListener("mousemove", function (ev) {
|
|
var x = 0, y = 0;
|
|
// Get the mouse position relative to the canvas element.
|
|
if (ev.layerX || ev.layerX === 0) { //for firefox
|
|
x = ev.layerX;
|
|
y = ev.layerY;
|
|
}
|
|
x -= canvasDOM.offsetLeft;
|
|
x += $(canvasDOM).position().left;
|
|
x = Math.floor(x * $this.settings.options.printMultiplier);
|
|
y -= canvasDOM.offsetTop;
|
|
y += $(canvasDOM).position().top;
|
|
y = Math.floor(y * $this.settings.options.printMultiplier);
|
|
|
|
$this.match = null;
|
|
for (var idx = 0, blocks = $this.linkTable.length; idx < blocks; idx++) {
|
|
var thisWord = $this.linkTable[idx];
|
|
if (x >= thisWord.x && x <= thisWord.x + thisWord.width && y >= thisWord.y && y <= thisWord.y + thisWord.height) {
|
|
// Smaller word will take precedence using this type of lookup.
|
|
$this.match = thisWord;
|
|
}
|
|
}
|
|
if ($this.match !== null) {
|
|
document.body.style.cursor = "pointer";
|
|
} else {
|
|
document.body.style.cursor = "";
|
|
}
|
|
}, false);
|
|
canvasDOM.addEventListener("click", function (ev) {
|
|
if ($this.match !== null) {
|
|
if ($this.match.target) {
|
|
window.open($this.match.link, $this.match.target);
|
|
} else {
|
|
window.location = $this.match.link;
|
|
}
|
|
}
|
|
}, false);
|
|
},
|
|
minimumFontSize: function () {
|
|
var ctxID = pluginName + "FontTest", fontCtx = this.createCanvas({
|
|
"id": ctxID,
|
|
"width": 50,
|
|
"height": 50,
|
|
"left": 0,
|
|
"top": 0
|
|
}), size = 20, hanWidth, mWidth;
|
|
while (size) {
|
|
fontCtx.font = size.toString(10) + "px sans-serif";
|
|
if (fontCtx.measureText("\uFF37").width === hanWidth && fontCtx.measureText("m").width === mWidth) {
|
|
this.destroyCanvas(ctxID);
|
|
return ( size + 1 ) / 2;
|
|
}
|
|
hanWidth = fontCtx.measureText("\uFF37").width;
|
|
mWidth = fontCtx.measureText("m").width;
|
|
size -= 1;
|
|
}
|
|
this.destroyCanvas(ctxID);
|
|
return 0;
|
|
},
|
|
createCanvas: function (options) {
|
|
var canvasID = options.id, canvasDOM, parent = $("body");
|
|
if (options.parent !== undefined) {
|
|
parent = options.parent;
|
|
}
|
|
parent.append("<canvas id=\"" + canvasID + "\" width=\"" + options.width + "\" height=\"" + options.height + "\">.</canvas>");
|
|
$("#" + canvasID).css("visibility", "hidden");
|
|
$("#" + canvasID).css("display", "none");
|
|
$("#" + canvasID).css("position", "relative");
|
|
$("#" + canvasID).css("z-index", 10000);
|
|
$("#" + canvasID).width(options.width);
|
|
$("#" + canvasID).height(options.height);
|
|
$("#" + canvasID).offset({ top: options.top, left: options.left });
|
|
canvasDOM = document.getElementById(canvasID);
|
|
canvasDOM.setAttribute("width", options.width);
|
|
canvasDOM.setAttribute("height", options.height);
|
|
return canvasDOM.getContext("2d");
|
|
},
|
|
destroyCanvas: function (id) {
|
|
$("#" + id).remove();
|
|
},
|
|
putWord: function (word, weight, link, target) {
|
|
var $this = this,
|
|
rotate = ( Math.random() < this.settings.options.rotationRatio ),
|
|
fontSize = this.settings.weightFactor(weight),
|
|
h = null,
|
|
w = null,
|
|
mapIndicesX,
|
|
mapIndicesY,
|
|
xIndex,
|
|
yIndex,
|
|
newObj = {},
|
|
font,
|
|
X, Y, gw, gh, center, R, T, r, t, rx, fc, fctx, ctxID, points;
|
|
if (fontSize <= this.settings.minSize) {
|
|
return false;
|
|
}
|
|
|
|
// Apply some tweaks to font size as by default the smallest are too small
|
|
fontSize += 10;
|
|
|
|
this.ctx.font = fontSize.toString(10) + "px " + this.settings.font;
|
|
if (rotate) {
|
|
h = this.ctx.measureText(word).width;
|
|
w = Math.max(fontSize, this.ctx.measureText("m").width, this.ctx.measureText("\uFF37").width);
|
|
if (/[Jgpqy]/.test(word)) {
|
|
w *= 3 / 2;
|
|
}
|
|
w += Math.floor(fontSize / 6);
|
|
h += Math.floor(fontSize / 6);
|
|
} else {
|
|
w = this.ctx.measureText(word).width;
|
|
h = Math.max(fontSize, this.ctx.measureText("m").width, this.ctx.measureText("\uFF37").width);
|
|
if (/[Jgpqy]/.test(word)) {
|
|
h *= 3 / 2;
|
|
}
|
|
h += Math.floor(fontSize / 6);
|
|
w += Math.floor(fontSize / 6);
|
|
}
|
|
w = Math.ceil(w);
|
|
h = Math.ceil(h);
|
|
gw = Math.ceil(w / this.settings.gridSize);
|
|
gh = Math.ceil(h / this.settings.gridSize);
|
|
center = [ this.ngx / 2, this.ngy / 2 ];
|
|
R = Math.floor(Math.sqrt(this.ngx * this.ngx + this.ngy * this.ngy));
|
|
T = this.ngx + this.ngy;
|
|
r = R + 1;
|
|
while (r--) {
|
|
t = T;
|
|
points = [];
|
|
while (t--) {
|
|
rx = this.settings.shape(t / T * 2 * Math.PI);
|
|
points.push([ Math.floor(center[ 0 ] + ( R - r ) * rx * Math.cos(-t / T * 2 * Math.PI) - gw / 2), Math.floor(center[ 1 ] + ( R - r ) * rx * this.settings.ellipticity * Math.sin(-t / T * 2 * Math.PI) - gh / 2), t / T * 2 * Math.PI ]);
|
|
}
|
|
if (points.shuffle().some(function (gxy) {
|
|
if ($this.canFitText(gxy[ 0 ], gxy[ 1 ], gw, gh)) {
|
|
X = Math.floor(gxy[ 0 ] * $this.settings.gridSize + ( gw * $this.settings.gridSize - w ) / 2);
|
|
Y = Math.floor(gxy[ 1 ] * $this.settings.gridSize + ( gh * $this.settings.gridSize - h ) / 2);
|
|
if (rotate) {
|
|
ctxID = pluginName + "Rotator";
|
|
fctx = $this.createCanvas({
|
|
"id": ctxID,
|
|
"width": w,
|
|
"height": h,
|
|
"left": 0,
|
|
"top": 0
|
|
});
|
|
fc = document.getElementById(ctxID);
|
|
fctx.fillStyle = $this.settings.color.background.rgba;
|
|
fctx.fillRect(0, 0, w, h);
|
|
fctx.fillStyle = $this.wordcolor(word, weight, fontSize, R - r, gxy[ 2 ]);
|
|
fctx.font = fontSize.toString(10) + "px " + $this.settings.font;
|
|
fctx.textBaseline = "top";
|
|
if (rotate) {
|
|
fctx.translate(0, h);
|
|
fctx.rotate(-Math.PI / 2);
|
|
}
|
|
fctx.fillText(word, Math.floor(fontSize / 6), 0);
|
|
$this.ctx.clearRect(X, Y, w, h);
|
|
$this.ctx.drawImage(fc, X, Y, w, h);
|
|
$this.destroyCanvas(ctxID);
|
|
} else {
|
|
font = fontSize.toString(10) + "px " + $this.settings.font;
|
|
$this.ctx.font = font;
|
|
$this.ctx.fillStyle = $this.wordcolor(word, weight, fontSize, R - r, gxy[ 2 ]);
|
|
$this.ctx.fillText(word, X, Y);
|
|
h = $this.getTextHeight(font).height;
|
|
w = $this.ctx.measureText(word).width;
|
|
}
|
|
|
|
// Build Lookup Table
|
|
if (link !== null) {
|
|
$this.linkTable.push({
|
|
"x": X,
|
|
"y": Y,
|
|
"width": w,
|
|
"height": h,
|
|
"word": word,
|
|
"link": link,
|
|
"target": target
|
|
});
|
|
}
|
|
|
|
$this.updateGrid(gxy[ 0 ], gxy[ 1 ], gw, gh);
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
})) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
canFitText: function (gx, gy, gw, gh) {
|
|
if (gx < 0 || gy < 0 || gx + gw > this.ngx || gy + gh > this.ngy) {
|
|
return false;
|
|
}
|
|
var x = gw, y;
|
|
while (x--) {
|
|
y = gh;
|
|
while (y--) {
|
|
if (!this.grid[ gx + x ][ gy + y ]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
wordcolor: function (word, weight, fontSize, radius, theta) {
|
|
var output = null;
|
|
switch (this.settings.options.color) {
|
|
case "gradient" :
|
|
output = "rgba(" + Math.round(this.settings.color.start.r + ( this.settings.color.increment.r * ( weight - this.settings.weight.lowest ) )) + "," + Math.round(this.settings.color.start.g + ( this.settings.color.increment.g * ( weight - this.settings.weight.lowest ) )) + "," + Math.round(this.settings.color.start.b + ( this.settings.color.increment.b * ( weight - this.settings.weight.lowest ) )) + "," + Math.round(this.settings.color.start.a + ( this.settings.color.increment.a * ( weight - this.settings.weight.lowest ) )) + ")";
|
|
break;
|
|
case "random-dark" :
|
|
output = "rgba(" + Math.floor(Math.random() * 128).toString(10) + "," + Math.floor(Math.random() * 128).toString(10) + "," + Math.floor(Math.random() * 128).toString(10) + ",1)";
|
|
break;
|
|
case "random-light" :
|
|
output = "rgba(" + Math.floor(Math.random() * 128 + 128).toString(10) + "," + Math.floor(Math.random() * 128 + 128).toString(10) + "," + Math.floor(Math.random() * 128 + 128).toString(10) + ",1)";
|
|
break;
|
|
default :
|
|
if (typeof this.settings.wordColor !== "function") {
|
|
output = "rgba(127,127,127,1)";
|
|
} else {
|
|
output = this.settings.wordColor(word, weight, fontSize, radius, theta);
|
|
}
|
|
break;
|
|
}
|
|
return output;
|
|
},
|
|
updateGrid: function (gx, gy, gw, gh, skipDiffChannel) {
|
|
var x = gw, y, imgData = this.ctx.getImageData(gx * this.settings.gridSize, gy * this.settings.gridSize, gw * this.settings.gridSize, gh * this.settings.gridSize);
|
|
while (x--) {
|
|
y = gh;
|
|
while (y--) {
|
|
if (!this.isGridEmpty(imgData, x * this.settings.gridSize, y * this.settings.gridSize, gw * this.settings.gridSize, gh * this.settings.gridSize, skipDiffChannel)) {
|
|
this.grid[ gx + x ][ gy + y ] = false;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
isGridEmpty: function (imgData, x, y, w, h, skipDiffChannel) {
|
|
var i = this.settings.gridSize, j, k;
|
|
if (!isNaN(this.diffChannel) && !skipDiffChannel) {
|
|
while (i--) {
|
|
j = this.settings.gridSize;
|
|
while (j--) {
|
|
if (this.getChannelData(imgData.data, x + i, y + j, w, h, this.diffChannel) !== this.bgPixel[ this.diffChannel ]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
while (i--) {
|
|
j = this.settings.gridSize;
|
|
while (j--) {
|
|
k = 4;
|
|
while (k--) {
|
|
if (this.getChannelData(imgData.data, x + i, y + j, w, h, k) !== this.bgPixel[ k ]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
getChannelData: function (data, x, y, w, h, c) {
|
|
return data[
|
|
( y * w + x ) * 4 + c
|
|
];
|
|
},
|
|
colorToRGBA: function (color) {
|
|
color = color.replace(/^\s*#|\s*$/g, "");
|
|
if (color.length === 3) {
|
|
color = color.replace(/(.)/g, "$1$1");
|
|
}
|
|
color = color.toLowerCase();
|
|
var namedColors = {
|
|
aliceblue: "f0f8ff",
|
|
antiquewhite: "faebd7",
|
|
aqua: "00ffff",
|
|
aquamarine: "7fffd4",
|
|
azure: "f0ffff",
|
|
beige: "f5f5dc",
|
|
bisque: "ffe4c4",
|
|
black: "000000",
|
|
blanchedalmond: "ffebcd",
|
|
blue: "0000ff",
|
|
blueviolet: "8a2be2",
|
|
brown: "a52a2a",
|
|
burlywood: "deb887",
|
|
cadetblue: "5f9ea0",
|
|
chartreuse: "7fff00",
|
|
chocolate: "d2691e",
|
|
coral: "ff7f50",
|
|
cornflowerblue: "6495ed",
|
|
cornsilk: "fff8dc",
|
|
crimson: "dc143c",
|
|
cyan: "00ffff",
|
|
darkblue: "00008b",
|
|
darkcyan: "008b8b",
|
|
darkgoldenrod: "b8860b",
|
|
darkgray: "a9a9a9",
|
|
darkgreen: "006400",
|
|
darkkhaki: "bdb76b",
|
|
darkmagenta: "8b008b",
|
|
darkolivegreen: "556b2f",
|
|
darkorange: "ff8c00",
|
|
darkorchid: "9932cc",
|
|
darkred: "8b0000",
|
|
darksalmon: "e9967a",
|
|
darkseagreen: "8fbc8f",
|
|
darkslateblue: "483d8b",
|
|
darkslategray: "2f4f4f",
|
|
darkturquoise: "00ced1",
|
|
darkviolet: "9400d3",
|
|
deeppink: "ff1493",
|
|
deepskyblue: "00bfff",
|
|
dimgray: "696969",
|
|
dodgerblue: "1e90ff",
|
|
feldspar: "d19275",
|
|
firebrick: "b22222",
|
|
floralwhite: "fffaf0",
|
|
forestgreen: "228b22",
|
|
fuchsia: "ff00ff",
|
|
gainsboro: "dcdcdc",
|
|
ghostwhite: "f8f8ff",
|
|
gold: "ffd700",
|
|
goldenrod: "daa520",
|
|
gray: "808080",
|
|
green: "008000",
|
|
greenyellow: "adff2f",
|
|
honeydew: "f0fff0",
|
|
hotpink: "ff69b4",
|
|
indianred: "cd5c5c",
|
|
indigo: "4b0082",
|
|
ivory: "fffff0",
|
|
khaki: "f0e68c",
|
|
lavender: "e6e6fa",
|
|
lavenderblush: "fff0f5",
|
|
lawngreen: "7cfc00",
|
|
lemonchiffon: "fffacd",
|
|
lightblue: "add8e6",
|
|
lightcoral: "f08080",
|
|
lightcyan: "e0ffff",
|
|
lightgoldenrodyellow: "fafad2",
|
|
lightgrey: "d3d3d3",
|
|
lightgreen: "90ee90",
|
|
lightpink: "ffb6c1",
|
|
lightsalmon: "ffa07a",
|
|
lightseagreen: "20b2aa",
|
|
lightskyblue: "87cefa",
|
|
lightslateblue: "8470ff",
|
|
lightslategray: "778899",
|
|
lightsteelblue: "b0c4de",
|
|
lightyellow: "ffffe0",
|
|
lime: "00ff00",
|
|
limegreen: "32cd32",
|
|
linen: "faf0e6",
|
|
magenta: "ff00ff",
|
|
maroon: "800000",
|
|
mediumaquamarine: "66cdaa",
|
|
mediumblue: "0000cd",
|
|
mediumorchid: "ba55d3",
|
|
mediumpurple: "9370d8",
|
|
mediumseagreen: "3cb371",
|
|
mediumslateblue: "7b68ee",
|
|
mediumspringgreen: "00fa9a",
|
|
mediumturquoise: "48d1cc",
|
|
mediumvioletred: "c71585",
|
|
midnightblue: "191970",
|
|
mintcream: "f5fffa",
|
|
mistyrose: "ffe4e1",
|
|
moccasin: "ffe4b5",
|
|
navajowhite: "ffdead",
|
|
navy: "000080",
|
|
oldlace: "fdf5e6",
|
|
olive: "808000",
|
|
olivedrab: "6b8e23",
|
|
orange: "ffa500",
|
|
orangered: "ff4500",
|
|
orchid: "da70d6",
|
|
palegoldenrod: "eee8aa",
|
|
palegreen: "98fb98",
|
|
paleturquoise: "afeeee",
|
|
palevioletred: "d87093",
|
|
papayawhip: "ffefd5",
|
|
peachpuff: "ffdab9",
|
|
peru: "cd853f",
|
|
pink: "ffc0cb",
|
|
plum: "dda0dd",
|
|
powderblue: "b0e0e6",
|
|
purple: "800080",
|
|
red: "ff0000",
|
|
rosybrown: "bc8f8f",
|
|
royalblue: "4169e1",
|
|
saddlebrown: "8b4513",
|
|
salmon: "fa8072",
|
|
sandybrown: "f4a460",
|
|
seagreen: "2e8b57",
|
|
seashell: "fff5ee",
|
|
sienna: "a0522d",
|
|
silver: "c0c0c0",
|
|
skyblue: "87ceeb",
|
|
slateblue: "6a5acd",
|
|
slategray: "708090",
|
|
snow: "fffafa",
|
|
springgreen: "00ff7f",
|
|
steelblue: "4682b4",
|
|
tan: "d2b48c",
|
|
teal: "008080",
|
|
thistle: "d8bfd8",
|
|
tomato: "ff6347",
|
|
turquoise: "40e0d0",
|
|
violet: "ee82ee",
|
|
violetred: "d02090",
|
|
wheat: "f5deb3",
|
|
white: "ffffff",
|
|
whitesmoke: "f5f5f5",
|
|
yellow: "ffff00",
|
|
yellowgreen: "9acd32"
|
|
}, colorDefs = [
|
|
{
|
|
re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
|
|
example: [ "rgb(123, 234, 45)", "rgb(255,234,245)" ],
|
|
process: function (bits) {
|
|
return [
|
|
parseInt(bits[ 1 ], 10), parseInt(bits[ 2 ], 10), parseInt(bits[ 3 ], 10), 1
|
|
];
|
|
}
|
|
},
|
|
{
|
|
re: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d+(?:\.\d+)?|\.\d+)\s*\)/,
|
|
example: [ "rgb(123, 234, 45, 1)", "rgb(255,234,245, 0.5)" ],
|
|
process: function (bits) {
|
|
return [
|
|
parseInt(bits[ 1 ], 10), parseInt(bits[ 2 ], 10), parseInt(bits[ 3 ], 10), parseFloat(bits[ 4 ])
|
|
];
|
|
}
|
|
},
|
|
{
|
|
re: /^(\w{2})(\w{2})(\w{2})$/,
|
|
example: [ "#00ff00", "336699" ],
|
|
process: function (bits) {
|
|
return [
|
|
parseInt(bits[ 1 ], 16), parseInt(bits[ 2 ], 16), parseInt(bits[ 3 ], 16), 1
|
|
];
|
|
}
|
|
},
|
|
{
|
|
re: /^(\w{1})(\w{1})(\w{1})$/,
|
|
example: [ "#fb0", "f0f" ],
|
|
process: function (bits) {
|
|
return [
|
|
parseInt(bits[ 1 ] + bits[ 1 ], 16), parseInt(bits[ 2 ] + bits[ 2 ], 16), parseInt(bits[ 3 ] + bits[ 3 ], 16), 1
|
|
];
|
|
}
|
|
}
|
|
], r, g, b, a, key, i, re, processor, bits, channels;
|
|
for (key in namedColors) {
|
|
if (color === key) {
|
|
color = namedColors[ key ];
|
|
}
|
|
}
|
|
// search through the definitions to find a match
|
|
for (i = 0; i < colorDefs.length; i++) {
|
|
re = colorDefs[ i ].re;
|
|
processor = colorDefs[ i ].process;
|
|
bits = re.exec(color);
|
|
if (bits) {
|
|
channels = processor(bits);
|
|
r = channels[ 0 ];
|
|
g = channels[ 1 ];
|
|
b = channels[ 2 ];
|
|
a = channels[ 3 ];
|
|
}
|
|
}
|
|
r = ( r < 0 || isNaN(r) ) ? 0 : ( ( r > 255 ) ? 255 : r );
|
|
g = ( g < 0 || isNaN(g) ) ? 0 : ( ( g > 255 ) ? 255 : g );
|
|
b = ( b < 0 || isNaN(b) ) ? 0 : ( ( b > 255 ) ? 255 : b );
|
|
a = ( a < 0 || isNaN(a) ) ? 0 : ( ( a > 1 ) ? 1 : a );
|
|
return {
|
|
r: r,
|
|
g: g,
|
|
b: b,
|
|
a: a,
|
|
rgba: "rgba(" + r + ", " + g + ", " + b + ", " + a + ")"
|
|
};
|
|
},
|
|
getTextHeight: function(font) {
|
|
// Get text height inside a canvas
|
|
var text = $("<span style=\"font: " + font + "\">Hg</span>"),
|
|
block = $("<div style=\"display: inline-block; width: 1px; height: 0px;\"></div>"),
|
|
div = $("<div></div>"),
|
|
body = $("body"),
|
|
result = {};
|
|
div.append(text, block);
|
|
body.append(div);
|
|
try {
|
|
result = {};
|
|
block.css({ verticalAlign: "baseline" });
|
|
result.ascent = block.offset().top - text.offset().top;
|
|
block.css({ verticalAlign: "bottom" });
|
|
result.height = block.offset().top - text.offset().top;
|
|
result.descent = result.height - result.ascent;
|
|
} finally {
|
|
div.remove();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
};
|
|
})(jQuery);
|
|
|
|
// http://jsfromhell.com/array/shuffle
|
|
Array.prototype.shuffle = function () {
|
|
"use strict";
|
|
for ( var j, x, i = this.length; i; j = parseInt( Math.random() * i, 10 ), x = this[ --i ], this[ i ] = this[ j ], this[ j ] = x );
|
|
return this;
|
|
};
|
|
|
|
// setImmediate polyfill.
|
|
if ( !window.setImmediate ) {
|
|
window.setImmediate = ( function () {
|
|
"use strict";
|
|
return window.msSetImmediate || window.webkitSetImmediate || window.mozSetImmediate || window.oSetImmediate || ( function () {
|
|
// setZeroTimeout: "hack" based on postMessage
|
|
// modified from http://dbaron.org/log/20100309-faster-timeouts
|
|
if ( window.postMessage && window.addEventListener ) {
|
|
var timeouts = [],
|
|
timerPassed = -1,
|
|
timerIssued = -1,
|
|
messageName = "zero-timeout-message",
|
|
// Like setTimeout, but only takes a function argument. There's
|
|
// no time argument (always zero) and no arguments (you have to
|
|
// use a closure).
|
|
setZeroTimeout = function ( fn ) {
|
|
timeouts.push( fn );
|
|
window.postMessage( messageName, "*" );
|
|
return ++timerIssued;
|
|
},
|
|
handleMessage = function ( event ) {
|
|
// Skipping checking event source, IE confused this window object with another in the presence of iframe
|
|
if ( /*event.source === window && */event.data === messageName ) {
|
|
event.stopPropagation();
|
|
if ( timeouts.length > 0 ) {
|
|
var fn = timeouts.shift();
|
|
fn();
|
|
timerPassed += 1;
|
|
}
|
|
}
|
|
},
|
|
fnId;
|
|
window.addEventListener( "message", handleMessage, true );
|
|
window.clearImmediate = function ( timer ) {
|
|
if ( typeof timer !== "number" || timer > timerIssued ) {
|
|
return;
|
|
}
|
|
fnId = timer - timerPassed - 1;
|
|
timeouts[ fnId ] = ( function () {} ); // overwrite the original fn
|
|
};
|
|
// Add the one thing we want added to the window object.
|
|
return setZeroTimeout;
|
|
}
|
|
} )() || function ( fn ) {
|
|
// fallback
|
|
window.setTimeout( fn, 0 );
|
|
};
|
|
} )();
|
|
}
|
|
if ( !window.clearImmediate ) {
|
|
window.clearImmediate = ( function () {
|
|
"use strict";
|
|
return window.msClearImmediate || window.webkitClearImmediate || window.mozClearImmediate || window.oClearImmediate || function ( timer ) {
|
|
// "clearZeroTimeout" is implement on the previous block ||
|
|
// fallback
|
|
window.clearTimeout( timer );
|
|
};
|
|
} )();
|
|
}
|