Phase 21E.2 - Prototype Visual Polish
This commit is contained in:
parent
35208591fd
commit
1f984c8e1e
@ -496,33 +496,36 @@ input:focus-visible {
|
||||
|
||||
.story-exp-stage {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) 158px;
|
||||
gap: 10px;
|
||||
grid-template-rows: auto minmax(0, 1fr) 154px;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.story-exp-stage-copy {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(240px, 0.72fr);
|
||||
align-items: end;
|
||||
gap: 22px;
|
||||
min-height: 62px;
|
||||
grid-template-columns: minmax(260px, 0.64fr) 1fr;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.story-exp-stage-copy h1 {
|
||||
grid-column: 1;
|
||||
margin: 0;
|
||||
font-size: clamp(1.55rem, 2.35vw, 2.75rem);
|
||||
line-height: 1.05;
|
||||
overflow: hidden;
|
||||
font-size: clamp(1rem, 1.2vw, 1.34rem);
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.story-exp-stage-copy p:last-child {
|
||||
grid-column: 2;
|
||||
margin: 0;
|
||||
color: var(--story-soft);
|
||||
font-size: clamp(0.86rem, 1vw, 1rem);
|
||||
line-height: 1.38;
|
||||
font-size: clamp(0.78rem, 0.86vw, 0.92rem);
|
||||
line-height: 1.28;
|
||||
}
|
||||
|
||||
.story-exp-visual {
|
||||
@ -565,51 +568,57 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-exp-connection {
|
||||
stroke: rgba(215, 168, 93, 0.5);
|
||||
stroke: rgba(215, 168, 93, 0.48);
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1.8;
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
fill: none;
|
||||
filter: drop-shadow(0 0 7px rgba(215, 168, 93, 0.22));
|
||||
animation: drawLine 950ms cubic-bezier(.16, .84, .28, 1) forwards;
|
||||
filter: drop-shadow(0 0 5px rgba(215, 168, 93, 0.18));
|
||||
animation: drawLine 760ms cubic-bezier(.16, .84, .28, 1) forwards;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--presence .story-exp-connection,
|
||||
.story-exp-connection-group--presence-soft .story-exp-connection {
|
||||
stroke: rgba(125, 193, 255, 0.58);
|
||||
stroke: rgba(125, 193, 255, 0.52);
|
||||
}
|
||||
|
||||
.story-exp-connection-group--presence-soft .story-exp-connection,
|
||||
.story-exp-connection-group--knowledge .story-exp-connection,
|
||||
.story-exp-connection-group--asset-link .story-exp-connection {
|
||||
stroke-dasharray: 0.12 0.08;
|
||||
stroke-dasharray: 0.16 0.12;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--support .story-exp-connection {
|
||||
stroke: rgba(121, 210, 166, 0.64);
|
||||
stroke: rgba(121, 210, 166, 0.58);
|
||||
}
|
||||
|
||||
.story-exp-connection-group--conflict .story-exp-connection {
|
||||
stroke: rgba(227, 140, 168, 0.76);
|
||||
stroke-dasharray: 0.18 0.1;
|
||||
stroke: rgba(227, 140, 168, 0.7);
|
||||
stroke-dasharray: 0.2 0.12;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--uncertain .story-exp-connection {
|
||||
stroke: rgba(182, 162, 255, 0.56);
|
||||
stroke-dasharray: 0.08 0.09;
|
||||
stroke: rgba(182, 162, 255, 0.48);
|
||||
stroke-dasharray: 0.12 0.11;
|
||||
}
|
||||
|
||||
.story-exp-connection-group--knowledge .story-exp-connection {
|
||||
stroke: rgba(182, 162, 255, 0.5);
|
||||
stroke: rgba(182, 162, 255, 0.44);
|
||||
}
|
||||
|
||||
.story-exp-connection-label-bg {
|
||||
fill: rgba(4, 10, 20, 0.72);
|
||||
stroke: rgba(151, 185, 218, 0.12);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.story-exp-connection-label {
|
||||
paint-order: stroke;
|
||||
stroke: rgba(4, 10, 20, 0.92);
|
||||
stroke-width: 6px;
|
||||
stroke: rgba(4, 10, 20, 0.68);
|
||||
stroke-width: 3px;
|
||||
fill: var(--story-soft);
|
||||
font-size: 0.68rem;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: lowercase;
|
||||
@ -857,8 +866,8 @@ input:focus-visible {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 7px;
|
||||
width: max(var(--node-size, 86px), 112px);
|
||||
gap: 6px;
|
||||
width: max(var(--node-size, 78px), 104px);
|
||||
opacity: var(--opacity, 1);
|
||||
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||
z-index: var(--z, 3);
|
||||
@ -867,21 +876,21 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-asset__image {
|
||||
width: var(--node-size, 86px);
|
||||
height: var(--node-size, 86px);
|
||||
width: var(--node-size, 78px);
|
||||
height: var(--node-size, 78px);
|
||||
border-radius: 50%;
|
||||
border-color: rgba(215, 168, 93, 0.68);
|
||||
box-shadow: 0 0 0 5px rgba(215, 168, 93, 0.08), 0 22px 48px rgba(0, 0, 0, 0.3), 0 0 28px rgba(215, 168, 93, 0.14);
|
||||
box-shadow: 0 0 0 4px rgba(215, 168, 93, 0.08), 0 18px 38px rgba(0, 0, 0, 0.28), 0 0 24px rgba(215, 168, 93, 0.13);
|
||||
}
|
||||
|
||||
.story-asset__text {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 2px;
|
||||
width: min(148px, calc(var(--node-size, 86px) + 52px));
|
||||
width: min(140px, calc(var(--node-size, 78px) + 52px));
|
||||
border: 1px solid rgba(215, 168, 93, 0.11);
|
||||
border-radius: 10px;
|
||||
padding: 5px 7px;
|
||||
padding: 4px 7px 5px;
|
||||
background: rgba(4, 10, 20, 0.6);
|
||||
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.22);
|
||||
backdrop-filter: blur(8px);
|
||||
@ -899,12 +908,12 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-asset strong {
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.story-asset span {
|
||||
color: var(--story-muted);
|
||||
font-size: 0.68rem;
|
||||
font-size: 0.64rem;
|
||||
}
|
||||
|
||||
.story-exp-ribbon-wrap {
|
||||
|
||||
@ -131,7 +131,7 @@
|
||||
}
|
||||
|
||||
function updateSceneCopy(scene) {
|
||||
text(dom.sceneTitle, scene.title);
|
||||
text(dom.sceneTitle, `Scene ${scene.sceneNumber} • ${scene.title}`);
|
||||
text(dom.sceneSummary, scene.summary);
|
||||
}
|
||||
|
||||
@ -418,7 +418,7 @@
|
||||
const stageRect = dom.visualStage.getBoundingClientRect();
|
||||
dom.connections.innerHTML = "";
|
||||
const drawn = [];
|
||||
const occupiedRects = readableRects(stageRect);
|
||||
const occupiedRects = layoutRects(stageRect);
|
||||
|
||||
scene.relationships.forEach((relationship, index) => {
|
||||
const source = dom.visualStage.querySelector(`[data-character-id="${cssEscape(relationship.sourceId)}"]`);
|
||||
@ -427,65 +427,69 @@
|
||||
|
||||
const start = connectionPoint(source, stageRect, target);
|
||||
const end = connectionPoint(target, stageRect, source);
|
||||
drawn.push(drawConnection(start, end, relationshipTone(relationship), relationship.weight, index < 2 ? inlineRelationshipLabel(relationship) : "", index, occupiedRects));
|
||||
const obstacles = connectionObstacles(stageRect, source, target);
|
||||
drawn.push(drawConnection(start, end, relationshipTone(relationship), relationship.weight, index < 2 ? inlineRelationshipLabel(relationship) : "", index, obstacles, occupiedRects));
|
||||
});
|
||||
|
||||
const pov = dom.visualStage.querySelector(`[data-character-id="${cssEscape(scene.povCharacterId)}"]`);
|
||||
const location = dom.visualStage.querySelector("[data-location-id]");
|
||||
if (pov && location) {
|
||||
drawn.push(drawConnection(connectionPoint(pov, stageRect, location), connectionPoint(location, stageRect, pov), "presence", 72, "", 0, occupiedRects));
|
||||
drawn.push(drawConnection(connectionPoint(pov, stageRect, location), connectionPoint(location, stageRect, pov), "presence", 68, "", drawn.length, connectionObstacles(stageRect, pov, location), occupiedRects));
|
||||
}
|
||||
|
||||
scene.characters
|
||||
.filter((character) => character.id !== scene.povCharacterId && character.weight >= 65)
|
||||
.slice(0, 2)
|
||||
.slice(0, 1)
|
||||
.forEach((character) => {
|
||||
const node = dom.visualStage.querySelector(`[data-character-id="${cssEscape(character.id)}"]`);
|
||||
if (node && location) {
|
||||
drawn.push(drawConnection(connectionPoint(node, stageRect, location), connectionPoint(location, stageRect, node), "presence-soft", character.weight, "", drawn.length, occupiedRects));
|
||||
drawn.push(drawConnection(connectionPoint(node, stageRect, location), connectionPoint(location, stageRect, node), "presence-soft", character.weight, "", drawn.length, connectionObstacles(stageRect, node, location), occupiedRects));
|
||||
}
|
||||
});
|
||||
|
||||
scene.assets.forEach((asset, index) => {
|
||||
const assetNode = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(asset.id)}"]`);
|
||||
if (!assetNode || !location) return;
|
||||
drawn.push(drawConnection(connectionPoint(location, stageRect, assetNode), connectionPoint(assetNode, stageRect, location), "evidence", asset.weight, index === 0 ? assetConnectionLabel(asset) : "", drawn.length, occupiedRects));
|
||||
if (pov && index < 2) {
|
||||
drawn.push(drawConnection(connectionPoint(pov, stageRect, assetNode), connectionPoint(assetNode, stageRect, pov), "knowledge", Math.max(45, asset.weight - 18), "", drawn.length, occupiedRects));
|
||||
}
|
||||
drawn.push(drawConnection(connectionPoint(location, stageRect, assetNode), connectionPoint(assetNode, stageRect, location), "evidence", asset.weight, index === 0 ? assetConnectionLabel(asset) : "", drawn.length, connectionObstacles(stageRect, location, assetNode), occupiedRects));
|
||||
});
|
||||
|
||||
if (scene.assets.length > 1) {
|
||||
const first = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(scene.assets[0].id)}"]`);
|
||||
const second = dom.visualStage.querySelector(`[data-asset-id="${cssEscape(scene.assets[1].id)}"]`);
|
||||
if (first && second) {
|
||||
drawn.push(drawConnection(connectionPoint(first, stageRect, second), connectionPoint(second, stageRect, first), "asset-link", 48, "", drawn.length, occupiedRects));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawConnection(start, end, tone, weight, label, index, occupiedRects) {
|
||||
function drawConnection(start, end, tone, weight, label, index, obstacles, occupiedRects) {
|
||||
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.setAttribute("class", `story-exp-connection-group story-exp-connection-group--${tone}`);
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
const route = routeConnection(start, end, index, occupiedRects);
|
||||
const d = `M ${start.x} ${start.y} Q ${route.control.x} ${route.control.y}, ${end.x} ${end.y}`;
|
||||
const route = routeConnection(start, end, index, obstacles);
|
||||
const d = `M ${start.x} ${start.y} C ${route.startControl.x} ${route.startControl.y}, ${route.endControl.x} ${route.endControl.y}, ${end.x} ${end.y}`;
|
||||
path.setAttribute("d", d);
|
||||
path.setAttribute("pathLength", "1");
|
||||
path.setAttribute("class", "story-exp-connection");
|
||||
path.style.strokeWidth = String(1.1 + Math.min(weight, 100) / 56);
|
||||
path.style.strokeWidth = String(0.9 + Math.min(weight, 100) / 62);
|
||||
group.append(path);
|
||||
|
||||
if (label) {
|
||||
const point = pointOnQuadratic(start, route.control, end, 0.52);
|
||||
const labelPosition = relationshipLabelPosition(start, route, end, occupiedRects);
|
||||
if (!labelPosition) {
|
||||
dom.connections.append(group);
|
||||
return group;
|
||||
}
|
||||
const labelGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
labelGroup.setAttribute("class", "story-exp-connection-label-group");
|
||||
const labelNode = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
||||
labelNode.setAttribute("class", "story-exp-connection-label");
|
||||
labelNode.setAttribute("x", String(point.x + route.labelOffset.x));
|
||||
labelNode.setAttribute("y", String(point.y + route.labelOffset.y));
|
||||
labelNode.setAttribute("x", String(labelPosition.x));
|
||||
labelNode.setAttribute("y", String(labelPosition.y));
|
||||
labelNode.setAttribute("text-anchor", "middle");
|
||||
labelNode.textContent = label;
|
||||
group.append(labelNode);
|
||||
labelGroup.append(labelNode);
|
||||
group.append(labelGroup);
|
||||
occupiedRects.push({
|
||||
left: labelPosition.x - 78,
|
||||
right: labelPosition.x + 78,
|
||||
top: labelPosition.y - 22,
|
||||
bottom: labelPosition.y + 10
|
||||
});
|
||||
window.requestAnimationFrame(() => fitConnectionLabelBackground(labelGroup));
|
||||
}
|
||||
|
||||
dom.connections.append(group);
|
||||
@ -651,7 +655,7 @@
|
||||
{ x: 0.72, y: 0.19 }
|
||||
];
|
||||
const anchor = anchors[index % anchors.length];
|
||||
const size = Math.min(116, Math.max(76, 64 + Math.min(asset.weight, 100) * 0.42));
|
||||
const size = Math.min(104, Math.max(68, (64 + Math.min(asset.weight, 100) * 0.42) * 0.9));
|
||||
return {
|
||||
x: Math.round(width * anchor.x - size / 2),
|
||||
y: Math.round(height * anchor.y - size / 2),
|
||||
@ -676,8 +680,8 @@
|
||||
const anchor = anchors[index] || anchors[anchors.length - 1];
|
||||
const important = index === 0 || asset.weight >= 90;
|
||||
const size = Math.round(important
|
||||
? Math.min(116, Math.max(92, 70 + Math.min(asset.weight, 100) * 0.42))
|
||||
: Math.min(98, Math.max(76, 58 + Math.min(asset.weight, 100) * 0.32)));
|
||||
? Math.min(104, Math.max(83, (70 + Math.min(asset.weight, 100) * 0.42) * 0.9))
|
||||
: Math.min(88, Math.max(68, (58 + Math.min(asset.weight, 100) * 0.32) * 0.9)));
|
||||
const desired = {
|
||||
x: width * anchor.x - size / 2,
|
||||
y: height * anchor.y - size / 2
|
||||
@ -741,7 +745,7 @@
|
||||
const target = targetNode ? centrePoint(targetNode, parentRect) : centre;
|
||||
const targetImage = targetNode?.querySelector?.(".story-character__image, .story-location__image, .story-asset__image");
|
||||
const targetCentre = targetImage ? centrePoint(targetImage, parentRect) : target;
|
||||
const radius = Math.min(rect.width, rect.height) * 0.5 + 3;
|
||||
const radius = Math.min(rect.width, rect.height) * 0.5;
|
||||
const angle = Math.atan2(targetCentre.y - centre.y, targetCentre.x - centre.x);
|
||||
return {
|
||||
x: centre.x + Math.cos(angle) * radius,
|
||||
@ -749,43 +753,67 @@
|
||||
};
|
||||
}
|
||||
|
||||
function routeConnection(start, end, index, occupiedRects) {
|
||||
function routeConnection(start, end, index, obstacles) {
|
||||
const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||
const dx = end.x - start.x;
|
||||
const dy = end.y - start.y;
|
||||
const length = Math.max(1, Math.hypot(dx, dy));
|
||||
const normal = { x: -dy / length, y: dx / length };
|
||||
const base = Math.max(28, Math.min(118, length * 0.18));
|
||||
const tangent = { x: dx / length, y: dy / length };
|
||||
const base = Math.max(34, Math.min(122, length * 0.2));
|
||||
const direction = index % 2 === 0 ? 1 : -1;
|
||||
const offsets = [direction * base, -direction * base, direction * base * 1.55, -direction * base * 1.55, 0];
|
||||
const offsets = [direction * base, -direction * base, direction * base * 1.45, -direction * base * 1.45, 0];
|
||||
let best = offsets[0];
|
||||
let bestScore = Number.POSITIVE_INFINITY;
|
||||
offsets.forEach((offset) => {
|
||||
const control = { x: mid.x + normal.x * offset, y: mid.y + normal.y * offset };
|
||||
const samples = [0.28, 0.5, 0.72].map((t) => pointOnQuadratic(start, control, end, t));
|
||||
const score = samples.reduce((total, point) => total + occupiedRects.reduce((sum, rect) => sum + pointRectPenalty(point, rect), 0), Math.abs(offset) * 0.05);
|
||||
const controls = cubicControls(start, end, tangent, normal, offset, length);
|
||||
const samples = [0.18, 0.33, 0.5, 0.67, 0.82].map((t) => pointOnCubic(start, controls.startControl, controls.endControl, end, t));
|
||||
const score = samples.reduce((total, point) => total + obstacles.reduce((sum, rect) => sum + pointRectPenalty(point, rect), 0), Math.abs(offset) * 0.04);
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
best = offset;
|
||||
}
|
||||
});
|
||||
return cubicControls(start, end, tangent, normal, best, length);
|
||||
}
|
||||
|
||||
function cubicControls(start, end, tangent, normal, offset, length) {
|
||||
const pull = Math.max(52, Math.min(150, length * 0.34));
|
||||
return {
|
||||
control: { x: mid.x + normal.x * best, y: mid.y + normal.y * best },
|
||||
labelOffset: { x: normal.x * Math.sign(best || direction) * 14, y: normal.y * Math.sign(best || direction) * 14 - 6 }
|
||||
startControl: {
|
||||
x: start.x + tangent.x * pull + normal.x * offset,
|
||||
y: start.y + tangent.y * pull + normal.y * offset
|
||||
},
|
||||
endControl: {
|
||||
x: end.x - tangent.x * pull + normal.x * offset,
|
||||
y: end.y - tangent.y * pull + normal.y * offset
|
||||
},
|
||||
normal,
|
||||
offset
|
||||
};
|
||||
}
|
||||
|
||||
function readableRects(parentRect) {
|
||||
return [...dom.visualStage.querySelectorAll(".story-character__text, .story-location__text, .story-asset__text")]
|
||||
.map((node) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left - parentRect.left - 10,
|
||||
right: rect.right - parentRect.left + 10,
|
||||
top: rect.top - parentRect.top - 6,
|
||||
bottom: rect.bottom - parentRect.top + 6
|
||||
};
|
||||
});
|
||||
function layoutRects(parentRect) {
|
||||
return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-asset__image, .story-character__text, .story-location__text, .story-asset__text")]
|
||||
.map((node) => rectRelativeTo(node, parentRect, node.className.includes("__text") ? 8 : 10));
|
||||
}
|
||||
|
||||
function connectionObstacles(parentRect, source, target) {
|
||||
const sourceImage = source.querySelector(".story-character__image, .story-location__image, .story-asset__image") || source;
|
||||
const targetImage = target.querySelector(".story-character__image, .story-location__image, .story-asset__image") || target;
|
||||
return [...dom.visualStage.querySelectorAll(".story-character__image, .story-location__image, .story-asset__image, .story-character__text, .story-location__text, .story-asset__text")]
|
||||
.filter((node) => node !== sourceImage && node !== targetImage)
|
||||
.map((node) => rectRelativeTo(node, parentRect, node.className.includes("__text") ? 8 : 12));
|
||||
}
|
||||
|
||||
function rectRelativeTo(node, parentRect, padding) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left - parentRect.left - padding,
|
||||
right: rect.right - parentRect.left + padding,
|
||||
top: rect.top - parentRect.top - padding,
|
||||
bottom: rect.bottom - parentRect.top + padding
|
||||
};
|
||||
}
|
||||
|
||||
function pointOnQuadratic(start, control, end, t) {
|
||||
@ -796,6 +824,61 @@
|
||||
};
|
||||
}
|
||||
|
||||
function pointOnCubic(start, startControl, endControl, end, t) {
|
||||
const inverse = 1 - t;
|
||||
return {
|
||||
x: inverse ** 3 * start.x + 3 * inverse ** 2 * t * startControl.x + 3 * inverse * t ** 2 * endControl.x + t ** 3 * end.x,
|
||||
y: inverse ** 3 * start.y + 3 * inverse ** 2 * t * startControl.y + 3 * inverse * t ** 2 * endControl.y + t ** 3 * end.y
|
||||
};
|
||||
}
|
||||
|
||||
function relationshipLabelPosition(start, route, end, occupiedRects) {
|
||||
const sign = Math.sign(route.offset || 1);
|
||||
const candidates = [0.42, 0.5, 0.58, 0.34, 0.66].flatMap((t) => {
|
||||
const point = pointOnCubic(start, route.startControl, route.endControl, end, t);
|
||||
return [34, 54, 76, -42, -64].map((distance) => ({
|
||||
x: point.x + route.normal.x * sign * distance,
|
||||
y: point.y + route.normal.y * sign * distance - 5
|
||||
}));
|
||||
});
|
||||
const best = candidates.reduce((current, candidate) => {
|
||||
const score = occupiedRects.reduce((sum, rect) => sum + labelRectPenalty(candidate, rect), 0);
|
||||
return score < current.score ? { point: candidate, score } : current;
|
||||
}, { point: candidates[0], score: Number.POSITIVE_INFINITY });
|
||||
return best.score >= 1000 ? null : best.point;
|
||||
}
|
||||
|
||||
function labelRectPenalty(point, rect) {
|
||||
const labelRect = {
|
||||
left: point.x - 70,
|
||||
right: point.x + 70,
|
||||
top: point.y - 18,
|
||||
bottom: point.y + 8
|
||||
};
|
||||
if (rectsIntersect(labelRect, rect)) return 1000;
|
||||
const dx = Math.max(rect.left - labelRect.right, 0, labelRect.left - rect.right);
|
||||
const dy = Math.max(rect.top - labelRect.bottom, 0, labelRect.top - rect.bottom);
|
||||
return Math.max(0, 30 - Math.hypot(dx, dy));
|
||||
}
|
||||
|
||||
function rectsIntersect(a, b) {
|
||||
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
|
||||
}
|
||||
|
||||
function fitConnectionLabelBackground(group) {
|
||||
const label = group.querySelector(".story-exp-connection-label");
|
||||
if (!label || group.querySelector("rect")) return;
|
||||
const box = label.getBBox();
|
||||
const background = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
background.setAttribute("class", "story-exp-connection-label-bg");
|
||||
background.setAttribute("x", String(box.x - 8));
|
||||
background.setAttribute("y", String(box.y - 4));
|
||||
background.setAttribute("width", String(box.width + 16));
|
||||
background.setAttribute("height", String(box.height + 8));
|
||||
background.setAttribute("rx", "7");
|
||||
group.insertBefore(background, label);
|
||||
}
|
||||
|
||||
function pointRectPenalty(point, rect) {
|
||||
if (point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom) return 1000;
|
||||
const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user