|
@@ -0,0 +1,223 @@
|
|
|
+import { draggable } from 'element-plus/es/components/color-picker/src/utils/draggable.mjs';
|
|
|
+import * as THREE from 'three';
|
|
|
+import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer';
|
|
|
+
|
|
|
+export class ColorBar {
|
|
|
+ constructor(scene, camera, containerElement, options = {}) {
|
|
|
+ this.scene = scene;
|
|
|
+ this.camera = camera;
|
|
|
+ this.containerElement = containerElement;
|
|
|
+ this.options = {
|
|
|
+ width: 10,
|
|
|
+ height: 300,
|
|
|
+ position: 'right',
|
|
|
+ title: 'Value',
|
|
|
+ colorMap: 'rainbow',
|
|
|
+ min: 0,
|
|
|
+ max: 1,
|
|
|
+ draggable: true,
|
|
|
+ ...options
|
|
|
+ };
|
|
|
+
|
|
|
+ this.init();
|
|
|
+ if (this.options.draggable) {
|
|
|
+ this.dragCleanup = this.enableDrag();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ init() {
|
|
|
+ // 创建主容器
|
|
|
+ this.container = document.createElement('div');
|
|
|
+ this.container.style.position = 'absolute';
|
|
|
+ this.container.style.pointerEvents = 'none';
|
|
|
+ this.container.style.display = 'flex';
|
|
|
+ this.container.style.flexDirection = 'column'; // 垂直堆叠,标题在上
|
|
|
+
|
|
|
+ // 标题元素(顶部)
|
|
|
+ this.titleElement = document.createElement('div');
|
|
|
+ this.titleElement.style.textAlign = 'center';
|
|
|
+ this.titleElement.style.fontWeight = 'bold';
|
|
|
+ this.titleElement.style.fontSize = '11px';
|
|
|
+ this.titleElement.style.marginBottom = '5px'; // 与颜色条间隔
|
|
|
+ this.titleElement.textContent = this.options.title;
|
|
|
+
|
|
|
+ // 子容器(颜色条和标签水平排列)
|
|
|
+ this.subContainer = document.createElement('div');
|
|
|
+ this.subContainer.style.display = 'flex';
|
|
|
+ this.subContainer.style.alignItems = 'flex-start';
|
|
|
+
|
|
|
+ // 颜色条元素
|
|
|
+ this.barElement = document.createElement('div');
|
|
|
+ this.barElement.className = 'color-bar';
|
|
|
+ this.barElement.style.width = `${this.options.width}px`;
|
|
|
+ this.barElement.style.height = `${this.options.height}px`;
|
|
|
+ this.barElement.style.background = this.createGradient();
|
|
|
+ this.barElement.style.borderRadius = '2px';
|
|
|
+ this.barElement.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
|
|
+
|
|
|
+ // 标签容器(右侧垂直排列)
|
|
|
+ this.labelsElement = document.createElement('div');
|
|
|
+ this.labelsElement.style.display = 'flex';
|
|
|
+ this.labelsElement.style.flexDirection = 'column';
|
|
|
+ this.labelsElement.style.justifyContent = 'space-between';
|
|
|
+ this.labelsElement.style.height = `${this.options.height}px`;
|
|
|
+ this.labelsElement.style.marginLeft = '8px';
|
|
|
+ this.labelsElement.style.fontSize = '11px';
|
|
|
+ this.labelsElement.innerHTML = `
|
|
|
+ <span>${this.options.max.toFixed(2)}</span>
|
|
|
+ <span>${((this.options.min + this.options.max) / 2).toFixed(2)}</span>
|
|
|
+ <span>${this.options.min.toFixed(2)}</span>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 组装 DOM
|
|
|
+ this.subContainer.appendChild(this.barElement);
|
|
|
+ this.subContainer.appendChild(this.labelsElement);
|
|
|
+ this.container.appendChild(this.titleElement);
|
|
|
+ this.container.appendChild(this.subContainer);
|
|
|
+ this.containerElement.appendChild(this.container);
|
|
|
+
|
|
|
+ // 设置位置
|
|
|
+ this.updatePosition();
|
|
|
+ window.addEventListener('resize', () => this.updatePosition());
|
|
|
+
|
|
|
+ if (this.options.draggable) {
|
|
|
+ this.container.style.cursor = 'move';
|
|
|
+ this.container.style.userSelect = 'none';
|
|
|
+ this.container.style.pointerEvents = 'auto';
|
|
|
+ this.container.title = '拖拽移动位置';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ createGradient() {
|
|
|
+ const stops = [];
|
|
|
+ for (let i = 0; i <= 100; i += 10) {
|
|
|
+ const t = i / 100;
|
|
|
+ const color = this.getColor(t);
|
|
|
+ stops.push(`${color} ${i}%`);
|
|
|
+ }
|
|
|
+ return `linear-gradient(to top, ${stops.join(', ')})`; // 从下到上渐变
|
|
|
+ }
|
|
|
+
|
|
|
+ getColor(t) {
|
|
|
+ const [r, g, b] = this.options.colorMap === 'rainbow'
|
|
|
+ ? this.rainbowColorMap(t)
|
|
|
+ : this.jetColorMap(t);
|
|
|
+ return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
|
|
|
+ }
|
|
|
+
|
|
|
+ rainbowColorMap(t) {
|
|
|
+ if (t < 0.25) {
|
|
|
+ return [0, t * 4, 1];
|
|
|
+ } else if (t < 0.5) {
|
|
|
+ return [0, 1, 1 - (t - 0.25) * 4];
|
|
|
+ } else if (t < 0.75) {
|
|
|
+ return [(t - 0.5) * 4, 1, 0];
|
|
|
+ } else {
|
|
|
+ return [1, 1 - (t - 0.75) * 4, 0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ jetColorMap(t) {
|
|
|
+ if (t < 0.125) {
|
|
|
+ return [0, 0, 0.5 + t * 4];
|
|
|
+ } else if (t < 0.375) {
|
|
|
+ return [0, (t - 0.125) * 4, 1];
|
|
|
+ } else if (t < 0.625) {
|
|
|
+ return [(t - 0.375) * 4, 1, 1 - (t - 0.375) * 4];
|
|
|
+ } else if (t < 0.875) {
|
|
|
+ return [1, 1 - (t - 0.625) * 4, 0];
|
|
|
+ } else {
|
|
|
+ return [1 - (t - 0.875) * 4, 0, 0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updatePosition() {
|
|
|
+ if (!this.containerElement || !this.container) return;
|
|
|
+
|
|
|
+ const { position, width, height } = this.options;
|
|
|
+ const padding = 15;
|
|
|
+ const containerRect = this.containerElement.getBoundingClientRect();
|
|
|
+ const titleHeight = 20; // 估算标题高度
|
|
|
+
|
|
|
+ let x, y;
|
|
|
+ switch (position) {
|
|
|
+ case 'left':
|
|
|
+ x = padding;
|
|
|
+ y = (containerRect.height - height - titleHeight) / 2; // 考虑标题高度
|
|
|
+ break;
|
|
|
+ case 'right':
|
|
|
+ default:
|
|
|
+ x = containerRect.width - width - padding - 40;
|
|
|
+ y = (containerRect.height - height - titleHeight) / 2;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.container.style.left = `${x}px`;
|
|
|
+ this.container.style.top = `${y}px`;
|
|
|
+ }
|
|
|
+
|
|
|
+ update(min, max, title) {
|
|
|
+ this.options.min = min;
|
|
|
+ this.options.max = max;
|
|
|
+ if (title) this.options.title = title;
|
|
|
+
|
|
|
+ this.barElement.style.background = this.createGradient();
|
|
|
+ this.labelsElement.innerHTML = `
|
|
|
+ <span>${max.toFixed(2)}</span>
|
|
|
+ <span>${((min + max) / 2).toFixed(2)}</span>
|
|
|
+ <span>${min.toFixed(2)}</span>
|
|
|
+ `;
|
|
|
+ this.titleElement.textContent = title || this.options.title;
|
|
|
+ }
|
|
|
+
|
|
|
+ enableDrag() {
|
|
|
+ let isDragging = false;
|
|
|
+ let offsetX, offsetY;
|
|
|
+
|
|
|
+ const onMouseDown = (e) => {
|
|
|
+ isDragging = true;
|
|
|
+ const rect = this.container.getBoundingClientRect();
|
|
|
+ offsetX = e.clientX - rect.left;
|
|
|
+ offsetY = e.clientY - rect.top;
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ };
|
|
|
+
|
|
|
+ const onMouseMove = (e) => {
|
|
|
+ if (!isDragging) return;
|
|
|
+ const containerRect = this.containerElement.getBoundingClientRect();
|
|
|
+ const maxX = containerRect.width - this.container.offsetWidth;
|
|
|
+ const maxY = containerRect.height - this.container.offsetHeight;
|
|
|
+ let newX = e.clientX - offsetX - containerRect.left;
|
|
|
+ let newY = e.clientY - offsetY - containerRect.top;
|
|
|
+ newX = Math.max(0, Math.min(newX, maxX));
|
|
|
+ newY = Math.max(0, Math.min(newY, maxY));
|
|
|
+ this.container.style.left = `${newX}px`;
|
|
|
+ this.container.style.top = `${newY}px`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const onMouseUp = () => {
|
|
|
+ isDragging = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ this.container.addEventListener('mousedown', onMouseDown);
|
|
|
+ document.addEventListener('mousemove', onMouseMove);
|
|
|
+ document.addEventListener('mouseup', onMouseUp);
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ this.container.removeEventListener('mousedown', onMouseDown);
|
|
|
+ document.removeEventListener('mousemove', onMouseMove);
|
|
|
+ document.removeEventListener('mouseup', onMouseUp);
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ dispose() {
|
|
|
+ if (this.dragCleanup) this.dragCleanup();
|
|
|
+ this.container.remove();
|
|
|
+ window.removeEventListener('resize', () => this.updatePosition());
|
|
|
+ if (this.labelObject) {
|
|
|
+ this.scene.remove(this.labelObject);
|
|
|
+ this.labelObject = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|