UNPKG

@soonspacejs/plugin-heat-map

Version:

Haet-map plugin for SoonSpace.js

556 lines (555 loc) 23.4 kB
import { Matrix4 as B, Vector2 as P, Shape as j, ShapeGeometry as T, Box2 as A, Plane as F, Vector3 as w, Matrix3 as O, PlaneGeometry as L, CanvasTexture as _, MeshStandardMaterial as z, Mesh as R, Box3 as k, DoubleSide as G } from "three"; var y = { defaultRadius: 40, defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1: "rgb(255,0,0)" }, defaultMaxOpacity: 1, defaultMinOpacity: 0, defaultBlur: 0.85, defaultXField: "x", defaultYField: "y", defaultValueField: "value" }, U = (function() { function s() { this.eStore = {}; } return s.prototype.on = function(t, e, a) { this.eStore[t] || (this.eStore[t] = []), this.eStore[t].push((function(i) { return e.call(a, i); })); }, s.prototype.emit = function(t, e) { this.eStore[t] && this.eStore[t].forEach((function(a) { return a(e); })); }, s; })(), V = (function() { function s(t) { this.coordinator = new U(), this.data = [], this.radi = [], this.min = 10, this.max = 1, this.xField = t.xField || y.defaultXField, this.yField = t.yField || y.defaultYField, this.valueField = t.valueField || y.defaultValueField, this.radius = t.radius || y.defaultRadius; } return s.prototype._organiseData = function(t, e) { var a = t[this.xField], i = t[this.yField], n = this.radi, r = this.data, o = this.max, h = this.min, d = t[this.valueField] || 1, c = t.radius || this.radius; n[a] || (r[a] = [], n[a] = []), n[a][i] ? r[a][i] += d : (r[a][i] = d, n[a][i] = c); var l = r[a][i]; return l ? l > o ? (e ? this.setDataMax(l) : this.max = l, !1) : l < h ? (e ? this.setDataMin(l) : this.min = l, !1) : void 0 : { x: a, y: i, value: d, radius: c, min: h, max: o }; }, s.prototype._unOrganizeData = function() { for (var t = [], e = 0; e < this.radi.length; e++) for (var a = 0; a < this.radi[e].length; a++) t.push({ x: e, y: a, radius: this.radi[e][a], value: this.radi[e][a] }); return { min: this.min, max: this.max, data: t }; }, s.prototype._onExtremaChange = function() { this.coordinator.emit("extremachange", { min: this.min, max: this.max }); }, s.prototype.addData = function(t) { var e = this._organiseData(t, !0); e && (this.data.length === 0 && (this.min = e.value, this.max = e.value), this.coordinator.emit("renderpartial", { min: this.min, max: this.max, data: [e] })); }, s.prototype.setData = function(t) { var e = t.data; this.data = [], this.radi = []; for (var a = 0; a < e.length; a++) this._organiseData(e[a], !1); return this.min = t.min || 0, this.max = t.max || 100, this._onExtremaChange(), this.coordinator.emit("renderall", this._getInternalData()), this; }, s.prototype.setDataMax = function(t) { return this.max = t, this._onExtremaChange(), this.coordinator.emit("renderall", this._getInternalData()), this; }, s.prototype.setDataMin = function(t) { return this.min = t, this._onExtremaChange(), this.coordinator.emit("renderall", this._getInternalData()), this; }, s.prototype._getInternalData = function() { return { max: this.max, min: this.min, data: this.data, radi: this.radi }; }, s.prototype.getData = function() { return this._unOrganizeData(); }, s; })(), N = (function() { function s(t) { this.canvas = t.canvas || document.createElement("canvas"), this.ctx = this.canvas.getContext("2d"), this.shadowCanvas = t.shadowCanvas || document.createElement("canvas"), this.shadowCtx = this.shadowCanvas.getContext("2d"), this.width = t.width || 512, this.height = t.height || 512, this.max = 100, this.min = 1, this.blur = 1, this.opacity = 1, this.maxOpacity = 1, this.minOpacity = 0, this.useGradientOpacity = !1, this.canvas.style.cssText = this.shadowCanvas.style.cssText = "position:absolute;left:0;top:0;", t.container && (t.container.style.position = "relative", t.container.appendChild(this.canvas)), this.renderBoundaries = [1e4, 1e4, 0, 0], this.palette = this._getColorPalette(t), this.templates = [], this._setStyles(t); } return s.prototype.renderPartial = function(t) { t.data.length > 0 && (this._drawAlpha(t), this._colorize()); }, s.prototype.renderAll = function(t) { this._clear(), t.data.length > 0 && (this._drawAlpha(this._prepareData(t)), this._colorize()); }, s.prototype.updateConfig = function(t) { t.gradient && this._updateGradient(t), this._setStyles(t); }, s.prototype.setDimensions = function(t, e) { this.width = this.canvas.width = this.shadowCanvas.width = t, this.height = this.canvas.height = this.shadowCanvas.height = e; }, s.prototype.getValueAt = function(t) { if (!this.shadowCtx) return 0; var e = this.shadowCtx.getImageData(t.x, t.y, 1, 1); return Math.abs(this.max - this.min) * (e.data[3] / 255) >> 0; }, s.prototype.getDataURL = function() { return this.canvas.toDataURL(); }, s.prototype._getColorPalette = function(t) { var e = t.gradient || y.defaultGradient, a = document.createElement("canvas"), i = a.getContext("2d"); if (a.width = 256, a.height = 1, !i) return new Uint8ClampedArray(1024); var n = i.createLinearGradient(0, 0, 256, 1); for (var r in e) n.addColorStop(Number(r), e[r]); return i.fillStyle = n, i.fillRect(0, 0, 256, 1), i.getImageData(0, 0, 256, 1).data; }, s.prototype._getPointTemplate = function(t, e) { var a = document.createElement("canvas"), i = a.getContext("2d"); if (!i) return a; var n = t, r = t; if (a.width = a.height = 2 * t, e === 1) i.beginPath(), i.arc(n, r, t, 0, 2 * Math.PI, !1), i.fillStyle = "rgba(0,0,0,1)", i.fill(); else { var o = i.createRadialGradient(n, r, t * e, n, r, t); o.addColorStop(0, "rgba(0,0,0,1)"), o.addColorStop(1, "rgba(0,0,0,0)"), i.fillStyle = o, i.fillRect(0, 0, 2 * t, 2 * t); } return a; }, s.prototype._prepareData = function(t) { for (var e = [], a = t.min, i = t.max, n = t.radi, r = t.data, o = Object.keys(r), h = o.length; h--; ) for (var d = o[h], c = Object.keys(r[d]), l = c.length; l--; ) { var p = c[l], u = r[d][p], m = n[d][p]; e.push({ x: Number(d), y: Number(p), value: u, radius: m }); } return { min: a, max: i, data: e }; }, s.prototype._setStyles = function(t) { this.blur = t.blur === 0 ? 0 : t.blur || y.defaultBlur, t.backgroundColor && (this.canvas.style.backgroundColor = t.backgroundColor), this.width = this.canvas.width = this.shadowCanvas.width = t.width || this.width, this.height = this.canvas.height = this.shadowCanvas.height = t.height || this.height, this.opacity = 255 * (t.opacity || 0), this.maxOpacity = 255 * (t.maxOpacity || y.defaultMaxOpacity), this.minOpacity = 255 * (t.minOpacity || y.defaultMinOpacity), this.useGradientOpacity = !!t.useGradientOpacity; }, s.prototype._updateGradient = function(t) { this.palette = this._getColorPalette(t); }, s.prototype._drawAlpha = function(t) { for (var e = this.min = t.min || 0, a = this.max = t.max || 100, i = t.data || [], n = i.length, r = 1 - this.blur; n--; ) { var o = i[n], h = o.x, d = o.y, c = o.radius, l = Math.min(o.value, a), p = h - c, u = d - c; if (!this.shadowCtx) return; var m = void 0; this.templates[c] ? m = this.templates[c] : this.templates[c] = m = this._getPointTemplate(c, r); var g = (l - e) / (a - e); this.shadowCtx.globalAlpha = g < 0.01 ? 0.01 : g, this.shadowCtx.drawImage(m, p, u), p < this.renderBoundaries[0] && (this.renderBoundaries[0] = p), u < this.renderBoundaries[1] && (this.renderBoundaries[1] = u), p + 2 * c > this.renderBoundaries[2] && (this.renderBoundaries[2] = p + 2 * c), u + 2 * c > this.renderBoundaries[3] && (this.renderBoundaries[3] = u + 2 * c); } }, s.prototype._colorize = function() { var t = this.renderBoundaries[0], e = this.renderBoundaries[1], a = this.renderBoundaries[2] - t, i = this.renderBoundaries[3] - e, n = this.width, r = this.height; if (t < 0 && (t = 0), e < 0 && (e = 0), t + a > n && (a = n - t), e + i > r && (i = r - e), this.ctx && this.shadowCtx) { for (var o = this.shadowCtx.getImageData(t, e, a, i), h = 3; h < o.data.length; h += 4) { var d, c = o.data[h], l = 4 * c; l && (d = this.opacity > 0 ? this.opacity : c < this.maxOpacity ? c < this.minOpacity ? this.minOpacity : c : this.maxOpacity, o.data[h - 3] = this.palette[l], o.data[h - 2] = this.palette[l + 1], o.data[h - 1] = this.palette[l + 2], o.data[h] = this.useGradientOpacity ? this.palette[l + 3] : d); } this.ctx.putImageData(o, t, e), this.renderBoundaries = [1e3, 1e3, 0, 0]; } }, s.prototype._clear = function() { this.ctx && this.shadowCtx && (this.ctx.clearRect(0, 0, this.width, this.height), this.shadowCtx.clearRect(0, 0, this.width, this.height)); }, s; })(), $ = (function() { function s(t) { this.config = t, this.renderer = new N(this.config), this.store = new V(this.config), this._init(); } return s.prototype._init = function() { var t = this; this.store.coordinator.on("renderpartial", this.renderer.renderPartial, this.renderer), this.store.coordinator.on("renderall", this.renderer.renderAll, this.renderer), this.store.coordinator.on("extremachange", (function(e) { t.config.onExtremaChange && t.config.onExtremaChange({ min: e.min, max: e.max, gradient: t.config.gradient || y.defaultGradient }); })); }, s.prototype.addData = function(t) { return this.store.addData(t), this; }, s.prototype.setData = function(t) { return this.store.setData(t), this; }, s.prototype.setDataMaxx = function(t) { return this.store.setDataMax(t), this; }, s.prototype.setDataMin = function(t) { return this.store.setDataMin(t), this; }, s.prototype.repaint = function() { return this.store.coordinator.emit("renderall", this.store._getInternalData()), this; }, s.prototype.getData = function() { return this.store.getData(); }, s.prototype.getDataURL = function() { return this.renderer.getDataURL(); }, s.prototype.getValueAt = function(t) { return this.renderer.getValueAt(t); }, s; })(); function W(s) { const t = H(s), e = new B(); e.setFromMatrix3(t); const a = s[0], i = e.clone(); i.setPosition(a); const n = i.clone().invert(), r = s.map((p) => { const u = p.clone().applyMatrix4(n); return new P(u.x, u.y); }), o = new j(r), h = new T(o), d = new A(); d.setFromPoints(r); const c = X(d); return h.getAttribute("uv").applyMatrix3(c), h.applyMatrix4(e), { geometry: h, polygonBox: d, modelMatrix: e, planeMatrix: i, projectionMatrix: n, position: a }; } function H(s) { const [t, e, a] = s, i = new F(); i.setFromCoplanarPoints(e, t, a); const n = i.normal, r = n.clone().cross(new w(0, 0, 1)); r.equals(new w()) && r.set(1, 0, 0); const o = n.clone().cross(r); r.normalize(), o.normalize(), n.normalize(); const h = new O(); return h.elements = [ r.x, r.y, r.z, o.x, o.y, o.z, n.x, n.y, n.z ], h; } function X(s) { const t = s.min, e = s.getSize(new P()), a = new O(); return a.elements = [ e.x, 0, 0, 0, e.y, 0, t.x, t.y, 1 ], a.invert(); } function K(s, t) { if (s >= t) throw new Error("Min value must be less than max value."); return Math.floor(Math.random() * (t - s + 1)) + s; } function Y(s, t, e, a, i, n) { const { min: r, max: o, radius: h, beforePointUpdate: d, value: c, distanceInterval: l } = e, p = i.object, u = n.viewport.getIntersects(s, [p]); if (!(u?.length > 0)) return null; const m = u[0].point.clone(); if (a.length > 0 && l) { const { x, y: D, z: M } = a[a.length - 1], C = new w(x, D, M); if (l > C.distanceTo(m)) return null; } const g = Array.isArray(c) ? K(c[0], c[1]) : c, v = { ...m, radius: h, value: g }, f = d?.(t, v, a); return f === !1 ? null : typeof f == "object" ? f : v; } class q { constructor(t, e) { this.heatMapPlugin = t; const { data: a = [] } = e; this.ssp = t.ssp, this.store = t.store, this.createDrawingParam = e, this.dataPoints = [...a], this.object = t.createPolygon(e); } store = /* @__PURE__ */ new Map(); ssp; // 用时间形式加热力点 intervalId = null; // 存储鼠标当前所在点事件 currentMouseEvent = null; // 最后一次获取热力点的时间,用于节流 lastTime; // 绑定事件 events = {}; // 拖拽 isDragging = !1; // 开始绘制 isStarting = !1; createDrawingParam; // 所有热力点 dataPoints = []; // 热力图实例 object; startResolve; startReject; // 开始绘制热力点 绑定事件 start = () => { const { addTriggerType: t, doneTriggerType: e, undoTriggerType: a } = this.createDrawingParam; return new Promise((i, n) => { if (this.startResolve = i, this.startReject = n, this.isStarting) { console.warn("请先取消绘制再调用start方法"); return; } this.isStarting = !0, this.handleEventListeners("add", t, this.handleAddPoint), this.handleEventListeners("add", e, this.done), this.handleEventListeners("add", a, this.popPoint); }); }; // 取消 cancel = () => { const { id: t } = this.createDrawingParam, e = [...this.dataPoints]; this.heatMapPlugin.setDataPolygon(t, []), this.remove(), this.startReject?.(e); }; // 销毁 dispose = () => { const { id: t } = this.createDrawingParam; this.remove(), this.heatMapPlugin.removeById(t); }; // 完成 done = () => { const t = [...this.dataPoints]; this.remove(), this.startResolve?.(t); }; // 移除监听 remove = () => { const { addTriggerType: t, doneTriggerType: e, undoTriggerType: a } = this.createDrawingParam; this.dataPoints = [], this.lastTime = void 0, this.setMouseEvent(null), this.handleEventListeners("remove", t, this.handleAddPoint), this.handleEventListeners("remove", e, this.done), this.handleEventListeners("remove", a, this.popPoint), this.events = {}, this.isDragging = !1, this.ssp.controls.enabled = !0, this.isStarting = !1; }; // 增加热力点 pushPoint = (t) => { if (!t) return; const { id: e, onAdd: a } = this.createDrawingParam, i = t; this.dataPoints.push(i), this.heatMapPlugin.setDataPolygon(e, this.dataPoints), a?.(i, this.dataPoints); }; // 删除上一个热力点 popPoint = () => { const t = this.dataPoints?.length; if (!(t > 0)) return; const { id: e, beforePointUpdate: a, onUndo: i } = this.createDrawingParam, n = a?.(E.undo, this.dataPoints[t - 1], this.dataPoints); if (n === !1) return; typeof n == "object" && (this.dataPoints[t - 1] = n); const r = this.dataPoints.pop(); r && (this.heatMapPlugin.setDataPolygon(e, this.dataPoints), i?.(r, this.dataPoints)); }; handleAddPoint = (t) => { if (!t?.clientX) throw new Error("addTriggerType仅支持:time、drag、click、dblClick、rightClick、mouseDown、mouseMove、mouseUp、mouseWheel"); this.setMouseEvent(t); const e = this.getPoint(t, E.add); this.pushPoint(e); }; // 设置鼠标事件 setMouseEvent = (t) => { this.currentMouseEvent = t; }; handleEventListeners = (t, e, a) => { e?.length > 0 && (e.forEach((i) => { if (typeof i == "object") { for (const [n, r] of Object.entries(i)) { const o = `${n}_${r.join("_")}`; t === "add" && (this.events[o] = (h) => { this.keyEvents(r, a, h); }), this.ssp.signals[n]?.[t]?.(this.events[o]); } return; } if (i === "time") { if (t === "add") { const { timeInterval: n } = this.createDrawingParam || {}; this.ssp.signals.mouseMove.add(this.setMouseEvent), this.intervalId = window.setInterval(() => { this.currentMouseEvent && a(this.currentMouseEvent); }, n); } else this.ssp.signals.mouseMove.remove(this.setMouseEvent); return; } if (i === "drag") { const n = this.ssp.viewport.container, r = i; t === "add" ? (this.events[r] = (o) => { this.onPointerMove(a, o); }, n.addEventListener("pointerdown", this.onPointerdown), n.addEventListener("pointermove", this.events[r]), n.addEventListener("pointerup", this.onPointerUp)) : (n.removeEventListener("pointerdown", this.onPointerdown), n.removeEventListener("pointermove", this.events[r]), n.removeEventListener("pointerup", this.onPointerUp)); return; } this.ssp.signals[i]?.[t]?.(a); }), t === "remove" && this.intervalId && (clearInterval(this.intervalId), this.intervalId = null)); }; keyEvents(t, e, a) { t?.includes(a.code) && e(); } // 通过鼠标事件获取热力点 getPoint(t, e) { const { id: a, timeInterval: i = 100 } = this.createDrawingParam; if (this.lastTime && i && i > (/* @__PURE__ */ new Date()).getTime() - this.lastTime.getTime()) return null; this.lastTime = /* @__PURE__ */ new Date(); const n = this.store.get(a); return Y(t, e, this.createDrawingParam, this.dataPoints, n, this.ssp); } onPointerdown = (t) => { this.ssp.controls.enabled = !1, this.isDragging = !0; }; onPointerMove = (t, e) => { e.preventDefault(), this.isDragging && t(e); }; onPointerUp = (t) => { this.isDragging = !1, this.ssp.controls.enabled = !0; }; } var E = /* @__PURE__ */ ((s) => (s.add = "add", s.undo = "undo", s))(E || {}); class Q { constructor(t) { this.ssp = t, this.hmInstance = null; } hmInstance; store = /* @__PURE__ */ new Map(); maxCanvasSize = 512; create(t) { const { id: e, name: a, yAxisHeight: i, minPosition: n, maxPosition: r, data: o, min: h = 0, max: d = 100, radius: c = 100, canvasScalar: l = 1 } = t, p = new w((r.x + n.x) / 2, i, (r.z + n.z) / 2), u = r.x - n.x, m = r.z - n.z, g = this._formatCanvasSize(u * l, m * l), { canvas: v, hmInstance: f } = this.createInitData({ ...g, radius: c }); f.setData({ max: d, min: h, data: this._formatData(o, n, { width: u, height: m }, g) }); const x = new L(u, m), D = new _(v), M = new z({ map: D, depthWrite: !1, transparent: !0 }), C = new R(x, M), b = this.ssp.createPluginObject({ id: e, name: a, position: p.clone(), rotation: { x: -Math.PI / 2, y: 0, z: 0 } }, C); return this.store.set(e, { object: b, canvas: v, param: { ...t, min: h, max: d }, width: u, height: m }), b; } createPolygon(t) { const { id: e, name: a, points: i, data: n, min: r = 0, max: o = 100, radius: h = 100 } = t, d = i.map((I) => new w(I.x, I.y, I.z)), { geometry: c, projectionMatrix: l, polygonBox: p, position: u } = W(d); new k().setFromPoints(d); const { x: g, y: v } = p.getSize(new P()), f = this._formatCanvasSize(g, v), { canvas: x, hmInstance: D } = this.createInitData({ ...f, radius: h }); n && n.length > 0 && D.setData({ max: o, min: r, data: this._formatData_Polygon(n, l, p, f) }); const M = new _(x), C = new z({ map: M, depthWrite: !1, transparent: !0, side: G }), b = new this.ssp.library.BaseMesh({ id: `${e}_mesh`, name: a }, c, C); b.renderOrder = 0; const S = this.ssp.createPluginObject({ id: e, name: a, position: u }, b); return this.store.set(e, { object: S, canvas: x, param: { ...t, min: r, max: o }, width: g, height: v, projectionMatrix: l, polygonBox: p, position: u.clone() }), S; } createDrawing(t) { const { data: e = [], addTriggerType: a = ["click", "mouseMove"], doneTriggerType: i = ["dblClick", { keyDown: ["Enter"] }], undoTriggerType: n = ["rightClick", { keyDown: ["Backspace"] }], timeInterval: r = 100, min: o = 0, max: h = 100, radius: d = 100, value: c = [o, h], distanceInterval: l = 5, ...p } = t, u = { data: e, addTriggerType: a, doneTriggerType: i, undoTriggerType: n, timeInterval: r, min: o, max: h, radius: d, value: c, distanceInterval: l, ...p }; return new q(this, u); } setData(t, e) { const a = this.store.get(t); if (a) { const { object: i, canvas: n, param: { minPosition: r, min: o, max: h }, width: d, height: c } = a, l = this.createInitData(), { canvas: p, hmInstance: u } = l; u.renderer.updateConfig({ width: n.width, height: n.height }), u.setData({ max: h, min: o, data: this._formatData(e, r, { width: d, height: c }, this._formatCanvasSize(d, c)) }); const m = i.children[0].material; return this.ssp.render(() => { const g = new _(p); m.map && m.map.dispose(), m.map = g; }), i; } else return console.warn(`In soonspacejs: 插件(plugin-heat-map)未找到 id 为 '"${t}"' 的热力图对象!`); } setDataPolygon(t, e) { const a = this.store.get(t); if (a) { const { object: i, canvas: n, param: { min: r, max: o }, projectionMatrix: h, polygonBox: d, position: c } = a; if (!h) throw new Error(`${t} 不是多边形热力图类型`); const p = this.getById(t)?.getWorldPosition(new w()), u = this.createInitData(), { canvas: m, hmInstance: g } = u; g.renderer.updateConfig({ width: n.width, height: n.height }); const v = d.getSize(new P()); g.setData({ max: o, min: r, data: this._formatData_Polygon(e, h, d, this._formatCanvasSize(v.x, v.y), c, p) }); const f = i.children[0].material; return this.ssp.render(() => { const x = new _(m); f.map && f.map.dispose(), f.map = x; }), i; } else return console.warn(`In soonspacejs: 插件(plugin-heat-map)未找到 id 为 '"${t}"' 的热力图对象!`); } getById(t) { return this.ssp.getObjectById(t); } getByName(t) { return this.ssp.getObjectByName(t); } removeById(t) { return this.store.has(t) ? (this.ssp.removeObjectById(t), this.store.delete(t), !0) : !1; } createInitData(t) { const e = this.hmInstance = new $(t || {}); return { hmInstance: e, canvas: e.renderer.canvas }; } _formatCanvasSize(t, e) { const a = t / e; return t > this.maxCanvasSize && (t = this.maxCanvasSize, e = t / a), e > this.maxCanvasSize && (e = this.maxCanvasSize, t = a * e), { width: t, height: e }; } _formatData(t, e, a, i) { return t.map((n) => ({ ...n, // 取整,否则不生效 x: Math.trunc((n.x - e.x) / a.width * i.width), y: Math.trunc((n.z - e.z) / a.height * i.height) })); } _formatData_Polygon(t, e, a, i, n, r) { const o = n && r ? r.clone().sub(n) : new w(0, 0, 0); return t.map((h) => { const d = new w(h.x, h.y, h.z); d.sub(o), d.applyMatrix4(e); const { x: c, y: l } = a.getParameter(new P(d.x, d.y), new P()); return { ...h, // 取整,否则不生效 x: Math.trunc(c * i.width), // 热力图图片的 y 轴的正方向是 从 图片上方 到 图片 下方 y: Math.trunc((1 - l) * i.height) }; }); } } export { E as StartEventType, Q as default };