tag2cloud
Version:
make the tags cloud
591 lines (580 loc) • 28.7 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.tag2cloud = {}));
}(this, (function (exports) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
var ZERO_STR = "00000000000000000000000000000000";
var TIMEOUT_MS = 100;
var Tag2Cloud = /** @class */ (function () {
function Tag2Cloud($container, options) {
this.defaultOptions = {
width: 200,
height: 200,
maskImage: false,
pixelRatio: 4,
lightThreshold: ((255 * 3) / 2) >> 0,
opacityThreshold: 255,
minFontSize: 10,
maxFontSize: 100,
angleFrom: -60,
angleTo: 60,
angleCount: 3,
family: "sans-serif",
cut: false,
padding: 5,
canvas: false,
shape: null
};
this.listeners = [];
this.pixels = {
width: 0,
height: 0,
data: []
};
this.maxTagWeight = 0;
this.minTagWeight = Infinity;
this.promised = Promise.resolve();
this.points = [];
this.$container = $container;
if (getComputedStyle(this.$container).position === "static") {
this.$container.style.position = "relative";
}
this.options = __assign(__assign({}, this.defaultOptions), options);
this.options.pixelRatio = Math.round(Math.max(this.options.pixelRatio, 1));
var _a = this.options, width = _a.width, height = _a.height;
this.$container.style.width = width + "px";
this.$container.style.height = height + "px";
this.$wrapper = document.createElement("div");
this.$wrapper.style.width = "0px";
this.$wrapper.style.height = "0px";
this.$canvas = document.createElement("canvas");
this.$canvas.width = width;
this.$canvas.height = height;
this.$canvas.style.display = "none";
this.$displayCanvas = document.createElement("canvas");
this.$displayCanvas.width = width;
this.$displayCanvas.height = height;
this.ctx = this.$canvas.getContext("2d");
this.ctx.textAlign = "center";
this.displayCtx = this.$displayCanvas.getContext("2d");
this.displayCtx.textAlign = "center";
this.$container.classList.add("tag2cloud");
this.$container.append(this.$canvas);
this.$container.append(this.$displayCanvas);
this.$container.append(this.$wrapper);
this.initPixels();
this.initPoints();
}
Tag2Cloud.prototype.draw = function (tags) {
if (tags === void 0) { tags = []; }
return __awaiter(this, void 0, void 0, function () {
var i, len, weight, result;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (tags.length === 0)
return [2 /*return*/, []];
return [4 /*yield*/, this.promised];
case 1:
_a.sent();
for (i = 0, len = tags.length; i < len; i++) {
weight = tags[i].weight;
if (weight > this.maxTagWeight) {
this.maxTagWeight = weight;
}
if (weight < this.minTagWeight) {
this.minTagWeight = weight;
}
}
return [4 /*yield*/, this.performDraw(tags)];
case 2:
result = _a.sent();
return [2 /*return*/, result];
}
});
});
};
Tag2Cloud.prototype.clear = function () {
var _a = this.options, width = _a.width, height = _a.height;
this.$wrapper.innerHTML = "";
this.displayCtx.clearRect(0, 0, width, height);
this.initPixels();
};
Tag2Cloud.prototype.destroy = function () {
if (this.$container) {
this.$container.innerHTML = "";
}
};
Tag2Cloud.prototype.shape = function (cb) {
var _a = this.options, width = _a.width, height = _a.height;
this.ctx.clearRect(0, 0, width, height);
this.ctx.textAlign = "left";
cb(this.ctx);
this.ctx.textAlign = "center";
var imgData = this.ctx.getImageData(0, 0, width, width);
this.pixels = this.getPixelsFromImgData(imgData, 2, 255 * 3, -1, false);
};
Tag2Cloud.prototype.onClick = function (listener) {
var _this = this;
if (listener instanceof Function) {
this.listeners.push(listener);
return function () {
_this.offClick(listener);
};
}
return function () { };
};
Tag2Cloud.prototype.offClick = function (listener) {
var index = this.listeners.indexOf(listener);
if (index !== -1) {
this.listeners.splice(index, 1);
}
};
Tag2Cloud.prototype.getCtx = function () {
return this.ctx;
};
Tag2Cloud.prototype.initPixels = function () {
var _this = this;
var _a = this.options, width = _a.width, height = _a.height, maskImage = _a.maskImage;
if (maskImage) {
var $img_1 = new Image();
this.promised = new Promise(function (resolve, reject) {
$img_1.onload = function () {
_this.pixels = _this.loadMaskImage($img_1);
resolve();
};
$img_1.onerror = reject;
});
$img_1.crossOrigin = "anonymous";
$img_1.src = maskImage;
}
else {
this.pixels = this.generatePixels(width, height, 0, false);
}
};
Tag2Cloud.prototype.initPoints = function () {
var _a = this.options, pixelRatio = _a.pixelRatio, width = _a.width, height = _a.height, shape = _a.shape;
var startX = width / 2;
var startY = height / 2;
var whRate = width / height;
var d = pixelRatio;
var theta = 0;
var l = (Math.sqrt(width * width + height * height) - 100) >> 0;
if (shape) {
var minShapeRate = 1;
while (d * minShapeRate < l) {
theta += (pixelRatio / d) * 2;
d += (pixelRatio / (d * 3)) * pixelRatio * 2;
var r = d / 2;
var shapeRate = shape(theta - Math.PI / 2);
minShapeRate = Math.max(.3, Math.min(shapeRate, minShapeRate));
var rs = r * shapeRate;
var x = (startX + Math.sin(theta) * rs * whRate) >> 0;
var y = (startY + Math.cos(theta) * rs) >> 0;
this.points.push(x);
this.points.push(y);
}
}
else {
while (d < l) {
theta += (pixelRatio / d) * 2;
d += (pixelRatio / (d * 3)) * pixelRatio * 2;
var r = d / 2;
var x = (startX + Math.sin(theta) * r * whRate) >> 0;
var y = (startY + Math.cos(theta) * r) >> 0;
this.points.push(x);
this.points.push(y);
}
}
};
Tag2Cloud.prototype.performDraw = function (tags) {
if (tags === void 0) { tags = []; }
return __awaiter(this, void 0, void 0, function () {
var sortTags, result, partial, expired, i, len, tagData, now;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
sortTags = tags.sort(function (a, b) { return b.weight - a.weight; });
result = [];
partial = [];
expired = performance.now() + TIMEOUT_MS;
i = 0, len = sortTags.length;
_a.label = 1;
case 1:
if (!(i < len)) return [3 /*break*/, 4];
tagData = this.handleTag(sortTags[i]);
now = performance.now();
result.push(tagData);
partial.push(tagData);
if (!(now > expired)) return [3 /*break*/, 3];
this.layout(partial);
partial = [];
return [4 /*yield*/, new Promise(function (r) {
setTimeout(r);
})];
case 2:
_a.sent();
expired = now + TIMEOUT_MS;
_a.label = 3;
case 3:
i++;
return [3 /*break*/, 1];
case 4:
this.layout(partial);
return [2 /*return*/, result];
}
});
});
};
Tag2Cloud.prototype.layout = function (data) {
if (this.options.canvas) {
this.layoutByCanvas(data);
}
else {
this.layoutByDom(data);
}
};
Tag2Cloud.prototype.layoutByCanvas = function (data) {
var family = this.options.family;
for (var i = 0, len = data.length; i < len; i++) {
var current = data[i];
if (!current.rendered)
continue;
var angle = current.angle, color = current.color, fontSize = current.fontSize, text = current.text, x = current.x, y = current.y;
this.displayCtx.save();
var theta = (-angle * Math.PI) / 180;
this.displayCtx.font = fontSize + "px " + family;
var textMetrics = this.displayCtx.measureText(text);
var fontBoundingBoxAscent = textMetrics.fontBoundingBoxAscent, fontBoundingBoxDescent = textMetrics.fontBoundingBoxDescent;
var height = fontBoundingBoxAscent + fontBoundingBoxDescent;
this.displayCtx.translate(x, y);
this.displayCtx.rotate(theta);
this.displayCtx.fillStyle = color;
this.displayCtx.fillText(text, 0, height / 2 - fontBoundingBoxDescent);
this.displayCtx.restore();
}
};
Tag2Cloud.prototype.layoutByDom = function (data) {
var fragment = document.createDocumentFragment();
for (var i = 0, len = data.length; i < len; i++) {
var current = data[i];
if (!current.rendered)
continue;
var $tag = document.createElement("span");
fragment.append($tag);
$tag.innerText = current.text;
$tag.style.color = current.color;
$tag.style.justifyContent = "center";
$tag.style.alignItems = "center";
$tag.style.lineHeight = "normal";
$tag.style.fontSize = current.fontSize + "px";
$tag.style.position = "absolute";
$tag.style.transform = "translate(calc(-50%), calc(-50%)) rotate(" + -current.angle + "deg)";
$tag.style.left = current.x + "px";
$tag.style.top = current.y + "px";
$tag.style.fontFamily = "" + this.options.family;
$tag.style.whiteSpace = "pre";
$tag.dataset.tag2cloud = current.text;
$tag.classList.add("tag2cloud__tag");
$tag.addEventListener("click", this.click.bind(this, current));
}
this.$wrapper.append(fragment);
};
Tag2Cloud.prototype.click = function (tagData) {
this.listeners.forEach(function (fn) {
fn(tagData);
});
};
Tag2Cloud.prototype.generatePixels = function (width, height, fill, forTag) {
if (fill === void 0) { fill = 0; }
if (forTag === void 0) { forTag = true; }
var _a = this.options, pixelRatio = _a.pixelRatio, cut = _a.cut;
var pixelXLength = Math.ceil(width / pixelRatio);
var pixelYLength = Math.ceil(height / pixelRatio);
var data = [];
var len = Math.ceil(pixelXLength / 32);
var tailOffset = pixelXLength % 32;
var tailFill = forTag || tailOffset === 0
? fill
: cut
? fill & (-1 << (32 - tailOffset))
: fill | (-1 >>> tailOffset);
for (var i = 0; i < pixelYLength; i++) {
var xData = new Array(len).fill(fill);
xData[len - 1] = tailFill;
data.push(xData);
}
return {
width: width,
height: height,
data: data
};
};
Tag2Cloud.prototype.handleTag = function (tag) {
var _a = this, minTagWeight = _a.minTagWeight, maxTagWeight = _a.maxTagWeight;
var _b = this.options, minFontSize = _b.minFontSize, maxFontSize = _b.maxFontSize, angleCount = _b.angleCount, angleFrom = _b.angleFrom, angleTo = _b.angleTo, padding = _b.padding;
var text = tag.text, weight = tag.weight, maybeAngle = tag.angle, maybeColor = tag.color;
var diffWeight = maxTagWeight - minTagWeight;
var fontSize = diffWeight > 0
? Math.round(minFontSize +
(maxFontSize - minFontSize) *
((weight - minTagWeight) / diffWeight))
: Math.round((maxFontSize + minFontSize) / 2);
var randomNum = (Math.random() * angleCount) >> 0;
var angle = maybeAngle === undefined
? angleCount === 1
? angleFrom
: angleFrom + (randomNum / (angleCount - 1)) * (angleTo - angleFrom)
: maybeAngle;
var color = maybeColor === undefined
? "#" +
(((0xffff00 * Math.random()) >> 0) + 0x1000000).toString(16).slice(1)
: maybeColor;
var pixels = this.getTagPixels({
text: text,
angle: angle,
fontSize: fontSize,
color: color,
padding: padding
});
var result = {
tag: tag,
text: text,
weight: weight,
fontSize: fontSize,
angle: angle,
color: color,
x: NaN,
y: NaN,
rendered: false
};
if (pixels === null)
return result;
var _c = this.placeTag(pixels), x = _c[0], y = _c[1];
if (!isNaN(x)) {
result.x = (x + pixels.width / 2) >> 0;
result.y = (y + pixels.height / 2) >> 0;
result.rendered = true;
this.ctx.save();
}
return result;
};
Tag2Cloud.prototype.placeTag = function (pixels) {
var _a = this.options; _a.width; _a.height; _a.pixelRatio;
var pixelsWidth = pixels.width, pixelsHeight = pixels.height;
var halfW = (pixelsWidth / 2) >> 0;
var halfH = (pixelsHeight / 2) >> 0;
for (var i = 0, len = this.points.length; i < len; i += 2) {
var _b = [this.points[i] - halfW, this.points[i + 1] - halfH], x = _b[0], y = _b[1];
if (this.tryPlaceTag(pixels, x, y)) {
return [x, y];
}
}
return [NaN, NaN];
};
Tag2Cloud.prototype.tryPlaceTag = function (pixels, x, y) {
var _a = this.options, pixelRatio = _a.pixelRatio, cut = _a.cut;
var data = pixels.data;
var thisData = this.pixels.data;
var pixelsX = Math.floor(x / pixelRatio);
var pixelsY = Math.floor(y / pixelRatio);
var offset = pixelsX % 32;
var fix = offset ? -1 : 0;
var xx = Math.floor(pixelsX / 32);
var out = cut ? 0 : -1;
for (var i = 0, len = data.length; i < len; i++) {
var yData = thisData[pixelsY + i] === undefined ? [] : thisData[pixelsY + i];
for (var j = 0, len_1 = data[i].length; j < len_1; j++) {
var current = yData[xx + j] === undefined ? out : yData[xx + j];
var next = (yData[xx + j + 1] === undefined ? out : yData[xx + j + 1]) & fix;
if (((current << offset) | (next >>> (32 - offset))) & data[i][j]) {
return false;
}
}
}
for (var i = 0, len = data.length; i < len; i++) {
var yData = thisData[pixelsY + i] === undefined ? [] : thisData[pixelsY + i];
for (var j = 0, len_2 = data[i].length; j < len_2; j++) {
var target = data[i][j];
if (yData[xx + j] !== undefined) {
yData[xx + j] |= target >>> offset;
}
if (yData[xx + j + 1] !== undefined && offset) {
yData[xx + j + 1] |= target << (32 - offset);
}
}
}
return true;
};
Tag2Cloud.prototype.getTagPixels = function (_a) {
var text = _a.text, angle = _a.angle, fontSize = _a.fontSize, color = _a.color, padding = _a.padding;
this.ctx.save();
var theta = (-angle * Math.PI) / 180;
var cosTheta = Math.cos(theta);
var sinTheta = Math.sin(theta);
this.ctx.font = fontSize + "px " + this.options.family;
var textMetrics = this.ctx.measureText(text);
var fontBoundingBoxAscent = textMetrics.fontBoundingBoxAscent, fontBoundingBoxDescent = textMetrics.fontBoundingBoxDescent, width = textMetrics.width;
var height = fontBoundingBoxAscent + fontBoundingBoxDescent;
var widthWithPadding = width + padding;
var heightWithPadding = height + padding;
var pixelWidth = (Math.abs(heightWithPadding * sinTheta) +
Math.abs(widthWithPadding * cosTheta)) >>
0;
var pixelHeight = (Math.abs(heightWithPadding * cosTheta) +
Math.abs(widthWithPadding * sinTheta)) >>
0;
if (pixelHeight > this.options.height || pixelWidth > this.options.width) {
return null;
}
this.ctx.clearRect(0, 0, pixelWidth, pixelHeight);
this.ctx.translate(pixelWidth / 2, pixelHeight / 2);
this.ctx.rotate(theta);
this.ctx.fillStyle = color;
this.ctx.lineWidth = padding;
this.ctx.strokeText(text, 0, height / 2 - fontBoundingBoxDescent);
this.ctx.fillText(text, 0, height / 2 - fontBoundingBoxDescent);
this.ctx.restore();
var imgData = this.ctx.getImageData(0, 0, pixelWidth, pixelHeight);
return this.getPixelsFromImgData(imgData, 2, 255 * 3);
};
Tag2Cloud.prototype.getPixelsFromImgData = function (imgData, opacityThreshold, lightThreshold, fill, forTag) {
if (fill === void 0) { fill = 0; }
if (forTag === void 0) { forTag = true; }
var _a = this.options, pixelRatio = _a.pixelRatio; _a.cut;
var data = imgData.data, width = imgData.width, height = imgData.height;
var pixels = this.generatePixels(width, height, fill, forTag);
var pixelsData = pixels.data;
var dataXLength = width << 2;
var pixelXLength = Math.ceil(width / pixelRatio);
var pixelYLength = Math.ceil(height / pixelRatio);
var pixelCount = pixelXLength * pixelYLength;
var pixelX = 0;
var pixelY = 0;
var edgeXLength = width % pixelRatio || pixelRatio;
var edgeYLength = height % pixelRatio || pixelRatio;
while (pixelCount--) {
var outerOffset = pixelY * pixelRatio * dataXLength + ((pixelX * pixelRatio) << 2);
var xLength = pixelX === pixelXLength - 1 ? edgeXLength : pixelRatio;
var yLength = pixelY === pixelYLength - 1 ? edgeYLength : pixelRatio;
var xIndex = (pixelX / 32) >> 0;
var y = 0;
outer: while (y < yLength) {
var x = 0;
var offset = outerOffset + y++ * dataXLength;
while (x < xLength) {
var pos = offset + (x++ << 2);
var opacity = data[pos + 3];
if (opacity < opacityThreshold) {
continue;
}
var light = data[pos] + data[pos + 1] + data[pos + 2];
if (light > lightThreshold) {
continue;
}
if (fill) {
pixelsData[pixelY][xIndex] &= ~(1 << -(pixelX + 1));
}
else {
pixelsData[pixelY][xIndex] |= 1 << -(pixelX + 1);
}
break outer;
}
}
pixelX++;
if (pixelX === pixelXLength) {
pixelX = 0;
pixelY++;
}
}
return {
width: width,
height: height,
data: pixelsData
};
};
Tag2Cloud.prototype.loadMaskImage = function ($maskImage) {
var _a = this.options, width = _a.width, height = _a.height, opacityThreshold = _a.opacityThreshold, lightThreshold = _a.lightThreshold;
this.ctx.clearRect(0, 0, width, height);
this.ctx.drawImage($maskImage, 0, 0, width, height);
var imgData = this.ctx.getImageData(0, 0, width, height);
var pixels = this.getPixelsFromImgData(imgData, opacityThreshold, lightThreshold, -1, false);
return pixels;
};
Tag2Cloud.prototype.printPixels = function (pixels) {
if (pixels === null)
return;
for (var i = 0, len = pixels.data.length; i < len; i++) {
console.log(pixels.data[i].map(this.binaryStrIfy).join("") + "_" + i);
}
};
Tag2Cloud.prototype.binaryStrIfy = function (num) {
if (num >= 0) {
var numStr = num.toString(2);
return ZERO_STR.slice(0, 32 - numStr.length) + numStr;
}
return (Math.pow(2, 32) + num).toString(2);
};
return Tag2Cloud;
}());
exports.Tag2Cloud = Tag2Cloud;
Object.defineProperty(exports, '__esModule', { value: true });
})));