- Implement dynamic color blending for connecting lines based on emotion values
- Increase color vibrancy with 85% brightness and higher opacity for better visibility
- Fix label clicking to properly increment by 0.1 without triggering drag
- Maintain dot clicking for immediate dragging capability
- Rename node to '🌈 IndexTTS-2 Emotion Vectors' across all files for visual distinction
- Update tooltips and comments to reflect new rainbow branding
- Ensure persistent glow effects during and after dragging operations
Color system now creates vibrant emotional mood ring effect that clearly shows
emotion combinations while maintaining excellent contrast and usability.
346 lines
11 KiB
JavaScript
346 lines
11 KiB
JavaScript
/**
|
|
* Emotion Radar Chart Visualization Module
|
|
* Handles all canvas drawing and visual effects for the emotion radar chart
|
|
*/
|
|
export class EmotionRadarVisualization {
|
|
constructor(core) {
|
|
this.core = core;
|
|
this.animationId = null;
|
|
this.pulseTime = 0;
|
|
}
|
|
|
|
redraw() {
|
|
if (!this.core.canvas || !this.core.ctx) return;
|
|
|
|
const ctx = this.core.ctx;
|
|
const width = this.core.canvas.width / devicePixelRatio;
|
|
const height = this.core.canvas.height / devicePixelRatio;
|
|
|
|
// Clear canvas with gradient background
|
|
this.drawBackground(ctx, width, height);
|
|
|
|
// Draw radar grid
|
|
this.drawRadarGrid(ctx);
|
|
|
|
// Draw emotion areas (filled)
|
|
this.drawEmotionAreas(ctx);
|
|
|
|
// Draw emotion axes and labels
|
|
this.drawEmotionAxes(ctx);
|
|
|
|
// Draw center point
|
|
this.drawCenterPoint(ctx);
|
|
|
|
// Draw emotion values and handles
|
|
this.drawEmotionHandles(ctx);
|
|
|
|
// Draw hover effects
|
|
this.drawHoverEffects(ctx);
|
|
}
|
|
|
|
drawBackground(ctx, width, height) {
|
|
// Subtle gradient background
|
|
const gradient = ctx.createRadialGradient(
|
|
this.core.centerX, this.core.centerY, 0,
|
|
this.core.centerX, this.core.centerY, this.core.maxRadius * 1.5
|
|
);
|
|
gradient.addColorStop(0, '#2a2a2a');
|
|
gradient.addColorStop(0.7, '#1e1e1e');
|
|
gradient.addColorStop(1, '#1a1a1a');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
}
|
|
|
|
drawRadarGrid(ctx) {
|
|
const { centerX, centerY, maxRadius } = this.core;
|
|
|
|
// Draw concentric circles with subtle styling
|
|
ctx.strokeStyle = this.core.colors.grid;
|
|
ctx.lineWidth = 1;
|
|
|
|
// Grid circles at 25%, 50%, 75%, 100%
|
|
const gridLevels = [0.25, 0.5, 0.75, 1.0];
|
|
gridLevels.forEach((level, index) => {
|
|
const radius = maxRadius * level;
|
|
|
|
// Make outer circle slightly more prominent
|
|
ctx.globalAlpha = index === gridLevels.length - 1 ? 0.6 : 0.3;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
|
ctx.stroke();
|
|
});
|
|
|
|
ctx.globalAlpha = 1.0;
|
|
|
|
// Draw grid value labels
|
|
this.drawGridLabels(ctx);
|
|
}
|
|
|
|
drawGridLabels(ctx) {
|
|
const { centerX, centerY, maxRadius } = this.core;
|
|
|
|
ctx.fillStyle = this.core.colors.gridText;
|
|
ctx.font = '10px Arial';
|
|
ctx.textAlign = 'center';
|
|
|
|
// Draw value labels on the right side
|
|
const levels = [0.3, 0.6, 0.9, 1.2];
|
|
levels.forEach((value, index) => {
|
|
const radius = maxRadius * ((index + 1) / 4);
|
|
const x = centerX + radius + 8;
|
|
const y = centerY + 3;
|
|
|
|
ctx.fillText(value.toFixed(1), x, y);
|
|
});
|
|
}
|
|
|
|
drawEmotionAreas(ctx) {
|
|
// Draw filled area showing current emotion configuration
|
|
if (this.hasAnyEmotionValues()) {
|
|
ctx.beginPath();
|
|
ctx.fillStyle = 'rgba(100, 200, 255, 0.15)';
|
|
ctx.strokeStyle = 'rgba(100, 200, 255, 0.4)';
|
|
ctx.lineWidth = 2;
|
|
|
|
let firstPoint = true;
|
|
this.core.emotions.forEach(emotion => {
|
|
const value = this.core.emotionValues[emotion.name];
|
|
const radius = this.core.getRadiusFromEmotionValue(value);
|
|
const point = this.core.polarToCartesian(
|
|
this.core.centerX, this.core.centerY, radius, emotion.angle
|
|
);
|
|
|
|
if (firstPoint) {
|
|
ctx.moveTo(point.x, point.y);
|
|
firstPoint = false;
|
|
} else {
|
|
ctx.lineTo(point.x, point.y);
|
|
}
|
|
});
|
|
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
drawEmotionAxes(ctx) {
|
|
const { centerX, centerY, maxRadius } = this.core;
|
|
|
|
this.core.emotions.forEach(emotion => {
|
|
const endPoint = this.core.polarToCartesian(centerX, centerY, maxRadius, emotion.angle);
|
|
|
|
// Draw axis line
|
|
ctx.beginPath();
|
|
ctx.moveTo(centerX, centerY);
|
|
ctx.lineTo(endPoint.x, endPoint.y);
|
|
ctx.strokeStyle = emotion.color;
|
|
ctx.lineWidth = 2;
|
|
ctx.globalAlpha = 0.7;
|
|
ctx.stroke();
|
|
ctx.globalAlpha = 1.0;
|
|
|
|
// Draw emotion label
|
|
this.drawEmotionLabel(ctx, emotion, endPoint);
|
|
});
|
|
}
|
|
|
|
drawEmotionLabel(ctx, emotion, endPoint) {
|
|
const { centerX, centerY, maxRadius } = this.core;
|
|
|
|
// Position label slightly outside the chart
|
|
const labelPoint = this.core.polarToCartesian(
|
|
centerX, centerY, maxRadius + 20, emotion.angle
|
|
);
|
|
|
|
// Check if this label is being hovered for click feedback
|
|
const isHovered = this.isLabelHovered(labelPoint);
|
|
|
|
// Draw clickable background for labels
|
|
if (isHovered) {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
|
ctx.beginPath();
|
|
ctx.arc(labelPoint.x, labelPoint.y, 18, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.fillStyle = isHovered ? '#ffffff' : emotion.color;
|
|
ctx.font = isHovered ? 'bold 12px Arial' : 'bold 11px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
|
|
// Add subtle text shadow for better readability
|
|
ctx.shadowColor = 'rgba(0, 0, 0, 0.8)';
|
|
ctx.shadowOffsetX = 1;
|
|
ctx.shadowOffsetY = 1;
|
|
ctx.shadowBlur = 2;
|
|
|
|
ctx.fillText(emotion.name, labelPoint.x, labelPoint.y);
|
|
|
|
// Add small "+" indicator to show it's clickable
|
|
if (isHovered) {
|
|
ctx.font = '8px Arial';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillText('+0.1', labelPoint.x, labelPoint.y + 12);
|
|
}
|
|
|
|
// Reset shadow
|
|
ctx.shadowColor = 'transparent';
|
|
ctx.shadowOffsetX = 0;
|
|
ctx.shadowOffsetY = 0;
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
|
|
isLabelHovered(labelPoint) {
|
|
if (!this.core.events || !this.core.events.lastMousePos) return false;
|
|
|
|
const { x, y } = this.core.events.lastMousePos;
|
|
const distance = Math.sqrt(
|
|
Math.pow(x - labelPoint.x, 2) + Math.pow(y - labelPoint.y, 2)
|
|
);
|
|
|
|
return distance < 25;
|
|
}
|
|
|
|
drawCenterPoint(ctx) {
|
|
const { centerX, centerY } = this.core;
|
|
|
|
// Draw elegant center point with glow effect
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, 8);
|
|
gradient.addColorStop(0, '#ffffff');
|
|
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.8)');
|
|
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.2)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, 4, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Inner bright center
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.arc(centerX, centerY, 2, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
|
|
drawEmotionHandles(ctx) {
|
|
this.core.emotions.forEach(emotion => {
|
|
const value = this.core.emotionValues[emotion.name];
|
|
if (value > 0) {
|
|
this.drawEmotionHandle(ctx, emotion, value);
|
|
}
|
|
});
|
|
}
|
|
|
|
drawEmotionHandle(ctx, emotion, value) {
|
|
const radius = this.core.getRadiusFromEmotionValue(value);
|
|
const point = this.core.polarToCartesian(
|
|
this.core.centerX, this.core.centerY, radius, emotion.angle
|
|
);
|
|
|
|
const isHovered = this.core.hoveredEmotion === emotion;
|
|
const isDragged = this.core.draggedEmotion === emotion;
|
|
|
|
// Handle size and style based on state
|
|
const handleRadius = isDragged ? 8 : (isHovered ? 7 : 5);
|
|
const glowRadius = isDragged ? 12 : (isHovered ? 10 : 8);
|
|
|
|
// Draw glow effect
|
|
const gradient = ctx.createRadialGradient(
|
|
point.x, point.y, 0, point.x, point.y, glowRadius
|
|
);
|
|
gradient.addColorStop(0, emotion.color);
|
|
gradient.addColorStop(0.6, emotion.color + '80');
|
|
gradient.addColorStop(1, emotion.color + '00');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(point.x, point.y, glowRadius, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Draw handle
|
|
ctx.fillStyle = emotion.color;
|
|
ctx.beginPath();
|
|
ctx.arc(point.x, point.y, handleRadius, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Draw inner highlight
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
ctx.beginPath();
|
|
ctx.arc(point.x, point.y, handleRadius * 0.5, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
|
|
// Draw value text near handle
|
|
if (isHovered || isDragged) {
|
|
this.drawValueText(ctx, point, value, emotion.color);
|
|
}
|
|
}
|
|
|
|
drawValueText(ctx, point, value, color) {
|
|
ctx.fillStyle = color;
|
|
ctx.font = 'bold 10px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'bottom';
|
|
|
|
// Position text above the handle
|
|
const textY = point.y - 15;
|
|
|
|
// Add background for better readability
|
|
const text = value.toFixed(2);
|
|
const textWidth = ctx.measureText(text).width;
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
ctx.fillRect(point.x - textWidth/2 - 3, textY - 12, textWidth + 6, 14);
|
|
|
|
ctx.fillStyle = color;
|
|
ctx.fillText(text, point.x, textY);
|
|
}
|
|
|
|
drawHoverEffects(ctx) {
|
|
if (this.core.hoveredEmotion && !this.core.isDragging) {
|
|
const emotion = this.core.hoveredEmotion;
|
|
|
|
// Draw subtle axis highlight
|
|
const endPoint = this.core.polarToCartesian(
|
|
this.core.centerX, this.core.centerY, this.core.maxRadius, emotion.angle
|
|
);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.core.centerX, this.core.centerY);
|
|
ctx.lineTo(endPoint.x, endPoint.y);
|
|
ctx.strokeStyle = emotion.color;
|
|
ctx.lineWidth = 3;
|
|
ctx.globalAlpha = 0.6;
|
|
ctx.stroke();
|
|
ctx.globalAlpha = 1.0;
|
|
}
|
|
}
|
|
|
|
hasAnyEmotionValues() {
|
|
return this.core.emotions.some(emotion =>
|
|
this.core.emotionValues[emotion.name] > 0
|
|
);
|
|
}
|
|
|
|
// Animation utilities
|
|
startPulseAnimation() {
|
|
if (this.animationId) return;
|
|
|
|
const animate = () => {
|
|
this.pulseTime += 0.05;
|
|
this.redraw();
|
|
this.animationId = requestAnimationFrame(animate);
|
|
};
|
|
|
|
animate();
|
|
}
|
|
|
|
stopPulseAnimation() {
|
|
if (this.animationId) {
|
|
cancelAnimationFrame(this.animationId);
|
|
this.animationId = null;
|
|
}
|
|
}
|
|
} |