Gozintograph: Unterschied zwischen den Versionen
Markierung: Zurückgesetzt |
Markierung: Manuelle Zurücksetzung |
||
| Zeile 26: | Zeile 26: | ||
<html> | <html> | ||
<style> | <style> | ||
.gozinto-wrap { width:95vw; max-width:1000px; height:70vw; max-height:600px; border:0px solid #ccc; } | |||
svg { width:100%; height:100%; touch-action:none; user-select:none; } | |||
.node-rect { fill:#3498db; stroke:#1f4e78; stroke-width:2; cursor:grab; } | |||
.node-text { font-family: sans-serif; font-size:13px; fill:#000; pointer-events:none; } | |||
.edge-line { stroke:#000; stroke-width:2; fill:none; } | |||
.edge-arrow { fill:#000; } | |||
.count-circle { fill:#fff; stroke:#000; stroke-width:1.5; } | |||
.count-text { font-family:sans-serif; font-size:12px; text-anchor:middle; dominant-baseline:middle; pointer-events:none; } | |||
</style> | </style> | ||
<svg id="gozinto_svg"></svg> | <div class="gozinto-wrap"> | ||
<svg id="gozinto_svg" viewBox="0 0 1400 700" preserveAspectRatio="xMidYMid meet"> | |||
<!-- edges will be inserted dynamically --> | |||
</svg> | |||
</div> | |||
<script> | <script> | ||
(function(){ | |||
const svg = document.getElementById('gozinto_svg'); | |||
function | // Layout scale (convert logical coords to svg px) | ||
const scale = 100; // 1 unit = 100px | |||
const yOffset = 50; | |||
const | // Utility: create SVG element | ||
function svgEl(name, attrs){ | |||
const el = document.createElementNS('http://www.w3.org/2000/svg', name); | |||
for(const k in (attrs||{})) el.setAttribute(k, attrs[k]); | |||
return el; | |||
} | |||
const | // Node factory: creates a rect group with fixed size but movable | ||
function createNode(id, cx, cy, w, h, label){ | |||
const g = svgEl('g', {class:'node', 'data-id':id}); | |||
const rect = svgEl('rect', { | |||
class:'node-rect', | |||
x: (cx - w/2)*scale, y: (cy - h/2)*scale + yOffset, | |||
width: w*scale, height: h*scale, | |||
rx:6, ry:6 | |||
}); | |||
const text = svgEl('text', {class:'node-text', x: cx*scale, y: cy*scale + yOffset, 'text-anchor':'middle', 'dominant-baseline':'middle'}); | |||
text.textContent = label; | |||
g.appendChild(rect); | g.appendChild(rect); | ||
g.appendChild( | g.appendChild(text); | ||
svg.appendChild(g); | svg.appendChild(g); | ||
// state | |||
} | const node = { | ||
id, cx, cy, w, h, g, rect, text | |||
}; | |||
// dragging | |||
let dragging = false; | |||
let start = null; | |||
rect.addEventListener('pointerdown', function(e){ | |||
rect.setPointerCapture(e.pointerId); | |||
dragging = true; | |||
start = {x:e.clientX, y:e.clientY, cx:node.cx, cy:node.cy}; | |||
}); | |||
rect.addEventListener('pointermove', function(e){ | |||
if(!dragging) return; | |||
const dx = (e.clientX - start.x)/scale; | |||
const dy = (e.clientY - start.y)/scale; | |||
node.cx = start.cx + dx; | |||
node.cy = start.cy + dy; | |||
updateNode(node); | |||
updateAllEdges(); | |||
}); | |||
rect.addEventListener('pointerup', function(e){ | |||
dragging = false; | |||
rect.releasePointerCapture(e.pointerId); | |||
}); | |||
rect.addEventListener('pointercancel', function(e){ dragging=false; }); | |||
return node; | |||
} | |||
function updateNode(node){ | |||
} | node.rect.setAttribute('x', (node.cx - node.w/2)*scale); | ||
node.rect.setAttribute('y', (node.cy - node.h/2)*scale + yOffset); | |||
node.text.setAttribute('x', node.cx*scale); | |||
node.text.setAttribute('y', node.cy*scale + yOffset); | |||
} | |||
function | // compute intersection of ray center->target with rectangle border (axis-aligned) | ||
const | function intersectRectBorder(node, targetX, targetY){ | ||
const cx = node.cx, cy = node.cy; | |||
const dx = targetX - cx, dy = targetY - cy; | |||
const xMin = node.cx - node.w/2, xMax = node.cx + node.w/2; | |||
const yMin = node.cy - node.h/2, yMax = node.cy + node.h/2; | |||
let tCandidates = []; | |||
if(Math.abs(dx) > 1e-9){ | |||
let t1 = (xMin - cx)/dx; | |||
let y1 = cy + t1*dy; | |||
if(t1>0 && y1>=yMin-1e-9 && y1<=yMax+1e-9) tCandidates.push({t:t1,x:xMin,y:y1}); | |||
let t2 = (xMax - cx)/dx; | |||
let y2 = cy + t2*dy; | |||
if(t2>0 && y2>=yMin-1e-9 && y2<=yMax+1e-9) tCandidates.push({t:t2,x:xMax,y:y2}); | |||
} | } | ||
if ( | if(Math.abs(dy) > 1e-9){ | ||
let t3 = (yMin - cy)/dy; | |||
let x3 = cx + t3*dx; | |||
if(t3>0 && x3>=xMin-1e-9 && x3<=xMax+1e-9) tCandidates.push({t:t3,x:x3,y:yMin}); | |||
let t4 = (yMax - cy)/dy; | |||
let x4 = cx + t4*dx; | |||
if(t4>0 && x4>=xMin-1e-9 && x4<=xMax+1e-9) tCandidates.push({t:t4,x:x4,y:yMax}); | |||
} | } | ||
} | if(tCandidates.length===0) return {x:cx,y:cy}; | ||
tCandidates.sort((a,b)=>a.t-b.t); | |||
return {x:tCandidates[0].x, y:tCandidates[0].y}; | |||
} | |||
function | // circle boundary point toward target | ||
const | function pointOnCircle(cx, cy, R, tx, ty){ | ||
const dx = tx - cx, dy = ty - cy; | |||
const d = Math.sqrt(dx*dx + dy*dy); | |||
if(d < 1e-9) return {x:cx, y:cy}; | |||
return {x: cx + R*dx/d, y: cy + R*dy/d}; | |||
} | |||
const | // arrowhead path (triangle) at (x,y) pointing to direction (ux,uy) | ||
const | function makeArrowHead(x, y, ux, uy, size){ | ||
// perpendicular | |||
const px = -uy, py = ux; | |||
const p1x = x, p1y = y; | |||
const p2x = x - ux*size + px*size*0.5; | |||
const p2y = y - uy*size + py*size*0.5; | |||
const p3x = x - ux*size - px*size*0.5; | |||
const p3y = y - uy*size - py*size*0.5; | |||
return `M ${p1x} ${p1y} L ${p2x} ${p2y} L ${p3x} ${p3y} Z`; | |||
} | |||
// Edge structure: {fromNode, toNode, amount, circle, lineA, lineB, arrow} | |||
const edges = []; | |||
const | function makeConnection(fromNode, toNode, amount, yMid, xOffset){ | ||
const | const group = svgEl('g', {}); | ||
const circle = svgEl('circle', {class:'count-circle'}); | |||
const text = svgEl('text', {class:'count-text'}); | |||
const lineA = svgEl('path', {class:'edge-line', fill:'none'}); // from rect -> circle (path to allow potential future styling) | |||
const lineB = svgEl('path', {class:'edge-line', fill:'none'}); | |||
const arrow = svgEl('path', {class:'edge-arrow'}); | |||
group.appendChild(lineA); | |||
group.appendChild(lineB); | |||
group.appendChild(circle); | |||
group.appendChild(text); | |||
group.appendChild(arrow); | |||
svg.appendChild(group); | |||
const | const e = {fromNode, toNode, amount, circle, text, lineA, lineB, arrow, yMid, xOffset}; | ||
edges.push(e); | |||
updateEdge(e); | |||
} | |||
function updateEdge(e){ | |||
// circle center is midpoint between centers with offset | |||
const cx = (e.fromNode.cx + e.toNode.cx)/2 + (e.xOffset||0); | |||
const cy = e.yMid; | |||
const R = 0.35; | |||
// compute in logical coords | |||
const pFrom = intersectRectBorder(e.fromNode, cx, cy); | |||
const pTo = intersectRectBorder(e.toNode, cx, cy); | |||
const cIn = pointOnCircle(cx, cy, R, pFrom.x, pFrom.y); | |||
const cOut = pointOnCircle(cx, cy, R, pTo.x, pTo.y); | |||
// convert to px | |||
function px(p){ return [p.x*scale, p.y*scale + yOffset]; } | |||
const pf = px(pFrom), pcIn = px(cIn), pcOut = px(cOut), pt = px(pTo); | |||
// line A: from rect edge -> circle edge (no arrow) | |||
lineAPath = `M ${pf[0]} ${pf[1]} L ${pcIn[0]} ${pcIn[1]}`; | |||
e.lineA.setAttribute('d', lineAPath); | |||
// | // line B: circle edge -> rect edge (arrowhead drawn separately) | ||
e.lineB.setAttribute('d', `M ${pcOut[0]} ${pcOut[1]} L ${pt[0]} ${pt[1]}`); | |||
// | // circle | ||
e.circle.setAttribute('cx', (cx*scale)); | |||
e.circle.setAttribute('cy', (cy*scale + yOffset)); | |||
e.circle.setAttribute('r', R*scale); | |||
e.circle.setAttribute('class','count-circle'); | |||
// | // text | ||
e.text.setAttribute('x', cx*scale); | |||
e.text.setAttribute('y', cy*scale + yOffset); | |||
e.text.textContent = e.amount; | |||
// | // arrow head: compute unit vector from cOut -> pTo | ||
var ux = (pt[0]-pcOut[0]), uy = (pt[1]-pcOut[1]); | |||
var L = Math.sqrt(ux*ux + uy*uy); | |||
if(L<1e-6) L=1; | |||
ux/=L; uy/=L; | |||
const arrowSize = 12; | |||
const arrowPath = makeArrowHead(pt[0], pt[1], ux, uy, arrowSize); | |||
e.arrow.setAttribute('d', arrowPath); | |||
e.arrow.setAttribute('class','edge-arrow'); | |||
} | |||
// create nodes (logical coords) | |||
const nodes = {}; | |||
nodes.E1 = createNode('E1', 0, 6.8, 1.0, 0.6, 'E1'); | |||
nodes.E2 = createNode('E2', 2.5, 6.8, 1.0, 0.6, 'E2'); | |||
nodes.E3 = createNode('E3', 5, 6.8, 1.0, 0.6, 'E3'); | |||
nodes.E4 = createNode('E4', 7.5, 6.8, 1.0, 0.6, 'E4'); | |||
nodes.B1 = createNode('B1', 0.75, 3, 1.0, 0.6, 'B1'); | |||
nodes.B2 = createNode('B2', 2.5, 3, 1.0, 0.6, 'B2'); | |||
nodes.B3 = createNode('B3', 5, 3, 1.0, 0.6, 'B3'); | |||
nodes.B4 = createNode('B4', 7.5, 3, 1.0, 0.6, 'B4'); | |||
nodes.B5 = createNode('B5', 10, 3, 1.0, 0.6, 'B5'); | |||
// create edges (with small xOffsets to avoid overlap) | |||
makeConnection(nodes.E1, nodes.B1, '2', 5.6, -0.2); | |||
makeConnection(nodes.E2, nodes.B1, '1', 5.6, 0.2); | |||
makeConnection(nodes.E1, nodes.B2, '2', 5.6, -0.2); | |||
makeConnection(nodes.E2, nodes.B2, '1', 5.6, 0.2); | |||
makeConnection(nodes.E1, nodes.B3, '1', 5.6, -0.3); | |||
makeConnection(nodes.E2, nodes.B3, '1', 5.6, 0.0); | |||
makeConnection(nodes.E3, nodes.B3, '1', 5.6, 0.3); | |||
makeConnection(nodes.E1, nodes.B4, '2', 5.6, -0.3); | |||
makeConnection(nodes.E3, nodes.B4, '1', 5.6, 0.0); | |||
makeConnection(nodes.E4, nodes.B4, '1', 5.6, 0.3); | |||
makeConnection(nodes.E1, nodes.B5, '1', 5.6, -0.2); | |||
makeConnection(nodes.E4, nodes.B5, '2', 5.6, 0.2); | |||
// | // update all edges | ||
function updateAllEdges(){ edges.forEach(e=>updateEdge(e)); } | |||
// | // initial update | ||
updateAllEdges(); | |||
// re-update edges also when window resizes (visual) | |||
window.addEventListener('resize', updateAllEdges); | |||
})(); | |||
</script> | </script> | ||
</html> | </html> | ||