{"id":49060621,"url":"https://github.com/ShipItAndPray/pretext-comic","last_synced_at":"2026-05-06T06:01:41.272Z","repository":{"id":348656759,"uuid":"1196304210","full_name":"ShipItAndPray/pretext-comic","owner":"ShipItAndPray","description":"Comic speech bubble tool with shape-aware text fitting. 6 built-in shapes, PNG export.","archived":false,"fork":false,"pushed_at":"2026-04-02T06:16:46.000Z","size":100,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-04T17:14:22.538Z","etag":null,"topics":["pretext","text-layout","typescript","typography"],"latest_commit_sha":null,"homepage":"https://shipitandpray.github.io/pretext-comic/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ShipItAndPray.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T15:10:49.000Z","updated_at":"2026-04-02T06:16:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ShipItAndPray/pretext-comic","commit_stats":null,"previous_names":["shipitandpray/pretext-comic"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ShipItAndPray/pretext-comic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-comic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-comic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-comic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-comic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ShipItAndPray","download_url":"https://codeload.github.com/ShipItAndPray/pretext-comic/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ShipItAndPray%2Fpretext-comic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32680890,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-06T02:33:58.958Z","status":"ssl_error","status_checked_at":"2026-05-06T02:33:39.611Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pretext","text-layout","typescript","typography"],"created_at":"2026-04-20T02:00:28.099Z","updated_at":"2026-05-06T06:01:41.227Z","avatar_url":"https://github.com/ShipItAndPray.png","language":"TypeScript","funding_links":[],"categories":["Ecosystem Catalog"],"sub_categories":["Graphics, Media, and Canvas Rendering"],"readme":"# @shipitandpray/pretext-comic\n\n[![Live Demo](https://img.shields.io/badge/demo-live-brightgreen)](https://shipitandpray.github.io/pretext-comic/) [![GitHub](https://img.shields.io/github/stars/ShipItAndPray/pretext-comic?style=social)](https://github.com/ShipItAndPray/pretext-comic)\n\n\u003e **[View Live Demo](https://shipitandpray.github.io/pretext-comic/)**\n\nFit text inside comic speech bubbles. Ellipse, cloud, thought, shout, custom shapes. Canvas + React. Export PNG.\n\n## Install\n\n```bash\nnpm install @shipitandpray/pretext-comic @pretext/core\n```\n\n## Quick Start\n\n```tsx\nimport { SpeechBubble } from '@shipitandpray/pretext-comic';\n\n\u003cSpeechBubble\n  shape=\"ellipse\"\n  text=\"Hello! I am a speech bubble with auto-fitting text!\"\n  width={200}\n  height={150}\n  autoFontSize\n  tail={{ angle: 240, length: 30, width: 20, style: 'pointed' }}\n/\u003e\n```\n\n## How It Works\n\nStandard text layout uses a fixed `maxWidth` for every line. **pretext-comic** uses shape-aware layout: each line's available width is calculated from the shape boundary at that Y position. This means text wraps naturally inside ellipses, clouds, starbursts -- any shape.\n\nThe core algorithm:\n1. Start at the top of the shape (after padding)\n2. For each line Y, call `shape.getWidthAtY(y)` to get available width\n3. Lay out words into that width using Pretext's `layoutNextLine()`\n4. Advance Y by `lineHeight` and repeat\n5. Binary search finds the optimal font size (converges in ~8 iterations)\n\n## Shape Gallery\n\n### Ellipse\n```tsx\n\u003cSpeechBubble shape=\"ellipse\" text=\"Classic speech bubble!\" width={200} height={150} autoFontSize /\u003e\n```\nStandard oval speech bubble. Width follows `2a * sqrt(1 - ((y-b)/b)^2)` with 15% padding.\n\n### Rectangle\n```tsx\n\u003cSpeechBubble shape=\"rectangle\" text=\"Narration box\" width={200} height={150} autoFontSize /\u003e\n```\nRounded rectangle. Constant width except near rounded corners.\n\n### Cloud\n```tsx\n\u003cSpeechBubble shape=\"cloud\" text=\"Fluffy thoughts!\" width={200} height={150} autoFontSize /\u003e\n```\nScalloped edge bubble. Overlapping circles along an ellipse perimeter.\n\n### Thought\n```tsx\n\u003cSpeechBubble\n  shape=\"thought\"\n  text=\"Hmm, I wonder...\"\n  width={180} height={120}\n  tail={{ angle: 200, length: 40, width: 15, style: 'thought' }}\n  autoFontSize\n/\u003e\n```\nCloud body + trail of decreasing circles as the tail.\n\n### Shout\n```tsx\n\u003cSpeechBubble shape=\"shout\" text=\"BOOM!\" width={200} height={150} autoFontSize /\u003e\n```\nStarburst shape. Width varies sinusoidally for a spiky effect.\n\n### Whisper\n```tsx\n\u003cSpeechBubble shape=\"whisper\" text=\"psst...\" width={180} height={120} autoFontSize /\u003e\n```\nEllipse geometry with dashed border stroke.\n\n## Custom Shapes\n\nDefine a shape by implementing `ShapeDefinition`:\n\n```ts\nimport { registerShape } from '@shipitandpray/pretext-comic';\n\nregisterShape('diamond', {\n  type: 'custom',\n  getWidthAtY(y, height, maxWidth) {\n    const mid = height / 2;\n    const ratio = Math.abs(y - mid) / mid;\n    return maxWidth * (1 - ratio) * 0.8;\n  },\n  getPath(width, height) {\n    const path = new Path2D();\n    path.moveTo(width / 2, 0);\n    path.lineTo(width, height / 2);\n    path.lineTo(width / 2, height);\n    path.lineTo(0, height / 2);\n    path.closePath();\n    return path;\n  },\n  getInnerPadding() {\n    return { top: 10, right: 10, bottom: 10, left: 10 };\n  },\n});\n```\n\nOr create a custom shape from an SVG path:\n\n```ts\nimport { createCustomShape, registerShape } from '@shipitandpray/pretext-comic';\n\nconst heartShape = createCustomShape('M 100 30 Q 100 0 70 0 ...');\nregisterShape('heart', heartShape);\n```\n\n## Components\n\n### SpeechBubble\n\nSingle bubble rendered on a canvas with high-DPI support.\n\n```tsx\n\u003cSpeechBubble\n  shape=\"ellipse\"\n  text=\"Hello!\"\n  width={200}\n  height={150}\n  autoFontSize\n  color=\"#000\"\n  backgroundColor=\"#fff\"\n  borderColor=\"#000\"\n  tail={{ angle: 240, length: 30, width: 20, style: 'pointed' }}\n  onClick={() =\u003e console.log('clicked')}\n/\u003e\n```\n\n### ComicPanel\n\nContainer for multiple bubbles with PNG export.\n\n```tsx\n\u003cComicPanel width={800} height={600} onExport={(blob) =\u003e saveAs(blob, 'panel.png')}\u003e\n  \u003cSpeechBubble shape=\"ellipse\" text=\"Look out!\" style={{ position: 'absolute', left: 50, top: 30 }} /\u003e\n  \u003cSpeechBubble shape=\"shout\" text=\"BOOM!\" style={{ position: 'absolute', left: 400, top: 200 }} autoFontSize /\u003e\n\u003c/ComicPanel\u003e\n```\n\n### BubbleEditor\n\nDrag-and-drop editor for placing, editing, and exporting bubbles.\n\n```tsx\n\u003cBubbleEditor\n  width={800}\n  height={600}\n  backgroundImage=\"/comic-panel.jpg\"\n  bubbles={bubbles}\n  onBubblesChange={setBubbles}\n  onExport={(blob) =\u003e downloadBlob(blob)}\n/\u003e\n```\n\n## Programmatic API\n\n```ts\nimport { layoutTextInShape, findOptimalFontSize } from '@shipitandpray/pretext-comic';\n\n// Layout text inside a shape\nconst layout = layoutTextInShape(\n  'This text wraps inside an ellipse!',\n  'ellipse', 200, 150,\n  { fontFamily: 'Comic Sans MS', fontSize: 14, lineHeight: 18 }\n);\nconsole.log(layout.lines);  // Each line with x, y, width\nconsole.log(layout.fits);   // true if all text fits\n\n// Find optimal font size via binary search\nconst { fontSize, layout: optLayout } = findOptimalFontSize(\n  'Auto-sized text!', 'cloud', 200, 150, 'Comic Sans MS', 8, 72\n);\n```\n\n## Comparison\n\n| Feature | pretext-comic | Photoshop | Canva Comics | Pixton |\n|---------|-------------|-----------|-------------|--------|\n| Shape-aware text wrap | Yes | Manual | No (rect only) | No |\n| Open source | Yes | No | No | No |\n| Programmatic API | Yes | Scripting | No | No |\n| Web-based | Yes | No | Yes | Yes |\n| Export PNG | Yes | Yes | Yes | Yes |\n| React components | Yes | No | No | No |\n| Free | Yes | $22/mo | $13/mo | $8/mo |\n\n## Use Cases\n\n- Webtoon/manga lettering\n- Meme generators\n- Educational comics\n- Social media comic strips\n- Automated comic pipelines\n\n## Performance\n\n| Operation | Target |\n|-----------|--------|\n| Layout single bubble | \u003c 1ms |\n| Font size binary search | \u003c 10ms |\n| Render single bubble | \u003c 5ms |\n| Render 20-bubble panel | \u003c 50ms |\n| Export 800x600 PNG | \u003c 100ms |\n| Bundle size | \u003c 15KB gzipped |\n\n## Development\n\n```bash\nnpm install\nnpm run build       # ESM + CJS via tsup\nnpm test            # Vitest\nnpm run demo:dev    # Vite dev server for demo\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FShipItAndPray%2Fpretext-comic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FShipItAndPray%2Fpretext-comic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FShipItAndPray%2Fpretext-comic/lists"}