// Arc-based drag system for Go90 Scale
console.log("Arc drag system loaded");
// Arc configuration
const ARC_CONFIG = {
centerX: 0, // Will be set based on canvas width
centerY: 0, // Will be set based on canvas height
radius: 300,
startAngle: -Math.PI / 2, // -90° (top, 0 rating)
endAngle: 0, // 0° (right, 90 rating)
baseDistance: 50, // Base distance from arc for service icons
crowdingOffset: 120, // Distance to push out for each overlapping service
iconSize: 80,
};
let canvas, ctx, canvasContainer;
let servicePositions = {}; // Store { domain: { rating, angle, distance } }
let currentDrag = null;
function initDragAndDrop() {
console.log("initDragAndDrop called - arc mode");
canvasContainer = document.getElementById("dragCanvas");
canvas = document.getElementById("arcCanvas");
ctx = canvas.getContext("2d");
// Set canvas size
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
// Draw the arc scale
drawArcScale();
// Setup service items for dragging
const serviceItems = document.querySelectorAll(".service-item");
serviceItems.forEach(setupServiceDrag);
}
function resizeCanvas() {
const rect = canvasContainer.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
// Set arc center
ARC_CONFIG.centerX = canvas.width / 2;
ARC_CONFIG.centerY = canvas.height - 100;
drawArcScale();
redrawServices();
}
function drawArcScale() {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const { centerX, centerY, radius, startAngle, endAngle } = ARC_CONFIG;
// Draw gradient arc
const gradient = ctx.createLinearGradient(
centerX,
centerY + radius * Math.sin(startAngle),
centerX + radius * Math.cos(endAngle),
centerY + radius * Math.sin(endAngle),
);
gradient.addColorStop(0, "#32cd32");
gradient.addColorStop(0.2, "#adff2f");
gradient.addColorStop(0.4, "#ffff00");
gradient.addColorStop(0.6, "#ffa500");
gradient.addColorStop(0.8, "#ff4500");
gradient.addColorStop(1, "#8b0000");
ctx.strokeStyle = gradient;
ctx.lineWidth = 6;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.stroke();
// Draw tick marks and labels
const totalRatings = 91; // 0 to 90
for (let rating = 0; rating <= 90; rating += 10) {
const angle = ratingToAngle(rating);
const tickLength = rating % 10 === 0 ? 20 : 10;
// Outer point on arc
const x1 = centerX + radius * Math.cos(angle);
const y1 = centerY + radius * Math.sin(angle);
// Inner point (tick mark)
const x2 = centerX + (radius - tickLength) * Math.cos(angle);
const y2 = centerY + (radius - tickLength) * Math.sin(angle);
ctx.strokeStyle = "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// Draw rating label every 10
if (rating % 10 === 0) {
const labelDistance = radius - 50;
const labelX = centerX + labelDistance * Math.cos(angle);
const labelY = centerY + labelDistance * Math.sin(angle);
ctx.fillStyle = rating === 90 ? "#ff5555" : "white";
ctx.font = "bold 32px 'Innovator Grotesk', sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(rating.toString(), labelX, labelY);
}
}
// Draw indicator lines for placed services
Object.entries(servicePositions).forEach(([domain, data]) => {
drawIndicatorLine(data.angle, data.distance);
});
}
function drawIndicatorLine(angle, distance) {
const { centerX, centerY, radius } = ARC_CONFIG;
// Point on arc
const arcX = centerX + radius * Math.cos(angle);
const arcY = centerY + radius * Math.sin(angle);
// Point at service icon
const iconX = centerX + distance * Math.cos(angle);
const iconY = centerY + distance * Math.sin(angle);
ctx.strokeStyle = "white";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(arcX, arcY);
ctx.lineTo(iconX, iconY);
ctx.stroke();
ctx.setLineDash([]);
}
function ratingToAngle(rating) {
const { startAngle, endAngle } = ARC_CONFIG;
const t = rating / 90;
return startAngle + t * (endAngle - startAngle);
}
function angleToRating(angle) {
const { startAngle, endAngle } = ARC_CONFIG;
const t = (angle - startAngle) / (endAngle - startAngle);
return Math.max(0, Math.min(90, Math.round(t * 90)));
}
function setupServiceDrag(element) {
element.style.cursor = "grab";
element.addEventListener("mousedown", startDrag);
}
function startDrag(e) {
const serviceItem = e.target.closest(".service-item, .service-on-canvas");
if (!serviceItem) return;
e.preventDefault();
const domain = serviceItem.dataset.domain || serviceItem.dataset.canvasDomain;
const name = serviceItem.dataset.name;
currentDrag = {
domain,
name,
element: serviceItem,
isFromCanvas: serviceItem.classList.contains("service-on-canvas"),
};
serviceItem.style.opacity = "0.5";
document.addEventListener("mousemove", onDragMove);
document.addEventListener("mouseup", onDragEnd);
}
function onDragMove(e) {
if (!currentDrag) return;
// Update visual feedback based on mouse position relative to arc
const canvasRect = canvas.getBoundingClientRect();
const mouseX = e.clientX - canvasRect.left;
const mouseY = e.clientY - canvasRect.top;
// Calculate angle from center to mouse
const dx = mouseX - ARC_CONFIG.centerX;
const dy = mouseY - ARC_CONFIG.centerY;
const angle = Math.atan2(dy, dx);
// Constrain to arc range
const { startAngle, endAngle } = ARC_CONFIG;
const constrainedAngle = Math.max(startAngle, Math.min(endAngle, angle));
// Show preview by updating element position if it's on canvas
if (currentDrag.isFromCanvas) {
const distance = calculateDistance(currentDrag.domain, constrainedAngle);
const x = ARC_CONFIG.centerX + distance * Math.cos(constrainedAngle);
const y = ARC_CONFIG.centerY + distance * Math.sin(constrainedAngle);
currentDrag.element.style.left = x - ARC_CONFIG.iconSize / 2 + "px";
currentDrag.element.style.top = y - ARC_CONFIG.iconSize / 2 + "px";
}
}
function onDragEnd(e) {
if (!currentDrag) return;
document.removeEventListener("mousemove", onDragMove);
document.removeEventListener("mouseup", onDragEnd);
const { domain, name, element, isFromCanvas } = currentDrag;
// Check if dropped on services bar (to remove rating)
const servicesBar = document.getElementById("servicesBar");
const servicesBarRect = servicesBar.getBoundingClientRect();
if (
e.clientY >= servicesBarRect.top &&
e.clientY <= servicesBarRect.bottom &&
e.clientX >= servicesBarRect.left &&
e.clientX <= servicesBarRect.right
) {
// Remove from canvas and add back to bar
removeServiceFromCanvas(domain, name);
element.style.opacity = "1";
currentDrag = null;
return;
}
// Calculate position on arc
const canvasRect = canvas.getBoundingClientRect();
const mouseX = e.clientX - canvasRect.left;
const mouseY = e.clientY - canvasRect.top;
const dx = mouseX - ARC_CONFIG.centerX;
const dy = mouseY - ARC_CONFIG.centerY;
const angle = Math.atan2(dy, dx);
// Constrain to arc range
const { startAngle, endAngle } = ARC_CONFIG;
const constrainedAngle = Math.max(startAngle, Math.min(endAngle, angle));
const rating = angleToRating(constrainedAngle);
// Place on canvas
placeServiceOnArc(domain, name, rating);
element.style.opacity = "1";
currentDrag = null;
}
function calculateDistance(domain, angle) {
// Find the furthest "valence ring" occupied within 5 degrees BEFORE this angle
// This prevents recursive offsetting - only look backwards along the arc
// Only services at the current ring level count as crowding
const angularTolerance = 10 * (Math.PI / 180); // 5 degrees in radians
const baseDistance = ARC_CONFIG.radius + ARC_CONFIG.baseDistance;
// Find all occupied rings in the angular range
let occupiedRings = new Set();
Object.entries(servicePositions).forEach(([d, data]) => {
if (d !== domain) {
const angleDiff = angle - data.angle; // Positive if data.angle is before this angle
if (angleDiff >= 0 && angleDiff <= angularTolerance) {
// Found a service in the 5 degrees before
occupiedRings.add(data.distance);
}
}
});
// Find the first unoccupied ring
let testDistance = baseDistance;
while (occupiedRings.has(testDistance)) {
testDistance += ARC_CONFIG.crowdingOffset;
}
return testDistance;
}
function placeServiceOnArc(domain, name, rating) {
const angle = ratingToAngle(rating);
const distance = calculateDistance(domain, angle);
// Store position
servicePositions[domain] = { rating, angle, distance };
// Store in pending ratings
if (typeof pendingRatings !== "undefined") {
pendingRatings[domain] = { domain, name, rating };
}
// Remove from services bar
const servicesBar = document.getElementById("servicesBar");
const inBar = servicesBar.querySelector(`[data-domain="${domain}"]`);
if (inBar) inBar.remove();
// Remove existing on canvas
const existing = canvasContainer.querySelector(`[data-canvas-domain="${domain}"]`);
if (existing) existing.remove();
// Calculate pixel position
const x = ARC_CONFIG.centerX + distance * Math.cos(angle);
const y = ARC_CONFIG.centerY + distance * Math.sin(angle);
// Create element
const el = document.createElement("div");
el.className = "service-on-canvas";
el.dataset.canvasDomain = domain;
el.dataset.domain = domain;
el.dataset.name = name;
el.style.left = x - ARC_CONFIG.iconSize / 2 + "px";
el.style.top = y - ARC_CONFIG.iconSize / 2 + "px";
el.innerHTML = `