{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import type {\n  Anchor,\n  Color,\n  Comp,\n  KAPLAYCtx,\n  Outline,\n  Rect,\n  SpriteData,\n  TextAlign,\n  Uniform,\n} from 'kaplay';\n\n/** Drop shadow style for styled text. */\nexport interface StyledTextShadow {\n  color: Color;\n  offsetX: number;\n  offsetY: number;\n  blur: number;\n}\n\n/** Gradient style for styled text. */\nexport interface StyledTextGradient {\n  from: Color;\n  to: Color;\n  direction: 'horizontal' | 'vertical';\n}\n\n/** Options for the {@link styledText} component. */\nexport interface StyledTextCompOpt {\n  size?: number;\n  font?: string;\n  fill?: Color;\n  outline?: Outline;\n  shadow?: StyledTextShadow;\n  gradient?: StyledTextGradient;\n  align?: TextAlign;\n  width?: number;\n  lineSpacing?: number;\n}\n\n/** The {@link styledText} component. */\nexport interface StyledTextComp extends Comp {\n  id: 'styledText';\n  text: string;\n  width: number;\n  height: number;\n  renderArea(): Rect;\n  setStyle(opt: Partial<StyledTextCompOpt>): void;\n}\n\ndeclare module 'kaplay' {\n  interface KAPLAYCtx {\n    styledText: (txt?: string, opt?: StyledTextCompOpt) => StyledTextComp;\n  }\n}\n\n/** Convert a KAPLAY {@link Color} to a CSS string. */\nfunction colorToCss(color: Color): string {\n  return `rgb(${String(color.r)}, ${String(color.g)}, ${String(color.b)})`;\n}\n\n/** Resolve the font from options, KAPLAY init, or default. */\nfunction resolveFont(k: KAPLAYCtx, font?: string): string {\n  if (font) return font;\n  const initFont = (k as { _k?: { globalOpt?: { font?: string } } })._k\n    ?.globalOpt?.font;\n  if (initFont) return initFont;\n  return 'monospace';\n}\n\n/** Styled text plugin for KAPLAY. */\nexport function styledTextPlugin(k: KAPLAYCtx) {\n  const styledText = (\n    txt = '',\n    opt: StyledTextCompOpt = {},\n  ): StyledTextComp => {\n    let currentText = txt;\n    let currentOpt: StyledTextCompOpt = { ...opt };\n    let canvas: HTMLCanvasElement | null = null;\n    let ctx: CanvasRenderingContext2D | null = null;\n    let spriteData: SpriteData | null = null;\n    let textWidth = 0;\n    let textHeight = 0;\n    let lastCanvasWidth = 0;\n    let lastCanvasHeight = 0;\n    let currentPadding = 0;\n\n    const getFont = (): string => {\n      return resolveFont(k, currentOpt.font);\n    };\n\n    const render = (): void => {\n      if (!canvas || !ctx) return;\n\n      const font = getFont();\n      const fontSize = currentOpt.size ?? 48;\n      const textAlign = currentOpt.align ?? 'left';\n      const ls = currentOpt.lineSpacing ?? 0;\n      const outlineWidth = currentOpt.outline?.width ?? 0;\n      const shadowOffsetX = currentOpt.shadow?.offsetX ?? 0;\n      const shadowOffsetY = currentOpt.shadow?.offsetY ?? 0;\n      const shadowBlur = currentOpt.shadow?.blur ?? 0;\n      const padding =\n        Math.max(outlineWidth, shadowBlur) +\n        Math.abs(shadowOffsetX) +\n        Math.abs(shadowOffsetY) +\n        2;\n      currentPadding = padding;\n\n      ctx.font = `${String(fontSize)}px ${font}`;\n      ctx.textAlign = textAlign;\n      ctx.textBaseline = 'top';\n\n      // Handle wrapping\n      const wrapW = currentOpt.width;\n      const lines = wrapText(ctx, currentText, wrapW);\n      const lineHeight = fontSize + ls;\n\n      // Measure widest line\n      let maxLineWidth = 0;\n      for (const line of lines) {\n        const metrics = ctx.measureText(line);\n        maxLineWidth = Math.max(maxLineWidth, metrics.width);\n      }\n\n      const canvasWidth = Math.ceil(maxLineWidth + padding * 2);\n      const canvasHeight = Math.ceil(lines.length * lineHeight + padding * 2);\n\n      // Resize canvas if dimensions changed\n      if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {\n        canvas.width = canvasWidth;\n        canvas.height = canvasHeight;\n        // Re-set context properties after resize (they reset)\n        ctx.font = `${String(fontSize)}px ${font}`;\n        ctx.textAlign = textAlign;\n        ctx.textBaseline = 'top';\n      }\n\n      // Clear canvas\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      // Draw each line\n      for (let i = 0; i < lines.length; i++) {\n        const line = lines[i];\n        const x = padding;\n        const y = padding + i * lineHeight;\n\n        // Apply shadow\n        if (currentOpt.shadow) {\n          ctx.shadowColor = colorToCss(currentOpt.shadow.color);\n          ctx.shadowOffsetX = currentOpt.shadow.offsetX;\n          ctx.shadowOffsetY = currentOpt.shadow.offsetY;\n          ctx.shadowBlur = currentOpt.shadow.blur;\n        }\n\n        // Draw outline (stroke)\n        if (currentOpt.outline) {\n          ctx.strokeStyle = currentOpt.outline.color\n            ? colorToCss(currentOpt.outline.color)\n            : 'black';\n          ctx.lineWidth = currentOpt.outline.width ?? 1;\n          ctx.lineJoin = 'round';\n          ctx.strokeText(line, x, y);\n\n          // Reset shadow before fill to avoid double shadow\n          ctx.shadowColor = 'transparent';\n          ctx.shadowOffsetX = 0;\n          ctx.shadowOffsetY = 0;\n          ctx.shadowBlur = 0;\n        }\n\n        // Apply fill or gradient\n        if (currentOpt.gradient) {\n          const grad =\n            currentOpt.gradient.direction === 'vertical'\n              ? ctx.createLinearGradient(0, padding, 0, canvasHeight - padding)\n              : ctx.createLinearGradient(padding, 0, canvasWidth - padding, 0);\n          grad.addColorStop(0, colorToCss(currentOpt.gradient.from));\n          grad.addColorStop(1, colorToCss(currentOpt.gradient.to));\n          ctx.fillStyle = grad;\n        } else if (currentOpt.fill) {\n          ctx.fillStyle = colorToCss(currentOpt.fill);\n        } else {\n          ctx.fillStyle = 'white';\n        }\n\n        ctx.fillText(line, x, y);\n      }\n\n      textWidth = canvasWidth;\n      textHeight = canvasHeight;\n\n      // Update or create sprite data\n      if (\n        spriteData &&\n        lastCanvasWidth === canvasWidth &&\n        lastCanvasHeight === canvasHeight\n      ) {\n        const frame = spriteData.frames[0];\n        const texX = Math.round(frame.x * spriteData.tex.width);\n        const texY = Math.round(frame.y * spriteData.tex.height);\n        spriteData.tex.update(canvas, texX, texY);\n      } else {\n        if (spriteData) {\n          spriteData.tex.free();\n        }\n        spriteData = k.SpriteData.fromImage(canvas, { singular: true });\n        lastCanvasWidth = canvasWidth;\n        lastCanvasHeight = canvasHeight;\n      }\n    };\n\n    const wrapText = (\n      c: CanvasRenderingContext2D,\n      text: string,\n      maxWidth?: number,\n    ): string[] => {\n      if (!maxWidth) return text.split('\\n');\n      const allLines: string[] = [];\n      for (const paragraph of text.split('\\n')) {\n        const words = paragraph.split(' ');\n        let currentLine = '';\n        for (const word of words) {\n          const testLine = currentLine ? `${currentLine} ${word}` : word;\n          const metrics = c.measureText(testLine);\n          if (metrics.width > maxWidth && currentLine) {\n            allLines.push(currentLine);\n            currentLine = word;\n          } else {\n            currentLine = testLine;\n          }\n        }\n        allLines.push(currentLine);\n      }\n      return allLines;\n    };\n\n    const comp: StyledTextComp = {\n      id: 'styledText',\n      get text() {\n        return currentText;\n      },\n      set text(value: string) {\n        currentText = value;\n        render();\n      },\n      get width() {\n        return textWidth;\n      },\n      get height() {\n        return textHeight;\n      },\n      renderArea(): Rect {\n        return new k.Rect(k.vec2(0), textWidth, textHeight);\n      },\n      setStyle(opt: Partial<StyledTextCompOpt>): void {\n        currentOpt = { ...currentOpt, ...opt };\n        render();\n      },\n      add() {\n        canvas = document.createElement('canvas');\n        ctx = canvas.getContext('2d');\n        if (ctx) {\n          render();\n        }\n      },\n      draw() {\n        if (spriteData) {\n          const obj = this as unknown as {\n            anchor?: Anchor;\n            color?: Color;\n            opacity?: number;\n            outline?: Outline;\n            shader?: string;\n            uniform?: Uniform;\n            flipX?: boolean;\n            flipY?: boolean;\n          };\n          k.drawSprite({\n            sprite: spriteData,\n            pos: k.vec2(-currentPadding, -currentPadding),\n            anchor: obj.anchor,\n            color: obj.color,\n            opacity: obj.opacity,\n            outline: obj.outline,\n            shader: obj.shader,\n            uniform: obj.uniform,\n            flipX: obj.flipX,\n            flipY: obj.flipY,\n          });\n        }\n      },\n      destroy() {\n        if (spriteData) {\n          spriteData.tex.free();\n          spriteData = null;\n        }\n        canvas = null;\n        ctx = null;\n      },\n    };\n\n    return comp;\n  };\n\n  return { styledText };\n}\n"],"mappings":";AAyDA,SAAS,EAAW,GAAsB;CACxC,OAAO,OAAO,OAAO,EAAM,CAAC,EAAE,IAAI,OAAO,EAAM,CAAC,EAAE,IAAI,OAAO,EAAM,CAAC,EAAE;AACxE;AAGA,SAAS,EAAY,GAAc,GAAuB;CAKxD,OAJI,KACc,EAAiD,IAC/D,WAAW,QAER;AACT;AAGA,SAAgB,EAAiB,GAAc;CA0O7C,OAAO,EAAE,aAxOP,IAAM,IACN,IAAyB,CAAC,MACP;EACnB,IAAI,IAAc,GACd,IAAgC,EAAE,GAAG,EAAI,GACzC,IAAmC,MACnC,IAAuC,MACvC,IAAgC,MAChC,IAAY,GACZ,IAAa,GACb,IAAkB,GAClB,IAAmB,GACnB,IAAiB,GAEf,UACG,EAAY,GAAG,EAAW,IAAI,GAGjC,UAAqB;GACzB,IAAI,CAAC,KAAU,CAAC,GAAK;GAErB,IAAM,IAAO,EAAQ,GACf,IAAW,EAAW,QAAQ,IAC9B,IAAY,EAAW,SAAS,QAChC,IAAK,EAAW,eAAe,GAC/B,IAAe,EAAW,SAAS,SAAS,GAC5C,IAAgB,EAAW,QAAQ,WAAW,GAC9C,IAAgB,EAAW,QAAQ,WAAW,GAC9C,IAAa,EAAW,QAAQ,QAAQ,GACxC,IACJ,KAAK,IAAI,GAAc,CAAU,IACjC,KAAK,IAAI,CAAa,IACtB,KAAK,IAAI,CAAa,IACtB;GAKF,AAJA,IAAiB,GAEjB,EAAI,OAAO,GAAG,OAAO,CAAQ,EAAE,KAAK,KACpC,EAAI,YAAY,GAChB,EAAI,eAAe;GAGnB,IAAM,IAAQ,EAAW,OACnB,IAAQ,EAAS,GAAK,GAAa,CAAK,GACxC,IAAa,IAAW,GAG1B,IAAe;GACnB,KAAK,IAAM,KAAQ,GAAO;IACxB,IAAM,IAAU,EAAI,YAAY,CAAI;IACpC,IAAe,KAAK,IAAI,GAAc,EAAQ,KAAK;GACrD;GAEA,IAAM,IAAc,KAAK,KAAK,IAAe,IAAU,CAAC,GAClD,IAAe,KAAK,KAAK,EAAM,SAAS,IAAa,IAAU,CAAC;GAatE,CAVI,EAAO,UAAU,KAAe,EAAO,WAAW,OACpD,EAAO,QAAQ,GACf,EAAO,SAAS,GAEhB,EAAI,OAAO,GAAG,OAAO,CAAQ,EAAE,KAAK,KACpC,EAAI,YAAY,GAChB,EAAI,eAAe,QAIrB,EAAI,UAAU,GAAG,GAAG,EAAO,OAAO,EAAO,MAAM;GAG/C,KAAK,IAAI,IAAI,GAAG,IAAI,EAAM,QAAQ,KAAK;IACrC,IAAM,IAAO,EAAM,IACb,IAAI,GACJ,IAAI,IAAU,IAAI;IA2BxB,IAxBI,EAAW,WACb,EAAI,cAAc,EAAW,EAAW,OAAO,KAAK,GACpD,EAAI,gBAAgB,EAAW,OAAO,SACtC,EAAI,gBAAgB,EAAW,OAAO,SACtC,EAAI,aAAa,EAAW,OAAO,OAIjC,EAAW,YACb,EAAI,cAAc,EAAW,QAAQ,QACjC,EAAW,EAAW,QAAQ,KAAK,IACnC,SACJ,EAAI,YAAY,EAAW,QAAQ,SAAS,GAC5C,EAAI,WAAW,SACf,EAAI,WAAW,GAAM,GAAG,CAAC,GAGzB,EAAI,cAAc,eAClB,EAAI,gBAAgB,GACpB,EAAI,gBAAgB,GACpB,EAAI,aAAa,IAIf,EAAW,UAAU;KACvB,IAAM,IACJ,EAAW,SAAS,cAAc,aAC9B,EAAI,qBAAqB,GAAG,GAAS,GAAG,IAAe,CAAO,IAC9D,EAAI,qBAAqB,GAAS,GAAG,IAAc,GAAS,CAAC;KAGnE,AAFA,EAAK,aAAa,GAAG,EAAW,EAAW,SAAS,IAAI,CAAC,GACzD,EAAK,aAAa,GAAG,EAAW,EAAW,SAAS,EAAE,CAAC,GACvD,EAAI,YAAY;IAClB,OAAO,AAAI,EAAW,OACpB,EAAI,YAAY,EAAW,EAAW,IAAI,IAE1C,EAAI,YAAY;IAGlB,EAAI,SAAS,GAAM,GAAG,CAAC;GACzB;GAMA,IAJA,IAAY,GACZ,IAAa,GAIX,KACA,MAAoB,KACpB,MAAqB,GACrB;IACA,IAAM,IAAQ,EAAW,OAAO,IAC1B,IAAO,KAAK,MAAM,EAAM,IAAI,EAAW,IAAI,KAAK,GAChD,IAAO,KAAK,MAAM,EAAM,IAAI,EAAW,IAAI,MAAM;IACvD,EAAW,IAAI,OAAO,GAAQ,GAAM,CAAI;GAC1C,OAME,AALI,KACF,EAAW,IAAI,KAAK,GAEtB,IAAa,EAAE,WAAW,UAAU,GAAQ,EAAE,UAAU,GAAK,CAAC,GAC9D,IAAkB,GAClB,IAAmB;EAEvB,GAEM,KACJ,GACA,GACA,MACa;GACb,IAAI,CAAC,GAAU,OAAO,EAAK,MAAM,IAAI;GACrC,IAAM,IAAqB,CAAC;GAC5B,KAAK,IAAM,KAAa,EAAK,MAAM,IAAI,GAAG;IACxC,IAAM,IAAQ,EAAU,MAAM,GAAG,GAC7B,IAAc;IAClB,KAAK,IAAM,KAAQ,GAAO;KACxB,IAAM,IAAW,IAAc,GAAG,EAAY,GAAG,MAAS;KAE1D,AADgB,EAAE,YAAY,CAC1B,CAAA,CAAQ,QAAQ,KAAY,KAC9B,EAAS,KAAK,CAAW,GACzB,IAAc,KAEd,IAAc;IAElB;IACA,EAAS,KAAK,CAAW;GAC3B;GACA,OAAO;EACT;EAmEA,OAAO;GAhEL,IAAI;GACJ,IAAI,OAAO;IACT,OAAO;GACT;GACA,IAAI,KAAK,GAAe;IAEtB,AADA,IAAc,GACd,EAAO;GACT;GACA,IAAI,QAAQ;IACV,OAAO;GACT;GACA,IAAI,SAAS;IACX,OAAO;GACT;GACA,aAAmB;IACjB,OAAO,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,GAAW,CAAU;GACpD;GACA,SAAS,GAAuC;IAE9C,AADA,IAAa;KAAE,GAAG;KAAY,GAAG;IAAI,GACrC,EAAO;GACT;GACA,MAAM;IAGJ,AAFA,IAAS,SAAS,cAAc,QAAQ,GACxC,IAAM,EAAO,WAAW,IAAI,GACxB,KACF,EAAO;GAEX;GACA,OAAO;IACL,IAAI,GAAY;KACd,IAAM,IAAM;KAUZ,EAAE,WAAW;MACX,QAAQ;MACR,KAAK,EAAE,KAAK,CAAC,GAAgB,CAAC,CAAc;MAC5C,QAAQ,EAAI;MACZ,OAAO,EAAI;MACX,SAAS,EAAI;MACb,SAAS,EAAI;MACb,QAAQ,EAAI;MACZ,SAAS,EAAI;MACb,OAAO,EAAI;MACX,OAAO,EAAI;KACb,CAAC;IACH;GACF;GACA,UAAU;IAMR,AALA,AAEE,OADA,EAAW,IAAI,KAAK,GACP,OAEf,IAAS,MACT,IAAM;GACR;EAGK;CACT,EAEoB;AACtB"}