{"id":764,"date":"2025-12-30T18:36:14","date_gmt":"2025-12-30T17:36:14","guid":{"rendered":"https:\/\/www.tvj.de\/kt\/?page_id=764"},"modified":"2026-01-16T14:16:23","modified_gmt":"2026-01-16T13:16:23","slug":"turtles-baseball-strategy-simulator","status":"publish","type":"page","link":"https:\/\/www.tvj.de\/kt\/turtles-baseball-strategy-simulator\/","title":{"rendered":"Turtles Baseball Strategy Simulator"},"content":{"rendered":"\n<div id=\"baseball\" style=\"margin-top: 1%;\">\n    <div id=\"contextMenu\" \n      style=\"position:absolute; display:none; background:#1f2937; \n              border:1px solid #444; padding:8px; border-radius:6px;\n              z-index:9999; color:white; font-size:14px;\">\n      <div id=\"menuAddHit\"    style=\"padding:5px; cursor:pointer;\">Schlaglinie hinzuf\u00fcgen (rot gestrichelt)<\/div>\n      <div id=\"menuAddRunning\" style=\"padding:5px; cursor:pointer;\">Lauflinie hinzuf\u00fcgen (wei\u00df)<\/div>\n      <div id=\"menuAddThrow\"  style=\"padding:5px; cursor:pointer;\">Wurflinie hinzuf\u00fcgen (wei\u00df gestrichelt)<\/div>\n      <div id=\"menuToggleCurve\" style=\"padding:5px; cursor:pointer;\">\n          Toggle Curved \/ Straight\n      <\/div>\n      <div id=\"menuDeleteLine\" style=\"padding:5px; cursor:pointer; color:#f87171;\">\n        Delete Line\n      <\/div>\n  <\/div>\n   \n<h1>Turtles Baseball Strategy Simulator<\/h1>\n<div class=\"script-info-box\">\n  <h3>Beschreibung<\/h3>\n  <p>\n    Dieser Turtles Baseball Strategy Simulator dient zur <strong>visualen Darstellung und Analyse von Baseball-Spielsituationen<\/strong>.\n    Auf einem Baseballfeld (Draufsicht) k\u00f6nnen Verteidiger und L\u00e4ufer frei positioniert, Spielz\u00fcge\n    eingezeichnet und Abl\u00e4ufe erkl\u00e4rt werden. Wurf-, Schlag- und Laufwege lassen sich interaktiv zeichnen,\n    bearbeiten und animieren. Zus\u00e4tzlich k\u00f6nnen Spielst\u00e4nde wie <strong>Outs<\/strong>, Spielerpositionen\n    und komplette Spielszenarien gespeichert und wieder geladen werden.\n  <\/p>\n  <p>\n    Das Tool eignet sich ideal f\u00fcr <strong>Trainer, Spieler, Analysten und Pr\u00e4sentationen<\/strong>, um\n    Taktiken, Spielsituationen und Trainingsinhalte anschaulich zu vermitteln \u2013 sowohl am Desktop als auch\n    auf Touch-Ger\u00e4ten.\n  <\/p>\n  <p>\n    Starten Sie das Tool am besten indem Sie das Preset &#8222;Default Field Setup&#8220; laden, indem Sie es aus dem &#8212; Preset &#8212; ausw\u00e4hlen.\n  <\/p>\n<\/div>\n    <div class=\"toolbar\" id=\"toolbar\">\n \n      <div class=\"toolgroup\">\n        <button class=\"toolbtn active\" id=\"toolSelect\" data-tool=\"select\">Ausw\u00e4hlen<\/button>\n        <button id=\"undoBtn\" class=\"toolbtn\">Undo<\/button>\n        <button id=\"redoBtn\" class=\"toolbtn\">Redo<\/button>\n        <button id=\"deleteLineBtn\" class=\"toolbtn\">Linie l\u00f6schen<\/button>\n<\/div>\n<div class=\"toolgroup\">\n<button class=\"toolbtn\" id=\"toolHit\" data-tool=\"hit\">Schlaglinie hinzuf\u00fcgen<span class=\"pill red\">rot gestrichelt<\/span><\/button>\n        <button class=\"toolbtn\" id=\"toolRun\" data-tool=\"running\">Lauflinie hinzuf\u00fcgen<span class=\"pill white\">wei\u00df<\/span><\/button>\n        <button class=\"toolbtn\" id=\"toolThrow\" data-tool=\"throw\">Wurflinie hinzuf\u00fcgen<span class=\"pill white\">wei\u00df gestrichelt<\/span><\/button>\n        <\/div>\n\n      <div class=\"toolgroup\">\n      <button id=\"exportBtn\" class=\"toolbtn\">Export<\/button>\n        <button id=\"importBtn\" class=\"toolbtn\">Import<\/button>\n        <select id=\"presetSelect\" class=\"toolbtn\" style=\"padding:9px 10px;\">\n            <option value=\"\">&#8212; Preset &#8212;<\/option>\n            <option value=\"default\">Default Field Setup<\/option>\n            <option value=\"noOutLeftOutField\">No Out &#8211; No Runner &#8211; Left Outfield<\/option>\n        <\/select>\n        \n      <\/div>\n    <\/div>\n\n    <canvas id=\"field\" width=\"775\" height=\"675\"><\/canvas>\n  <\/div>\n\n\n\n<script>\nconst presets = {\n        default: {\n         \n  \"players\": [\n    {\n      \"x\": 388.5,\n      \"y\": 424.5,\n      \"label\": \"P\",\n      \"color\": \"#f59e0b\"\n    },\n    {\n      \"x\": 391.5,\n      \"y\": 592.5,\n      \"label\": \"C\",\n      \"color\": \"#f59e0b\"\n    },\n    {\n      \"x\": 522.5,\n      \"y\": 387.5,\n      \"label\": \"1B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 471.5,\n      \"y\": 296.5,\n      \"label\": \"2B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 262.5,\n      \"y\": 377.5,\n      \"label\": \"3B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 311,\n      \"y\": 300.5,\n      \"label\": \"SS\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 175.5,\n      \"y\": 221.5,\n      \"label\": \"LF\",\n      \"color\": \"#22c55e\"\n    },\n    {\n      \"x\": 381.5,\n      \"y\": 108.5,\n      \"label\": \"CF\",\n      \"color\": \"#22c55e\"\n    },\n    {\n      \"x\": 592.5,\n      \"y\": 204.5,\n      \"label\": \"RF\",\n      \"color\": \"#22c55e\"\n    }\n  ],\n  \"runners\": [\n    {\n      \"x\": 391.5,\n      \"y\": 555.5,\n      \"number\": 1\n    },\n    {\n      \"x\": 604.5,\n      \"y\": 595.5,\n      \"number\": 2\n    },\n    {\n      \"x\": 636.5,\n      \"y\": 597.5,\n      \"number\": 3\n    },\n    {\n      \"x\": 666.5,\n      \"y\": 596.5,\n      \"number\": 4\n    }\n  ],\n  \"lines\": [],\n  \"outs\": [false, false]\n\n        },\n        noOutLeftOutField: {\n          \"runners\": [\n    {\n      \"x\": 391.5,\n      \"y\": 555.5,\n      \"number\": 1\n    },\n    {\n      \"x\": 604.5,\n      \"y\": 595.5,\n      \"number\": 2\n    },\n    {\n      \"x\": 636.5,\n      \"y\": 597.5,\n      \"number\": 3\n    },\n    {\n      \"x\": 666.5,\n      \"y\": 596.5,\n      \"number\": 4\n    }\n  ],\n  \"players\": [\n    { \n      \"x\": 392.5,\n      \"y\": 428.5,\n      \"label\": \"P\",\n      \"color\": \"#f59e0b\"\n    },\n    {\n      \"x\": 391.5,\n      \"y\": 594.5,\n      \"label\": \"C\",\n      \"color\": \"#f97316\"\n    },\n    {\n      \"x\": 523.5,\n      \"y\": 380.5,\n      \"label\": \"1B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 456.5,\n      \"y\": 290.5,\n      \"label\": \"2B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 265.5,\n      \"y\": 377.5,\n      \"label\": \"3B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 326,\n      \"y\": 289.5,\n      \"label\": \"SS\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 173.5,\n      \"y\": 219.5,\n      \"label\": \"LF\",\n      \"color\": \"#22c55e\"\n    },\n    {\n      \"x\": 388.5,\n      \"y\": 140.5,\n      \"label\": \"CF\",\n      \"color\": \"#22c55e\"\n    },\n    {\n      \"x\": 594.5,\n      \"y\": 206.5,\n      \"label\": \"RF\",\n      \"color\": \"#22c55e\"\n    }\n  ],\n  \"runners\": [\n    {\n      \"x\": 577.5,\n      \"y\": 644.5\n    },\n    {\n      \"x\": 626.5,\n      \"y\": 644.5\n    },\n    {\n      \"x\": 391.5,\n      \"y\": 559.5\n    }\n  ],\n  \"lines\": [\n    {\n      \"type\": \"hit\",\n      \"x1\": 389.5,\n      \"y1\": 542,\n      \"x2\": 290.5,\n      \"y2\": 104,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0\n    },\n    {\n      \"type\": \"running\",\n      \"x1\": 183.5,\n      \"y1\": 213,\n      \"x2\": 270.5,\n      \"y2\": 111,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0\n    },\n    {\n      \"type\": \"running\",\n      \"x1\": 376.5,\n      \"y1\": 129,\n      \"x2\": 300.5,\n      \"y2\": 88,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0\n    },\n    {\n      \"type\": \"throw\",\n      \"x1\": 301.5,\n      \"y1\": 111,\n      \"x2\": 384.5,\n      \"y2\": 294,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0.9133000000000008\n    },\n    {\n      \"type\": \"running\",\n      \"x1\": 438.5,\n      \"y1\": 297,\n      \"x2\": 395.5,\n      \"y2\": 311,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0\n    },\n    {\n      \"type\": \"running\",\n      \"x1\": 342,\n      \"y1\": 300,\n      \"x2\": 413,\n      \"y2\": 346,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0\n    },\n    {\n      \"type\": \"running\",\n      \"x1\": 334,\n      \"y1\": 270,\n      \"x2\": 346,\n      \"y2\": 221,\n      \"curved\": false,\n      \"cx\": null,\n      \"cy\": null,\n      \"ballPos\": 0\n    }\n  ]\n}\n    };\n\n    function applyPreset(name) {\n        const preset = presets[name];\n        if (!preset) return;\n\n        \/\/ Enable undo\n        pushUndoState();\n\n        \/\/ Replace game state\n        players = JSON.parse(JSON.stringify(preset.players));\n        runners = JSON.parse(JSON.stringify(preset.runners));\n        lines   = JSON.parse(JSON.stringify(preset.lines));\n\n        \n        outs    = preset.outs ? JSON.parse(JSON.stringify(preset.outs)) : outs;\n\n        drawAll();\n    }\n\n    document.getElementById(\"presetSelect\").addEventListener(\"change\", (e) => {\n        const value = e.target.value;\n        if (value) applyPreset(value);\n    });\n\n    const canvas = document.getElementById(\"field\");\n    const ctx = canvas.getContext(\"2d\");\n    let deleteTargetLine = null;\n    let draggingWholeLine = null; \/\/ { index, offsetX, offsetY }\n    let draggingControlPoint = null; \/\/ NEW: curve handle\n    let undoStack = [];\n    let redoStack = [];\n\n\n    function cloneState() {\n        return {\n            players: JSON.parse(JSON.stringify(players)),\n            runners: JSON.parse(JSON.stringify(runners)),\n            lines:   JSON.parse(JSON.stringify(lines)),\n            outs:    JSON.parse(JSON.stringify(outs)) \/\/ NEW\n        };\n    }\n\n    function restoreState(state) {\n        players = JSON.parse(JSON.stringify(state.players));\n        runners = JSON.parse(JSON.stringify(state.runners));\n        lines   = JSON.parse(JSON.stringify(state.lines));\n        outs    = state.outs ? JSON.parse(JSON.stringify(state.outs)) : [false, false]; \/\/ NEW\n        drawAll();\n    }\n\n    function pushUndoState() {\n        undoStack.push(cloneState());\n        \/\/ once we do a new action, redo history is invalid\n        redoStack.length = 0;\n    }\n\n    function getPos(evt) {\n      const rect = canvas.getBoundingClientRect();\n      const cX = evt.touches ? evt.touches[0].clientX : evt.clientX;\n      const cY = evt.touches ? evt.touches[0].clientY : evt.clientY;\n      return {\n        x: (cX - rect.left) * (canvas.width \/ rect.width),\n        y: (cY - rect.top) * (canvas.height \/ rect.height),\n      };\n    }\n\n    const centerX = canvas.width \/ 2;\n    const centerY = canvas.height \/ 2;\n\n    const infieldRadius = 130;\n    const baseSize = 18;\n    const moundRadius = 20;\n    const foulLength = 260;\n    const warningTrackR = 250;\n\n    const playerRadius = 12;\n    let players = [\n      { x: centerX, y: centerY - infieldRadius \/ 2, label: \"P\", color: \"#f59e0b\" },\n      { x: centerX, y: centerY + infieldRadius * 0.8, label: \"C\", color: \"#f59e0b\" },\n      { x: centerX + infieldRadius * 0.7, y: centerY + infieldRadius * 0.7, label: \"1B\", color: \"#3b82f6\" },\n      { x: centerX - infieldRadius * 0.5, y: centerY, label: \"2B\", color: \"#3b82f6\" },\n      { x: centerX - infieldRadius * 0.7, y: centerY + infieldRadius * 0.7, label: \"3B\", color: \"#3b82f6\" },\n      { x: centerX - infieldRadius * 0.75, y: centerY - infieldRadius * 0.2, label: \"SS\", color: \"#3b82f6\" },\n      { x: centerX - 160, y: centerY - 160, label: \"LF\", color: \"#22c55e\" },\n      { x: centerX, y: centerY - 200, label: \"CF\", color: \"#22c55e\" },\n      { x: centerX + 160, y: centerY - 160, label: \"RF\", color: \"#22c55e\" },\n    ];\n\n\n    \/\/ ================= EXPORT \/ IMPORT =================\n    \/\/ Export now downloads a file. Import opens a file picker and loads JSON directly.\n\n    function safeFileName(name) {\n      const cleaned = (name || \"layout\")\n        .trim()\n        .replace(\/[^a-zA-Z0-9._-]+\/g, \"_\")\n        .replace(\/^_+|_+$\/g, \"\");\n      return cleaned || \"layout\";\n    }\n\n    function timestampFileSafe() {\n      const d = new Date();\n      const pad = (n) => String(n).padStart(2, \"0\");\n      return (\n        d.getFullYear() +\n        pad(d.getMonth() + 1) +\n        pad(d.getDate()) +\n        \"-\" +\n        pad(d.getHours()) +\n        pad(d.getMinutes()) +\n        pad(d.getSeconds())\n      );\n    }\n\n    function downloadTextFile(filename, text) {\n      const blob = new Blob([text], { type: \"application\/json;charset=utf-8\" });\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = filename;\n      document.body.appendChild(a);\n      a.click();\n      a.remove();\n      URL.revokeObjectURL(url);\n    }\n\n    function normalizeImportedObject(obj) {\n      \/\/ Support both formats:\n      \/\/ 1) { players, runners, lines, outs }\n      \/\/ 2) { meta: {...}, state: { players, runners, lines, outs } }\n      if (obj) {\n        if (obj.state) {\n          if (obj.state.players) return obj.state;\n        }\n      }\n      return obj;\n    }\n\n    document.getElementById(\"exportBtn\").onclick = () => {\n      const description = window.prompt(\"Description (optional):\", \"\") ?? \"\";\n      const defaultName = `layout-${timestampFileSafe()}.json`;\n      const fileNameInput = window.prompt(\"Filename:\", defaultName);\n      if (fileNameInput === null) return; \/\/ user cancelled\n\n      const fileName = safeFileName(fileNameInput.endsWith(\".json\") ? fileNameInput : `${fileNameInput}.json`);\n\n      const state = cloneState();\n      const payload = {\n        meta: {\n          description,\n          exportedAt: new Date().toISOString(),\n        },\n        state,\n      };\n\n      downloadTextFile(fileName, JSON.stringify(payload, null, 2));\n    };\n\n    \/\/ Hidden file input for import\n    const importInput = document.createElement(\"input\");\n    importInput.type = \"file\";\n    importInput.accept = \".json,.txt,application\/json,text\/plain\";\n    importInput.style.display = \"none\";\n    document.body.appendChild(importInput);\n\n    importInput.addEventListener(\"change\", () => {\n      let file = null;\n      if (importInput.files) {\n        file = importInput.files[0];\n      }\n      importInput.value = \"\"; \/\/ allow selecting same file again later\n      if (!file) return;\n\n      const reader = new FileReader();\n      reader.onload = () => {\n        try {\n          const obj = JSON.parse(String(reader.result || \"\"));\n          const state = normalizeImportedObject(obj);\n          if (!state || !state.players || !state.runners || !state.lines) {\n            alert(\"Invalid layout JSON file.\");\n            return;\n          }\n          pushUndoState();\n          restoreState(state);\n        } catch (e) {\n          alert(\"Could not parse JSON file: \" + e.message);\n        }\n      };\n      reader.onerror = () => alert(\"Could not read file.\");\n      reader.readAsText(file);\n    });\n\n    document.getElementById(\"importBtn\").onclick = () => {\n      importInput.click();\n    };\n\n\n    \/\/ ================= OUTS INDICATOR =================\n    let outs = [false, false]; \/\/ false=white, true=red\n\n    const outsUI = {\n      r: 10,\n      gap: 14,\n      y: canvas.height - 22, \/\/ bottom margin\n      xStart: 0              \/\/ computed below\n    };\n    \/\/ Center 2 outs circles\n    outsUI.xStart = canvas.width \/ 2 - (2 * outsUI.r * 2 + 1 * outsUI.gap) \/ 2;\n\n    function getOutCircleCenters() {\n      const centers = [];\n      for (let i = 0; i < 2; i++) {\n        centers.push({\n          x: outsUI.xStart + i * (outsUI.r * 2 + outsUI.gap) + outsUI.r,\n          y: outsUI.y\n        });\n      }\n      return centers;\n    }\n\n    \/\/ ================= RUNNERS =================\n    const runnerRadius = 10;\n    let runners = [\n      { x: centerX - 40, y: centerY + infieldRadius - 10, number: 1 },\n      { x: centerX + infieldRadius - 20, y: centerY + 20, number: 2 },\n      { x: centerX, y: centerY - infieldRadius - 20, number: 3 },\n      { x: centerX + 40, y: centerY + infieldRadius - 10, number: 4 }\n    ];\n\n\n    \/\/ ========================== LINE SYSTEM ================================\n  let lines = [];          \/\/ all lines\n  let drawingNewLine = null; \/\/ holds {type, x1, y1}\nlet drawingPreviewPos = null; \/\/ {x,y} current mouse position while drawing\nlet draggingLine = null;   \/\/ { index, endpoint:\"A\"|\"B\" }\n\n    let ballLine = null; \/\/ {x1,y1,x2,y2}\n    let drawingLine = false;\n    let lineDragEnd = null; \/\/ \"A\" or \"B\" endpoint being dragged\n    const endpointRadius = 10;\n\n    function addNewLinePoint(pos) {\n      if (!ballLine) {\n        ballLine = { x1: pos.x, y1: pos.y, x2: pos.x, y2: pos.y };\n        drawingLine = true;\n      } else if (drawingLine) {\n        ballLine.x2 = pos.x;\n        ballLine.y2 = pos.y;\n        drawingLine = false;\n      }\n    }\n\n    function hitTestEndpoint(pos) {\n      if (!ballLine) return null;\n      const d1 = Math.hypot(pos.x - ballLine.x1, pos.y - ballLine.y1);\n      if (d1 < endpointRadius + 4) return \"A\";\n      const d2 = Math.hypot(pos.x - ballLine.x2, pos.y - ballLine.y2);\n      if (d2 < endpointRadius + 4) return \"B\";\n      return null;\n    }\n\n    function hitTestLineSegment(x, y, L) {\n      \/\/ distance from point to segment\n      const x1 = L.x1, y1 = L.y1, x2 = L.x2, y2 = L.y2;\n\n      const A = x - x1;\n      const B = y - y1;\n      const C = x2 - x1;\n      const D = y2 - y1;\n\n      const dot = A * C + B * D;\n      const len_sq = C * C + D * D;\n      let param = -1;\n      if (len_sq !== 0) param = dot \/ len_sq;\n\n      let xx, yy;\n\n      if (param < 0) {\n          xx = x1; yy = y1;\n      } else if (param > 1) {\n          xx = x2; yy = y2;\n      } else {\n          xx = x1 + param * C;\n          yy = y1 + param * D;\n      }\n\n      const dx = x - xx;\n      const dy = y - yy;\n      return Math.sqrt(dx * dx + dy * dy); \/\/ distance to segment\n  }\n    \/\/ ========================== RIGHT CLICK MENU ================================\n    const menu = document.getElementById(\"contextMenu\");\n    let menuClickPos = {x:0, y:0};\n\n    \/\/ disable default context menu on canvas\n    \n\/\/ Open the context menu at a given client position + canvas position.\nfunction openContextMenu(clientX, clientY, pos) {\n    deleteTargetLine = null;\n\n    \/\/ find closest line (use your hitTestLineSegment or similar)\n    for (let i = lines.length - 1; i >= 0; i--) {\n        const d = hitTestLineSegment(pos.x, pos.y, lines[i]);\n        if (d < 10) {\n            deleteTargetLine = i;\n            break;\n        }\n    }\n\n    \/\/ show\/hide delete option (only on lines)\n    const del = document.getElementById(\"menuDeleteLine\");\n    if (del) del.style.display = (deleteTargetLine === null ? \"none\" : \"block\");\n\n    \/\/ show\/hide curve option (only on lines)\n    const curve = document.getElementById(\"menuToggleCurve\");\n    if (curve) curve.style.display = (deleteTargetLine === null ? \"none\" : \"block\");\n\n    menuClickPos = pos;\n    menu.style.left = clientX + \"px\";\n    menu.style.top  = clientY + \"px\";\n    menu.style.display = \"block\";\n}\n\n\/\/ Desktop: right click\ncanvas.addEventListener(\"contextmenu\", (e) => {\n    e.preventDefault();\n    openContextMenu(e.clientX, e.clientY, getPos(e));\n});\n\n\/\/ Touch: long-press to open the same menu\nconst LONG_PRESS_MS = 550;\nconst LONG_PRESS_MOVE_TOL = 10; \/\/ px in screen coords\nlet longPressTimer = null;\nlet longPressStart = null;\nlet longPressPointerId = null;\n\nfunction clearLongPress() {\n    if (longPressTimer) {\n        clearTimeout(longPressTimer);\n        longPressTimer = null;\n    }\n    longPressStart = null;\n    longPressPointerId = null;\n}\n\n    document.getElementById(\"menuToggleCurve\").onclick = () => {\n        if (deleteTargetLine !== null) {\n            pushUndoState();\n            const L = lines[deleteTargetLine];\n            L.curved = !L.curved;\n\n            if (L.curved) {\n                \/\/ set a default control point slightly above the midpoint\n                const mx = (L.x1 + L.x2) \/ 2;\n                const my = (L.y1 + L.y2) \/ 2;\n                L.cx = mx;\n                L.cy = my - 50;\n            } else {\n                L.cx = null;\n                L.cy = null;\n            }\n            drawAll();\n        }\n        menu.style.display = \"none\";\n    };\n\n    document.getElementById(\"menuDeleteLine\").onclick = () => {\n        if (deleteTargetLine !== null) {\n            pushUndoState();\n            lines.splice(deleteTargetLine, 1);\n            deleteTargetLine = null;\n            drawAll();\n        }\n        menu.style.display = \"none\";\n    };\n\n    document.addEventListener(\"click\", () => {\n      menu.style.display = \"none\";\n    });\n\n    document.getElementById(\"menuAddHit\").onclick = () => {\n        drawingNewLine = { type:\"hit\", x1:menuClickPos.x, y1:menuClickPos.y };\n        drawingPreviewPos = { x: menuClickPos.x, y: menuClickPos.y };\n        menu.style.display = \"none\";\n    };\n\n    document.getElementById(\"menuAddThrow\").onclick = () => {\n        drawingNewLine = { type:\"throw\", x1:menuClickPos.x, y1:menuClickPos.y };\n        drawingPreviewPos = { x: menuClickPos.x, y: menuClickPos.y };\n        menu.style.display = \"none\";\n    };\n\n    document.getElementById(\"menuAddRunning\").onclick = () => {\n        drawingNewLine = { type: \"running\", x1: menuClickPos.x, y1: menuClickPos.y };\n        drawingPreviewPos = { x: menuClickPos.x, y: menuClickPos.y };\n        menu.style.display = \"none\";\n    };\n\n\n    document.getElementById(\"undoBtn\").onclick = () => {\n        if (undoStack.length === 0) return;\n        const current = cloneState();\n        const prev = undoStack.pop();\n        redoStack.push(current);\n        restoreState(prev);\n    };\n\n    document.getElementById(\"redoBtn\").onclick = () => {\n        if (redoStack.length === 0) return;\n        const current = cloneState();\n        const next = redoStack.pop();\n        undoStack.push(current);\n        restoreState(next);\n    };\n\n    \/\/ ========================== FIELD DRAWING ==============================\n\n    \/\/ ============= FIELD IMAGE CONFIGURATION ===============\n    const fieldImg = new Image();\n\n    \/\/ Example image (replace this with your preferred URL)\n    fieldImg.src = \"https:\/\/www.tvj.de\/kt\/wp-content\/uploads\/diamondhigh2.png\";\n\n    \/\/ Adjustable image size + position\n    let fieldX = 0;        \/\/ left offset\n    let fieldY = 0;        \/\/ top offset\n    let fieldW = 775;      \/\/ width\n    let fieldH = 675;      \/\/ height\n    \/\/--------------------------------------------------------\n\n    \n    \/\/ Make sure players draw in front of the image\n    fieldImg.onload = () => {\n        drawAll();   \/\/ <--- This is the fix\n    };\n\n    \/*\n    function drawField() {\n        ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      \/\/ Draw the field image when ready\n      if (fieldImg.complete) {\n        ctx.drawImage(fieldImg, fieldX, fieldY, fieldW, fieldH);\n      } else {\n        fieldImg.onload = () => ctx.drawImage(fieldImg, fieldX, fieldY, fieldW, fieldH);\n      }\n    }\n      *\/\n   \nfunction drawField() {\n  ctx.drawImage(fieldImg, fieldX, fieldY, fieldW, fieldH);\n}\n\n    function rotate(cx, cy, x, y, ang) {\n      const dx = x - cx, dy = y - cy;\n      return {\n        x: cx + dx * Math.cos(ang) - dy * Math.sin(ang),\n        y: cy + dx * Math.sin(ang) + dy * Math.cos(ang)\n      };\n    }\n\n    function drawBase(x, y, size, isHome=false) {\n      ctx.save();\n      ctx.translate(x, y);\n      ctx.rotate(Math.PI \/ 4);\n      ctx.fillStyle = \"#fff\";\n      if (isHome) {\n        ctx.beginPath();\n        ctx.moveTo(-size\/2, 0);\n        ctx.lineTo(-size\/2, -size);\n        ctx.lineTo(size\/2, -size);\n        ctx.lineTo(size\/2, 0);\n        ctx.lineTo(0, size\/2);\n        ctx.closePath();\n        ctx.fill();\n      } else {\n        ctx.fillRect(-size\/2, -size\/2, size, size);\n      }\n      ctx.restore();\n    }\n\n    function drawPlayers() {\n      players.forEach(p => {\n        ctx.beginPath();\n        ctx.fillStyle = p.color;\n        ctx.arc(p.x, p.y, playerRadius, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.strokeStyle = \"rgba(0,0,0,0.5)\";\n        ctx.lineWidth = 2;\n        ctx.stroke();\n\n        ctx.fillStyle = \"#fff\";\n        ctx.font = \"11px Arial\";\n        ctx.textAlign = \"center\";\n        \/\/ ctx.fillText(p.label, p.x, p.y - playerRadius - 10);\n        ctx.fillText(p.label, p.x, p.y);\n      });\n    }\n\n    \nfunction drawLines() {\n\n  function drawArrowhead(x, y, angle, color) {\n    const size = 14;\n    ctx.fillStyle = color;\n    ctx.beginPath();\n    ctx.moveTo(x, y);\n    ctx.lineTo(\n      x - size * Math.cos(angle - Math.PI \/ 6),\n      y - size * Math.sin(angle - Math.PI \/ 6)\n    );\n    ctx.lineTo(\n      x - size * Math.cos(angle + Math.PI \/ 6),\n      y - size * Math.sin(angle + Math.PI \/ 6)\n    );\n    ctx.closePath();\n    ctx.fill();\n  }\n\n  for (const L of lines) {\n    \/\/ STYLE\n    if (L.type === \"hit\") {\n      ctx.strokeStyle = \"red\";\n      ctx.setLineDash([10, 6]);\n    } else if (L.type === \"throw\") {\n      ctx.strokeStyle = \"white\";\n      ctx.setLineDash([5, 5]); \/\/ throw: dashed\n    } else {\n      \/\/ running\n      ctx.strokeStyle = \"white\";\n      ctx.setLineDash([]); \/\/ running: solid\n    }\n    ctx.lineWidth = 3;\n\n    \/\/ DRAW LINE \/ CURVE\n    ctx.beginPath();\n    ctx.moveTo(L.x1, L.y1);\n\n    if (L.curved) {\n      if (L.cx != null) {\n        if (L.cy != null) {\n          ctx.quadraticCurveTo(L.cx, L.cy, L.x2, L.y2);\n        } else {\n          ctx.lineTo(L.x2, L.y2);\n        }\n      } else {\n        ctx.lineTo(L.x2, L.y2);\n      }\n    } else {\n      ctx.lineTo(L.x2, L.y2);\n    }\n\n    ctx.stroke();\n    ctx.setLineDash([]); \/\/ reset\n\n    \/\/ CONTROL POINT (IF CURVED)\n    if (L.curved) {\n      if (L.cx != null) {\n        if (L.cy != null) {\n          ctx.strokeStyle = \"rgba(255,255,255,0.5)\";\n          ctx.lineWidth = 1;\n          ctx.setLineDash([4, 4]);\n          ctx.beginPath();\n          ctx.moveTo(L.x1, L.y1);\n          ctx.lineTo(L.cx, L.cy);\n          ctx.lineTo(L.x2, L.y2);\n          ctx.stroke();\n          ctx.setLineDash([]);\n\n          ctx.fillStyle = \"yellow\";\n          ctx.beginPath();\n          ctx.arc(L.cx, L.cy, 6, 0, Math.PI * 2);\n          ctx.fill();\n        }\n      }\n    }\n\n    \/\/ ARROWHEAD AT END\n    let angle;\n    if (L.curved) {\n      if (L.cx != null) {\n        \/\/ tangent at end of quadratic bezier (t=1)\n        const dx = 2 * (L.x2 - L.cx);\n        const dy = 2 * (L.y2 - L.cy);\n        angle = Math.atan2(dy, dx);\n      } else {\n        angle = Math.atan2(L.y2 - L.y1, L.x2 - L.x1);\n      }\n    } else {\n      angle = Math.atan2(L.y2 - L.y1, L.x2 - L.x1);\n    }\n\n    drawArrowhead(L.x2, L.y2, angle, (L.type === \"hit\" ? \"red\" : \"white\"));\n\n    \/\/ BALL ANIMATION FOR THROW LINES\n    if (L.type === \"throw\") {\n      const p = getPointOnLine(L, L.ballPos);\n      ctx.fillStyle = \"white\";\n      ctx.beginPath();\n      ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);\n      ctx.fill();\n\n      ctx.strokeStyle = \"red\";\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      ctx.arc(p.x, p.y, 6, 0.3, Math.PI - 0.3);\n      ctx.arc(p.x, p.y, 6, Math.PI + 0.3, Math.PI * 2 - 0.3);\n      ctx.stroke();\n    }\n  }\n\n  \/\/ Live preview while drawing the second point\n  if (drawingNewLine) {\n    if (drawingPreviewPos) {\n      const tmp = {\n        type: drawingNewLine.type,\n        x1: drawingNewLine.x1,\n        y1: drawingNewLine.y1,\n        x2: drawingPreviewPos.x,\n        y2: drawingPreviewPos.y,\n        curved: false,\n        cx: null,\n        cy: null,\n        ballPos: 0\n      };\n\n      ctx.save();\n      if (tmp.type === \"hit\") {\n        ctx.strokeStyle = \"red\";\n        ctx.setLineDash([10, 6]);\n      } else if (tmp.type === \"throw\") {\n        ctx.strokeStyle = \"white\";\n        ctx.setLineDash([5, 5]);\n      } else {\n        ctx.strokeStyle = \"white\";\n        ctx.setLineDash([]);\n      }\n      ctx.lineWidth = 3;\n      ctx.beginPath();\n      ctx.moveTo(tmp.x1, tmp.y1);\n      ctx.lineTo(tmp.x2, tmp.y2);\n      ctx.stroke();\n      ctx.restore();\n    }\n  }\n}\n\n\nfunction drawAll() {\n        drawField();\n        drawPlayers();\n        drawRunners();\n        drawLines();   \/\/ <--- NEW\n        drawOuts();\n    }\n\n    function drawRunners() {\n      runners.forEach(r => {\n\n        \/\/ Outer diamond shape (runner)\n        ctx.save();\n        ctx.translate(r.x, r.y);\n        ctx.rotate(Math.PI \/ 4);\n\n        ctx.fillStyle = \"red\";\n        ctx.beginPath();\n        ctx.rect(-runnerRadius, -runnerRadius, runnerRadius * 2, runnerRadius * 2);\n        ctx.fill();\n\n        ctx.restore();\n\n        \/\/ Runner number\n        ctx.fillStyle = \"white\";\n        ctx.font = \"bold 14px Arial\";\n        ctx.textAlign = \"center\";\n        ctx.textBaseline = \"middle\";\n        ctx.fillText(r.number, r.x, r.y);\n      });\n    }\n\n    function drawOuts() {\n      const centers = getOutCircleCenters();\n\n      \/\/ optional label\n      ctx.save();\n      ctx.fillStyle = \"white\";\n      ctx.font = \"12px Arial\";\n      ctx.textAlign = \"right\";\n      ctx.textBaseline = \"middle\";\n      ctx.fillText(\"OUTS:\", outsUI.xStart - 10, outsUI.y);\n      ctx.restore();\n\n      for (let i = 0; i < 2; i++) {\n        ctx.beginPath();\n        ctx.arc(centers[i].x, centers[i].y, outsUI.r, 0, Math.PI * 2);\n        ctx.fillStyle = outs[i] ? \"red\" : \"white\";\n        ctx.fill();\n        ctx.strokeStyle = \"black\";\n        ctx.lineWidth = 1;\n        ctx.stroke();\n      }\n    }\n\n\/\/ ========================== INTERACTION =================================\nlet draggingPlayer = null;\nlet draggingRunner = null;\nlet offsetX = 0, offsetY = 0;\n\n function hitTestPlayer(pos) {\n      for (let i = players.length - 1; i >= 0; i--) {\n        const p = players[i];\n        if (Math.hypot(pos.x - p.x, pos.y - p.y) < playerRadius + 3) return i;\n      }\n      return null;\n    }\n\n  function hitTestRunner(pos) {\n  for (let i = runners.length - 1; i >= 0; i--) {\n    const r = runners[i];\n    const dx = Math.abs(pos.x - r.x);\n    const dy = Math.abs(pos.y - r.y);\n    if (dx < runnerRadius) {\n      if (dy < runnerRadius) {\n        return i;\n      }\n    }\n  }\n  return null;\n}\n\nfunction pointerDown(evt) {\n    evt.preventDefault();\n    const pos = getPos(evt);\n\n    pushUndoState();\n\n    \/\/ OUTS: click to toggle (no dragging)\n    const outIdx = hitTestOuts(pos);\n    if (outIdx !== null) {\n      pushUndoState();\n      outs[outIdx] = !outs[outIdx];\n      drawAll();\n      return;\n    }\n\n    \/\/ If creating a second point for a new line\n    if (drawingNewLine) {\n        \/\/ second click: finish line\n        lines.push({\n            type: drawingNewLine.type,\n            x1: drawingNewLine.x1,\n            y1: drawingNewLine.y1,\n            x2: pos.x,\n            y2: pos.y,\n            curved: false,\n            cx: null,\n            cy: null,\n            ballPos: 0      \/\/ NEW: for throw animation\n        });\n        drawingNewLine = null;\n        drawingPreviewPos = null;\n        drawAll();\n        return;\n    }\n\n    \/\/ Check if clicking endpoints\n    for (let i=lines.length-1; i>=0; i--) {\n        const L = lines[i];\n\n        if (Math.hypot(pos.x-L.x1, pos.y-L.y1) < 10) {\n            draggingLine = {index:i, endpoint:\"A\"};\n            return;\n        }\n        if (Math.hypot(pos.x-L.x2, pos.y-L.y2) < 10) {\n            draggingLine = {index:i, endpoint:\"B\"};\n            return;\n        }\n    }\n\n    \/\/ check for clicking control point (for curved lines)\n    for (let i = lines.length - 1; i >= 0; i--) {\n        const L = lines[i];\n        if (!L.curved || L.cx == null) continue;\n\n        if (Math.hypot(pos.x - L.cx, pos.y - L.cy) < 10) {\n            draggingControlPoint = i;\n            canvas.classList.add(\"dragging\");\n            return;\n        }\n    }\n\n    \/\/ Check whole-line dragging (not endpoints)\n    for (let i = lines.length - 1; i >= 0; i--) {\n        const L = lines[i];\n        const d = hitTestLineSegment(pos.x, pos.y, L);\n        if (d < 8) {  \/\/ near the segment\n            draggingWholeLine = {\n                index: i,\n                offsetX1: pos.x - L.x1,\n                offsetY1: pos.y - L.y1,\n                offsetX2: pos.x - L.x2,\n                offsetY2: pos.y - L.y2,\n            };\n            canvas.classList.add(\"dragging\");\n            return;\n        }\n    }\n\n  \/\/ 2. Check if an endpoint of the line is clicked\n  const endpoint = hitTestEndpoint(pos);\n  if (endpoint) {\n    if (ballLine) {\n    lineDragEnd = endpoint;\n    canvas.classList.add(\"dragging\");\n      return; \/\/ Important: do NOT fall through to player drag\n    }\n  }\n\n  \/\/ 3. Check players\n  const pIndex = hitTestPlayer(pos);\n  if (pIndex !== null) {\n    draggingPlayer = pIndex;\n    offsetX = pos.x - players[pIndex].x;\n    offsetY = pos.y - players[pIndex].y;\n    canvas.classList.add(\"dragging\");\n    return;\n  }\n\n  \/\/ 4. Check runners\n  const rIndex = hitTestRunner(pos);\n  if (rIndex !== null) {\n    draggingRunner = rIndex;\n    offsetX = pos.x - runners[rIndex].x;\n    offsetY = pos.y - runners[rIndex].y;\n    canvas.classList.add(\"dragging\");\n    return;\n  }\n}\n\nfunction pointerMove(evt) {\n  evt.preventDefault();\n  const pos = getPos(evt);\n\n  \/\/ While drawing a new line, show a live preview to the current cursor.\n  if (drawingNewLine) {\n    if (!draggingLine) {\n      if (!draggingWholeLine) {\n        if (draggingPlayer === null) {\n          if (draggingRunner === null) {\n            if (draggingControlPoint === null) {\n              drawingPreviewPos = { x: pos.x, y: pos.y };\n              drawAll();\n              return;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  \/\/ Moving a line endpoint (legacy ballLine)\n  if (lineDragEnd) {\n    if (ballLine) {\n      if (lineDragEnd === \"A\") {\n        ballLine.x1 = pos.x;\n        ballLine.y1 = pos.y;\n      } else {\n        ballLine.x2 = pos.x;\n        ballLine.y2 = pos.y;\n      }\n      drawAll();\n      return;\n    }\n  }\n\n  \/\/ Moving curve control point\n  if (draggingControlPoint !== null) {\n    const L = lines[draggingControlPoint];\n    L.cx = pos.x;\n    L.cy = pos.y;\n    drawAll();\n    return;\n  }\n\n  \/\/ Moving whole line\n  if (draggingWholeLine) {\n    const L = lines[draggingWholeLine.index];\n    L.x1 = pos.x - draggingWholeLine.offsetX1;\n    L.y1 = pos.y - draggingWholeLine.offsetY1;\n    L.x2 = pos.x - draggingWholeLine.offsetX2;\n    L.y2 = pos.y - draggingWholeLine.offsetY2;\n    drawAll();\n    return;\n  }\n\n  \/\/ Moving a player\n  if (draggingPlayer !== null) {\n    const p = players[draggingPlayer];\n    p.x = pos.x - offsetX;\n    p.y = pos.y - offsetY;\n    p.x = Math.max(playerRadius, Math.min(canvas.width - playerRadius, p.x));\n    p.y = Math.max(playerRadius, Math.min(canvas.height - playerRadius, p.y));\n    drawAll();\n    return;\n  }\n\n  \/\/ Moving a runner\n  if (draggingRunner !== null) {\n    const r = runners[draggingRunner];\n    r.x = pos.x - offsetX;\n    r.y = pos.y - offsetY;\n    drawAll();\n    return;\n  }\n\n  \/\/ Moving a line endpoint (new line system)\n  if (draggingLine) {\n    const L = lines[draggingLine.index];\n    if (draggingLine.endpoint === \"A\") {\n      L.x1 = pos.x;\n      L.y1 = pos.y;\n    } else {\n      L.x2 = pos.x;\n      L.y2 = pos.y;\n    }\n    drawAll();\n    return;\n  }\n}\n\nfunction hitTestOuts(pos) {\n  const centers = getOutCircleCenters();\n  for (let i = 0; i < 2; i++) {\n    if (Math.hypot(pos.x - centers[i].x, pos.y - centers[i].y) <= outsUI.r + 3) {\n      return i;\n    }\n  }\n  return null;\n}\n\nfunction pointerUp(evt) {\n  draggingPlayer = null;\n  draggingRunner = null;\n  lineDragEnd = null;\n  draggingLine = null;\n  draggingWholeLine = null;\n  draggingControlPoint = null;\n  canvas.classList.remove(\"dragging\");\n}\n\n    \n\/\/ Events (Pointer Events: works for mouse + touch + pen)\ncanvas.addEventListener(\"pointerdown\", (e) => {\n  \/\/ Prevent scrolling\/zoom gestures on touch\n  e.preventDefault();\n\n  \/\/ Start long press only for touch\n  if (e.pointerType === \"touch\") {\n    longPressPointerId = e.pointerId;\n    longPressStart = { x: e.clientX, y: e.clientY };\n\n    \/\/ schedule context menu open\n    longPressTimer = setTimeout(() => {\n      \/\/ Only open if still active and not moved\n      if (longPressPointerId !== null) {\n        if (longPressStart) {\n          \/\/ Cancel any dragging that may have started\n          pointerUp(e);\n          openContextMenu(longPressStart.x, longPressStart.y, getPos(e));\n        }\n      }\n      clearLongPress();\n    }, LONG_PRESS_MS);\n  }\n\n  \/\/ Capture pointer so move\/up fire even if finger leaves canvas\n  try { canvas.setPointerCapture(e.pointerId); } catch (_) {}\n\n  pointerDown(e);\n}, { passive: false });\n\ncanvas.addEventListener(\"pointermove\", (e) => {\n  e.preventDefault();\n\n  \/\/ Cancel long press if moved too much\n  if (e.pointerType === \"touch\") {\n    if (longPressStart) {\n      if (longPressPointerId === e.pointerId) {\n    const dx = e.clientX - longPressStart.x;\n    const dy = e.clientY - longPressStart.y;\n    if (Math.hypot(dx, dy) > LONG_PRESS_MOVE_TOL) {\n      clearLongPress();\n      }\n    }\n  }\n  }\n\n  pointerMove(e);\n}, { passive: false });\n\nfunction finishPointer(e) {\n  e.preventDefault();\n  clearLongPress();\n  pointerUp(e);\n  try { canvas.releasePointerCapture(e.pointerId); } catch (_) {}\n}\n\ncanvas.addEventListener(\"pointerup\", finishPointer, { passive: false });\ncanvas.addEventListener(\"pointercancel\", finishPointer, { passive: false });\n\n\/\/ ========================= BUTTONS ======================================\n    let creatingLineMode = false;\n\n    let lastTime = 0;\n\n    function animateBall(timestamp) {\n        const dt = (timestamp - lastTime) \/ 1000;\n        lastTime = timestamp;\n\n        \/\/ update all throw lines\n        for (const L of lines) {\n            if (L.type === \"throw\") {\n                L.ballPos += dt * 0.2; \/\/ speed factor\n                if (L.ballPos > 1) L.ballPos = 0;\n            }\n        }\n\n        drawAll();\n        requestAnimationFrame(animateBall);\n    }\n\n    requestAnimationFrame(animateBall);\n\n    function getPointOnLine(L, t) {\n      if (L.curved) {\n        if (L.cx != null) {\n          if (L.cy != null) {\n            \/\/ quadratic Bezier interpolation\n            const x =\n              (1 - t) * (1 - t) * L.x1 +\n              2 * (1 - t) * t * L.cx +\n              t * t * L.x2;\n\n            const y =\n              (1 - t) * (1 - t) * L.y1 +\n              2 * (1 - t) * t * L.cy +\n              t * t * L.y2;\n\n            return { x: x, y: y };\n          }\n        }\n      }\n\n      \/\/ straight line fallback\n      return {\n        x: L.x1 + (L.x2 - L.x1) * t,\n        y: L.y1 + (L.y2 - L.y1) * t\n      };\n    }\n\n    drawAll();\n\/*\n    let basic=[{\n  \"players\": [\n    {\n      \"x\": 392.5,\n      \"y\": 428.5,\n      \"label\": \"P\",\n      \"color\": \"#f59e0b\"\n    },\n    {\n      \"x\": 391.5,\n      \"y\": 594.5,\n      \"label\": \"C\",\n      \"color\": \"#f97316\"\n    },\n    {\n      \"x\": 523.5,\n      \"y\": 380.5,\n      \"label\": \"1B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 456.5,\n      \"y\": 290.5,\n      \"label\": \"2B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 265.5,\n      \"y\": 377.5,\n      \"label\": \"3B\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 326,\n      \"y\": 289.5,\n      \"label\": \"SS\",\n      \"color\": \"#3b82f6\"\n    },\n    {\n      \"x\": 173.5,\n      \"y\": 219.5,\n      \"label\": \"LF\",\n      \"color\": \"#22c55e\"\n    },\n    {\n      \"x\": 388.5,\n      \"y\": 140.5,\n      \"label\": \"CF\",\n      \"color\": \"#22c55e\"\n    },\n    {\n      \"x\": 594.5,\n      \"y\": 206.5,\n      \"label\": \"RF\",\n      \"color\": \"#22c55e\"\n    }\n  ],\n  \"runners\": [\n    {\n      \"x\": 670.5,\n      \"y\": 642.5\n    },\n    {\n      \"x\": 711.5,\n      \"y\": 640.5\n    },\n    {\n      \"x\": 391.5,\n      \"y\": 559.5\n    }\n  ],\n  \"lines\": []\n}];\n*\/\n\n\n\/* ===================== Toolbar + Selection Fixes =====================\n   This block wires the top toolbar buttons and adds selection highlighting\n   for players, runners, and lines. It also clears selection when clicking\n   empty space, and enables deleting the currently selected line via the\n   toolbar button #deleteLineBtn.\n====================================================================== *\/\n\n\/\/ Selection state\nlet selectedPlayerIndex = null;\nlet selectedRunnerIndex = null;\nlet selectedLineIndex = null;\n\n\/\/ Tool state: \"select\" | \"hit\" | \"throw\" | \"running\"\nlet currentTool = \"select\";\n\nfunction clearSelection() {\n  selectedPlayerIndex = null;\n  selectedRunnerIndex = null;\n  selectedLineIndex = null;\n}\n\nfunction setTool(tool) {\n  currentTool = tool || \"select\";\n  \/\/ Visual active state on toolbar buttons (if present)\n  const btns = [\"toolSelect\",\"toolHit\",\"toolThrow\",\"toolRun\"];\n  for (const id of btns) {\n    const el = document.getElementById(id);\n    if (!el) continue;\n    const t = el.getAttribute(\"data-tool\");\n    el.classList.toggle(\"active\", t === currentTool);\n  }\n}\n\n\/\/ Wire toolbar buttons (if present)\n(function wireToolbar(){\n  const selectBtn = document.getElementById(\"toolSelect\");\n  const hitBtn = document.getElementById(\"toolHit\");\n  const throwBtn = document.getElementById(\"toolThrow\");\n  const runBtn = document.getElementById(\"toolRun\");\n  const delBtn = document.getElementById(\"deleteLineBtn\");\n\n  if (selectBtn) selectBtn.addEventListener(\"click\", () => setTool(\"select\"));\n  if (hitBtn) hitBtn.addEventListener(\"click\", () => setTool(\"hit\"));\n  if (throwBtn) throwBtn.addEventListener(\"click\", () => setTool(\"throw\"));\n  if (runBtn) runBtn.addEventListener(\"click\", () => setTool(\"running\"));\n\n  if (delBtn) {\n    delBtn.addEventListener(\"click\", () => {\n      if (selectedLineIndex === null || selectedLineIndex < 0 || selectedLineIndex >= lines.length) return;\n      pushUndoState();\n      lines.splice(selectedLineIndex, 1);\n      selectedLineIndex = null;\n      drawAll();\n    });\n  }\n\n  \/\/ Default\n  setTool(\"select\");\n})();\n\nfunction hitTestLineIndex(pos, threshold=10) {\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const d = hitTestLineSegment(pos.x, pos.y, lines[i]);\n    if (d < threshold) return i;\n  }\n  return null;\n}\n\n\/\/ ----------------- Override draw functions with highlighting -----------------\nconst _oldDrawPlayers = typeof drawPlayers === \"function\" ? drawPlayers : null;\ndrawPlayers = function drawPlayersHighlighted() {\n  players.forEach((p, i) => {\n    \/\/ highlight ring\n    if (i === selectedPlayerIndex) {\n      ctx.save();\n      ctx.beginPath();\n      ctx.arc(p.x, p.y, playerRadius + 6, 0, Math.PI * 2);\n      ctx.strokeStyle = \"yellow\";\n      ctx.lineWidth = 4;\n      ctx.stroke();\n      ctx.restore();\n    }\n\n    ctx.beginPath();\n    ctx.fillStyle = p.color;\n    ctx.arc(p.x, p.y, playerRadius, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.strokeStyle = \"rgba(0,0,0,0.5)\";\n    ctx.lineWidth = 2;\n    ctx.stroke();\n\n    ctx.fillStyle = \"#fff\";\n    ctx.font = \"11px Arial\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    \/\/ label centered on dot\n    ctx.fillText(p.label, p.x, p.y);\n  });\n};\n\nconst _oldDrawRunners = typeof drawRunners === \"function\" ? drawRunners : null;\ndrawRunners = function drawRunnersHighlighted() {\n  runners.forEach((r, i) => {\n    \/\/ highlight ring\n    if (i === selectedRunnerIndex) {\n      ctx.save();\n      ctx.beginPath();\n      ctx.arc(r.x, r.y, runnerRadius + 8, 0, Math.PI * 2);\n      ctx.strokeStyle = \"yellow\";\n      ctx.lineWidth = 4;\n      ctx.stroke();\n      ctx.restore();\n    }\n\n    \/\/ red diamond\n    ctx.save();\n    ctx.translate(r.x, r.y);\n    ctx.rotate(Math.PI \/ 4);\n    ctx.fillStyle = \"red\";\n    ctx.beginPath();\n    ctx.rect(-runnerRadius, -runnerRadius, runnerRadius * 2, runnerRadius * 2);\n    ctx.fill();\n    ctx.restore();\n\n    \/\/ number\n    ctx.fillStyle = \"white\";\n    ctx.font = \"bold 14px Arial\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillText(r.number ?? (i+1), r.x, r.y);\n  });\n};\n\nconst _oldDrawLines = typeof drawLines === \"function\" ? drawLines : null;\ndrawLines = function drawLinesHighlighted() {\n  \/\/ draw all saved lines using original function\n  if (_oldDrawLines) _oldDrawLines();\n\n  \/\/ highlight selected line (draw on top so it's obvious)\n  if (selectedLineIndex !== null) {\n    if (lines[selectedLineIndex]) {\n    const L = lines[selectedLineIndex];\n    ctx.save();\n    ctx.lineWidth = 8;\n    ctx.strokeStyle = \"rgba(255,255,0,0.6)\";\n    ctx.setLineDash([]); \/\/ highlight is solid glow\n\n    ctx.beginPath();\n    ctx.moveTo(L.x1, L.y1);\n    if (L.curved) {\n      if (L.cx != null) {\n        if (L.cy != null) {\n      ctx.quadraticCurveTo(L.cx, L.cy, L.x2, L.y2);\n        }\n      }\n    } else {\n      ctx.lineTo(L.x2, L.y2);\n    }\n    ctx.stroke();\n    ctx.restore();\n    }\n  }\n};\n\n\/\/ ----------------- Override pointer handlers to support selection + toolbar tools -----------------\nconst _oldPointerDown = pointerDown;\nconst _oldPointerMove = pointerMove;\nconst _oldPointerUp = pointerUp;\n\npointerDown = function pointerDownWithSelection(evt) {\n  evt.preventDefault();\n  const pos = getPos(evt);\n\n  \/\/ If a drawing tool is selected and we're not currently placing point 2, start a line at click\n  if (!drawingNewLine) {\n    if (currentTool === \"hit\" || currentTool === \"throw\" || currentTool === \"running\") {\n    pushUndoState();\n    clearSelection();\n    drawingNewLine = { type: currentTool, x1: pos.x, y1: pos.y };\n    drawingPreviewPos = { x: pos.x, y: pos.y };\n    drawAll();\n    return;\n    }\n  }\n\n  \/\/ If placing the second point, keep old logic (it finalizes the line)\n  if (drawingNewLine) {\n    \/\/ finalize should be undoable as part of the action already pushed at start; avoid double-push\n    \/\/ so temporarily suppress additional push in the old handler by not calling it.\n    lines.push({\n      type: drawingNewLine.type,\n      x1: drawingNewLine.x1,\n      y1: drawingNewLine.y1,\n      x2: pos.x,\n      y2: pos.y,\n      curved: false,\n      cx: null,\n      cy: null,\n      ballPos: 0\n    });\n    drawingNewLine = null;\n    drawingPreviewPos = null;\n    setTool(\"select\"); \/\/ return to select after placing a line\n    drawAll();\n    return;\n  }\n\n  \/\/ OUTS toggle\n  const outIdx = hitTestOuts(pos);\n  if (outIdx !== null) {\n    pushUndoState();\n    outs[outIdx] = !outs[outIdx];\n    drawAll();\n    return;\n  }\n\n  \/\/ Line endpoint drag \/ select\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const L = lines[i];\n    if (Math.hypot(pos.x - L.x1, pos.y - L.y1) < 10) {\n      pushUndoState();\n      draggingLine = { index: i, endpoint: \"A\" };\n      selectedLineIndex = i;\n      selectedPlayerIndex = null;\n      selectedRunnerIndex = null;\n      return;\n    }\n    if (Math.hypot(pos.x - L.x2, pos.y - L.y2) < 10) {\n      pushUndoState();\n      draggingLine = { index: i, endpoint: \"B\" };\n      selectedLineIndex = i;\n      selectedPlayerIndex = null;\n      selectedRunnerIndex = null;\n      return;\n    }\n  }\n\n  \/\/ Control point drag \/ select\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const L = lines[i];\n    if (!L.curved || L.cx == null) continue;\n    if (Math.hypot(pos.x - L.cx, pos.y - L.cy) < 10) {\n      pushUndoState();\n      draggingControlPoint = i;\n      selectedLineIndex = i;\n      selectedPlayerIndex = null;\n      selectedRunnerIndex = null;\n      canvas.classList.add(\"dragging\");\n      return;\n    }\n  }\n\n  \/\/ Whole-line drag \/ select\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const L = lines[i];\n    const d = hitTestLineSegment(pos.x, pos.y, L);\n    if (d < 8) {\n      pushUndoState();\n      draggingWholeLine = {\n        index: i,\n        offsetX1: pos.x - L.x1,\n        offsetY1: pos.y - L.y1,\n        offsetX2: pos.x - L.x2,\n        offsetY2: pos.y - L.y2\n      };\n      selectedLineIndex = i;\n      selectedPlayerIndex = null;\n      selectedRunnerIndex = null;\n      canvas.classList.add(\"dragging\");\n      return;\n    }\n  }\n\n  \/\/ Player select\/drag\n  const pIndex = hitTestPlayer(pos);\n  if (pIndex !== null) {\n    pushUndoState();\n    draggingPlayer = pIndex;\n    selectedPlayerIndex = pIndex;\n    selectedRunnerIndex = null;\n    selectedLineIndex = null;\n    offsetX = pos.x - players[pIndex].x;\n    offsetY = pos.y - players[pIndex].y;\n    canvas.classList.add(\"dragging\");\n    drawAll();\n    return;\n  }\n\n  \/\/ Runner select\/drag\n  const rIndex = hitTestRunner(pos);\n  if (rIndex !== null) {\n    pushUndoState();\n    draggingRunner = rIndex;\n    selectedRunnerIndex = rIndex;\n    selectedPlayerIndex = null;\n    selectedLineIndex = null;\n    offsetX = pos.x - runners[rIndex].x;\n    offsetY = pos.y - runners[rIndex].y;\n    canvas.classList.add(\"dragging\");\n    drawAll();\n    return;\n  }\n\n  \/\/ Clicked empty space -> clear selection\n  clearSelection();\n  drawAll();\n};\n\npointerMove = function pointerMoveWithPreview(evt) {\n  evt.preventDefault();\n  const pos = getPos(evt);\n\n  \/\/ Live preview while drawing\n  if (drawingNewLine) {\n    if (!draggingLine) {\n      if (!draggingWholeLine) {\n        if (draggingPlayer === null) {\n          if (draggingRunner === null) {\n            if (draggingControlPoint === null) {\n    drawingPreviewPos = { x: pos.x, y: pos.y };\n    drawAll();\n    return;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  \/\/ Use existing move logic for drags\n  if (_oldPointerMove) {\n    _oldPointerMove(evt);\n  }\n};\n\npointerUp = function pointerUpWithSelection(evt) {\n  if (_oldPointerUp) _oldPointerUp(evt);\n  \/\/ keep selection after drag; nothing else\n};\n\n\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Schlaglinie hinzuf\u00fcgen (rot gestrichelt) Lauflinie hinzuf\u00fcgen (wei\u00df) Wurflinie hinzuf\u00fcgen (wei\u00df gestrichelt) Toggle Curved \/ Straight Delete Line Turtles Baseball Strategy Simulator Beschreibung Dieser Turtles Baseball Strategy Simulator dient zur visualen Darstellung und Analyse von Baseball-Spielsituationen. Auf einem Baseballfeld (Draufsicht) k\u00f6nnen Verteidiger und L\u00e4ufer frei positioniert, Spielz\u00fcge eingezeichnet und Abl\u00e4ufe erkl\u00e4rt werden. Wurf-, Schlag- und Laufwege &#8230; <a title=\"Turtles Baseball Strategy Simulator\" class=\"read-more\" href=\"https:\/\/www.tvj.de\/kt\/turtles-baseball-strategy-simulator\/\" aria-label=\"Mehr bei Turtles Baseball Strategy Simulator\">Weiterlesen &#8230;<\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_EventAllDay":false,"_EventTimezone":"","_EventStartDate":"","_EventEndDate":"","_EventStartDateUTC":"","_EventEndDateUTC":"","_EventShowMap":false,"_EventShowMapLink":false,"_EventURL":"","_EventCost":"","_EventCostDescription":"","_EventCurrencySymbol":"","_EventCurrencyCode":"","_EventCurrencyPosition":"","_EventDateTimeSeparator":"","_EventTimeRangeSeparator":"","_EventOrganizerID":[],"_EventVenueID":[],"_OrganizerEmail":"","_OrganizerPhone":"","_OrganizerWebsite":"","_VenueAddress":"","_VenueCity":"","_VenueCountry":"","_VenueProvince":"","_VenueState":"","_VenueZip":"","_VenuePhone":"","_VenueURL":"","_VenueStateProvince":"","_VenueLat":"","_VenueLng":"","_VenueShowMap":false,"_VenueShowMapLink":false,"footnotes":""},"class_list":["post-764","page","type-page","status-publish"],"_links":{"self":[{"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/pages\/764","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/comments?post=764"}],"version-history":[{"count":30,"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/pages\/764\/revisions"}],"predecessor-version":[{"id":827,"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/pages\/764\/revisions\/827"}],"wp:attachment":[{"href":"https:\/\/www.tvj.de\/kt\/wp-json\/wp\/v2\/media?parent=764"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}