203 lines
5.8 KiB
JavaScript
203 lines
5.8 KiB
JavaScript
"use strict";
|
||
const common_vendor = require("./common/vendor.js");
|
||
const _sfc_main = {
|
||
name: "WordCloud",
|
||
props: {
|
||
// 词云数据:[{ text: '关键词', value: 100 }]
|
||
wordData: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 画布宽度
|
||
width: {
|
||
type: Number,
|
||
default: 300
|
||
},
|
||
// 画布高度
|
||
height: {
|
||
type: Number,
|
||
default: 300
|
||
},
|
||
// 颜色:外 / 中 / 内
|
||
colorList: {
|
||
type: Array,
|
||
default: () => ["#60A5FA", "#FEC200", "#EF4444"]
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
ctx: null,
|
||
// canvas 2d 上下文
|
||
placedWords: [],
|
||
// 已放置文字的包围盒(用于碰撞检测)
|
||
centerWords: [],
|
||
// value 最大的 3 个
|
||
otherWords: []
|
||
// 其余词
|
||
};
|
||
},
|
||
watch: {
|
||
// 词云数据变化时重绘
|
||
wordData: {
|
||
deep: true,
|
||
handler() {
|
||
this.drawWordCloud();
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.initCanvas();
|
||
},
|
||
methods: {
|
||
// 初始化 canvas
|
||
async initCanvas() {
|
||
await new Promise((r) => setTimeout(r, 50));
|
||
const query = common_vendor.index.createSelectorQuery().in(this);
|
||
query.select(".word-cloud-canvas").fields({ node: true }).exec((res) => {
|
||
if (!res || !res[0] || !res[0].node)
|
||
return;
|
||
const canvas = res[0].node;
|
||
const ctx = canvas.getContext("2d");
|
||
const dpr = common_vendor.index.getSystemInfoSync().pixelRatio || 1;
|
||
canvas.width = this.width * dpr;
|
||
canvas.height = this.height * dpr;
|
||
ctx.scale(dpr, dpr);
|
||
this.ctx = ctx;
|
||
this.drawWordCloud();
|
||
});
|
||
},
|
||
// 绘制词云
|
||
drawWordCloud() {
|
||
if (!this.ctx || !this.wordData.length)
|
||
return;
|
||
this.ctx.clearRect(0, 0, this.width, this.height);
|
||
this.placedWords = [];
|
||
const sorted = [...this.wordData].sort((a, b) => b.value - a.value);
|
||
this.centerWords = sorted.slice(0, 3);
|
||
this.otherWords = sorted.slice(3);
|
||
this.centerWords.forEach((word, index) => {
|
||
this.placeWord(word, "center", index);
|
||
});
|
||
this.otherWords.forEach((word) => {
|
||
this.placeWord(word, "other");
|
||
});
|
||
this.$emit("rendered");
|
||
},
|
||
// 放置单个词
|
||
placeWord(word, type, index = 0) {
|
||
const ctx = this.ctx;
|
||
const text = word.text || word.name;
|
||
const maxAttempts = 200;
|
||
let fontSize = 24;
|
||
let layer = "middle";
|
||
if (type === "center") {
|
||
fontSize = 32;
|
||
layer = "center";
|
||
} else {
|
||
layer = Math.random() > 0.5 ? "middle" : "outer";
|
||
fontSize = layer === "outer" ? 18 : 24;
|
||
}
|
||
const angleLimit = Math.random() > 0.5 ? 60 * Math.PI / 180 : 30 * Math.PI / 180;
|
||
const angle = (Math.random() - 0.5) * 2 * angleLimit;
|
||
ctx.font = `bold ${fontSize}px sans-serif`;
|
||
const textWidth = ctx.measureText(text).width;
|
||
const textHeight = fontSize * 1.05;
|
||
for (let i = 0; i < maxAttempts; i++) {
|
||
let x, y;
|
||
if (layer === "center") {
|
||
const centerX = this.width / 2;
|
||
const centerY = this.height / 2;
|
||
const offsets = [
|
||
{ x: 0, y: 0 },
|
||
// 最大的,正中
|
||
{ x: -80, y: 0 },
|
||
// 左
|
||
{ x: 80, y: 0 }
|
||
// 右
|
||
];
|
||
const pos = offsets[index] || offsets[0];
|
||
x = centerX + pos.x;
|
||
y = centerY + pos.y;
|
||
} else {
|
||
x = this.width * 0.05 + Math.random() * this.width * 0.9;
|
||
y = this.height * 0.05 + Math.random() * this.height * 0.9;
|
||
}
|
||
const rect = this.getBoundingRect(
|
||
x,
|
||
y,
|
||
textWidth,
|
||
textHeight,
|
||
angle,
|
||
2
|
||
);
|
||
if (layer === "outer") {
|
||
if (rect.left < 0 || rect.right > this.width || rect.top < 0 || rect.bottom > this.height) {
|
||
continue;
|
||
}
|
||
}
|
||
if (this.checkOverlapRect(rect))
|
||
continue;
|
||
ctx.fillStyle = layer === "center" ? this.colorList[2] : layer === "middle" ? this.colorList[1] : this.colorList[0];
|
||
this.drawText(text, x, y, angle);
|
||
this.placedWords.push({ rect });
|
||
break;
|
||
}
|
||
},
|
||
// 碰撞检测(AABB)
|
||
checkOverlapRect(current) {
|
||
for (const item of this.placedWords) {
|
||
const r = item.rect;
|
||
if (current.left < r.right && current.right > r.left && current.top < r.bottom && current.bottom > r.top) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
// 计算旋转后的包围盒
|
||
getBoundingRect(x, y, width, height, angle, gap = 2) {
|
||
const cos = Math.cos(angle);
|
||
const sin = Math.sin(angle);
|
||
const halfW = (width - gap) / 2;
|
||
const halfH = (height - gap) / 2;
|
||
const points = [
|
||
{ x: -halfW, y: -halfH },
|
||
{ x: halfW, y: -halfH },
|
||
{ x: halfW, y: halfH },
|
||
{ x: -halfW, y: halfH }
|
||
];
|
||
const xs = [];
|
||
const ys = [];
|
||
points.forEach((p) => {
|
||
xs.push(x + p.x * cos - p.y * sin);
|
||
ys.push(y + p.x * sin + p.y * cos);
|
||
});
|
||
return {
|
||
left: Math.min(...xs),
|
||
right: Math.max(...xs),
|
||
top: Math.min(...ys),
|
||
bottom: Math.max(...ys)
|
||
};
|
||
},
|
||
// 绘制文字
|
||
drawText(text, x, y, angle) {
|
||
const ctx = this.ctx;
|
||
ctx.save();
|
||
ctx.translate(x, y);
|
||
ctx.rotate(angle);
|
||
ctx.textAlign = "center";
|
||
ctx.textBaseline = "middle";
|
||
ctx.fillText(text, 0, 0);
|
||
ctx.restore();
|
||
}
|
||
}
|
||
};
|
||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||
return {
|
||
a: $props.width + "px",
|
||
b: $props.height + "px"
|
||
};
|
||
}
|
||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-cab45d13"]]);
|
||
exports.MiniProgramPage = MiniProgramPage;
|
||
//# sourceMappingURL=../.sourcemap/mp-weixin/WordCloud.js.map
|