实现一个简单的文字云
分析需求
首先我们要实现的文字云效果如下:
由图可知,该文字云的效果是,一个大标签文字在区域中心,其它小标签文字围绕这个大标签文字。
其中,这些文字有随机的颜色。
除了大标签文字,其它标签文字大小也随机。
然后,围绕
这个效果呢,想象一下火影忍者的轮回眼
。
其实就像一颗小石头扔向湖面,泛起阵阵涟漪(圆圈)向外扩散。
之后,文字之间不能交叉,也就是说不能碰撞,那就是说不能重叠。
好了,让我们撸起袖子
撸函数
由以上的分析,需要撸一个获取随机颜色的值
函数。
function getRandomColor(){
var arr = [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'];
var color = "#";
for(i=0;i<6;i++){
var c = parseInt(Math.random()*16);
c = arr[c];
color = color + c;
}
return color;
}
再撸一个获取随机的文字大小
函数。
function getRandomFontSize(){
var arr = [28,30,34,40];
var res = [];
for (var i = 0, len = arr.length; i < len; i++) {
var j = Math.floor(Math.random() * arr.length);
res[i] = arr[j];
arr.splice(j, 1);
}
return res[0];
}
以上两个函数代码简单粗暴,不再说明。
既然最大的标签文字待在区域中心,那么获取区域的中心点坐标
函数必不可少。
function getCenterPoint(DOMElement){
var rect = DOMElement.getBoundingClientRect();
var rectTop = rect.top;
var rectLeft = rect.left;
var ngx = Math.ceil(rect.width);
var ngy = Math.ceil(rect.height);
var center = [(ngx / 2) + rectLeft, (ngy / 2) + rectTop];
return center;
}
一句话讲清,长宽取各一半再加上区域自己的坐标即可。
接下来就是围绕
这个效果对应的函数,
function getPointsAtRadius(radius, center ,offsetY, multiple){
var T = radius * 8;
var t = T;
var points = [];
var offsetY = offsetY || 1;
var multiple = multiple || 30;
if (radius === 0) {
points.push([center[0], center[1]]);
}
while (t) {
points.push(
[
center[0] + (radius*multiple) * Math.cos( (t * 2 * Math.PI )/ T ),
center[1] + (radius*multiple) * Math.sin( (t * 2 * Math.PI )/ T ) * offsetY,
]
);
t = t - 1;
}
return points;
}
初中学的三角函数派上用场,它用来获取圆上点的横坐标和纵坐标。
这里除了半径(radius)和圆中心坐标(center)两个必要参数,还加上了Y轴偏移(offsetY),和单位距离(multiple)参数。
其中Y轴偏移(offsetY)可用来缩小或扩大选取的Y坐标,从而改变生成的文字云形状。
单位距离(multiple)参数,是确定以多少像素作为一个半径单位。
这年代,屏幕分辨率都很大,不可能以单个像素进行画圈圈吧。
然后我们也没必要拿到圆上的每个点的坐标。
那我们拿圆上多少个坐标比较合适?
文字标签是矩形,一个矩形可被八个矩形直接包围。间接包围n8个矩形。
故取n8个坐标即可。
接下来是判断两个矩形是否相交
。
function isCorssRect(array1, array2){
var Xa1 = array1[0][0];
var Ya1 = array1[0][1];
var Xa2 = array1[1][0];
var Ya2 = array1[1][1];
var Xb1 = array2[0][0];
var Yb1 = array2[0][1];
var Xb2 = array2[1][0];
var Yb2 = array2[1][1];
var Xc1 = Math.max(Xa1,Xb1);
var Yc1 = Math.max(Ya1,Yb1);
var Xc2 = Math.min(Xa2,Xb2);
var Yc2 = Math.min(Ya2,Yb2);
return (Xc1 <= Xc2 && Yc1 <= Yc2);
}
首先一个矩形可由左上角坐标和右下角坐标来定义。
那么两个矩形相交,则表明两个矩形的左上角坐标最大值 要小于等于 两个矩形的右下角坐标最小值。
请看图想象。
撸业务
有了以上几个函数,我们就可以开始构思业务逻辑。
假设输入的数据是一个数组,比如["紅樓夢","賈寶玉","林黛玉","薛寶釵","王熙鳳","李紈","賈元春","賈迎春","賈探春","賈惜春","秦可卿","賈巧姐","史湘雲","妙玉","賈政","賈赦","賈璉","賈珍","賈環","賈母","王夫人","薛姨媽","尤氏","平兒","鴛鴦","襲人","晴雯","香菱","紫鵑","麝月","小紅","金釧","甄士隱","賈雨村"]
。
- 我们要依次取出一个词,并且计算这个词的宽高。
- 通过
围绕
函数获取将要放置的坐标。 - 通过词的宽高 和 将要放置的坐标,可以得到这个词的左上角坐标和右下角坐标信息。
- 然后跟已画上去的词云左上角坐标,右下角坐标进行比较,看两个矩形是否
相交
。 - 不相交,则画上去,并把它的左上角坐标,右下角坐标信息进行存储。
- 相交,则回到第2步,获取下一个将要放置的坐标。
具体代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>草珊瑚的文字云</title>
<style>
body{
margin: 0;
}
#html-canvas{
/*position: absolute;
left:10px;
top: 100px;*/
width: 1415px;
height: 796px;
background-color: rgb(240, 240, 240);
position: relative;
}
</style>
</head>
<body>
<div id="html-canvas"></div>
</body>
<script>
/**
* 获取画布的中心点坐标
@method getCenterPoint
@param {HTMLDOMElement} DOMElement 通常是一个div
@return {array} [x, y] 包含中心点坐标的数组
*/
function getCenterPoint(DOMElement){
var rect = DOMElement.getBoundingClientRect();
var rectTop = rect.top;
var rectLeft = rect.left;
var ngx = Math.ceil(rectLeft + rect.width);
var ngy = Math.ceil(rectTop + rect.height);
var center = [(ngx / 2) + rectLeft, (ngy / 2) + rectTop];
console.log('rect', rect);
return center;
}
// var tagCanvas = document.getElementById('html-canvas');
// console.log("所选DOM的中心点为",getCenterPoint(tagCanvas));
// 获取最大半径
function getMaxRadius(DOMElement, cellSpace){
var cellSpace = cellSpace || 1;
var rect = DOMElement.getBoundingClientRect();
var ngx = Math.ceil(rect.width / cellSpace);
var ngy = Math.ceil(rect.height / cellSpace);
var maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy));
return maxRadius;
}
/**
* 获取圆上每个点的坐标
@method getCenterPoint
@param {int} radius 半径
@param {array} center 类似[11,22]中心点
@param {int} offsetY y坐标的偏移位置,默认为1
@return {array} [x, y] 圆上坐标的数组
*/
function getPointsAtRadius(radius, center ,offsetY, multiple){
// 因为像素是一个正方形,一个正方形的四周有8块正方形可包围。
var T = radius * 8;
var t = T;
var points = [];
var offsetY = offsetY || 1;
var multiple = multiple || 30;
if (radius === 0) {
points.push([center[0], center[1]]);
}
while (t) {
// 参考http://www.cnblogs.com/xieon1986/archive/2013/01/28/2880367.html
// 圆上每个点的
// Y坐标=圆心y坐标 + Math.sin(2*Math.PI / 360) * 半径
// X坐标=圆心x坐标 + Math.cos(2*Math.PI / 360) * 半径
// 弧度=(2*PI/360)*角度
// 基于圆心的x坐标 X坐标=圆心x坐标 + Math.sin((2*Math.PI / 360) * 1) * 半径
// 这里的T同360°的意义
points.push(
[
center[0] + (radius*multiple) * Math.cos( (t * 2 * Math.PI )/ T ),
center[1] + (radius*multiple) * Math.sin( (t * 2 * Math.PI )/ T ) * offsetY,
]
);
t = t - 1;
}
return points;
}
// console.log('圆上各点坐标', getPointsAtRadius(1,[731.5, 475]));
/**
* 获取文本的宽高
@method getCenterPoint
@param {HTMLDOMElement} DOMElement 通常是一个div
@return {array} [x, y] 包含中心点坐标的数组
*/
function getTextInfo(word, userCSS){
var fontSize = getRandomFontSize();
if(userCSS){
fontSize = userCSS.fontSize;
}
// 通过canvas的API来获取文本的各种信息
var fcanvas = document.createElement('canvas');
var fctx = fcanvas.getContext('2d');
fctx.font = 'normal ' + fontSize + 'px Hiragino Mincho Pro, serif';
// 通过canvas的measureText方法获取文本 像素级别的宽度
// http://www.w3school.com.cn/tags/canvas_measuretext.asp
var fw = fctx.measureText(word).width;
// 通过字体大小获取高度
var fh = fontSize;
return {
width: Math.ceil(fw),
height: fh,
word: word,
fontSize: fontSize
}
}
// console.log('你输入的文字长宽为', getTextInfo('你好'));
/**
* 判断两个矩形是否相交
* 参考:http://www.cnblogs.com/avril/archive/2012/11/13/2767577.html
* http://www.cnblogs.com/avril/archive/2013/04/01/2993875.html
@method isCorssRect
@param {array}
@param {array}
@return {bool} true表示有重叠,false表示没有重叠
*/
function isCorssRect(array1, array2){
var Xa1 = array1[0][0];
var Ya1 = array1[0][1];
var Xa2 = array1[1][0];
var Ya2 = array1[1][1];
var Xb1 = array2[0][0];
var Yb1 = array2[0][1];
var Xb2 = array2[1][0];
var Yb2 = array2[1][1];
var Xc1 = Math.max(Xa1,Xb1);
var Yc1 = Math.max(Ya1,Yb1);
var Xc2 = Math.min(Xa2,Xb2);
var Yc2 = Math.min(Ya2,Yb2);
return (Xc1 <= Xc2 && Yc1 <= Yc2);
}
//获取随机颜色的值
function getRandomColor(){
var arr = [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'];
var color = "#";
for(i=0;i<6;i++){
var c = parseInt(Math.random()*16);
c = arr[c];
color = color + c;
}
return color;
}
// 获取随机文字的大小
function getRandomFontSize(){
var arr = [28,30,34,40];
var res = [];
for (var i = 0, len = arr.length; i < len; i++) {
var j = Math.floor(Math.random() * arr.length);
res[i] = arr[j];
arr.splice(j, 1);
}
return res[0];
}
function drawText(position, textInfo, canvas ){
var span = document.createElement('span');
var styleRules = {
'position': 'absolute',
'display': 'block',
'font': 'normal' + ' '+textInfo.fontSize+'px ' + 'Hiragino Mincho Pro, serif',
'left': position[0] + 'px',
'top': position[1] + 'px',
'width': textInfo.width + 'px',
'height': textInfo.height + 'px',
'lineHeight': 1,
'color': getRandomColor(),
};
span.textContent = textInfo.word;
for (var cssProp in styleRules) {
span.style[cssProp] = styleRules[cssProp];
}
canvas.appendChild(span);
}
/**
* 获取文本的左上右下坐标,
@param {array} topLeft 类似
@return {object} textInfo 包含中心点坐标的数组
*/
function getTopLeft(topLeft, textInfo){
var left1 = topLeft[0];
var top1 = topLeft[1];
return [
[left1, top1],
[left1+ textInfo[0], top1 + textInfo[1]]
];
}
// 获取可安置的坐标
function getDrawPosition(textInfo, maxRadius, center, cellSpace ,drawArray){
var textInfo_width = textInfo.width;
var textInfo_height = textInfo.height;
var cellSpace = cellSpace || 10;
for(var i=0; i<maxRadius; i++){
var points = getPointsAtRadius(i, center, 0.64, cellSpace);
pointsLoop:
for(var j=0; j<points.length; j++){
var topLeft = getTopLeft(points[j], [textInfo_width, textInfo_height]);
// 是否和已存的文字碰撞
for(var z =0 ; z< drawArray.length; z++){
if(isCorssRect(topLeft, drawArray[z])){
continue pointsLoop;
}
}
drawArray.push(topLeft);
return points[j];
}
}
return null;
}
function start(){
var tagCanvas = document.getElementById('html-canvas');
var center = getCenterPoint(tagCanvas);
var cellSpace = 20;
var maxRadius = getMaxRadius(tagCanvas, cellSpace);
var data = ["紅樓夢","賈寶玉","林黛玉","薛寶釵","王熙鳳","李紈","賈元春","賈迎春","賈探春","賈惜春","秦可卿","賈巧姐","史湘雲","妙玉","賈政","賈赦","賈璉","賈珍","賈環","賈母","王夫人","薛姨媽","尤氏","平兒","鴛鴦","襲人","晴雯","香菱","紫鵑","麝月","小紅","金釧","甄士隱","賈雨村"];
var drawArray = [];
var tempDrawPositionArray = [];
for(var i =0; i< data.length; i++){
var dataItem = data[i];
var textInfo = getTextInfo(dataItem);
var drawPosition = null;
if(i != 0){
drawPosition = getDrawPosition(textInfo, maxRadius, center, cellSpace, drawArray);
}
else{
textInfo = getTextInfo(dataItem, {fontSize:60});
drawPosition = getDrawPosition(
textInfo,
maxRadius,
[center[0]-(textInfo.width/2), center[1]-(textInfo.height/2)],
cellSpace,
drawArray
);
}
if(drawPosition){
tempDrawPositionArray.push([drawPosition, textInfo, tagCanvas]);
}
}
var timer = setInterval(function(){
var textItem = tempDrawPositionArray.shift();
if(textItem){
drawText(textItem[0], textItem[1], textItem[2]);
}
else{
clearInterval(timer);
}
}, 0);
}
start();
</script>
</html>
本文实现思路和实验数据参考了wordcloud2,
并重写其百分之九十的代码。
意在理解其思路。
合乎自然而生生不息。。。