https://github.com/ShipItAndPray/pretext-comic
Comic speech bubble tool with shape-aware text fitting. 6 built-in shapes, PNG export.
https://github.com/ShipItAndPray/pretext-comic
pretext text-layout typescript typography
Last synced: about 1 month ago
JSON representation
Comic speech bubble tool with shape-aware text fitting. 6 built-in shapes, PNG export.
- Host: GitHub
- URL: https://github.com/ShipItAndPray/pretext-comic
- Owner: ShipItAndPray
- Created: 2026-03-30T15:10:49.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-02T06:16:46.000Z (2 months ago)
- Last Synced: 2026-04-04T17:14:22.538Z (2 months ago)
- Topics: pretext, text-layout, typescript, typography
- Language: TypeScript
- Homepage: https://shipitandpray.github.io/pretext-comic/
- Size: 97.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-pretext - `pretext-comic` - aware text fitting for comics. | [live](https://shipitandpray.github.io/pretext-comic/) | (Ecosystem Catalog / Graphics, Media, and Canvas Rendering)
README
# @shipitandpray/pretext-comic
[](https://shipitandpray.github.io/pretext-comic/) [](https://github.com/ShipItAndPray/pretext-comic)
> **[View Live Demo](https://shipitandpray.github.io/pretext-comic/)**
Fit text inside comic speech bubbles. Ellipse, cloud, thought, shout, custom shapes. Canvas + React. Export PNG.
## Install
```bash
npm install @shipitandpray/pretext-comic @pretext/core
```
## Quick Start
```tsx
import { SpeechBubble } from '@shipitandpray/pretext-comic';
```
## How It Works
Standard 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.
The core algorithm:
1. Start at the top of the shape (after padding)
2. For each line Y, call `shape.getWidthAtY(y)` to get available width
3. Lay out words into that width using Pretext's `layoutNextLine()`
4. Advance Y by `lineHeight` and repeat
5. Binary search finds the optimal font size (converges in ~8 iterations)
## Shape Gallery
### Ellipse
```tsx
```
Standard oval speech bubble. Width follows `2a * sqrt(1 - ((y-b)/b)^2)` with 15% padding.
### Rectangle
```tsx
```
Rounded rectangle. Constant width except near rounded corners.
### Cloud
```tsx
```
Scalloped edge bubble. Overlapping circles along an ellipse perimeter.
### Thought
```tsx
```
Cloud body + trail of decreasing circles as the tail.
### Shout
```tsx
```
Starburst shape. Width varies sinusoidally for a spiky effect.
### Whisper
```tsx
```
Ellipse geometry with dashed border stroke.
## Custom Shapes
Define a shape by implementing `ShapeDefinition`:
```ts
import { registerShape } from '@shipitandpray/pretext-comic';
registerShape('diamond', {
type: 'custom',
getWidthAtY(y, height, maxWidth) {
const mid = height / 2;
const ratio = Math.abs(y - mid) / mid;
return maxWidth * (1 - ratio) * 0.8;
},
getPath(width, height) {
const path = new Path2D();
path.moveTo(width / 2, 0);
path.lineTo(width, height / 2);
path.lineTo(width / 2, height);
path.lineTo(0, height / 2);
path.closePath();
return path;
},
getInnerPadding() {
return { top: 10, right: 10, bottom: 10, left: 10 };
},
});
```
Or create a custom shape from an SVG path:
```ts
import { createCustomShape, registerShape } from '@shipitandpray/pretext-comic';
const heartShape = createCustomShape('M 100 30 Q 100 0 70 0 ...');
registerShape('heart', heartShape);
```
## Components
### SpeechBubble
Single bubble rendered on a canvas with high-DPI support.
```tsx
console.log('clicked')}
/>
```
### ComicPanel
Container for multiple bubbles with PNG export.
```tsx
saveAs(blob, 'panel.png')}>
```
### BubbleEditor
Drag-and-drop editor for placing, editing, and exporting bubbles.
```tsx
downloadBlob(blob)}
/>
```
## Programmatic API
```ts
import { layoutTextInShape, findOptimalFontSize } from '@shipitandpray/pretext-comic';
// Layout text inside a shape
const layout = layoutTextInShape(
'This text wraps inside an ellipse!',
'ellipse', 200, 150,
{ fontFamily: 'Comic Sans MS', fontSize: 14, lineHeight: 18 }
);
console.log(layout.lines); // Each line with x, y, width
console.log(layout.fits); // true if all text fits
// Find optimal font size via binary search
const { fontSize, layout: optLayout } = findOptimalFontSize(
'Auto-sized text!', 'cloud', 200, 150, 'Comic Sans MS', 8, 72
);
```
## Comparison
| Feature | pretext-comic | Photoshop | Canva Comics | Pixton |
|---------|-------------|-----------|-------------|--------|
| Shape-aware text wrap | Yes | Manual | No (rect only) | No |
| Open source | Yes | No | No | No |
| Programmatic API | Yes | Scripting | No | No |
| Web-based | Yes | No | Yes | Yes |
| Export PNG | Yes | Yes | Yes | Yes |
| React components | Yes | No | No | No |
| Free | Yes | $22/mo | $13/mo | $8/mo |
## Use Cases
- Webtoon/manga lettering
- Meme generators
- Educational comics
- Social media comic strips
- Automated comic pipelines
## Performance
| Operation | Target |
|-----------|--------|
| Layout single bubble | < 1ms |
| Font size binary search | < 10ms |
| Render single bubble | < 5ms |
| Render 20-bubble panel | < 50ms |
| Export 800x600 PNG | < 100ms |
| Bundle size | < 15KB gzipped |
## Development
```bash
npm install
npm run build # ESM + CJS via tsup
npm test # Vitest
npm run demo:dev # Vite dev server for demo
```
## License
MIT