koishi-plugin-chat-analyse 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Analyse.d.ts +24 -0
- package/lib/Collector.d.ts +86 -0
- package/lib/WhoAt.d.ts +21 -0
- package/lib/index.js +24 -15
- package/lib/wordcloud.d.ts +1 -0
- package/package.json +1 -1
package/lib/Analyse.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Context, Command } from 'koishi';
|
|
2
|
+
import { Config } from './index';
|
|
3
|
+
export interface WordCloudData {
|
|
4
|
+
title: string;
|
|
5
|
+
time: Date;
|
|
6
|
+
words: [string, number][];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* @class Analyse
|
|
10
|
+
* @description 提供文本分析功能,如生成词云。
|
|
11
|
+
*/
|
|
12
|
+
export declare class Analyse {
|
|
13
|
+
private ctx;
|
|
14
|
+
private config;
|
|
15
|
+
private renderer;
|
|
16
|
+
private readonly jieba;
|
|
17
|
+
constructor(ctx: Context, config: Config);
|
|
18
|
+
/**
|
|
19
|
+
* @public @method registerCommands
|
|
20
|
+
* @description 在主命令下注册子命令。
|
|
21
|
+
* @param cmd - 主命令实例。
|
|
22
|
+
*/
|
|
23
|
+
registerCommands(cmd: Command): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from './index';
|
|
3
|
+
declare module 'koishi' {
|
|
4
|
+
interface Tables {
|
|
5
|
+
analyse_user: {
|
|
6
|
+
uid: number;
|
|
7
|
+
channelId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
channelName: string;
|
|
10
|
+
userName: string;
|
|
11
|
+
};
|
|
12
|
+
analyse_cmd: {
|
|
13
|
+
uid: number;
|
|
14
|
+
command: string;
|
|
15
|
+
count: number;
|
|
16
|
+
timestamp: Date;
|
|
17
|
+
};
|
|
18
|
+
analyse_msg: {
|
|
19
|
+
uid: number;
|
|
20
|
+
type: string;
|
|
21
|
+
count: number;
|
|
22
|
+
timestamp: Date;
|
|
23
|
+
};
|
|
24
|
+
analyse_rank: {
|
|
25
|
+
uid: number;
|
|
26
|
+
type: string;
|
|
27
|
+
count: number;
|
|
28
|
+
timestamp: Date;
|
|
29
|
+
};
|
|
30
|
+
analyse_cache: {
|
|
31
|
+
id: number;
|
|
32
|
+
uid: number;
|
|
33
|
+
content: string;
|
|
34
|
+
timestamp: Date;
|
|
35
|
+
};
|
|
36
|
+
analyse_at: {
|
|
37
|
+
id: number;
|
|
38
|
+
uid: number;
|
|
39
|
+
target: string;
|
|
40
|
+
content: string;
|
|
41
|
+
timestamp: Date;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @class Collector
|
|
47
|
+
* @description 核心数据收集器。根据配置,高效地监听、收集、缓冲并持久化聊天数据。
|
|
48
|
+
*/
|
|
49
|
+
export declare class Collector {
|
|
50
|
+
private ctx;
|
|
51
|
+
private config;
|
|
52
|
+
private static readonly FLUSH_INTERVAL;
|
|
53
|
+
private static readonly BUFFER_THRESHOLD;
|
|
54
|
+
private msgStatBuffer;
|
|
55
|
+
private rankStatBuffer;
|
|
56
|
+
private cmdStatBuffer;
|
|
57
|
+
private oriCacheBuffer;
|
|
58
|
+
private whoAtBuffer;
|
|
59
|
+
private userCache;
|
|
60
|
+
private channelCache;
|
|
61
|
+
private pendingRequests;
|
|
62
|
+
private flushInterval;
|
|
63
|
+
/**
|
|
64
|
+
* @param ctx - Koishi 的插件上下文。
|
|
65
|
+
* @param config - 插件的配置对象。
|
|
66
|
+
*/
|
|
67
|
+
constructor(ctx: Context, config: Config);
|
|
68
|
+
/**
|
|
69
|
+
* @private @method onMessage
|
|
70
|
+
* @description 统一的消息事件处理器,解析消息并更新各类统计数据的缓冲区。
|
|
71
|
+
* @param session - Koishi 的会话对象。
|
|
72
|
+
*/
|
|
73
|
+
private onMessage;
|
|
74
|
+
/**
|
|
75
|
+
* @private @method sanitizeContent
|
|
76
|
+
* @description 将 Koishi 消息元素数组净化为纯文本字符串。
|
|
77
|
+
* @param elements - 消息元素数组。
|
|
78
|
+
* @returns 净化后的纯文本。
|
|
79
|
+
*/
|
|
80
|
+
private sanitizeContent;
|
|
81
|
+
/**
|
|
82
|
+
* @private @method flushBuffers
|
|
83
|
+
* @description 将所有内存中的数据缓冲区批量写入数据库,并清空缓冲区。
|
|
84
|
+
*/
|
|
85
|
+
private flushBuffers;
|
|
86
|
+
}
|
package/lib/WhoAt.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context, Command } from 'koishi';
|
|
2
|
+
import { Config } from './index';
|
|
3
|
+
/**
|
|
4
|
+
* @class WhoAt
|
|
5
|
+
* @description 负责处理谁提及我相关功能,包括查询和定时清理。
|
|
6
|
+
*/
|
|
7
|
+
export declare class WhoAt {
|
|
8
|
+
private ctx;
|
|
9
|
+
private config;
|
|
10
|
+
/**
|
|
11
|
+
* @param ctx - Koishi 的插件上下文。
|
|
12
|
+
* @param config - 插件的配置对象。
|
|
13
|
+
*/
|
|
14
|
+
constructor(ctx: Context, config: Config);
|
|
15
|
+
/**
|
|
16
|
+
* @public @method registerCommand
|
|
17
|
+
* @description 在主命令下注册子命令。
|
|
18
|
+
* @param cmd - 主命令实例。
|
|
19
|
+
*/
|
|
20
|
+
registerCommand(cmd: Command): void;
|
|
21
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -95,9 +95,10 @@ var Collector = class _Collector {
|
|
|
95
95
|
* @param session - Koishi 的会话对象。
|
|
96
96
|
*/
|
|
97
97
|
async onMessage(session) {
|
|
98
|
-
const { userId, channelId, content, timestamp, argv, elements, bot } = session;
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
const { userId, guildId, channelId, content, timestamp, argv, elements, bot } = session;
|
|
99
|
+
const effectiveChannelId = guildId || channelId;
|
|
100
|
+
if (!effectiveChannelId || !userId || !content?.trim()) return;
|
|
101
|
+
const cacheKey = `${effectiveChannelId}:${userId}`;
|
|
101
102
|
let user;
|
|
102
103
|
if (this.userCache.has(cacheKey)) {
|
|
103
104
|
user = this.userCache.get(cacheKey);
|
|
@@ -106,13 +107,19 @@ var Collector = class _Collector {
|
|
|
106
107
|
} else {
|
|
107
108
|
const promise = (async () => {
|
|
108
109
|
try {
|
|
109
|
-
const [dbUser] = await this.ctx.database.get("analyse_user", { channelId, userId });
|
|
110
|
+
const [dbUser] = await this.ctx.database.get("analyse_user", { channelId: effectiveChannelId, userId });
|
|
110
111
|
const currentUserName = session.username ?? "";
|
|
111
|
-
let currentChannelName = this.channelCache.get(
|
|
112
|
+
let currentChannelName = this.channelCache.get(effectiveChannelId);
|
|
112
113
|
if (currentChannelName === void 0) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
let channelInfo = null;
|
|
115
|
+
if (bot.getGuild && typeof bot.getGuild === "function") {
|
|
116
|
+
channelInfo = await bot.getGuild(effectiveChannelId).catch(() => null);
|
|
117
|
+
}
|
|
118
|
+
if (!channelInfo && bot.getChannel && typeof bot.getChannel === "function") {
|
|
119
|
+
channelInfo = await bot.getChannel(effectiveChannelId).catch(() => null);
|
|
120
|
+
}
|
|
121
|
+
currentChannelName = channelInfo?.name ?? "";
|
|
122
|
+
if (currentChannelName) this.channelCache.set(effectiveChannelId, currentChannelName);
|
|
116
123
|
}
|
|
117
124
|
if (dbUser) {
|
|
118
125
|
if (currentUserName && dbUser.userName !== currentUserName || currentChannelName && dbUser.channelName !== currentChannelName) {
|
|
@@ -124,7 +131,7 @@ var Collector = class _Collector {
|
|
|
124
131
|
this.userCache.set(cacheKey, cacheData2);
|
|
125
132
|
return cacheData2;
|
|
126
133
|
}
|
|
127
|
-
const createdUser = await this.ctx.database.create("analyse_user", { channelId, userId, userName: currentUserName, channelName: currentChannelName });
|
|
134
|
+
const createdUser = await this.ctx.database.create("analyse_user", { channelId: effectiveChannelId, userId, userName: currentUserName, channelName: currentChannelName });
|
|
128
135
|
const cacheData = { uid: createdUser.uid, userName: createdUser.userName };
|
|
129
136
|
this.userCache.set(cacheKey, cacheData);
|
|
130
137
|
return cacheData;
|
|
@@ -1861,7 +1868,7 @@ var Stat = class {
|
|
|
1861
1868
|
if (options.all) return { uids: void 0, scopeDesc };
|
|
1862
1869
|
if (options.user) scopeDesc.userId = import_koishi3.h.select(options.user, "at")[0]?.attrs.id ?? options.user.trim();
|
|
1863
1870
|
if (options.guild) scopeDesc.guildId = options.guild;
|
|
1864
|
-
if (!scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId;
|
|
1871
|
+
if (!scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId || session.channelId;
|
|
1865
1872
|
if (!scopeDesc.guildId && !scopeDesc.userId) return { error: "请指定查询范围", scopeDesc };
|
|
1866
1873
|
if (scopeDesc.guildId) query.channelId = scopeDesc.guildId;
|
|
1867
1874
|
if (scopeDesc.userId) query.userId = scopeDesc.userId;
|
|
@@ -2286,9 +2293,10 @@ var Data = class {
|
|
|
2286
2293
|
if (time && isNaN(until.getTime())) return "时间格式无效";
|
|
2287
2294
|
try {
|
|
2288
2295
|
const userQuery = {};
|
|
2296
|
+
const effectiveChannelId = session.guildId || session.channelId;
|
|
2289
2297
|
if (!options.guild && !options.user) {
|
|
2290
|
-
if (!
|
|
2291
|
-
userQuery.channelId =
|
|
2298
|
+
if (!effectiveChannelId) return "请指定查询范围";
|
|
2299
|
+
userQuery.channelId = effectiveChannelId;
|
|
2292
2300
|
} else {
|
|
2293
2301
|
if (options.guild) userQuery.channelId = options.guild;
|
|
2294
2302
|
if (options.user) userQuery.userId = import_koishi5.Element.select(options.user, "at")[0]?.attrs.id ?? options.user;
|
|
@@ -2414,9 +2422,10 @@ var Analyse = class {
|
|
|
2414
2422
|
}
|
|
2415
2423
|
if (this.config.enableSimilarActivity) {
|
|
2416
2424
|
cmd.subcommand("simiactive", "相似活跃分析").usage("分析你和群友的活跃规律,找出谁和你的作息最相似。").option("hours", "-n <hours:number> 指定时长", { fallback: 24 }).option("separate", "-p 分时分析").action(async ({ session, options }) => {
|
|
2417
|
-
|
|
2425
|
+
const effectiveChannelId = session.guildId || session.channelId;
|
|
2426
|
+
if (!effectiveChannelId) return "请在群组中使用此命令";
|
|
2418
2427
|
try {
|
|
2419
|
-
const guildUsers = await this.ctx.database.get("analyse_user", { channelId:
|
|
2428
|
+
const guildUsers = await this.ctx.database.get("analyse_user", { channelId: effectiveChannelId });
|
|
2420
2429
|
if (guildUsers.length < 2) return "暂无用户数据";
|
|
2421
2430
|
const selfUser = guildUsers.find((u) => u.userId === session.userId);
|
|
2422
2431
|
const guildUserUids = guildUsers.map((u) => u.uid);
|
|
@@ -2523,7 +2532,7 @@ var Config3 = import_koishi7.Schema.intersect([
|
|
|
2523
2532
|
async function parseQueryScope(ctx, session, options) {
|
|
2524
2533
|
const scopeDesc = { guildId: options.guild, userId: void 0 };
|
|
2525
2534
|
if (options.user) scopeDesc.userId = import_koishi7.h.select(options.user, "at")[0]?.attrs.id ?? options.user.trim();
|
|
2526
|
-
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId;
|
|
2535
|
+
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) scopeDesc.guildId = session.guildId || session.channelId;
|
|
2527
2536
|
if (!options.all && !scopeDesc.guildId && !scopeDesc.userId) return { error: "请指定查询范围", scopeDesc };
|
|
2528
2537
|
const query = {};
|
|
2529
2538
|
if (scopeDesc.guildId) query.channelId = scopeDesc.guildId;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const wordCloudScript = "\n/*!\n * wordcloud2.js\n * http://timdream.org/wordcloud2.js/\n *\n * Copyright 2011 - 2019 Tim Guan-tin Chien and contributors.\n * Released under the MIT license\n */\n\n'use strict'\n\n// setImmediate\nif (!window.setImmediate) {\n window.setImmediate = (function setupSetImmediate () {\n return window.msSetImmediate ||\n window.webkitSetImmediate ||\n window.mozSetImmediate ||\n window.oSetImmediate ||\n (function setupSetZeroTimeout () {\n if (!window.postMessage || !window.addEventListener) {\n return null\n }\n\n var callbacks = [undefined]\n var message = 'zero-timeout-message'\n\n // Like setTimeout, but only takes a function argument. There's\n // no time argument (always zero) and no arguments (you have to\n // use a closure).\n var setZeroTimeout = function setZeroTimeout (callback) {\n var id = callbacks.length\n callbacks.push(callback)\n window.postMessage(message + id.toString(36), '*')\n\n return id\n }\n\n window.addEventListener('message', function setZeroTimeoutMessage (evt) {\n // Skipping checking event source, retarded IE confused this window\n // object with another in the presence of iframe\n if (typeof evt.data !== 'string' ||\n evt.data.substr(0, message.length) !== message/* ||\n evt.source !== window */) {\n return\n }\n\n evt.stopImmediatePropagation()\n\n var id = parseInt(evt.data.substr(message.length), 36)\n if (!callbacks[id]) {\n return\n }\n\n callbacks[id]()\n callbacks[id] = undefined\n }, true)\n\n /* specify clearImmediate() here since we need the scope */\n window.clearImmediate = function clearZeroTimeout (id) {\n if (!callbacks[id]) {\n return\n }\n\n callbacks[id] = undefined\n }\n\n return setZeroTimeout\n })() ||\n // fallback\n function setImmediateFallback (fn) {\n window.setTimeout(fn, 0)\n }\n })()\n}\n\nif (!window.clearImmediate) {\n window.clearImmediate = (function setupClearImmediate () {\n return window.msClearImmediate ||\n window.webkitClearImmediate ||\n window.mozClearImmediate ||\n window.oClearImmediate ||\n // \"clearZeroTimeout\" is implement on the previous block ||\n // fallback\n function clearImmediateFallback (timer) {\n window.clearTimeout(timer)\n }\n })()\n}\n\n(function (global) {\n // Check if WordCloud can run on this browser\n var isSupported = (function isSupported () {\n var canvas = document.createElement('canvas')\n if (!canvas || !canvas.getContext) {\n return false\n }\n\n var ctx = canvas.getContext('2d')\n if (!ctx) {\n return false\n }\n if (!ctx.getImageData) {\n return false\n }\n if (!ctx.fillText) {\n return false\n }\n\n if (!Array.prototype.some) {\n return false\n }\n if (!Array.prototype.push) {\n return false\n }\n\n return true\n }())\n\n // Find out if the browser impose minium font size by\n // drawing small texts on a canvas and measure it's width.\n var minFontSize = (function getMinFontSize () {\n if (!isSupported) {\n return\n }\n\n var ctx = document.createElement('canvas').getContext('2d')\n\n // start from 20\n var size = 20\n\n // two sizes to measure\n var hanWidth, mWidth\n\n while (size) {\n ctx.font = size.toString(10) + 'px sans-serif'\n if ((ctx.measureText('\uFF37').width === hanWidth) &&\n (ctx.measureText('m').width) === mWidth) {\n return (size + 1)\n }\n\n hanWidth = ctx.measureText('\uFF37').width\n mWidth = ctx.measureText('m').width\n\n size--\n }\n\n return 0\n })()\n\n var getItemExtraData = function (item) {\n if (Array.isArray(item)) {\n var itemCopy = item.slice()\n // remove data we already have (word and weight)\n itemCopy.splice(0, 2)\n return itemCopy\n } else {\n return []\n }\n }\n\n // Based on http://jsfromhell.com/array/shuffle\n var shuffleArray = function shuffleArray (arr) {\n for (var j, x, i = arr.length; i;) {\n j = Math.floor(Math.random() * i)\n x = arr[--i]\n arr[i] = arr[j]\n arr[j] = x\n }\n return arr\n }\n\n var timer = {};\n var WordCloud = function WordCloud (elements, options) {\n if (!isSupported) {\n return\n }\n\n var timerId = Math.floor(Math.random() * Date.now())\n\n if (!Array.isArray(elements)) {\n elements = [elements]\n }\n\n elements.forEach(function (el, i) {\n if (typeof el === 'string') {\n elements[i] = document.getElementById(el)\n if (!elements[i]) {\n throw new Error('The element id specified is not found.')\n }\n } else if (!el.tagName && !el.appendChild) {\n throw new Error('You must pass valid HTML elements, or ID of the element.')\n }\n })\n\n /* Default values to be overwritten by options object */\n var settings = {\n list: [],\n fontFamily: '\"Trebuchet MS\", \"Heiti TC\", \"\u5FAE\u8EDF\u6B63\u9ED1\u9AD4\", ' +\n '\"Arial Unicode MS\", \"Droid Fallback Sans\", sans-serif',\n fontWeight: 'normal',\n color: 'random-dark',\n minSize: 0, // 0 to disable\n weightFactor: 1,\n clearCanvas: true,\n backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1)\n\n gridSize: 8,\n drawOutOfBound: false,\n shrinkToFit: false,\n origin: null,\n\n drawMask: false,\n maskColor: 'rgba(255,0,0,0.3)',\n maskGapWidth: 0.3,\n\n wait: 0,\n abortThreshold: 0, // disabled\n abort: function noop () {},\n\n minRotation: -Math.PI / 2,\n maxRotation: Math.PI / 2,\n rotationSteps: 0,\n\n shuffle: true,\n rotateRatio: 0.1,\n\n shape: 'circle',\n ellipticity: 0.65,\n\n classes: null,\n\n hover: null,\n click: null\n }\n\n if (options) {\n for (var key in options) {\n if (key in settings) {\n settings[key] = options[key]\n }\n }\n }\n\n /* Convert weightFactor into a function */\n if (typeof settings.weightFactor !== 'function') {\n var factor = settings.weightFactor\n settings.weightFactor = function weightFactor (pt) {\n return pt * factor // in px\n }\n }\n\n /* Convert shape into a function */\n if (typeof settings.shape !== 'function') {\n switch (settings.shape) {\n case 'circle':\n /* falls through */\n default:\n // 'circle' is the default and a shortcut in the code loop.\n settings.shape = 'circle'\n break\n\n case 'cardioid':\n settings.shape = function shapeCardioid (theta) {\n return 1 - Math.sin(theta)\n }\n break\n\n /*\n To work out an X-gon, one has to calculate \"m\",\n where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0))\n http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28\n 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29\n Copy the solution into polar equation r = 1/(cos(t') + m*sin(t'))\n where t' equals to mod(t, 2PI/X)\n */\n\n case 'diamond':\n // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+\n // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D\n // +0+..+2*PI\n settings.shape = function shapeSquare (theta) {\n var thetaPrime = theta % (2 * Math.PI / 4)\n return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime))\n }\n break\n\n case 'square':\n // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t\n // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI\n settings.shape = function shapeSquare (theta) {\n return Math.min(\n 1 / Math.abs(Math.cos(theta)),\n 1 / Math.abs(Math.sin(theta))\n )\n }\n break\n\n case 'triangle-forward':\n // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+\n // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29\n // %29%29%2C+t+%3D+0+..+2*PI\n settings.shape = function shapeTriangle (theta) {\n var thetaPrime = theta % (2 * Math.PI / 3)\n return 1 / (Math.cos(thetaPrime) +\n Math.sqrt(3) * Math.sin(thetaPrime))\n }\n break\n\n case 'triangle':\n case 'triangle-upright':\n settings.shape = function shapeTriangle (theta) {\n var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3)\n return 1 / (Math.cos(thetaPrime) +\n Math.sqrt(3) * Math.sin(thetaPrime))\n }\n break\n\n case 'pentagon':\n settings.shape = function shapePentagon (theta) {\n var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5)\n return 1 / (Math.cos(thetaPrime) +\n 0.726543 * Math.sin(thetaPrime))\n }\n break\n\n case 'star':\n settings.shape = function shapeStar (theta) {\n var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10)\n if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) {\n return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) +\n 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime))\n } else {\n return 1 / (Math.cos(thetaPrime) +\n 3.07768 * Math.sin(thetaPrime))\n }\n }\n break\n }\n }\n\n /* Make sure gridSize is a whole number and is not smaller than 4px */\n settings.gridSize = Math.max(Math.floor(settings.gridSize), 4)\n\n /* shorthand */\n var g = settings.gridSize\n var maskRectWidth = g - settings.maskGapWidth\n\n /* normalize rotation settings */\n var rotationRange = Math.abs(settings.maxRotation - settings.minRotation)\n var rotationSteps = Math.abs(Math.floor(settings.rotationSteps))\n var minRotation = Math.min(settings.maxRotation, settings.minRotation)\n\n /* information/object available to all functions, set when start() */\n var grid, // 2d array containing filling information\n ngx, ngy, // width and height of the grid\n center, // position of the center of the cloud\n maxRadius\n\n /* timestamp for measuring each putWord() action */\n var escapeTime\n\n /* function for getting the color of the text */\n var getTextColor\n function randomHslColor (min, max) {\n return 'hsl(' +\n (Math.random() * 360).toFixed() + ',' +\n (Math.random() * 30 + 70).toFixed() + '%,' +\n (Math.random() * (max - min) + min).toFixed() + '%)'\n }\n switch (settings.color) {\n case 'random-dark':\n getTextColor = function getRandomDarkColor () {\n return randomHslColor(10, 50)\n }\n break\n\n case 'random-light':\n getTextColor = function getRandomLightColor () {\n return randomHslColor(50, 90)\n }\n break\n\n default:\n if (typeof settings.color === 'function') {\n getTextColor = settings.color\n }\n break\n }\n\n /* function for getting the font-weight of the text */\n var getTextFontWeight\n if (typeof settings.fontWeight === 'function') {\n getTextFontWeight = settings.fontWeight\n }\n\n /* function for getting the classes of the text */\n var getTextClasses = null\n if (typeof settings.classes === 'function') {\n getTextClasses = settings.classes\n }\n\n /* Interactive */\n var interactive = false\n var infoGrid = []\n var hovered\n\n var getInfoGridFromMouseTouchEvent =\n function getInfoGridFromMouseTouchEvent (evt) {\n var canvas = evt.currentTarget\n var rect = canvas.getBoundingClientRect()\n var clientX\n var clientY\n /** Detect if touches are available */\n if (evt.touches) {\n clientX = evt.touches[0].clientX\n clientY = evt.touches[0].clientY\n } else {\n clientX = evt.clientX\n clientY = evt.clientY\n }\n var eventX = clientX - rect.left\n var eventY = clientY - rect.top\n\n var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g)\n var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g)\n\n return infoGrid[x][y]\n }\n\n var wordcloudhover = function wordcloudhover (evt) {\n var info = getInfoGridFromMouseTouchEvent(evt)\n\n if (hovered === info) {\n return\n }\n\n hovered = info\n if (!info) {\n settings.hover(undefined, undefined, evt)\n\n return\n }\n\n settings.hover(info.item, info.dimension, evt)\n }\n\n var wordcloudclick = function wordcloudclick (evt) {\n var info = getInfoGridFromMouseTouchEvent(evt)\n if (!info) {\n return\n }\n\n settings.click(info.item, info.dimension, evt)\n evt.preventDefault()\n }\n\n /* Get points on the grid for a given radius away from the center */\n var pointsAtRadius = []\n var getPointsAtRadius = function getPointsAtRadius (radius) {\n if (pointsAtRadius[radius]) {\n return pointsAtRadius[radius]\n }\n\n // Look for these number of points on each radius\n var T = radius * 8\n\n // Getting all the points at this radius\n var t = T\n var points = []\n\n if (radius === 0) {\n points.push([center[0], center[1], 0])\n }\n\n while (t--) {\n // distort the radius to put the cloud in shape\n var rx = 1\n if (settings.shape !== 'circle') {\n rx = settings.shape(t / T * 2 * Math.PI) // 0 to 1\n }\n\n // Push [x, y, t] t is used solely for getTextColor()\n points.push([\n center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI),\n center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) *\n settings.ellipticity,\n t / T * 2 * Math.PI])\n }\n\n pointsAtRadius[radius] = points\n return points\n }\n\n /* Return true if we had spent too much time */\n var exceedTime = function exceedTime () {\n return ((settings.abortThreshold > 0) &&\n ((new Date()).getTime() - escapeTime > settings.abortThreshold))\n }\n\n /* Get the deg of rotation according to settings, and luck. */\n var getRotateDeg = function getRotateDeg () {\n if (settings.rotateRatio === 0) {\n return 0\n }\n\n if (Math.random() > settings.rotateRatio) {\n return 0\n }\n\n if (rotationRange === 0) {\n return minRotation\n }\n\n if (rotationSteps > 0) {\n // Min rotation + zero or more steps * span of one step\n return minRotation +\n Math.floor(Math.random() * rotationSteps) *\n rotationRange / (rotationSteps - 1)\n } else {\n return minRotation + Math.random() * rotationRange\n }\n }\n\n var getTextInfo = function getTextInfo (word, weight, rotateDeg, extraDataArray) {\n // calculate the acutal font size\n // fontSize === 0 means weightFactor function wants the text skipped,\n // and size < minSize means we cannot draw the text.\n var debug = false\n var fontSize = settings.weightFactor(weight)\n if (fontSize <= settings.minSize) {\n return false\n }\n\n // Scale factor here is to make sure fillText is not limited by\n // the minium font size set by browser.\n // It will always be 1 or 2n.\n var mu = 1\n if (fontSize < minFontSize) {\n mu = (function calculateScaleFactor () {\n var mu = 2\n while (mu * fontSize < minFontSize) {\n mu += 2\n }\n return mu\n })()\n }\n\n // Get fontWeight that will be used to set fctx.font\n var fontWeight\n if (getTextFontWeight) {\n fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray)\n } else {\n fontWeight = settings.fontWeight\n }\n\n var fcanvas = document.createElement('canvas')\n var fctx = fcanvas.getContext('2d', { willReadFrequently: true })\n\n fctx.font = fontWeight + ' ' +\n (fontSize * mu).toString(10) + 'px ' + settings.fontFamily\n\n // Estimate the dimension of the text with measureText().\n var fw = fctx.measureText(word).width / mu\n var fh = Math.max(fontSize * mu,\n fctx.measureText('m').width,\n fctx.measureText('\uFF37').width\n ) / mu\n\n // Create a boundary box that is larger than our estimates,\n // so text don't get cut of (it sill might)\n var boxWidth = fw + fh * 2\n var boxHeight = fh * 3\n var fgw = Math.ceil(boxWidth / g)\n var fgh = Math.ceil(boxHeight / g)\n boxWidth = fgw * g\n boxHeight = fgh * g\n\n // Calculate the proper offsets to make the text centered at\n // the preferred position.\n\n // This is simply half of the width.\n var fillTextOffsetX = -fw / 2\n // Instead of moving the box to the exact middle of the preferred\n // position, for Y-offset we move 0.4 instead, so Latin alphabets look\n // vertical centered.\n var fillTextOffsetY = -fh * 0.4\n\n // Calculate the actual dimension of the canvas, considering the rotation.\n var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) +\n boxHeight * Math.abs(Math.cos(rotateDeg))) / g)\n var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) +\n boxHeight * Math.abs(Math.sin(rotateDeg))) / g)\n var width = cgw * g\n var height = cgh * g\n\n fcanvas.setAttribute('width', width)\n fcanvas.setAttribute('height', height)\n\n if (debug) {\n // Attach fcanvas to the DOM\n document.body.appendChild(fcanvas)\n // Save it's state so that we could restore and draw the grid correctly.\n fctx.save()\n }\n\n // Scale the canvas with |mu|.\n fctx.scale(1 / mu, 1 / mu)\n fctx.translate(width * mu / 2, height * mu / 2)\n fctx.rotate(-rotateDeg)\n\n // Once the width/height is set, ctx info will be reset.\n // Set it again here.\n fctx.font = fontWeight + ' ' +\n (fontSize * mu).toString(10) + 'px ' + settings.fontFamily\n\n // Fill the text into the fcanvas.\n // XXX: We cannot because textBaseline = 'top' here because\n // Firefox and Chrome uses different default line-height for canvas.\n // Please read https://bugzil.la/737852#c6.\n // Here, we use textBaseline = 'middle' and draw the text at exactly\n // 0.5 * fontSize lower.\n fctx.fillStyle = '#000'\n fctx.textBaseline = 'middle'\n fctx.fillText(\n word, fillTextOffsetX * mu,\n (fillTextOffsetY + fontSize * 0.5) * mu\n )\n\n // Get the pixels of the text\n var imageData = fctx.getImageData(0, 0, width, height).data\n\n if (exceedTime()) {\n return false\n }\n\n if (debug) {\n // Draw the box of the original estimation\n fctx.strokeRect(\n fillTextOffsetX * mu,\n fillTextOffsetY, fw * mu, fh * mu\n )\n fctx.restore()\n }\n\n // Read the pixels and save the information to the occupied array\n var occupied = []\n var gx = cgw\n var gy, x, y\n var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]\n while (gx--) {\n gy = cgh\n while (gy--) {\n y = g\n /* eslint no-labels: [\"error\", { \"allowLoop\": true }] */\n singleGridLoop: while (y--) {\n x = g\n while (x--) {\n if (imageData[((gy * g + y) * width +\n (gx * g + x)) * 4 + 3]) {\n occupied.push([gx, gy])\n\n if (gx < bounds[3]) {\n bounds[3] = gx\n }\n if (gx > bounds[1]) {\n bounds[1] = gx\n }\n if (gy < bounds[0]) {\n bounds[0] = gy\n }\n if (gy > bounds[2]) {\n bounds[2] = gy\n }\n\n if (debug) {\n fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'\n fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5)\n }\n break singleGridLoop\n }\n }\n }\n if (debug) {\n fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'\n fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5)\n }\n }\n }\n\n if (debug) {\n fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'\n fctx.fillRect(\n bounds[3] * g,\n bounds[0] * g,\n (bounds[1] - bounds[3] + 1) * g,\n (bounds[2] - bounds[0] + 1) * g\n )\n }\n\n // Return information needed to create the text on the real canvas\n return {\n mu: mu,\n occupied: occupied,\n bounds: bounds,\n gw: cgw,\n gh: cgh,\n fillTextOffsetX: fillTextOffsetX,\n fillTextOffsetY: fillTextOffsetY,\n fillTextWidth: fw,\n fillTextHeight: fh,\n fontSize: fontSize\n }\n }\n\n /* Determine if there is room available in the given dimension */\n var canFitText = function canFitText (gx, gy, gw, gh, occupied) {\n // Go through the occupied points,\n // return false if the space is not available.\n var i = occupied.length\n while (i--) {\n var px = gx + occupied[i][0]\n var py = gy + occupied[i][1]\n\n if (px >= ngx || py >= ngy || px < 0 || py < 0) {\n if (!settings.drawOutOfBound) {\n return false\n }\n continue\n }\n\n if (!grid[px][py]) {\n return false\n }\n }\n return true\n }\n\n /* Actually draw the text on the grid */\n var drawText = function drawText (gx, gy, info, word, weight, distance, theta, rotateDeg, attributes, extraDataArray) {\n var fontSize = info.fontSize\n var color\n if (getTextColor) {\n color = getTextColor(word, weight, fontSize, distance, theta, extraDataArray)\n } else {\n color = settings.color\n }\n\n // get fontWeight that will be used to set ctx.font and font style rule\n var fontWeight\n if (getTextFontWeight) {\n fontWeight = getTextFontWeight(word, weight, fontSize, extraDataArray)\n } else {\n fontWeight = settings.fontWeight\n }\n\n var classes\n if (getTextClasses) {\n classes = getTextClasses(word, weight, fontSize, extraDataArray)\n } else {\n classes = settings.classes\n }\n\n elements.forEach(function (el) {\n if (el.getContext) {\n var ctx = el.getContext('2d')\n var mu = info.mu\n\n // Save the current state before messing it\n ctx.save()\n ctx.scale(1 / mu, 1 / mu)\n\n ctx.font = fontWeight + ' ' +\n (fontSize * mu).toString(10) + 'px ' + settings.fontFamily\n ctx.fillStyle = color\n\n // Translate the canvas position to the origin coordinate of where\n // the text should be put.\n ctx.translate(\n (gx + info.gw / 2) * g * mu,\n (gy + info.gh / 2) * g * mu\n )\n\n if (rotateDeg !== 0) {\n ctx.rotate(-rotateDeg)\n }\n\n // Finally, fill the text.\n\n // XXX: We cannot because textBaseline = 'top' here because\n // Firefox and Chrome uses different default line-height for canvas.\n // Please read https://bugzil.la/737852#c6.\n // Here, we use textBaseline = 'middle' and draw the text at exactly\n // 0.5 * fontSize lower.\n ctx.textBaseline = 'middle'\n ctx.fillText(\n word, info.fillTextOffsetX * mu,\n (info.fillTextOffsetY + fontSize * 0.5) * mu\n )\n\n // The below box is always matches how <span>s are positioned\n /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY,\n info.fillTextWidth, info.fillTextHeight) */\n\n // Restore the state.\n ctx.restore()\n } else {\n // drawText on DIV element\n var span = document.createElement('span')\n var transformRule = ''\n transformRule = 'rotate(' + (-rotateDeg / Math.PI * 180) + 'deg) '\n if (info.mu !== 1) {\n transformRule +=\n 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' +\n 'scale(' + (1 / info.mu) + ')'\n }\n var styleRules = {\n position: 'absolute',\n display: 'block',\n font: fontWeight + ' ' +\n (fontSize * info.mu) + 'px ' + settings.fontFamily,\n left: ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px',\n top: ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px',\n width: info.fillTextWidth + 'px',\n height: info.fillTextHeight + 'px',\n lineHeight: fontSize + 'px',\n whiteSpace: 'nowrap',\n transform: transformRule,\n webkitTransform: transformRule,\n msTransform: transformRule,\n transformOrigin: '50% 40%',\n webkitTransformOrigin: '50% 40%',\n msTransformOrigin: '50% 40%'\n }\n if (color) {\n styleRules.color = color\n }\n span.textContent = word\n for (var cssProp in styleRules) {\n span.style[cssProp] = styleRules[cssProp]\n }\n if (attributes) {\n for (var attribute in attributes) {\n span.setAttribute(attribute, attributes[attribute])\n }\n }\n if (classes) {\n span.className += classes\n }\n el.appendChild(span)\n }\n })\n }\n\n /* Help function to updateGrid */\n var fillGridAt = function fillGridAt (x, y, drawMask, dimension, item) {\n if (x >= ngx || y >= ngy || x < 0 || y < 0) {\n return\n }\n\n grid[x][y] = false\n\n if (drawMask) {\n var ctx = elements[0].getContext('2d')\n ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth)\n }\n\n if (interactive) {\n infoGrid[x][y] = { item: item, dimension: dimension }\n }\n }\n\n /* Update the filling information of the given space with occupied points.\n Draw the mask on the canvas if necessary. */\n var updateGrid = function updateGrid (gx, gy, gw, gh, info, item) {\n var occupied = info.occupied\n var drawMask = settings.drawMask\n var ctx\n if (drawMask) {\n ctx = elements[0].getContext('2d')\n ctx.save()\n ctx.fillStyle = settings.maskColor\n }\n\n var dimension\n if (interactive) {\n var bounds = info.bounds\n dimension = {\n x: (gx + bounds[3]) * g,\n y: (gy + bounds[0]) * g,\n w: (bounds[1] - bounds[3] + 1) * g,\n h: (bounds[2] - bounds[0] + 1) * g\n }\n }\n\n var i = occupied.length\n while (i--) {\n var px = gx + occupied[i][0]\n var py = gy + occupied[i][1]\n\n if (px >= ngx || py >= ngy || px < 0 || py < 0) {\n continue\n }\n\n fillGridAt(px, py, drawMask, dimension, item)\n }\n\n if (drawMask) {\n ctx.restore()\n }\n }\n\n /* putWord() processes each item on the list,\n calculate it's size and determine it's position, and actually\n put it on the canvas. */\n var putWord = function putWord (item) {\n var word, weight, attributes\n if (Array.isArray(item)) {\n word = item[0]\n weight = item[1]\n } else {\n word = item.word\n weight = item.weight\n attributes = item.attributes\n }\n var rotateDeg = getRotateDeg()\n\n var extraDataArray = getItemExtraData(item)\n\n // get info needed to put the text onto the canvas\n var info = getTextInfo(word, weight, rotateDeg, extraDataArray)\n\n // not getting the info means we shouldn't be drawing this one.\n if (!info) {\n return false\n }\n\n if (exceedTime()) {\n return false\n }\n\n // If drawOutOfBound is set to false,\n // skip the loop if we have already know the bounding box of\n // word is larger than the canvas.\n if (!settings.drawOutOfBound && !settings.shrinkToFit) {\n var bounds = info.bounds;\n if ((bounds[1] - bounds[3] + 1) > ngx ||\n (bounds[2] - bounds[0] + 1) > ngy) {\n return false\n }\n }\n\n // Determine the position to put the text by\n // start looking for the nearest points\n var r = maxRadius + 1\n\n var tryToPutWordAtPoint = function (gxy) {\n var gx = Math.floor(gxy[0] - info.gw / 2)\n var gy = Math.floor(gxy[1] - info.gh / 2)\n var gw = info.gw\n var gh = info.gh\n\n // If we cannot fit the text at this position, return false\n // and go to the next position.\n if (!canFitText(gx, gy, gw, gh, info.occupied)) {\n return false\n }\n\n // Actually put the text on the canvas\n drawText(gx, gy, info, word, weight,\n (maxRadius - r), gxy[2], rotateDeg, attributes, extraDataArray)\n\n // Mark the spaces on the grid as filled\n updateGrid(gx, gy, gw, gh, info, item)\n\n // Return true so some() will stop and also return true.\n return true\n }\n\n while (r--) {\n var points = getPointsAtRadius(maxRadius - r)\n\n if (settings.shuffle) {\n points = [].concat(points)\n shuffleArray(points)\n }\n\n // Try to fit the words by looking at each point.\n // array.some() will stop and return true\n // when putWordAtPoint() returns true.\n // If all the points returns false, array.some() returns false.\n var drawn = points.some(tryToPutWordAtPoint)\n\n if (drawn) {\n // leave putWord() and return true\n return true\n }\n }\n if (settings.shrinkToFit) {\n if (Array.isArray(item)) {\n item[1] = item[1] * 3 / 4\n } else {\n item.weight = item.weight * 3 / 4\n }\n return putWord(item)\n }\n // we tried all distances but text won't fit, return false\n return false\n }\n\n /* Send DOM event to all elements. Will stop sending event and return\n if the previous one is canceled (for cancelable events). */\n var sendEvent = function sendEvent (type, cancelable, details) {\n if (cancelable) {\n return !elements.some(function (el) {\n var event = new CustomEvent(type, {\n detail: details || {}\n })\n return !el.dispatchEvent(event)\n }, this)\n } else {\n elements.forEach(function (el) {\n var event = new CustomEvent(type, {\n detail: details || {}\n })\n el.dispatchEvent(event)\n }, this)\n }\n }\n\n /* Start drawing on a canvas */\n var start = function start () {\n // For dimensions, clearCanvas etc.,\n // we only care about the first element.\n var canvas = elements[0]\n\n if (canvas.getContext) {\n ngx = Math.ceil(canvas.width / g)\n ngy = Math.ceil(canvas.height / g)\n } else {\n var rect = canvas.getBoundingClientRect()\n ngx = Math.ceil(rect.width / g)\n ngy = Math.ceil(rect.height / g)\n }\n\n // Sending a wordcloudstart event which cause the previous loop to stop.\n // Do nothing if the event is canceled.\n if (!sendEvent('wordcloudstart', true)) {\n return\n }\n\n // Determine the center of the word cloud\n center = (settings.origin)\n ? [settings.origin[0] / g, settings.origin[1] / g]\n : [ngx / 2, ngy / 2]\n\n // Maxium radius to look for space\n maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy))\n\n /* Clear the canvas only if the clearCanvas is set,\n if not, update the grid to the current canvas state */\n grid = []\n\n var gx, gy, i\n if (!canvas.getContext || settings.clearCanvas) {\n elements.forEach(function (el) {\n if (el.getContext) {\n var ctx = el.getContext('2d')\n ctx.fillStyle = settings.backgroundColor\n ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1))\n ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1))\n } else {\n el.textContent = ''\n el.style.backgroundColor = settings.backgroundColor\n el.style.position = 'relative'\n }\n })\n\n /* fill the grid with empty state */\n gx = ngx\n while (gx--) {\n grid[gx] = []\n gy = ngy\n while (gy--) {\n grid[gx][gy] = true\n }\n }\n } else {\n /* Determine bgPixel by creating\n another canvas and fill the specified background color. */\n var bctx = document.createElement('canvas').getContext('2d')\n\n bctx.fillStyle = settings.backgroundColor\n bctx.fillRect(0, 0, 1, 1)\n var bgPixel = bctx.getImageData(0, 0, 1, 1).data\n\n /* Read back the pixels of the canvas we got to tell which part of the\n canvas is empty.\n (no clearCanvas only works with a canvas, not divs) */\n var imageData =\n canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data\n\n gx = ngx\n var x, y\n while (gx--) {\n grid[gx] = []\n gy = ngy\n while (gy--) {\n y = g\n /* eslint no-labels: [\"error\", { \"allowLoop\": true }] */\n singleGridLoop: while (y--) {\n x = g\n while (x--) {\n i = 4\n while (i--) {\n if (imageData[((gy * g + y) * ngx * g +\n (gx * g + x)) * 4 + i] !== bgPixel[i]) {\n grid[gx][gy] = false\n break singleGridLoop\n }\n }\n }\n }\n if (grid[gx][gy] !== false) {\n grid[gx][gy] = true\n }\n }\n }\n\n imageData = bctx = bgPixel = undefined\n }\n\n // fill the infoGrid with empty state if we need it\n if (settings.hover || settings.click) {\n interactive = true\n\n /* fill the grid with empty state */\n gx = ngx + 1\n while (gx--) {\n infoGrid[gx] = []\n }\n\n if (settings.hover) {\n canvas.addEventListener('mousemove', wordcloudhover)\n }\n\n if (settings.click) {\n canvas.addEventListener('click', wordcloudclick)\n canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'\n }\n\n canvas.addEventListener('wordcloudstart', function stopInteraction () {\n canvas.removeEventListener('wordcloudstart', stopInteraction)\n canvas.removeEventListener('mousemove', wordcloudhover)\n canvas.removeEventListener('click', wordcloudclick)\n hovered = undefined\n })\n }\n\n i = 0\n var loopingFunction, stoppingFunction\n if (settings.wait !== 0) {\n loopingFunction = window.setTimeout\n stoppingFunction = window.clearTimeout\n } else {\n loopingFunction = window.setImmediate\n stoppingFunction = window.clearImmediate\n }\n\n var addEventListener = function addEventListener (type, listener) {\n elements.forEach(function (el) {\n el.addEventListener(type, listener)\n }, this)\n }\n\n var removeEventListener = function removeEventListener (type, listener) {\n elements.forEach(function (el) {\n el.removeEventListener(type, listener)\n }, this)\n }\n\n var anotherWordCloudStart = function anotherWordCloudStart () {\n removeEventListener('wordcloudstart', anotherWordCloudStart)\n stoppingFunction(timer[timerId])\n }\n\n addEventListener('wordcloudstart', anotherWordCloudStart)\n timer[timerId] = loopingFunction(function loop () {\n if (i >= settings.list.length) {\n stoppingFunction(timer[timerId])\n sendEvent('wordcloudstop', false)\n removeEventListener('wordcloudstart', anotherWordCloudStart)\n delete timer[timerId];\n return\n }\n escapeTime = (new Date()).getTime()\n var drawn = putWord(settings.list[i])\n var canceled = !sendEvent('wordclouddrawn', true, {\n item: settings.list[i],\n drawn: drawn\n })\n if (exceedTime() || canceled) {\n stoppingFunction(timer[timerId])\n settings.abort()\n sendEvent('wordcloudabort', false)\n sendEvent('wordcloudstop', false)\n removeEventListener('wordcloudstart', anotherWordCloudStart)\n delete timer[timerId]\n return\n }\n i++\n timer[timerId] = loopingFunction(loop, settings.wait)\n }, settings.wait)\n }\n\n // All set, start the drawing\n start()\n }\n\n WordCloud.isSupported = isSupported\n WordCloud.minFontSize = minFontSize\n WordCloud.stop = function stop () {\n if (timer) {\n for (var timerId in timer) {\n window.clearImmediate(timer[timerId])\n }\n }\n }\n\n // Expose the library as an AMD module\n if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef\n global.WordCloud = WordCloud\n define('wordcloud', [], function () { return WordCloud }) // eslint-disable-line no-undef\n } else if (typeof module !== 'undefined' && module.exports) { // eslint-disable-line no-undef\n module.exports = WordCloud // eslint-disable-line no-undef\n } else {\n global.WordCloud = WordCloud\n }\n})(this) // jshint ignore:line\n";
|