{"id":8635,"date":"2025-02-02T21:34:05","date_gmt":"2025-02-02T21:34:05","guid":{"rendered":"https:\/\/tlap-sports.com\/en\/?page_id=8635"},"modified":"2025-03-01T22:58:07","modified_gmt":"2025-03-01T22:58:07","slug":"play-generator","status":"publish","type":"page","link":"https:\/\/tlap-sports.com\/en\/play-generator\/","title":{"rendered":"Play Generator"},"content":{"rendered":"\n<div style=\"height:30px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <title>Konva Play Designer &#8211; Zones, Dotted Arrow<\/title>\n  <!-- Konva from CDN -->\n  <script src=\"https:\/\/unpkg.com\/konva@latest\/konva.min.js\"><\/script>\n  <!-- jsPDF from CDN (UMD build) -->\n  <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf\/2.5.1\/jspdf.umd.min.js\"><\/script>\n  <style>\n    body {\n      font-family: sans-serif;\n      margin: 0;\n      padding: 0;\n      text-align: center;\n    }\n    \/* Top controls bar *\/\n    #controls {\n      margin: 10px auto;\n      width: 800px; \/* match your Konva container width *\/\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      gap: 10px;\n      flex-wrap: wrap;\n      font-size: 12px;\n    }\n    \/* Left controls for route\/block\/dotted lines *\/\n    #leftControls {\n      width: 120px;\n      position: absolute;\n      left: 50px;\n      top: 300px;\n      display: flex;\n      flex-direction: column;\n      gap: 10px;\n    }\n    \/* Right controls for zones + route dropdown + formation save\/load *\/\n    #rightControls {\n      width: 150px;\n      position: absolute;\n      right: 50px;\n      top: 300px;\n      display: flex;\n      flex-direction: column;\n      gap: 10px;\n    }\n    #container {\n      border: 1px solid #ccc;\n      width: 800px;\n      height: 500px;\n      margin: 20px auto;\n      display: block;\n      position: relative;\n    }\n    button {\n      padding: 8px 16px;\n      font-size: 14px;\n      cursor: pointer;\n    }\n    label {\n      font-weight: bold;\n    }\n    \/* styling for the player info section *\/\n    #playerInfos {\n      width: 800px;\n      margin: 0 auto 30px;\n      text-align: left;\n    }\n    .player-info-item {\n      margin: 5px 0;\n    }\n    .player-info-label {\n      font-weight: bold;\n      margin-right: 8px;\n    }\n    .player-info-textarea {\n      width: 100%;\n      max-width: 400px;\n      min-height: 40px;\n    }\n    #formationSelect {\n      font-size: 12px; \/* or whatever size *\/\n    }\n    #routeTypeSelect {\n      font-size: 12px; \/* or whatever size *\/\n    }\n    #fieldSelect {\n      font-size: 12px; \/* or whatever size *\/\n    }\n    #playerTypeSelect {\n      font-size: 12px; \/* or whatever size *\/\n    }\n    #playerColor {\n      width: 30px;  \/* Adjust width *\/\n      height: 30px; \/* Adjust height *\/\n      padding: 2px;\n    }\n  <\/style>\n<\/head>\n<body>\n\n  <!-- Top Controls -->\n  <div id=\"controls\">\n    <label for=\"playerColor\">Player Color:<\/label>\n    <input type=\"color\" id=\"playerColor\" value=\"#ff0000\" \/>\n\n    <!-- Player buttons -->\n    <label for=\"playerTypeSelect\"><\/label>\n    <select id=\"playerTypeSelect\">\n      <option value=\"offense\">Offense<\/option>\n      <option value=\"defense\">Defense<\/option>\n    <\/select>\n\n    <button id=\"addPlayerBtn\">Add Player<\/button>\n\n    <!-- Add O-Line -->\n    <button id=\"addOlineBtn\">Add O-Line<\/button>\n\n    <!-- Undo Last -->\n    <button id=\"UndoBtn\">Undo Last<\/button>\n\n    <!-- Clear All -->\n    <button id=\"clearAllBtn\">Clear All<\/button>\n\n  <\/div>\n\n  <!-- Left Controls -->\n  <div id=\"leftControls\">\n    <!-- Field Select -->\n    <label for=\"fieldSelect\" style=\"font-size: 12px;\">Choose Field:<\/label>\n    <select id=\"fieldSelect\">\n      <option value=\"normal\">Field<\/option>\n      <option value=\"redzone\">Red Zone<\/option>\n    <\/select>\n\n    <!-- Basic route & block -->\n    <button id=\"addRouteBtn\">Add Custom Arrow<\/button>\n    <button id=\"addBlockBtn\">Add Block<\/button>\n    <button id=\"addDottedBtn\">Add Dotted Arrow<\/button>\n    <!-- Custom Zone -->\n    <button id=\"addCustomZoneBtn\">Add Custom Zone<\/button>\n  <\/div>\n\n  <!-- Right Controls: Zones + 6-route dropdown + multi-formation save\/load -->\n  <div id=\"rightControls\">\n    <button id=\"addZone40Btn\">Add Large Zone<\/button>\n    <button id=\"addZone25Btn\">Add Medium Zone<\/button>\n    <button id=\"addZone15Btn\">Add Small Zone<\/button>\n\n    <!-- Single dropdown for 6 route shapes -->\n    <select id=\"routeTypeSelect\">\n      <option value=\"\">&#8212; Choose a route &#8212;<\/option>\n      <option value=\"slant\">Slant\/Post<\/option>\n      <option value=\"cut\">In<\/option>\n      <option value=\"comeback\">Comeback<\/option>\n      <option value=\"corner\">Corner<\/option>\n      <option value=\"out\">Out<\/option>\n      <option value=\"hitch\">Hitch\/Curl<\/option>\n      <option value=\"go\">Go\/Streak<\/option>\n      <option value=\"flat\">Flat<\/option>\n      <option value=\"swing\">Swing\/Bubble<\/option>\n    <\/select>\n    <button id=\"addLockedRouteBtn\">Add Route<\/button>\n\n    <!-- MULTI-FORMATION saving -->\n    <select id=\"formationSelect\" style=\"margin-top: 6px;\">\n      <option value=\"\">&#8212; Formations &#8212;<\/option>\n    <\/select>\n    <button id=\"saveFormationBtn\">Save Formation<\/button>\n    <button id=\"loadFormationBtn\">Load Formation<\/button>\n    <button id=\"removeFormationBtn\">Delete Formation<\/button>\n\n    <!-- Save as PDF -->\n    <button id=\"savePDFBtn\">Save Play as PDF<\/button>\n  <\/div>\n\n  <div id=\"container\"><\/div>\n  <div id=\"playerInfos\"><\/div>\n\n  <script>\n    \/\/ === Stage + Layer ===\n    const stageWidth = 800;\n    const stageHeight = 500;\n    let stage = new Konva.Stage({\n      container: 'container',\n      width: stageWidth,\n      height: stageHeight,\n    });\n    let layer = new Konva.Layer();\n    stage.add(layer);\n\n    const normalFieldURL = 'https:\/\/tlap-sports.com\/en\/wp-content\/uploads\/sites\/3\/2025\/02\/MITTEL-FELD-ohne-ball-scaled.jpg';\n    const redZoneFieldURL = 'https:\/\/tlap-sports.com\/en\/wp-content\/uploads\/sites\/3\/2025\/02\/Redzone-ohne-Ball-1-1024x640.jpg';\n    let backgroundImage = null;\n    function setFieldBackground(url) {\n      if (backgroundImage) {\n        backgroundImage.destroy();\n        backgroundImage = null;\n      }\n\n      const imgObj = new Image();\n      imgObj.crossOrigin = 'anonymous';\n      imgObj.src = url;\n\n      imgObj.onload = () => {\n        backgroundImage = new Konva.Image({\n          x: 0,\n          y: 0,\n          image: imgObj,\n          width: stageWidth,\n          height: stageHeight,\n        });\n        layer.add(backgroundImage);\n        layer.draw();\n      };\n    }\n\n    document.getElementById('fieldSelect').addEventListener('change', (e) => {\n      const value = e.target.value;\n      if (value === 'normal') {\n        setFieldBackground(normalFieldURL);\n      } else if (value === 'redzone') {\n        setFieldBackground(redZoneFieldURL);\n      }\n    });\n    \/\/ Optionally set an initial background on page load\n    setFieldBackground(normalFieldURL);\n\n    \/\/ Arrays\/counters\n    const players = [];\n    let playerCount = 0;\n    const playerDetails = [];\n\n    \/\/ We'll store minimal data: (type, x, y, color, label, playerType)\n    const playerData = [];\n    \/\/ Store all items in here so we can do Undo\n    const history = [];\n\n    \/\/ Route arrays\n    const slantRoutes = [];\n    const cutRoutes = [];\n    const comebackRoutes = [];\n    const cornerRoutes = [];\n    const outRoutes = [];\n    const hitchRoutes = [];\n    const goRoutes = [];\n    const flatRoutes = [];\n    const swingRoutes = [];\n\n    \/\/ Basic route\n    let isDrawingRoute = false;\n    let routeInProgress = null;\n    let routePoints = [];\n    const routes = [];\n\n    \/\/ Blocks\n    let isDrawingBlock = false;\n    let blockInProgress = null;\n    let blockLine = null;\n    let blockTBar = null;\n    let blockPoints = [];\n    const blocks = [];\n\n    \/\/ Dotted\n    let isDrawingDotted = false;\n    let dottedInProgress = null;\n    let dottedPoints = [];\n    const dottedLines = [];\n\n    \/\/ Zones\n    const zones = [];\n    const customZones = [];\n\n    \/\/ Helper: Re-center text so it stays in the middle\n    function centerText(text) {\n      const textWidth = text.width();\n      text.offsetX(textWidth \/ 2);\n      text.offsetY(7);\n    }\n\n    \/\/ ================== createLockedAngleRoute =====================\n    function createLockedAngleRoute(anglesAndRadii, arrowColor) {\n      const group = new Konva.Group({\n        x: stageWidth \/ 2,\n        y: stageHeight \/ 2,\n        draggable: true,\n        scaleX: 1,\n      });\n      layer.add(group);\n\n      const arrow = new Konva.Arrow({\n        points: [],\n        stroke: arrowColor,\n        fill: arrowColor,\n        strokeWidth: 3,\n        pointerLength: 10,\n        pointerWidth: 10,\n      });\n      group.add(arrow);\n\n      const anchors = [];\n      let currentX = 0;\n      let currentY = 0;\n\n      const anchor0 = new Konva.Circle({\n        x: currentX,\n        y: currentY,\n        radius: 4,\n        fill: 'black',\n        stroke: 'black',\n        strokeWidth: 2,\n        draggable: false,\n        opacity: 0,\n      });\n      group.add(anchor0);\n      anchors.push(anchor0);\n\n      anglesAndRadii.forEach((seg) => {\n        const angleRad = (seg.angleDeg * Math.PI) \/ 180;\n        currentX += seg.radius * Math.cos(angleRad);\n        currentY += seg.radius * Math.sin(angleRad);\n\n        const anchor = new Konva.Circle({\n          x: currentX,\n          y: currentY,\n          radius: 6,\n          fill: 'black',\n          stroke: 'black',\n          strokeWidth: 2,\n          draggable: true,\n          opacity: 0,\n        });\n        anchor._lockedAngleRad = angleRad;\n        anchor._segmentLength = seg.radius;\n        group.add(anchor);\n        anchors.push(anchor);\n      });\n\n      function updateArrow() {\n        const pts = anchors.map(a => [a.x(), a.y()]).flat();\n        arrow.points(pts);\n        layer.batchDraw();\n      }\n\n      anchor0.draggable(false);\n      for (let i = 1; i < anchors.length; i++) {\n        const thisAnchor = anchors[i];\n        const prevAnchor = anchors[i - 1];\n        thisAnchor.on('dragmove', () => {\n          const dx = thisAnchor.x() - prevAnchor.x();\n          const dy = thisAnchor.y() - prevAnchor.y();\n          const lockedAngle = thisAnchor._lockedAngleRad;\n          const newDist = Math.sqrt(dx * dx + dy * dy);\n          const newX = prevAnchor.x() + newDist * Math.cos(lockedAngle);\n          const newY = prevAnchor.y() + newDist * Math.sin(lockedAngle);\n          thisAnchor.position({ x: newX, y: newY });\n          thisAnchor._segmentLength = newDist;\n          updateArrow();\n        });\n      }\n\n      group.on('dragmove', () => {\n        updateArrow();\n        if (group.x() < stageWidth \/ 2) {\n          group.scaleX(-1);\n        } else {\n          group.scaleX(1);\n        }\n      });\n\n      updateArrow();\n      return group;\n    }\n\n    \/\/ === Attach dragend so we update (x,y) in playerData\n    function attachDragEnd(group, dataObj) {\n      group.on('dragend', () => {\n        dataObj.x = group.x();\n        dataObj.y = group.y();\n      });\n    }\n\n    \/\/ ================== createSinglePlayer (Offense circle vs. Defense square) =====================\n    function createSinglePlayer(x, y, label, color, playerType = 'offense') {\n      const group = new Konva.Group({ x, y, draggable: true });\n\n      let shape;\n      if (playerType === 'defense') {\n        \/\/ square\n        shape = new Konva.Rect({\n          width: 30,\n          height: 30,\n          fill: color,\n          stroke: 'black',\n          strokeWidth: 2,\n          offsetX: 15,\n          offsetY: 15,\n        });\n      } else {\n        \/\/ offense => circle\n        shape = new Konva.Circle({\n          radius: 15,\n          fill: color,\n          stroke: 'black',\n          strokeWidth: 2,\n        });\n      }\n\n      const text = new Konva.Text({\n        text: label,\n        fontSize: 14,\n        fontFamily: 'Calibri',\n        fill: 'black',\n      });\n      centerText(text);\n\n      group.add(shape);\n      group.add(text);\n      layer.add(group);\n      layer.draw();\n\n      return group;\n    }\n\n    \/\/ ================== createOlineGroup =====================\n    function createOlineGroup(x, y, color, label='OLINE') {\n      const group = new Konva.Group({ x, y, draggable: true });\n      const labels = ['LT','LG','C','RG','RT'];\n      const spacing = 40;\n\n      labels.forEach((lab, i) => {\n        const cx = i * spacing;\n        const circle = new Konva.Circle({\n          x: cx,\n          y: 0,\n          radius: 15,\n          fill: color,\n          stroke: 'black',\n          strokeWidth: 2,\n        });\n        const text = new Konva.Text({\n          x: cx,\n          y: 0,\n          text: lab,\n          fontSize: 14,\n          fontFamily: 'Calibri',\n          fill: 'black',\n        });\n        const w = text.width();\n        text.offsetX(w\/2);\n        text.offsetY(7);\n        group.add(circle);\n        group.add(text);\n      });\n\n      layer.add(group);\n      layer.draw();\n      return group;\n    }\n\n    \/\/ ================== createPlayerInfoUI =====================\n    function createPlayerInfoUI(playerLabel) {\n      const container = document.getElementById('playerInfos');\n      const textAreaId = 'playerDetailInput_' + playerLabel;\n      const div = document.createElement('div');\n      div.className = 'player-info-item';\n      div.id = 'info_' + playerLabel;\n\n      const labelSpan = document.createElement('span');\n      labelSpan.className = 'player-info-label';\n      labelSpan.textContent = `Extra info for ${playerLabel}:`;\n\n      const textArea = document.createElement('textarea');\n      textArea.className = 'player-info-textarea';\n      textArea.id = textAreaId;\n\n      const removeBtn = document.createElement('button');\n      removeBtn.innerHTML = '\u274c';\n      removeBtn.className = 'remove-info-btn';\n      removeBtn.style.border = 'none';\n      removeBtn.style.background = 'none';\n      removeBtn.style.cursor = 'pointer';\n      removeBtn.style.fontSize = '14px';\n      removeBtn.style.marginLeft = '8px';\n      removeBtn.style.color = 'red';\n\n      removeBtn.onclick = function () {\n        div.remove();\n        const index = playerDetails.findIndex(p => p.label === playerLabel);\n        if (index !== -1) {\n          playerDetails.splice(index, 1);\n        }\n      };\n\n      div.appendChild(labelSpan);\n      div.appendChild(textArea);\n      div.appendChild(removeBtn);\n      container.appendChild(div);\n\n      playerDetails.push({ label: playerLabel, detailsId: textAreaId });\n    }\n\n    \/\/ ================== ADD PLAYER BUTTON =====================\n    document.getElementById('addPlayerBtn').addEventListener('click', () => {\n      const playerType = document.getElementById('playerTypeSelect').value;\n      if (!playerType) {\n        alert('Please select \"Offense\" or \"Defense\" first!');\n        return;\n      }\n\n      const color = document.getElementById('playerColor').value || '#0000ff';\n      playerCount++;\n      const xPos = 50;\n      const yPos = 50;\n      let defaultLabel = 'P' + playerCount;\n\n      const userLabel = prompt(\"Enter a label (up to 2 letters) for this player:\", \"\");\n      if (userLabel) {\n        defaultLabel = userLabel.substring(0, 2);\n      }\n\n      \/\/ Pass playerType\n      const group = createSinglePlayer(xPos, yPos, defaultLabel, color, playerType);\n      players.push(group);\n\n      history.push({\n        shape: group,\n        array: players,\n        label: defaultLabel\n      });\n\n      \/\/ Store minimal data + the new playerType\n      const dataObj = {\n        type: 'player',\n        x: xPos,\n        y: yPos,\n        color,\n        label: defaultLabel,\n\n        \/\/ \u2605 store the offense\/defense info so we can load it\n        playerType: playerType\n      };\n      playerData.push(dataObj);\n\n      attachDragEnd(group, dataObj);\n      createPlayerInfoUI(defaultLabel);\n    });\n\n    \/\/ ================== ADD O-LINE =====================\n    document.getElementById('addOlineBtn').addEventListener('click', () => {\n      const color = document.getElementById('playerColor').value || '#888888';\n      playerCount++;\n      const xPos = 320;\n      const yPos = 375;\n      const defaultLabel = 'OLINE';\n\n      const group = createOlineGroup(xPos, yPos, color, defaultLabel);\n      players.push(group);\n      history.push({\n        shape: group,\n        array: players,\n        label: defaultLabel\n      });\n      const dataObj = {\n        type: 'oline',\n        x: xPos,\n        y: yPos,\n        color: color,\n        label: defaultLabel\n      };\n      playerData.push(dataObj);\n\n      attachDragEnd(group, dataObj);\n      createPlayerInfoUI(defaultLabel);\n    });\n\n    \/\/ ================== ADD CUSTOM ZONE =====================\n    document.getElementById('addCustomZoneBtn').addEventListener('click', () => {\n      const x = 150;\n      const y = 100;\n      const width = 150;\n      const height = 100;\n      const color = 'purple';\n\n      const zoneGroup = new Konva.Group({\n        x: x,\n        y: y,\n        draggable: true,\n      });\n\n      const rect = new Konva.Rect({\n        width: width,\n        height: height,\n        fill: color,\n        opacity: 0.4,\n        stroke: 'black',\n        strokeWidth: 2,\n      });\n\n      const handles = ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].map((pos) => {\n        return new Konva.Circle({\n          radius: 6,\n          fill: 'black',\n          draggable: true,\n          name: pos,\n          opacity: 0,\n          hitStrokeWidth: 20,\n        });\n      });\n\n      function updateHandles() {\n        handles[0].position({ x: 0, y: 0 });\n        handles[1].position({ x: rect.width(), y: 0 });\n        handles[2].position({ x: 0, y: rect.height() });\n        handles[3].position({ x: rect.width(), y: rect.height() });\n      }\n      updateHandles();\n\n      handles.forEach((handle) => {\n        handle.on('dragmove', function () {\n          const pos = handle.position();\n          switch (handle.name()) {\n            case 'topLeft':\n              rect.width(rect.width() - pos.x);\n              rect.height(rect.height() - pos.y);\n              zoneGroup.x(zoneGroup.x() + pos.x);\n              zoneGroup.y(zoneGroup.y() + pos.y);\n              break;\n            case 'topRight':\n              rect.width(pos.x);\n              rect.height(rect.height() - pos.y);\n              zoneGroup.y(zoneGroup.y() + pos.y);\n              break;\n            case 'bottomLeft':\n              rect.width(rect.width() - pos.x);\n              rect.height(pos.y);\n              zoneGroup.x(zoneGroup.x() + pos.x);\n              break;\n            case 'bottomRight':\n              rect.width(pos.x);\n              rect.height(pos.y);\n              break;\n          }\n          updateHandles();\n          layer.batchDraw();\n        });\n      });\n\n      zoneGroup.add(rect);\n      handles.forEach((handle) => zoneGroup.add(handle));\n      layer.add(zoneGroup);\n      layer.draw();\n\n      customZones.push(zoneGroup);\n      history.push({\n        shape: zoneGroup,\n        array: customZones,\n      });\n    });\n\n    \/\/ ================== CLEAR ALL =====================\n    document.getElementById('clearAllBtn').addEventListener('click', () => {\n      players.forEach((p) => p.destroy());\n      players.length = 0;\n      playerCount = 0;\n      playerData.length = 0;\n\n      playerDetails.forEach((pd) => {\n        const ta = document.getElementById(pd.detailsId);\n        if (ta) ta.parentNode.remove();\n      });\n      playerDetails.length = 0;\n\n      routes.forEach((r) => r.destroy());\n      routes.length = 0;\n\n      blocks.forEach((b) => b.destroy());\n      blocks.length = 0;\n\n      zones.forEach((z) => z.destroy());\n      zones.length = 0;\n\n      customZones.forEach((cz) => cz.destroy());\n      customZones.length = 0;\n\n      dottedLines.forEach((d) => d.destroy());\n      dottedLines.length = 0;\n\n      slantRoutes.forEach((r) => r.destroy());\n      slantRoutes.length = 0;\n      cutRoutes.forEach((r) => r.destroy());\n      cutRoutes.length = 0;\n      comebackRoutes.forEach((r) => r.destroy());\n      comebackRoutes.length = 0;\n      cornerRoutes.forEach((r) => r.destroy());\n      cornerRoutes.length = 0;\n      outRoutes.forEach((r) => r.destroy());\n      outRoutes.length = 0;\n      hitchRoutes.forEach((r) => r.destroy());\n      hitchRoutes.length = 0;\n      goRoutes.forEach((r) => r.destroy());\n      hitchRoutes.length = 0;\n      flatRoutes.forEach((r) => r.destroy());\n      hitchRoutes.length = 0;\n      swingRoutes.forEach((r) => r.destroy());\n      hitchRoutes.length = 0;\n\n      isDrawingRoute = false;\n      routeInProgress = null;\n      routePoints = [];\n      isDrawingBlock = false;\n      blockInProgress = null;\n      blockPoints = [];\n      isDrawingDotted = false;\n      dottedInProgress = null;\n      dottedPoints = [];\n\n      document.getElementById('addRouteBtn').textContent = 'Add Custom Arrow';\n      document.getElementById('addBlockBtn').textContent = 'Add Block';\n      document.getElementById('addDottedBtn').textContent = 'Add Dotted Arrow';\n\n      layer.draw();\n    });\n\n    \/\/ ================== SAVE PDF =====================\n    const savePDFBtn = document.getElementById('savePDFBtn');\n    savePDFBtn.addEventListener('click', async () => {\n      try {\n        let playName = prompt(\"Enter the name of your play:\", \"\");\n        if (!playName) playName = \"\";\n        playName = playName.substring(0, 40);\n\n        const dataUrl = stage.toDataURL({ pixelRatio: 2 });\n        const { jsPDF } = window.jspdf;\n        const pdf = new jsPDF({\n          orientation: 'portrait',\n          unit: 'px',\n          format: 'a4',\n        });\n\n        pdf.setFontSize(16);\n        pdf.text(playName, 20, 40);\n\n        const pdfWidth = 400;\n        const aspectRatio = stageHeight \/ stageWidth;\n        const pdfHeight = pdfWidth * aspectRatio;\n\n        pdf.addImage(dataUrl, 'PNG', 20, 60, pdfWidth, pdfHeight);\n\n        let yOffset = 60 + pdfHeight + 20;\n        pdf.setFontSize(12);\n\n        if (playerDetails.length > 0) {\n          pdf.text('Player Instructions:', 20, yOffset);\n          yOffset += 20;\n        }\n\n        playerDetails.forEach((pd) => {\n          const textArea = document.getElementById(pd.detailsId);\n          if (textArea) {\n            const infoText = textArea.value.trim();\n            const labelName = pd.label;\n            if (infoText) {\n              const line = labelName + ' : ' + infoText;\n              const splitted = pdf.splitTextToSize(line, 400);\n              pdf.text(splitted, 20, yOffset);\n              yOffset += (splitted.length * 14) + 10;\n            } else {\n              pdf.text(labelName + ' => (no extra instructions)', 20, yOffset);\n              yOffset += 20;\n            }\n          }\n        });\n\n        pdf.save(playName + '.pdf');\n      } catch (err) {\n        console.error('Error generating PDF:', err);\n        alert('Sorry, there was an error generating the PDF.');\n      }\n    });\n\n    \/\/ ================== ROUTES, BLOCKS, DOTTED, ZONES EVENTS =====================\n    document.getElementById('addRouteBtn').addEventListener('click', () => {\n      if (!isDrawingRoute) {\n        isDrawingRoute = true;\n        routePoints = [];\n        routeInProgress = new Konva.Arrow({\n          points: [],\n          stroke: 'black',\n          fill: 'black',\n          strokeWidth: 3,\n          pointerLength: 10,\n          pointerWidth: 10,\n        });\n        layer.add(routeInProgress);\n        layer.draw();\n\n        document.getElementById('addRouteBtn').textContent = 'Finish Route';\n      } else {\n        if (routePoints.length < 0) {\n          alert('Please add at least 2 clicks before finishing the route.');\n          return;\n        }\n\n        routes.push(routeInProgress);\n        history.push({\n          shape: routeInProgress,\n          array: routes\n        });\n        routeInProgress = null;\n        routePoints = [];\n        isDrawingRoute = false;\n        document.getElementById('addRouteBtn').textContent = 'Add Custom Arrow';\n        layer.draw();\n      }\n    });\n\n    document.getElementById('addBlockBtn').addEventListener('click', () => {\n      if (isDrawingRoute || isDrawingDotted) {\n        alert('Finish your current drawing before adding a block.');\n        return;\n      }\n      if (!isDrawingBlock) {\n        isDrawingBlock = true;\n        blockPoints = [];\n\n        blockInProgress = new Konva.Group();\n        layer.add(blockInProgress);\n\n        blockLine = new Konva.Line({\n          points: [],\n          stroke: 'orange',\n          strokeWidth: 3,\n        });\n        blockInProgress.add(blockLine);\n\n        blockTBar = new Konva.Line({\n          points: [],\n          stroke: 'orange',\n          strokeWidth: 3,\n        });\n        blockInProgress.add(blockTBar);\n\n        layer.draw();\n        document.getElementById('addBlockBtn').textContent = 'Finish Block';\n      } else {\n        if (blockPoints.length < 0) {\n          alert('Please add at least 2 clicks before finishing the block.');\n          return;\n        }\n        blocks.push(blockInProgress);\n        history.push({\n          shape: blockInProgress,\n          array: blocks\n        });\n        blockInProgress = null;\n        blockLine = null;\n        blockTBar = null;\n        blockPoints = [];\n        isDrawingBlock = false;\n        document.getElementById('addBlockBtn').textContent = 'Add Block';\n        layer.draw();\n      }\n    });\n\n    document.getElementById('addDottedBtn').addEventListener('click', () => {\n      if (isDrawingRoute || isDrawingBlock) {\n        alert('Finish your current drawing before adding a dotted arrow.');\n        return;\n      }\n      if (!isDrawingDotted) {\n        isDrawingDotted = true;\n        dottedPoints = [];\n        dottedInProgress = new Konva.Arrow({\n          points: [],\n          stroke: 'black',\n          fill: 'black',\n          strokeWidth: 3,\n          dash: [10, 5],\n          pointerLength: 10,\n          pointerWidth: 10,\n        });\n        layer.add(dottedInProgress);\n        layer.draw();\n\n        document.getElementById('addDottedBtn').textContent = 'Finish Dotted';\n      } else {\n        if (dottedPoints.length < 0) {\n          alert('Please add at least 2 clicks before finishing the dotted arrow.');\n          return;\n        }\n        dottedLines.push(dottedInProgress);\n        history.push({\n          shape: dottedInProgress,\n          array: dottedLines\n        });\n        dottedInProgress = null;\n        dottedPoints = [];\n        isDrawingDotted = false;\n        document.getElementById('addDottedBtn').textContent = 'Add Dotted Arrow';\n        layer.draw();\n      }\n    });\n\n    stage.on('click tap', (e) => {\n      if (isDrawingRoute && routeInProgress) {\n        const pos = stage.getPointerPosition();\n        routePoints.push(pos.x, pos.y);\n        routeInProgress.points(routePoints);\n        layer.batchDraw();\n        return;\n      }\n      if (isDrawingBlock && blockInProgress) {\n        const pos = stage.getPointerPosition();\n        blockPoints.push(pos.x, pos.y);\n        blockLine.points(blockPoints);\n        updateTBar(blockPoints, blockTBar);\n        layer.batchDraw();\n        return;\n      }\n      if (isDrawingDotted && dottedInProgress) {\n        const pos = stage.getPointerPosition();\n        dottedPoints.push(pos.x, pos.y);\n        dottedInProgress.points(dottedPoints);\n        layer.batchDraw();\n        return;\n      }\n    });\n\n    function updateTBar(points, tBarLine) {\n      if (points.length < 4) {\n        tBarLine.points([]);\n        return;\n      }\n      const len = points.length;\n      const x1 = points[len - 4];\n      const y1 = points[len - 3];\n      const x2 = points[len - 2];\n      const y2 = points[len - 1];\n\n      let dx = x2 - x1;\n      let dy = y2 - y1;\n      const segLength = Math.sqrt(dx*dx + dy*dy);\n      if (segLength === 0) {\n        tBarLine.points([]);\n        return;\n      }\n      dx \/= segLength;\n      dy \/= segLength;\n\n      const px = -dy;\n      const py = dx;\n      const halfWidth = 5;\n\n      const xA = x2 - px * halfWidth;\n      const yA = y2 - py * halfWidth;\n      const xB = x2 + px * halfWidth;\n      const yB = y2 + py * halfWidth;\n\n      tBarLine.points([xA, yA, xB, yB]);\n    }\n\n    \/\/ === Zones\n    function createZone(widthPercent, color) {\n      const zoneWidth = stageWidth * widthPercent;\n      const zoneHeight = stageHeight * 0.2;\n      const xPos = (stageWidth - zoneWidth) \/ 2;\n      const yPos = 50;\n\n      const zoneRect = new Konva.Rect({\n        x: xPos,\n        y: yPos,\n        width: zoneWidth,\n        height: zoneHeight,\n        fill: color,\n        opacity: 0.3,\n        stroke: 'black',\n        strokeWidth: 2,\n        draggable: true,\n        cornerRadius: 20,\n      });\n\n      layer.add(zoneRect);\n      layer.draw();\n      return zoneRect;\n    }\n\n    document.getElementById('addZone40Btn').addEventListener('click', () => {\n      const z = createZone(0.35, 'blue');\n      zones.push(z);\n      history.push({\n        shape: z,\n        array: zones\n      });\n    });\n    document.getElementById('addZone25Btn').addEventListener('click', () => {\n      const z = createZone(0.2, 'red');\n      zones.push(z);\n      history.push({\n        shape: z,\n        array: zones\n      });\n    });\n    document.getElementById('addZone15Btn').addEventListener('click', () => {\n      const z = createZone(0.12, 'yellow');\n      zones.push(z);\n      history.push({\n        shape: z,\n        array: zones\n      });\n    });\n\n    \/\/ === 6 locked-angle routes\n    document.getElementById('addLockedRouteBtn').addEventListener('click', () => {\n      const routeType = document.getElementById('routeTypeSelect').value;\n      if (!routeType) {\n        alert('Please select a route type first.');\n        return;\n      }\n      let anglesAndRadii = [];\n\n      switch(routeType) {\n        case 'slant':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 90 },\n            { angleDeg: -150, radius: 100 }\n          ];\n          const slantShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          slantRoutes.push(slantShape);\n          history.push({\n            shape: slantShape,\n            array: slantRoutes\n          });\n          break;\n        case 'cut':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 90 },\n            { angleDeg: 180, radius: 90 }\n          ];\n          const cutShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          cutRoutes.push(cutShape);\n          history.push({\n            shape: cutShape,\n            array: cutRoutes\n          });\n          break;\n        case 'comeback':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 180 },\n            { angleDeg: 35, radius: 30 }\n          ];\n          const comebackShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          comebackRoutes.push(comebackShape);\n          history.push({\n            shape: comebackShape,\n            array: comebackRoutes\n          });\n          break;\n        case 'corner':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 180 },\n            { angleDeg: -35, radius: 100 }\n          ];\n          const cornerShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          cornerRoutes.push(cornerShape);\n          history.push({\n            shape: cornerShape,\n            array: cornerRoutes\n          });\n          break;\n        case 'out':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 90 },\n            { angleDeg: 0, radius: 90 }\n          ];\n          const outShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          outRoutes.push(outShape);\n          history.push({\n            shape: outShape,\n            array: outRoutes\n          });\n          break;\n        case 'hitch':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 80 },\n            { angleDeg: 130, radius: 30 }\n          ];\n          const hitchShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          hitchRoutes.push(hitchShape);\n          history.push({\n            shape: hitchShape,\n            array: hitchRoutes\n          });\n          break;\n        case 'go':\n          anglesAndRadii = [\n            { angleDeg: -90, radius: 350}\n          ];\n          const goShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          goRoutes.push(goShape);\n          history.push({\n            shape: goShape,\n            array: goRoutes\n          });\n          break;\n        case 'flat':\n          anglesAndRadii = [\n            { angleDeg: -20, radius: 90 },\n            { angleDeg: 0, radius: 80 }\n          ];\n          const flatShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          flatRoutes.push(flatShape);\n          history.push({\n            shape: flatShape,\n            array: flatRoutes\n          });\n          break;\n        case 'swing':\n          anglesAndRadii = [\n            { angleDeg: 10, radius: 50 },\n            { angleDeg: 0, radius: 50 },\n            { angleDeg: -10, radius: 50 },\n            { angleDeg: -40, radius: 30 }\n          ];\n          const swingShape = createLockedAngleRoute(anglesAndRadii, 'black');\n          swingRoutes.push(swingShape);\n          history.push({\n            shape: swingShape,\n            array: swingRoutes\n          });\n          break;\n      }\n    });\n\n    \/\/ UNDO LAST BUTTON\n    document.getElementById('UndoBtn').addEventListener('click', () => {\n      if (history.length === 0) {\n        alert('No more items to remove!');\n        return;\n      }\n\n      const last = history.pop();\n      const arr = last.array;\n      const shape = last.shape;\n      const label = last.label;\n\n      const idx = arr.indexOf(shape);\n      if (idx !== -1) {\n        arr.splice(idx, 1);\n      }\n\n      shape.destroy();\n\n      if (label) {\n        const pdIndex = playerDetails.findIndex(pd => pd.label === label);\n        if (pdIndex !== -1) {\n          const pdObj = playerDetails[pdIndex];\n          playerDetails.splice(pdIndex, 1);\n          const textAreaEl = document.getElementById(pdObj.detailsId);\n          if (textAreaEl && textAreaEl.parentNode) {\n            textAreaEl.parentNode.remove();\n          }\n        }\n      }\n\n      const customZoneIdx = customZones.findIndex(zone => zone === shape);\n      if (customZoneIdx !== -1) {\n        customZones.splice(customZoneIdx, 1);\n      }\n\n      layer.draw();\n    });\n\n    \/\/ === MULTI-FORMATION (players only)\n    window.addEventListener('DOMContentLoaded', populateFormationDropdown);\n\n    function populateFormationDropdown() {\n      const formationSelect = document.getElementById(\"formationSelect\");\n      if (!formationSelect) return;\n      formationSelect.innerHTML = \"\";\n      const defOpt = document.createElement(\"option\");\n      defOpt.value = \"\";\n      defOpt.textContent = \"-- Formations --\";\n      formationSelect.appendChild(defOpt);\n\n      let list = JSON.parse(localStorage.getItem(\"formationList\") || \"[]\");\n      list.forEach((name) => {\n        const opt = document.createElement(\"option\");\n        opt.value = name;\n        opt.textContent = name;\n        formationSelect.appendChild(opt);\n      });\n    }\n\n    \/\/ Save => store playerData only\n    document.getElementById(\"saveFormationBtn\").addEventListener(\"click\", () => {\n      const formationName = prompt(\"Enter a name for this formation:\", \"\");\n      if (!formationName) return;\n\n      \/\/ Here we have playerData containing type, x, y, color, label, and playerType\n      const dataStr = JSON.stringify(playerData);\n      localStorage.setItem(\"formation_\" + formationName, dataStr);\n\n      let list = JSON.parse(localStorage.getItem(\"formationList\") || \"[]\");\n      if (!list.includes(formationName)) {\n        list.push(formationName);\n        localStorage.setItem(\"formationList\", JSON.stringify(list));\n      }\n      alert(`Formation '${formationName}' saved (players only)!`);\n      populateFormationDropdown();\n    });\n\n    \/\/ Load => re-create each item by type\n    document.getElementById(\"loadFormationBtn\").addEventListener(\"click\", () => {\n      const formationSelect = document.getElementById(\"formationSelect\");\n      const chosen = formationSelect.value;\n      if (!chosen) {\n        alert(\"Select a formation first!\");\n        return;\n      }\n      const dataStr = localStorage.getItem(\"formation_\" + chosen);\n      if (!dataStr) {\n        alert(\"No data found for formation: \" + chosen);\n        return;\n      }\n\n      \/\/ Clear existing players\n      players.forEach((p) => p.destroy());\n      players.length = 0;\n      playerData.length = 0;\n      playerCount = 0;\n\n      \/\/ remove old textareas\n      playerDetails.forEach((pd) => {\n        const ta = document.getElementById(pd.detailsId);\n        if (ta) ta.parentNode.remove();\n      });\n      playerDetails.length = 0;\n\n      \/\/ parse\n      const arr = JSON.parse(dataStr);\n      for (const pObj of arr) {\n        let newGroup = null;\n        if (pObj.type === 'oline') {\n          \/\/ re-create 5 circles\n          newGroup = createOlineGroup(pObj.x, pObj.y, pObj.color, pObj.label);\n        } else {\n          \/\/ default single player => pass pObj.playerType to keep shape\n          newGroup = createSinglePlayer(\n            pObj.x,\n            pObj.y,\n            pObj.label,\n            pObj.color,\n            pObj.playerType || 'offense' \/\/ if not stored, default offense\n          );\n        }\n        players.push(newGroup);\n        playerData.push(pObj);\n        playerCount++;\n\n        createPlayerInfoUI(pObj.label);\n\n        \/\/ attach dragend\n        attachDragEnd(newGroup, pObj);\n      }\n      alert(`Formation '${chosen}' loaded (players only)!`);\n    });\n\n    document.getElementById('removeFormationBtn').addEventListener('click', () => {\n      const formationSelect = document.getElementById(\"formationSelect\");\n      const chosen = formationSelect.value;\n      if (!chosen) {\n        alert(\"Please select a formation to remove!\");\n        return;\n      }\n      localStorage.removeItem(\"formation_\" + chosen);\n\n      let list = JSON.parse(localStorage.getItem(\"formationList\") || \"[]\");\n      list = list.filter((name) => name !== chosen);\n      localStorage.setItem(\"formationList\", JSON.stringify(list));\n\n      alert(`Formation '${chosen}' removed from storage!`);\n      populateFormationDropdown();\n    });\n  <\/script>\n<\/body>\n<\/html>\n\n\n\n\n\n\n<div style=\"height:60px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n","protected":false},"excerpt":{"rendered":"<p>Konva Play Designer &#8211; Zones, Dotted Arrow Player Color: OffenseDefense Add Player Add O-Line Undo Last Clear All Choose Field: FieldRed Zone Add Custom Arrow Add Block Add Dotted Arrow Add Custom Zone Add Large Zone Add Medium Zone Add Small Zone &#8212; Choose a route &#8212;Slant\/PostInComebackCornerOutHitch\/CurlGo\/StreakFlatSwing\/Bubble Add Route &#8212; Formations &#8212; Save Formation Load &hellip;<\/p>\n<p class=\"read-more\"> <a class=\"\" href=\"https:\/\/tlap-sports.com\/en\/play-generator\/\"> <span class=\"screen-reader-text\">Play Generator<\/span> Read More &raquo;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"prwc_products":"","prwc_not_all_products_required":false,"prwc_not_bought_page":"","prwc_redirect_not_bought":false,"prwc_not_logged_in_page":"","prwc_redirect_not_logged_in":false,"prwc_timeout_days":0,"prwc_timeout_hours":0,"prwc_timeout_minutes":0,"prwc_timeout_seconds":0,"prwc_timeout_views":0,"pmpro_default_level":"","site-sidebar-layout":"default","site-content-layout":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","footnotes":""},"class_list":["post-8635","page","type-page","status-publish","hentry","pmpro-has-access"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.6 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Play Generator - TLAP Sports english<\/title>\n<meta name=\"description\" content=\"This tool lets you create play and your entire playbook. Just create a play and safe it as a pdf. Easy to use.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/tlap-sports.com\/en\/play-generator\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Play Generator - TLAP Sports english\" \/>\n<meta property=\"og:description\" content=\"This tool lets you create play and your entire playbook. Just create a play and safe it as a pdf. Easy to use.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/tlap-sports.com\/en\/play-generator\/\" \/>\n<meta property=\"og:site_name\" content=\"TLAP Sports english\" \/>\n<meta property=\"article:modified_time\" content=\"2025-03-01T22:58:07+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/tlap-sports.com\/en\/play-generator\/\",\"url\":\"https:\/\/tlap-sports.com\/en\/play-generator\/\",\"name\":\"Play Generator - TLAP Sports english\",\"isPartOf\":{\"@id\":\"https:\/\/tlap-sports.com\/en\/#website\"},\"datePublished\":\"2025-02-02T21:34:05+00:00\",\"dateModified\":\"2025-03-01T22:58:07+00:00\",\"description\":\"This tool lets you create play and your entire playbook. Just create a play and safe it as a pdf. Easy to use.\",\"breadcrumb\":{\"@id\":\"https:\/\/tlap-sports.com\/en\/play-generator\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/tlap-sports.com\/en\/play-generator\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/tlap-sports.com\/en\/play-generator\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/tlap-sports.com\/en\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Play Generator\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/tlap-sports.com\/en\/#website\",\"url\":\"https:\/\/tlap-sports.com\/en\/\",\"name\":\"TLAP Sports english\",\"description\":\"Train like a pro no matter what background and resources you have\",\"publisher\":{\"@id\":\"https:\/\/tlap-sports.com\/en\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/tlap-sports.com\/en\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/tlap-sports.com\/en\/#organization\",\"name\":\"TLAP Sports english\",\"url\":\"https:\/\/tlap-sports.com\/en\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/tlap-sports.com\/en\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/tlap-sports.com\/en\/wp-content\/uploads\/sites\/3\/2025\/09\/cropped-logo-tlap-sports-white-red.png\",\"contentUrl\":\"https:\/\/tlap-sports.com\/en\/wp-content\/uploads\/sites\/3\/2025\/09\/cropped-logo-tlap-sports-white-red.png\",\"width\":\"1331\",\"height\":\"1420\",\"caption\":\"TLAP Sports english\"},\"image\":{\"@id\":\"https:\/\/tlap-sports.com\/en\/#\/schema\/logo\/image\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Play Generator - TLAP Sports english","description":"This tool lets you create play and your entire playbook. Just create a play and safe it as a pdf. Easy to use.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/tlap-sports.com\/en\/play-generator\/","og_locale":"en_US","og_type":"article","og_title":"Play Generator - TLAP Sports english","og_description":"This tool lets you create play and your entire playbook. Just create a play and safe it as a pdf. Easy to use.","og_url":"https:\/\/tlap-sports.com\/en\/play-generator\/","og_site_name":"TLAP Sports english","article_modified_time":"2025-03-01T22:58:07+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/tlap-sports.com\/en\/play-generator\/","url":"https:\/\/tlap-sports.com\/en\/play-generator\/","name":"Play Generator - TLAP Sports english","isPartOf":{"@id":"https:\/\/tlap-sports.com\/en\/#website"},"datePublished":"2025-02-02T21:34:05+00:00","dateModified":"2025-03-01T22:58:07+00:00","description":"This tool lets you create play and your entire playbook. Just create a play and safe it as a pdf. Easy to use.","breadcrumb":{"@id":"https:\/\/tlap-sports.com\/en\/play-generator\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/tlap-sports.com\/en\/play-generator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/tlap-sports.com\/en\/play-generator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/tlap-sports.com\/en\/"},{"@type":"ListItem","position":2,"name":"Play Generator"}]},{"@type":"WebSite","@id":"https:\/\/tlap-sports.com\/en\/#website","url":"https:\/\/tlap-sports.com\/en\/","name":"TLAP Sports english","description":"Train like a pro no matter what background and resources you have","publisher":{"@id":"https:\/\/tlap-sports.com\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/tlap-sports.com\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/tlap-sports.com\/en\/#organization","name":"TLAP Sports english","url":"https:\/\/tlap-sports.com\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/tlap-sports.com\/en\/#\/schema\/logo\/image\/","url":"https:\/\/tlap-sports.com\/en\/wp-content\/uploads\/sites\/3\/2025\/09\/cropped-logo-tlap-sports-white-red.png","contentUrl":"https:\/\/tlap-sports.com\/en\/wp-content\/uploads\/sites\/3\/2025\/09\/cropped-logo-tlap-sports-white-red.png","width":"1331","height":"1420","caption":"TLAP Sports english"},"image":{"@id":"https:\/\/tlap-sports.com\/en\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/pages\/8635","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/comments?post=8635"}],"version-history":[{"count":131,"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/pages\/8635\/revisions"}],"predecessor-version":[{"id":9056,"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/pages\/8635\/revisions\/9056"}],"wp:attachment":[{"href":"https:\/\/tlap-sports.com\/en\/wp-json\/wp\/v2\/media?parent=8635"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}