From cf1690ce2af10dc1f78282ac68560dd45c5ab3f9 Mon Sep 17 00:00:00 2001 From: xiaowen <372193233@qq.com> Date: Wed, 6 Jul 2022 13:39:35 +0800 Subject: [PATCH] cxw-010203 --- components/word-cloud/WordCloud.js | 1113 ++++++++++----------------- components/word-cloud/index.js | 116 ++- components/word-cloud/index.wxml | 12 +- components/word-cloud/index.wxss | 17 +- components/word-cloud/word-cloud.js | 247 ++++++ pages/insight/index.js | 1 - 6 files changed, 776 insertions(+), 730 deletions(-) create mode 100644 components/word-cloud/word-cloud.js diff --git a/components/word-cloud/WordCloud.js b/components/word-cloud/WordCloud.js index 305db49..5affe1a 100644 --- a/components/word-cloud/WordCloud.js +++ b/components/word-cloud/WordCloud.js @@ -1,789 +1,528 @@ -/*! - * wordcloud2.js - * http://timdream.org/wordcloud2.js/ - * - * Copyright 2011 - 2013 Tim Chien - * Released under the MIT license - */ - -'use strict'; -let drawSpan = []; -// Find out if the browser impose minium font size by -// drawing small texts on a canvas and measure it's width. -function getMinFontSize(ctx) { +const DEFAULT_SETTING = { + width: 750, + height: 700, + list: [], + fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + + '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', + fontWeight: 'normal', + color: 'random-dark', + minSize: 0, // 0 to disable + weightFactor: 1, + clearCanvas: true, + backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) + + gridSize: 8, + drawOutOfBound: false, + origin: null, + + drawMask: false, + maskColor: 'rgba(255,0,0,0.3)', + maskGapWidth: 0.3, + + wait: 0, + abortThreshold: 0, // disabled + abort: function noop() { }, + + minRotation: - Math.PI / 2, + maxRotation: Math.PI / 2, + rotationSteps: 0, + + shuffle: true, + rotateRatio: 0.1, + + shape: 'circle', + ellipticity: 0.65, +}; - // start from 20 - var size = 20; +class WordCloud { - // two sizes to measure - var hanWidth, mWidth; + constructor(canvas, options) { + this.fcanvas = canvas + this.fctx = canvas.getContext('2d') + this.wordData = [] + this._initSettings(options) - while (size) { - ctx.setFontSize(size) - if ((ctx.measureText('\uFF37').width === hanWidth) && - (ctx.measureText('m').width) === mWidth) { - return (size + 1); + } + _initSettings(options) { + /* Default values to be overwritten by options object */ + this.settings = DEFAULT_SETTING + this._initOptions(options) + this._initWeightFactor() + this._initShape() + this.settings.gridSize = Math.max(Math.floor(this.settings.gridSize), 4); + this.g = this.settings.gridSize; + this.rotationRange = Math.abs(this.settings.maxRotation - this.settings.minRotation); + this.rotationSteps = Math.abs(Math.floor(this.settings.rotationSteps)); + this.minRotation = Math.min(this.settings.maxRotation, this.settings.minRotation); + this.grid = [] + this.ngx = Math.ceil(this.settings.width / this.g); + this.ngy = Math.ceil(this.settings.height / this.g); + this.center = (this.settings.origin) ? + [this.settings.origin[0] / this.g, this.settings.origin[1] / this.g] : + [this.ngx / 2, this.ngy / 2]; + this.maxRadius = Math.floor(Math.sqrt(this.ngx * this.ngx + this.ngy * this.ngy)); + + this.escapeTime = null + this.getTextColor = null + this._initColor() + this.getTextFontWeight = null; + if (typeof this.settings.fontWeight === 'function') { + this.getTextFontWeight = this.settings.fontWeight; } - - hanWidth = ctx.measureText('\uFF37').width; - mWidth = ctx.measureText('m').width; - - size--; + this.pointsAtRadius = []; + this.minFontSize = 0 + this._initGrid() } - - return 0; -}; - -// Based on http://jsfromhell.com/array/shuffle -var shuffleArray = function shuffleArray(arr) { - for (var j, x, i = arr.length; i; j = Math.floor(Math.random() * i), - x = arr[--i], arr[i] = arr[j], - arr[j] = x) {} - return arr; -}; - -function WordCloud(ctx, mycanvas, options, self, callback) { - - var minFontSize = getMinFontSize(ctx) - - /* Default values to be overwritten by options object */ - var settings = { - list: [], - fontFamily: '"Trebuchet MS", "Heiti TC", "微軟正黑體", ' + - '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', - fontWeight: 'normal', - color: 'random-dark', - minSize: 0, // 0 to disable - weightFactor: 1, - clearCanvas: true, - backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) - - gridSize: 8, - drawOutOfBound: false, - shrinkToFit: false, - origin: null, - - maskColor: 'rgba(255,0,0,0.3)', - maskGapWidth: 0.3, - - wait: 0, - abortThreshold: 0, // disabled - abort: function noop() {}, - - minRotation: -Math.PI / 2, - maxRotation: Math.PI / 2, - rotationSteps: 0, - - shuffle: true, - rotateRatio: 0.1, - - shape: 'circle', - ellipticity: 0.65, - - classes: null, - - hover: null, - click: null - }; - - if (options) { - for (var key in options) { - if (key in settings) { - settings[key] = options[key]; + _initOptions(options) { + if (options) { + for (var key in options) { + if (key in this.settings) { + this.settings[key] = options[key]; + } } } } - - /* Convert weightFactor into a function */ - if (typeof settings.weightFactor !== 'function') { - var factor = settings.weightFactor; - settings.weightFactor = function weightFactor(pt) { - return pt * factor; //in px - }; + _initWeightFactor() { + if (typeof this.settings.weightFactor !== 'function') { + let factor = this.settings.weightFactor; + this.settings.weightFactor = function weightFactor(pt) { + return pt * factor; //in px + }; + } } + _initShape() { + if (typeof this.settings.shape !== 'function') { + switch (this.settings.shape) { + case 'circle': + default: + this.settings.shape = 'circle'; + break; + + case 'cardioid': + this.settings.shape = function shapeCardioid(theta) { + return 1 - Math.sin(theta); + }; + break; + + case 'diamond': + + this.settings.shape = function shapeSquare(theta) { + let thetaPrime = theta % (2 * Math.PI / 4); + return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); + }; + break; + + case 'square': + this.settings.shape = function shapeSquare(theta) { + return Math.min( + 1 / Math.abs(Math.cos(theta)), + 1 / Math.abs(Math.sin(theta)) + ); + }; + break; + + case 'triangle-forward': + this.settings.shape = function shapeTriangle(theta) { + let thetaPrime = theta % (2 * Math.PI / 3); + return 1 / (Math.cos(thetaPrime) + + Math.sqrt(3) * Math.sin(thetaPrime)); + }; + break; + + case 'triangle': + case 'triangle-upright': + this.settings.shape = function shapeTriangle(theta) { + let thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3); + return 1 / (Math.cos(thetaPrime) + + Math.sqrt(3) * Math.sin(thetaPrime)); + }; + break; - /* Convert shape into a function */ - if (typeof settings.shape !== 'function') { - switch (settings.shape) { - case 'circle': - /* falls through */ - default: - // 'circle' is the default and a shortcut in the code loop. - settings.shape = 'circle'; - break; - - case 'cardioid': - settings.shape = function shapeCardioid(theta) { - return 1 - Math.sin(theta); - }; - break; - - /* - To work out an X-gon, one has to calculate "m", - where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) - http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 - 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 - Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) - where t' equals to mod(t, 2PI/X); - */ - - case 'diamond': - // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ - // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D - // +0+..+2*PI - settings.shape = function shapeSquare(theta) { - var thetaPrime = theta % (2 * Math.PI / 4); - return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); - }; - break; - - case 'square': - // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t - // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI - settings.shape = function shapeSquare(theta) { - return Math.min( - 1 / Math.abs(Math.cos(theta)), - 1 / Math.abs(Math.sin(theta)) - ); - }; - break; - - case 'triangle-forward': - // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ - // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 - // %29%29%2C+t+%3D+0+..+2*PI - settings.shape = function shapeTriangle(theta) { - var thetaPrime = theta % (2 * Math.PI / 3); - return 1 / (Math.cos(thetaPrime) + - Math.sqrt(3) * Math.sin(thetaPrime)); - }; - break; - - case 'triangle': - case 'triangle-upright': - settings.shape = function shapeTriangle(theta) { - var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3); - return 1 / (Math.cos(thetaPrime) + - Math.sqrt(3) * Math.sin(thetaPrime)); + case 'pentagon': + this.settings.shape = function shapePentagon(theta) { + let thetaPrime = (theta + 0.955) % (2 * Math.PI / 5); + return 1 / (Math.cos(thetaPrime) + + 0.726543 * Math.sin(thetaPrime)); + }; + break; + + case 'star': + this.settings.shape = function shapeStar(theta) { + let thetaPrime = (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) - thetaPrime) + + 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime)); + } else { + return 1 / (Math.cos(thetaPrime) + + 3.07768 * Math.sin(thetaPrime)); + } + }; + break; + } + } + } + _initColor() { + switch (this.settings.color) { + case 'random-dark': + this.getTextColor = function getRandomDarkColor() { + return this.random_hsl_color(10, 50); }; break; - case 'pentagon': - settings.shape = function shapePentagon(theta) { - var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5); - return 1 / (Math.cos(thetaPrime) + - 0.726543 * Math.sin(thetaPrime)); + case 'random-light': + this.getTextColor = function getRandomLightColor() { + return this.random_hsl_color(50, 90); }; break; - case 'star': - settings.shape = function shapeStar(theta) { - var thetaPrime = (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) - thetaPrime) + - 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime)); - } else { - return 1 / (Math.cos(thetaPrime) + - 3.07768 * Math.sin(thetaPrime)); - } - }; + default: + if (typeof this.settings.color === 'function') { + this.getTextColor = settings.color; + } break; } } - - /* Make sure gridSize is a whole number and is not smaller than 4px */ - settings.gridSize = Math.max(Math.floor(settings.gridSize), 4); - - /* shorthand */ - var g = settings.gridSize; - var maskRectWidth = g - settings.maskGapWidth; - - /* normalize rotation settings */ - var rotationRange = Math.abs(settings.maxRotation - settings.minRotation); - var rotationSteps = Math.abs(Math.floor(settings.rotationSteps)); - var minRotation = Math.min(settings.maxRotation, settings.minRotation); - - /* information/object available to all functions, set when start() */ - var grid, // 2d array containing filling information - ngx, ngy, // width and height of the grid - center, // position of the center of the cloud - maxRadius; - - /* timestamp for measuring each putWord() action */ - var escapeTime; - - /* function for getting the color of the text */ - var getTextColor; - - function random_hsl_color(min, max) { - return 'rgb(' + - (Math.random() * 255).toFixed() + ',' + - (Math.random() * 30 + 70).toFixed() + ',' + - (Math.random() * (max - min) + min).toFixed() + ')'; - } - switch (settings.color) { - case 'random-dark': - getTextColor = function getRandomDarkColor() { - return random_hsl_color(10, 50); - }; - break; - - case 'random-light': - getTextColor = function getRandomLightColor() { - return random_hsl_color(50, 90); - }; - break; - - default: - if (typeof settings.color === 'function') { - getTextColor = settings.color; + _initGrid() { + this.grid = []; + let gx, gy; + gx = this.ngx; + while (gx--) { + this.grid[gx] = []; + gy = this.ngy; + while (gy--) { + this.grid[gx][gy] = true; } - break; - } - - /* function for getting the font-weight of the text */ - var getTextFontWeight; - if (typeof settings.fontWeight === 'function') { - getTextFontWeight = settings.fontWeight; + } } - - /* function for getting the classes of the text */ - var getTextClasses = null; - if (typeof settings.classes === 'function') { - getTextClasses = settings.classes; + random_hsl_color(min, max) { + return 'hsl(' + + (Math.random() * 360).toFixed() + ',' + + (Math.random() * 30 + 70).toFixed() + '%,' + + (Math.random() * (max - min) + min).toFixed() + '%)'; } - - /* Interactive */ - var interactive = false; - var infoGrid = []; - var hovered; - - /* Get points on the grid for a given radius away from the center */ - var pointsAtRadius = []; - var getPointsAtRadius = function getPointsAtRadius(radius) { + getPointsAtRadius(pointsAtRadius, radius, center, settings) { if (pointsAtRadius[radius]) { return pointsAtRadius[radius]; } - - // Look for these number of points on each radius - var T = radius * 8; - - // Getting all the points at this radius - var t = T; - var points = []; - + let T = radius * 8; + let t = T; + let points = []; if (radius === 0) { points.push([center[0], center[1], 0]); } - while (t--) { - // distort the radius to put the cloud in shape - var rx = 1; + let rx = 1; if (settings.shape !== 'circle') { rx = settings.shape(t / T * 2 * Math.PI); // 0 to 1 } - - // Push [x, y, t]; t is used solely for getTextColor() points.push([ center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI), center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) * settings.ellipticity, - t / T * 2 * Math.PI - ]); + t / T * 2 * Math.PI]); } - pointsAtRadius[radius] = points; return points; }; - - /* Return true if we had spent too much time */ - var exceedTime = function exceedTime() { - return ((settings.abortThreshold > 0) && - ((new Date()).getTime() - escapeTime > settings.abortThreshold)); + _exceedTime() { + return ((this.settings.abortThreshold > 0) && + ((new Date()).getTime() - this.escapeTime > this.settings.abortThreshold)); }; - - /* Get the deg of rotation according to settings, and luck. */ - var getRotateDeg = function getRotateDeg() { - if (settings.rotateRatio === 0) { + _getRotateDeg() { + if (this.settings.rotateRatio === 0) { return 0; } - - if (Math.random() > settings.rotateRatio) { + if (Math.random() > this.settings.rotateRatio) { return 0; } - - if (rotationRange === 0) { - return minRotation; + if (this.rotationRange === 0) { + return this.minRotation; } - - if (rotationSteps > 0) { - // Min rotation + zero or more steps * span of one step + if (this.rotationSteps > 0) { return minRotation + - Math.floor(Math.random() * rotationSteps) * - rotationRange / (rotationSteps - 1); - } else { - return minRotation + Math.random() * rotationRange; + Math.floor(Math.random() * this.rotationSteps) * + this.rotationRange / (this.rotationSteps - 1); + } + else { + return this.minRotation + Math.random() * this.rotationRange; } }; - - function getTextInfo(word, weight, rotateDeg) { - // calculate the acutal font size - // fontSize === 0 means weightFactor function wants the text skipped, - // and size < minSize means we cannot draw the text. - var debug = false; - var fontSize = settings.weightFactor(weight); - if (fontSize <= settings.minSize) { - return Promise.resolve(false); + _canFitText(gx, gy, gw, gh, occupied) { + let i = occupied.length; + while (i--) { + let px = gx + occupied[i][0]; + let py = gy + occupied[i][1]; + if (px >= this.ngx || py >= this.ngy || px < 0 || py < 0) { + if (!this.settings.drawOutOfBound) { + return false; + } + continue; + } + if (!this.grid[px][py]) { + return false; + } } + return true; + }; + _shuffleArray(arr) { + for (var j, x, i = arr.length; i; + j = Math.floor(Math.random() * i), + x = arr[--i], arr[i] = arr[j], + arr[j] = x) { } + return arr; + }; - // Scale factor here is to make sure fillText is not limited by - // the minium font size set by browser. - // It will always be 1 or 2n. - var mu = 1; - if (fontSize < minFontSize) { + _getTextInfo(word, weight, rotateDeg) { + let fontSize = this.settings.weightFactor(weight); + if (fontSize <= this.settings.minSize) { + return false; + } + let mu = 1; + if (fontSize < this.minFontSize) { mu = (function calculateScaleFactor() { var mu = 2; - while (mu * fontSize < minFontSize) { + while (mu * fontSize < this.minFontSize) { mu += 2; } return mu; })(); } - - // Get fontWeight that will be used to set fctx.font - var fontWeight; - if (getTextFontWeight) { - fontWeight = getTextFontWeight(word, weight, fontSize); + let fontWeight; + if (this.getTextFontWeight) { + fontWeight = this.getTextFontWeight(word, weight, fontSize); } else { - fontWeight = settings.fontWeight; + fontWeight = this.settings.fontWeight; } - - - - var fctx = wx.createCanvasContext("canvas",self); - fctx.setFontSize(fontSize * mu) - fctx.font = fontWeight + ' ' + - (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; - - // Estimate the dimension of the text with measureText(). - var fw = fctx.measureText(word).width / mu; - var fh = Math.max(fontSize * mu, - fctx.measureText('m').width, - fctx.measureText('\uFF37').width) / mu; - - // Create a boundary box that is larger than our estimates, - // so text don't get cut of (it sill might) - var boxWidth = fw + fh * 2; - var boxHeight = fh * 3; - var fgw = Math.ceil(boxWidth / g); - var fgh = Math.ceil(boxHeight / g); - boxWidth = fgw * g; - boxHeight = fgh * g; - - // Calculate the proper offsets to make the text centered at - // the preferred position. - - // This is simply half of the width. - var fillTextOffsetX = -fw / 2; - // Instead of moving the box to the exact middle of the preferred - // position, for Y-offset we move 0.4 instead, so Latin alphabets look - // vertical centered. - var fillTextOffsetY = -fh * 0.4; - - // Calculate the actual dimension of the canvas, considering the rotation. - var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) + - boxHeight * Math.abs(Math.cos(rotateDeg))) / g); - var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) + - boxHeight * Math.abs(Math.sin(rotateDeg))) / g); - var width = cgw * g; - var height = cgh * g; - - // Scale the canvas with |mu|. - fctx.scale(1 / mu, 1 / mu); - fctx.translate(width * mu / 2, height * mu / 2); - fctx.rotate(-rotateDeg); - - // Once the width/height is set, ctx info will be reset. - // Set it again here. - fctx.font = fontWeight + ' ' + - (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; - - // Fill the text into the fcanvas. - // XXX: We cannot because textBaseline = 'top' here because - // Firefox and Chrome uses different default line-height for canvas. - // Please read https://bugzil.la/737852#c6. - // Here, we use textBaseline = 'middle' and draw the text at exactly - // 0.5 * fontSize lower. - fctx.fillStyle = '#000'; - fctx.textBaseline = 'middle'; - fctx.fillText(word, fillTextOffsetX * mu, + this.fctx.font = fontWeight + ' ' + + (fontSize * mu).toString(10) + 'px ' + this.settings.fontFamily; + let fw = this.fctx.measureText(word).width / mu; + let fh = Math.max(fontSize * mu, + this.fctx.measureText('m').width, + this.fctx.measureText('\uFF37').width) / mu; + let boxWidth = fw + fh * 2; + let boxHeight = fh * 2; + let fgw = Math.ceil(boxWidth / this.g); + let fgh = Math.ceil(boxHeight / this.g); + boxWidth = fgw * this.g; + boxHeight = fgh * this.g; + let fillTextOffsetX = - fw / 2; + let fillTextOffsetY = - fh * 0.4; + let cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) + + boxHeight * Math.abs(Math.cos(rotateDeg))) / this.g); + let cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) + + boxHeight * Math.abs(Math.sin(rotateDeg))) / this.g); + let width = cgw * this.g; + let height = cgh * this.g; + this.fcanvas.width = width + this.fcanvas.height = height + this.fctx.scale(1 / mu, 1 / mu); + this.fctx.translate(width * mu / 2, height * mu / 2); + this.fctx.rotate(- rotateDeg); + this.fctx.font = fontWeight + ' ' + + (fontSize * mu).toString(10) + 'px ' + this.settings.fontFamily; + this.fctx.fillStyle = '#000'; + this.fctx.textBaseline = 'middle'; + this.fctx.fillText(word, fillTextOffsetX * mu, (fillTextOffsetY + fontSize * 0.5) * mu); - fctx.draw(); - // Get the pixels of the text - var imageData; - return new Promise(resolve => { - wx.canvasGetImageData({ - canvasId: 'canvas', - x: 0, - y: 0, - width: width, - height: height, - success(res) { - imageData = res.data; - - if (exceedTime()) { - return false; - } - - if (debug) { - // Draw the box of the original estimation - fctx.strokeRect(fillTextOffsetX * mu, - fillTextOffsetY, fw * mu, fh * mu); - fctx.restore(); - } - - // Read the pixels and save the information to the occupied array - var occupied = []; - var gx = cgw, - gy, x, y; - var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; - while (gx--) { - gy = cgh; - while (gy--) { - y = g; - singleGridLoop: { - while (y--) { - x = g; - while (x--) { - if (imageData[((gy * g + y) * width + - (gx * g + x)) * 4 + 3]) { - occupied.push([gx, gy]); - - if (gx < bounds[3]) { - bounds[3] = gx; - } - if (gx > bounds[1]) { - bounds[1] = gx; - } - if (gy < bounds[0]) { - bounds[0] = gy; - } - if (gy > bounds[2]) { - bounds[2] = gy; - } - - if (debug) { - fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; - fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); - } - break singleGridLoop; - } - } + // 获取文本像素 + let imageData = this.fctx.getImageData(0, 0, width, height).data; + if (this._exceedTime()) { + return false; + } + let occupied = []; + let gx = cgw, gy, x, y; + let bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; + while (gx--) { + gy = cgh; + while (gy--) { + y = this.g; + singleGridLoop: { + while (y--) { + x = this.g; + while (x--) { + if (imageData[((gy * this.g + y) * width + + (gx * this.g + x)) * 4 + 3]) { + occupied.push([gx, gy]); + + if (gx < bounds[3]) { + bounds[3] = gx; } - if (debug) { - fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; - fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); + if (gx > bounds[1]) { + bounds[1] = gx; } + if (gy < bounds[0]) { + bounds[0] = gy; + } + if (gy > bounds[2]) { + bounds[2] = gy; + } + break singleGridLoop; } } } - if (debug) { - fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; - fctx.fillRect(bounds[3] * g, - bounds[0] * g, - (bounds[1] - bounds[3] + 1) * g, - (bounds[2] - bounds[0] + 1) * g); - } - - resolve({ - mu: mu, - occupied: occupied, - bounds: bounds, - gw: cgw, - gh: cgh, - fillTextOffsetX: fillTextOffsetX, - fillTextOffsetY: fillTextOffsetY, - fillTextWidth: fw, - fillTextHeight: fh, - fontSize: fontSize - }) - }, - fail(error){ - console.log(word, error) } - },self) - }) - }; - - /* Determine if there is room available in the given dimension */ - var canFitText = function canFitText(gx, gy, gw, gh, occupied) { - // Go through the occupied points, - // return false if the space is not available. - var i = occupied.length; - while (i--) { - var px = gx + occupied[i][0]; - var py = gy + occupied[i][1]; - - if (px >= ngx || py >= ngy || px < 0 || py < 0) { - if (!settings.drawOutOfBound) { - return false; - } - continue; - } - - if (!grid[px][py]) { - return false; } } - return true; + return { + mu: mu, + occupied: occupied, + bounds: bounds, + gw: cgw, + gh: cgh, + fillTextOffsetX: fillTextOffsetX, + fillTextOffsetY: fillTextOffsetY, + fillTextWidth: fw, + fillTextHeight: fh, + fontSize: fontSize + }; }; - - /* Actually draw the text on the grid */ - var drawText = function drawText(gx, gy, info, word, weight, - distance, theta, rotateDeg, attributes) { - var transformRule = ''; - transformRule = 'rotate(' + (-rotateDeg / Math.PI * 180) + 'deg) '; - if (info.mu !== 1) { - transformRule += - 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' + - 'scale(' + (1 / info.mu) + ')'; - } - - var fontSize = info.fontSize; - var color; - if (getTextColor) { - color = getTextColor(word, weight, fontSize, distance, theta); - } else { - color = settings.color; + _fillGridAt(x, y, drawMask, dimension, item) { + if (x >= this.ngx || y >= this.ngy || x < 0 || y < 0) { + return; } - - var fontWeight; - if (getTextFontWeight) { - fontWeight = getTextFontWeight(word, weight, fontSize); + this.grid[x][y] = false; + }; + _drawText(gx, gy, info, word, weight, + distance, theta, rotateDeg, item) { + let fontSize = info.fontSize; + let color; + if (this.getTextColor) { + color = this.getTextColor(word, weight, fontSize, distance, theta); } else { - fontWeight = settings.fontWeight; + color = this.settings.color; } - - var classes; - if (getTextClasses) { - classes = getTextClasses(word, weight, fontSize); + let fontWeight; + if (this.getTextFontWeight) { + fontWeight = this.getTextFontWeight(word, weight, fontSize); } else { - classes = settings.classes; - } - - callback({ - g, - gx, - gy, - info, - word, - weight, - color, - fontWeight, - fontSize, - distance, - theta, - rotateDeg, - transformRule, - attributes - }) //将要绘制的span参数通过回调函数传出 - }; - - /* Help function to updateGrid */ - var fillGridAt = function fillGridAt(x, y, dimension, item) { - if (x >= ngx || y >= ngy || x < 0 || y < 0) { - return; + fontWeight = this.settings.fontWeight; } - - grid[x][y] = false; - - if (interactive) { - infoGrid[x][y] = { - item: item, - dimension: dimension - }; - } - }; - - /* Update the filling information of the given space with occupied points. - Draw the mask on the canvas if necessary. */ - var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) { - var occupied = info.occupied; - var dimension; - if (interactive) { - var bounds = info.bounds; - dimension = { - x: (gx + bounds[3]) * g, - y: (gy + bounds[0]) * g, - w: (bounds[1] - bounds[3] + 1) * g, - h: (bounds[2] - bounds[0] + 1) * g - }; + let dimension; + let bounds = info.bounds; + dimension = { + x: (gx + bounds[3]) * this.g, + y: (gy + bounds[0]) * this.g, + w: (bounds[1] - bounds[3] + 1) * this.g, + h: (bounds[2] - bounds[0] + 1) * this.g + }; + let transformRule = ''; + transformRule = 'rotate(' + (- rotateDeg / Math.PI * 180) + 'deg) '; + if (info.mu !== 1) { + transformRule += + 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' + + 'scale(' + (1 / info.mu) + ')'; } - - var i = occupied.length; - while (i--) { - var px = gx + occupied[i][0]; - var py = gy + occupied[i][1]; - - if (px >= ngx || py >= ngy || px < 0 || py < 0) { - continue; - } - - fillGridAt(px, py, dimension, item); + let styleRules = { + 'position': 'absolute', + 'display': 'block', + 'font': fontWeight + ' ' + + (fontSize * info.mu) + 'rpx ' + this.settings.fontFamily, + 'fontSize': (fontSize * info.mu), + 'left': ((gx + info.gw / 2) * this.g + info.fillTextOffsetX), + 'top': ((gy + info.gh / 2) * this.g + info.fillTextOffsetY), + 'width': info.fillTextWidth, + 'height': info.fillTextHeight, + 'lineHeight': fontSize, + 'whiteSpace': 'nowrap', + 'transform': transformRule, + 'webkitTransform': transformRule, + 'msTransform': transformRule, + 'transformOrigin': '50% 40%', + }; + if (color) { + styleRules.color = color; + } + if (Array.isArray(item)) { + color = item[3] || color; + styleRules.color = color; + } else { + color = item.color || color ; + styleRules.color = color; } + let result = { word, ...info, ...styleRules, target: item } + this.wordData.push(result) + return result }; - /* putWord() processes each item on the list, - calculate it's size and determine it's position, and actually - put it on the canvas. */ - function putWord(item) { - var word, weight, attributes; + _putWord(item,) { + let word, weight; if (Array.isArray(item)) { word = item[0]; weight = item[1]; } else { word = item.word; weight = item.weight; - attributes = item.attributes; } - var rotateDeg = getRotateDeg(); - // get info needed to put the text onto the canvas - return new Promise(resolve => { - getTextInfo(word, weight, rotateDeg).then(res => { - resolve(res) - }) - }).then(res => { - let info = res; - // not getting the info means we shouldn't be drawing this one. - if (!info) { + let rotateDeg = this._getRotateDeg(); + let info = this._getTextInfo(word, weight, rotateDeg); + if (!info) { + return false; + } + if (this._exceedTime()) { + return false; + } + if (!this.settings.drawOutOfBound) { + let bounds = info.bounds; + if ((bounds[1] - bounds[3] + 1) > this.ngx || + (bounds[2] - bounds[0] + 1) > this.ngy) { return false; } - - if (exceedTime()) { + } + let r = this.maxRadius + 1; + let that = this + let tryToPutWordAtPoint = function (gxy) { + let gx = Math.floor(gxy[0] - info.gw / 2); + let gy = Math.floor(gxy[1] - info.gh / 2); + let gw = info.gw; + let gh = info.gh; + if (!that._canFitText(gx, gy, gw, gh, info.occupied)) { return false; } - - // If drawOutOfBound is set to false, - // skip the loop if we have already know the bounding box of - // word is larger than the canvas. - if (!settings.drawOutOfBound) { - var bounds = info.bounds; - if ((bounds[1] - bounds[3] + 1) > ngx || - (bounds[2] - bounds[0] + 1) > ngy) { - return false; - } + that._drawText(gx, gy, info, word, weight, + (that.maxRadius - r), gxy[2], rotateDeg, item); + that._updateGrid(gx, gy, gw, gh, info, item); + return true; + }; + while (r--) { + let points = this.getPointsAtRadius(this.pointsAtRadius, this.maxRadius - r, this.center, this.settings); + if (this.settings.shuffle) { + points = [].concat(points); + this._shuffleArray(points); } + let drawn = points.some(tryToPutWordAtPoint); - // Determine the position to put the text by - // start looking for the nearest points - var r = maxRadius + 1; - - var tryToPutWordAtPoint = function(gxy) { - var gx = Math.floor(gxy[0] - info.gw / 2); - var gy = Math.floor(gxy[1] - info.gh / 2); - var gw = info.gw; - var gh = info.gh; - - // If we cannot fit the text at this position, return false - // and go to the next position. - if (!canFitText(gx, gy, gw, gh, info.occupied)) { - return false; - } - - // Actually put the text on the canvas - drawText(gx, gy, info, word, weight, - (maxRadius - r), gxy[2], rotateDeg, attributes); - // Mark the spaces on the grid as filled - updateGrid(gx, gy, gw, gh, info, item); - - // Return true so some() will stop and also return true. + if (drawn) { return true; - }; - - while (r--) { - var points = getPointsAtRadius(maxRadius - r); - - if (settings.shuffle) { - points = [].concat(points); - shuffleArray(points); - } - - // Try to fit the words by looking at each point. - // array.some() will stop and return true - // when putWordAtPoint() returns true. - // If all the points returns false, array.some() returns false. - var drawn = points.some(tryToPutWordAtPoint); - - if (drawn) { - return true; - } - } - // we tried all distances but text won't fit, return false - return false; - }) - }; - - /* Start drawing on a canvas */ - function start() { - // For dimensions, clearCanvas etc., - // we only care about the first element. - ngx = Math.ceil(mycanvas.width / g); - ngy = Math.ceil(mycanvas.height / g); - - // Determine the center of the word cloud - center = (settings.origin) ? [settings.origin[0] / g, settings.origin[1] / g] : [ngx / 2, ngy / 2]; - - // Maxium radius to look for space - maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)); - - /* Clear the canvas only if the clearCanvas is set, - if not, update the grid to the current canvas state */ - grid = []; - - var gx, gy, i; - if (settings.clearCanvas) { - ctx.fillStyle = settings.backgroundColor; - ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)); - ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)); - - /* fill the grid with empty state */ - gx = ngx; - while (gx--) { - grid[gx] = []; - gy = ngy; - while (gy--) { - grid[gx][gy] = true; - } } } + return false; + } - i = 0; - var loopingFunction, stoppingFunction; - - loopingFunction = setTimeout; - stoppingFunction = clearTimeout; - var timer = loopingFunction(function loop() { - if (i >= settings.list.length) { - stoppingFunction(timer); - return; + _updateGrid(gx, gy, gw, gh, info, item) { + let occupied = info.occupied; + let drawMask = this.settings.drawMask; + let dimension; + let i = occupied.length; + while (i--) { + let px = gx + occupied[i][0]; + let py = gy + occupied[i][1]; + if (px >= this.ngx || py >= this.ngy || px < 0 || py < 0) { + continue; } - escapeTime = (new Date()).getTime(); - putWord(settings.list[i]).then(res => { - if (exceedTime()) { - stoppingFunction(timer); - settings.abort(); - return; - } - i++; - timer = loopingFunction(loop, settings.wait); - }).catch(error => console.log(error)) - }, settings.wait); + this._fillGridAt(px, py, drawMask, dimension, item); + } }; - // All set, start the drawing - start(); -}; + start() { + let that = this + this.settings.list.forEach(function(item) { + that._putWord(item); + }) + console.log(this.wordData) + return this.wordData + }; +} -exports.WordCloud = WordCloud \ No newline at end of file +export default WordCloud \ No newline at end of file diff --git a/components/word-cloud/index.js b/components/word-cloud/index.js index 56ccb88..2984a0b 100644 --- a/components/word-cloud/index.js +++ b/components/word-cloud/index.js @@ -1,50 +1,98 @@ -// word-cloud/index.js -const { - WordCloud -} = require("./WordCloud.js") +// components/word-cloud/index.js +import WordCloud from './wordcloud' +const options = { + "list": [], + "gridSize": 6, // size of the grid in pixels + "weightFactor": 1.5, // number to multiply for size of each word in the list + "fontWeight": 'normal', // 'normal', 'bold' or a callback + "fontFamily": 'Times, serif', // font to use + "color": 'red', // 'random-dark' or 'random-light' + "backgroundColor": '#fff', // the color of canvas + "rotateRatio": 0, // probability for the word to rotate. 1 means always + "minFontSize": 12, //最小字号 + "maxFontSize": 60, //最大字号 + "fontSizeFactor": 5, + "ellipticity": 1, + "shuffle": false, + "figPath": '../../images/carSide.png' +} + Component({ /** * 组件的属性列表 */ properties: { - list: { - type: Array, - value: [], + width: { + type: Number, + value: 375 }, height: { - type: String, - value: "200" + type: Number, + value: 450 }, - width: { + bgColor: { type: String, - value: "" + value: '#fff', }, - color: { - type: String, - value: "random-light" + list: { + type: Array, + value: [], + } + }, + lifetimes: { + attached: function() { + // 在组件实例进入页面节点树时执行 + const query = this.createSelectorQuery() + query.select('#myCanvas') + .fields({ node: true, size: true }) + .exec((res) => { + this.data.canvas = res[0].node + }) }, }, - + observers: { + list(newVal) { + if(!newVal || !Array.isArray(newVal) || newVal.length === 0) return + let { width, height, list, wordData } = this.data + list = newVal; + if (!width || isNaN(Number(width))) { + width = 375 + } + if (!height || isNaN(Number(height))) { + height = 450 + } + const dpr = wx.getSystemInfoSync().pixelRatio + options.height = height * dpr + options.width = width * dpr + options.list = list + setTimeout(()=>{ + if (this.data.canvas) { + const wordCloud = new WordCloud(this.data.canvas, options) + wordData = wordCloud.start() + this.setData({ + wordData, + width, + height, + }) + } + }, 500) + } + }, + /** + * 组件的初始数据 + */ data: { - spans: [] + wordData: [], + canvas: null, }, - ready() { - let that = this; - this.ctx = wx.createCanvasContext('canvas', this); - let query = this.createSelectorQuery(); - query.select("#canvas").boundingClientRect(function (res) { - WordCloud(that.ctx, res, { - minSize: 8, - list: that.data.list, - color: that.data.color - }, that, function (val) { - that.data.spans.push(val) - // w.to_file("../../images/carSide.png") - that.setData({ - spans: that.data.spans - }) - }) - }).exec(); + /** + * 组件的方法列表 + */ + methods: { + bindWord(event) { + const { detail } = event.currentTarget.dataset + this.triggerEvent('detail', detail); + }, } -}) +}) \ No newline at end of file diff --git a/components/word-cloud/index.wxml b/components/word-cloud/index.wxml index c66c247..f01de20 100644 --- a/components/word-cloud/index.wxml +++ b/components/word-cloud/index.wxml @@ -1,8 +1,6 @@ - - - - - - {{item.word}} - + + + + {{ item.word }} + \ No newline at end of file diff --git a/components/word-cloud/index.wxss b/components/word-cloud/index.wxss index 9ed5446..4e8ad90 100644 --- a/components/word-cloud/index.wxss +++ b/components/word-cloud/index.wxss @@ -1 +1,16 @@ -/* word-cloud/index.wxss */ \ No newline at end of file +/* components/word-cloud/index.wxss */ + +.wc-canvas { + display: none; + position: absolute; + z-index: -1; + } + .wc-main { + position: relative; + } + .wc-item { + white-space: nowrap; + position: absolute; + display: block; + transform-origin: 50% 40%; + } \ No newline at end of file diff --git a/components/word-cloud/word-cloud.js b/components/word-cloud/word-cloud.js new file mode 100644 index 0000000..9b559a3 --- /dev/null +++ b/components/word-cloud/word-cloud.js @@ -0,0 +1,247 @@ +function WordCloud(wdata, laySize) { + + var cloudRadians = Math.PI / 180, + cw = 1 << 11 >> 5, + ch = 1 << 11; + let data = wdata + let words = wdata + let canvas = cloudCanvas + let random = Math.random + let size = laySize + let spiral = archimedeanSpiral + + + step() + console.log('===', words) + + function zeroArray(n) { + var a = [], + i = -1; + while (++i < n) a[i] = 0; + return a; + } + + function step() { + var contextAndRatio = getContext(canvas()), + board = zeroArray((size[0] >> 5) * size[1]), + bounds = null, + n = words.length, + i = 0, + tags = [] + for (i; i < n; i++) { + var d = data[i]; + d.x = (size[0] * (random() + .5)) >> 1; + d.y = (size[1] * (random() + .5)) >> 1; + cloudSprite(contextAndRatio, d, data, i); + if (d.hasText && place(board, d, bounds)) { + tags.push(d); + + if (bounds) cloudBounds(bounds, d); + else bounds = [{ x: d.x + d.x0, y: d.y + d.y0 }, { x: d.x + d.x1, y: d.y + d.y1 }]; + // Temporary hack + d.x -= size[0] >> 1; + d.y -= size[1] >> 1; + } + } + } + + function cloudCanvas() { + return document.createElement("canvas"); + } + + function getContext(canvas) { + canvas.width = canvas.height = 1; + var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2); + canvas.width = (cw << 5) / ratio; + canvas.height = ch / ratio; + + var context = canvas.getContext("2d"); + context.fillStyle = context.strokeStyle = "red"; + context.textAlign = "center"; + + return { context: context, ratio: ratio }; + } + + + function cloudSprite(contextAndRatio, d, data, di) { + if (d.sprite) return; + var c = contextAndRatio.context, + ratio = contextAndRatio.ratio; + + c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); + var x = 0, + y = 0, + maxh = 0, + n = data.length; + --di; + while (++di < n) { + d = data[di]; + c.save(); + + // c.font = ~~((d.size + 1) / ratio) + "px " + (d.font || "Arial"); + c.font = `${(((d.size) / ratio))}px ${(d.font || "Arial")}` + var w = c.measureText(d.text + "m").width * ratio, + h = d.size << 1; + if (d.rotate) { + var sr = Math.sin(d.rotate * cloudRadians), + cr = Math.cos(d.rotate * cloudRadians), + wcr = w * cr, + wsr = w * sr, + hcr = h * cr, + hsr = h * sr; + w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5; + h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); + } else { + w = (w + 0x1f) >> 5 << 5; + } + if (h > maxh) maxh = h; + if (x + w >= (cw << 5)) { + x = 0; + y += maxh; + maxh = 0; + } + if (y + h >= ch) break; + c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); + if (d.rotate) c.rotate(d.rotate * cloudRadians); + c.fillText(d.text, 0, 0); + if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0); + c.restore(); + d.width = w; + d.height = h; + d.xoff = x; + d.yoff = y; + d.x1 = w >> 1; + d.y1 = h >> 1; + d.x0 = -d.x1; + d.y0 = -d.y1; + d.hasText = true; + x += w; + } + var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, + sprite = []; + while (--di >= 0) { + d = data[di]; + if (!d.hasText) continue; + var w = d.width, + w32 = w >> 5, + h = d.y1 - d.y0; + // Zero the buffer + for (var i = 0; i < h * w32; i++) sprite[i] = 0; + x = d.xoff; + if (x == null) return; + y = d.yoff; + var seen = 0, + seenRow = -1; + for (var j = 0; j < h; j++) { + for (var i = 0; i < w; i++) { + var k = w32 * j + (i >> 5), + m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0; + sprite[k] |= m; + seen |= m; + } + if (seen) seenRow = j; + else { + d.y0++; + h--; + j--; + y++; + } + } + d.y1 = d.y0 + seenRow; + d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); + } + } + + function place(board, tag, bounds) { + var perimeter = [{ x: 0, y: 0 }, { x: size[0], y: size[1] }], + startX = tag.x, + startY = tag.y, + maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), + s = spiral(size), + dt = random() < .5 ? 1 : -1, + t = -dt, + dxdy, + dx, + dy; + + while (dxdy = s(t += dt)) { + dx = ~~dxdy[0]; + dy = ~~dxdy[1]; + + if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; + + tag.x = startX + dx; + tag.y = startY + dy; + + if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || + tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue; + // TODO only check for collisions within current bounds. + if (!bounds || !cloudCollide(tag, board, size[0])) { + if (!bounds || collideRects(tag, bounds)) { + var sprite = tag.sprite, + w = tag.width >> 5, + sw = size[0] >> 5, + lx = tag.x - (w << 4), + sx = lx & 0x7f, + msx = 32 - sx, + h = tag.y1 - tag.y0, + x = (tag.y + tag.y0) * sw + (lx >> 5), + last; + for (var j = 0; j < h; j++) { + last = 0; + for (var i = 0; i <= w; i++) { + board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); + } + x += sw; + } + delete tag.sprite; + return true; + } + } + } + return false; + } + + function cloudCollide(tag, board, sw) { + sw >>= 5; + var sprite = tag.sprite, + w = tag.width >> 5, + lx = tag.x - (w << 4), + sx = lx & 0x7f, + msx = 32 - sx, + h = tag.y1 - tag.y0, + x = (tag.y + tag.y0) * sw + (lx >> 5), + last; + for (var j = 0; j < h; j++) { + last = 0; + for (var i = 0; i <= w; i++) { + if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) + & board[x + i]) return true; + } + x += sw; + } + return false; + } + + function collideRects(a, b) { + return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y; + } + + function cloudBounds(bounds, d) { + var b0 = bounds[0], + b1 = bounds[1]; + if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0; + if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0; + if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1; + if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1; + } + + function archimedeanSpiral(size) { + var e = size[0] / size[1]; + return function (t) { + return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)]; + }; + } + +} + diff --git a/pages/insight/index.js b/pages/insight/index.js index 41e0e8c..5f8f9a1 100644 --- a/pages/insight/index.js +++ b/pages/insight/index.js @@ -29,7 +29,6 @@ const words = [ ] Page({ data: { - canvasId: 'w1', words: words.map(item => { item[1] = item[1] + 12 return item;