Files
diodiogod-TTS-Audio-Suite/web/emotion_radar_visualization.js
diodiogod e2253d4543 Enhance IndexTTS-2 radar chart with dynamic color blending and refined interactions
- 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.
2025-09-15 22:22:38 -03:00

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;
}
}
}