@svg-gen/tech-svg-generator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +320 -0
- package/dist/cartoon.d.ts +53 -0
- package/dist/cartoon.d.ts.map +1 -0
- package/dist/cartoon.js +233 -0
- package/dist/characters.d.ts +47 -0
- package/dist/characters.d.ts.map +1 -0
- package/dist/characters.js +422 -0
- package/dist/generator.d.ts +36 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +103 -0
- package/dist/icons.d.ts +10 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +44 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/parser.d.ts +83 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +204 -0
- package/dist/primitives.d.ts +42 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +141 -0
- package/dist/scenes.d.ts +11 -0
- package/dist/scenes.d.ts.map +1 -0
- package/dist/scenes.js +257 -0
- package/dist/themes.d.ts +32 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +106 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Satishkumar Dhule
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Tech SVG Generator
|
|
2
|
+
|
|
3
|
+
Generate clean, professional SVG illustrations for technical content. Perfect for blogs, documentation, presentations, and README files.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/open-tech-svg-gen/tech-svg-generator/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/tech-svg-generator)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[](https://github.com/open-tech-svg-gen/tech-svg-generator/pulls)
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- 🎨 **14 Scene Types** - Architecture, scaling, database, deployment, security, debugging, testing, performance, API, monitoring, frontend, success, error, and default
|
|
15
|
+
- 🗨️ **Cartoon Strips** - Generate comic-style strips with characters, emotions, and speech bubbles
|
|
16
|
+
- 📝 **YAML/JSON Support** - Define scenes and cartoons in declarative format
|
|
17
|
+
- 🔍 **Auto-Detection** - Automatically selects the best scene based on title keywords
|
|
18
|
+
- 🌙 **Multiple Themes** - GitHub Dark (default), Dracula, Nord, One Dark
|
|
19
|
+
- 👤 **6 Character Presets** - Diverse developer characters with customizable styles
|
|
20
|
+
- 😀 **9 Emotions** - Neutral, happy, sad, angry, surprised, thinking, confused, excited, worried
|
|
21
|
+
- 📦 **Minimal Dependencies** - Only js-yaml for YAML parsing
|
|
22
|
+
- 🖼️ **Consistent Style** - Professional, clean illustrations every time
|
|
23
|
+
- ✏️ **Monospace Fonts** - All text uses developer-friendly monospace fonts
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install tech-svg-generator
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { generateSVG } from 'tech-svg-generator';
|
|
35
|
+
|
|
36
|
+
// Auto-detect scene from title
|
|
37
|
+
const result = generateSVG('Database Replication Strategies');
|
|
38
|
+
console.log(result.svg); // SVG string
|
|
39
|
+
console.log(result.scene); // 'database'
|
|
40
|
+
|
|
41
|
+
// Save to file
|
|
42
|
+
import fs from 'fs';
|
|
43
|
+
fs.writeFileSync('illustration.svg', result.svg);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### `generateSVG(title, content?, options?)`
|
|
49
|
+
|
|
50
|
+
Generate an SVG illustration.
|
|
51
|
+
|
|
52
|
+
**Parameters:**
|
|
53
|
+
- `title` (string) - The title/topic for the illustration
|
|
54
|
+
- `content` (string, optional) - Additional content for better scene detection
|
|
55
|
+
- `options` (object, optional):
|
|
56
|
+
- `width` (number) - SVG width (default: 700)
|
|
57
|
+
- `height` (number) - SVG height (default: 420)
|
|
58
|
+
- `theme` (string) - Theme name: 'github-dark', 'dracula', 'nord', 'one-dark'
|
|
59
|
+
- `scene` (string) - Force a specific scene type
|
|
60
|
+
|
|
61
|
+
**Returns:** `GenerateResult`
|
|
62
|
+
- `svg` (string) - The generated SVG string
|
|
63
|
+
- `scene` (string) - The detected/used scene type
|
|
64
|
+
- `width` (number) - SVG width
|
|
65
|
+
- `height` (number) - SVG height
|
|
66
|
+
|
|
67
|
+
### `detectScene(title, content?)`
|
|
68
|
+
|
|
69
|
+
Detect the best scene type for given text.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { detectScene } from 'tech-svg-generator';
|
|
73
|
+
|
|
74
|
+
detectScene('Kubernetes Pod Scheduling'); // 'scaling'
|
|
75
|
+
detectScene('JWT Authentication Flow'); // 'security'
|
|
76
|
+
detectScene('React Performance Tips'); // 'frontend'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `getAvailableScenes()`
|
|
80
|
+
|
|
81
|
+
Get list of all available scene types.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { getAvailableScenes } from 'tech-svg-generator';
|
|
85
|
+
|
|
86
|
+
console.log(getAvailableScenes());
|
|
87
|
+
// ['architecture', 'scaling', 'database', 'deployment', 'security', ...]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Scene Types
|
|
91
|
+
|
|
92
|
+
| Scene | Keywords | Description |
|
|
93
|
+
|-------|----------|-------------|
|
|
94
|
+
| `architecture` | architecture, design, pattern, system, microservice | System architecture diagrams |
|
|
95
|
+
| `scaling` | scale, kubernetes, docker, cluster, load | Horizontal scaling visualization |
|
|
96
|
+
| `database` | database, sql, postgres, redis, cache | Database with replication |
|
|
97
|
+
| `deployment` | deploy, ci, cd, pipeline, release | CI/CD pipeline flow |
|
|
98
|
+
| `security` | security, auth, jwt, oauth, firewall | Security flow diagram |
|
|
99
|
+
| `debugging` | debug, bug, error, fix, trace | Code debugging scene |
|
|
100
|
+
| `testing` | test, jest, coverage, unit, e2e | Test results dashboard |
|
|
101
|
+
| `performance` | performance, optimize, latency, cpu | Performance metrics |
|
|
102
|
+
| `api` | api, rest, graphql, endpoint, http | API request/response |
|
|
103
|
+
| `monitoring` | monitor, metric, log, alert, grafana | Monitoring dashboard |
|
|
104
|
+
| `frontend` | frontend, react, vue, css, component | Web vitals metrics |
|
|
105
|
+
| `success` | success, complete, launch, shipped | Success celebration |
|
|
106
|
+
| `error` | fail, crash, outage, incident, 503 | Error/incident scene |
|
|
107
|
+
| `default` | - | Generic system overview |
|
|
108
|
+
|
|
109
|
+
## Themes
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { generateSVG, THEMES } from 'tech-svg-generator';
|
|
113
|
+
|
|
114
|
+
// Use a specific theme
|
|
115
|
+
const result = generateSVG('API Gateway Design', '', { theme: 'dracula' });
|
|
116
|
+
|
|
117
|
+
// Available themes
|
|
118
|
+
console.log(Object.keys(THEMES)); // ['github-dark', 'dracula', 'nord', 'one-dark']
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Examples
|
|
122
|
+
|
|
123
|
+
### Blog Post Illustration
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { generateSVG } from 'tech-svg-generator';
|
|
127
|
+
import fs from 'fs';
|
|
128
|
+
|
|
129
|
+
const posts = [
|
|
130
|
+
'How Netflix Handles Database Sharding',
|
|
131
|
+
'Implementing Zero Trust Security',
|
|
132
|
+
'React Server Components Deep Dive',
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
posts.forEach((title, i) => {
|
|
136
|
+
const { svg, scene } = generateSVG(title);
|
|
137
|
+
fs.writeFileSync(`post-${i + 1}-${scene}.svg`, svg);
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Force Specific Scene
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const result = generateSVG('My Custom Topic', '', { scene: 'architecture' });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Custom Dimensions
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const result = generateSVG('Wide Banner', '', {
|
|
151
|
+
width: 1200,
|
|
152
|
+
height: 300
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Output Example
|
|
157
|
+
|
|
158
|
+
The generated SVGs feature:
|
|
159
|
+
- Clean, fixed-grid layouts with no overlapping elements
|
|
160
|
+
- Professional GitHub-inspired dark theme
|
|
161
|
+
- Contextual icons and metrics
|
|
162
|
+
- Multi-line title support for long titles
|
|
163
|
+
- Consistent monospace typography
|
|
164
|
+
|
|
165
|
+
## Cartoon Strips
|
|
166
|
+
|
|
167
|
+
Generate comic-style strips with characters having conversations!
|
|
168
|
+
|
|
169
|
+
### Quick Start
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { generateCartoonStrip } from 'tech-svg-generator';
|
|
173
|
+
import fs from 'fs';
|
|
174
|
+
|
|
175
|
+
const svg = generateCartoonStrip({
|
|
176
|
+
title: 'The Daily Standup',
|
|
177
|
+
theme: 'github-dark',
|
|
178
|
+
width: 800,
|
|
179
|
+
height: 400,
|
|
180
|
+
layout: '2x1',
|
|
181
|
+
characters: {
|
|
182
|
+
dev: { name: 'Developer', preset: 'dev1' },
|
|
183
|
+
pm: { name: 'PM', preset: 'dev2' }
|
|
184
|
+
},
|
|
185
|
+
panels: [
|
|
186
|
+
{
|
|
187
|
+
characters: ['dev', 'pm'],
|
|
188
|
+
caption: 'Monday morning...',
|
|
189
|
+
dialogue: [
|
|
190
|
+
{ character: 'pm', text: 'How is the feature going?', emotion: 'neutral' },
|
|
191
|
+
{ character: 'dev', text: 'Almost done!', emotion: 'happy' }
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
characters: ['dev', 'pm'],
|
|
196
|
+
caption: 'Friday...',
|
|
197
|
+
dialogue: [
|
|
198
|
+
{ character: 'dev', text: 'Shipped!', emotion: 'excited' },
|
|
199
|
+
{ character: 'pm', text: 'Great work!', emotion: 'happy' }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
fs.writeFileSync('standup.svg', svg);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Character Presets
|
|
209
|
+
|
|
210
|
+
| Preset | Description |
|
|
211
|
+
|--------|-------------|
|
|
212
|
+
| `alex` | Indigo short hair, blue hoodie, glasses |
|
|
213
|
+
| `sam` | Purple curly hair, green t-shirt |
|
|
214
|
+
| `jordan` | Blonde long hair, red shirt, headphones |
|
|
215
|
+
| `casey` | Dark spiky hair, purple hoodie |
|
|
216
|
+
| `riley` | Pink ponytail, indigo t-shirt |
|
|
217
|
+
| `morgan` | Teal mohawk, orange hoodie, sunglasses |
|
|
218
|
+
| `taylor` | Red short hair, dark formal attire |
|
|
219
|
+
| `robot` | Gray robot character with blue accents |
|
|
220
|
+
|
|
221
|
+
Legacy aliases (`dev1`-`dev5`) are also supported for backward compatibility.
|
|
222
|
+
|
|
223
|
+
### Emotions
|
|
224
|
+
|
|
225
|
+
Characters can express: `neutral`, `happy`, `sad`, `angry`, `surprised`, `thinking`, `confused`, `excited`, `worried`
|
|
226
|
+
|
|
227
|
+
### Speech Bubble Types
|
|
228
|
+
|
|
229
|
+
- `speech` (default) - Regular speech bubble
|
|
230
|
+
- `thought` - Cloud-style thought bubble
|
|
231
|
+
- `shout` - Spiky exclamation bubble
|
|
232
|
+
|
|
233
|
+
### Layout Options
|
|
234
|
+
|
|
235
|
+
- `auto` - Automatically arrange panels
|
|
236
|
+
- `1x2`, `2x1` - Single row/column
|
|
237
|
+
- `2x2` - 2x2 grid
|
|
238
|
+
- `3x1`, `1x3` - Three panels in row/column
|
|
239
|
+
- `2x3`, `3x2` - Six panel layouts
|
|
240
|
+
|
|
241
|
+
## YAML/JSON Scene Descriptions
|
|
242
|
+
|
|
243
|
+
Define scenes and cartoons in declarative YAML or JSON format!
|
|
244
|
+
|
|
245
|
+
### Scene from YAML
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { generateFromYAML } from 'tech-svg-generator';
|
|
249
|
+
|
|
250
|
+
const yaml = `
|
|
251
|
+
type: scene
|
|
252
|
+
title: "Database Migration Strategy"
|
|
253
|
+
content: "PostgreSQL replication and failover"
|
|
254
|
+
scene: database
|
|
255
|
+
theme: github-dark
|
|
256
|
+
width: 700
|
|
257
|
+
height: 420
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
const svg = generateFromYAML(yaml);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Cartoon from YAML
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { generateFromYAML } from 'tech-svg-generator';
|
|
267
|
+
|
|
268
|
+
const yaml = `
|
|
269
|
+
type: cartoon
|
|
270
|
+
title: "Code Review"
|
|
271
|
+
theme: dracula
|
|
272
|
+
layout: "2x1"
|
|
273
|
+
|
|
274
|
+
characters:
|
|
275
|
+
alice:
|
|
276
|
+
name: Alice
|
|
277
|
+
preset: dev1
|
|
278
|
+
bob:
|
|
279
|
+
name: Bob
|
|
280
|
+
preset: dev2
|
|
281
|
+
|
|
282
|
+
panels:
|
|
283
|
+
- characters: [alice, bob]
|
|
284
|
+
dialogue:
|
|
285
|
+
- character: alice
|
|
286
|
+
text: "Can you review my PR?"
|
|
287
|
+
emotion: happy
|
|
288
|
+
- character: bob
|
|
289
|
+
text: "Sure!"
|
|
290
|
+
emotion: neutral
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
const svg = generateFromYAML(yaml);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### JSON Support
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { generateFromJSON } from 'tech-svg-generator';
|
|
300
|
+
|
|
301
|
+
const json = JSON.stringify({
|
|
302
|
+
type: 'cartoon',
|
|
303
|
+
title: 'Debugging',
|
|
304
|
+
characters: {
|
|
305
|
+
dev: { name: 'Dev', preset: 'dev1' }
|
|
306
|
+
},
|
|
307
|
+
panels: [{
|
|
308
|
+
characters: ['dev'],
|
|
309
|
+
dialogue: [
|
|
310
|
+
{ character: 'dev', text: 'Found it!', emotion: 'excited' }
|
|
311
|
+
]
|
|
312
|
+
}]
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const svg = generateFromJSON(json);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT © Satishkumar Dhule
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cartoon strip generator with characters and speech bubbles
|
|
3
|
+
*/
|
|
4
|
+
import type { ThemeColors } from './themes.js';
|
|
5
|
+
import { type Emotion } from './characters.js';
|
|
6
|
+
export interface DialogLine {
|
|
7
|
+
/** Character ID speaking */
|
|
8
|
+
character: string;
|
|
9
|
+
/** The dialogue text */
|
|
10
|
+
text: string;
|
|
11
|
+
/** Emotion for this line */
|
|
12
|
+
emotion?: Emotion;
|
|
13
|
+
/** Type of speech bubble */
|
|
14
|
+
type?: 'speech' | 'thought' | 'shout';
|
|
15
|
+
}
|
|
16
|
+
export interface CartoonPanel {
|
|
17
|
+
/** Characters in this panel (by ID) */
|
|
18
|
+
characters: string[];
|
|
19
|
+
/** Dialogue lines in order */
|
|
20
|
+
dialogue: DialogLine[];
|
|
21
|
+
/** Optional panel title/caption */
|
|
22
|
+
caption?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CartoonStripConfig {
|
|
25
|
+
/** Title of the strip */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** Character definitions */
|
|
28
|
+
characters: Record<string, {
|
|
29
|
+
name: string;
|
|
30
|
+
preset?: string;
|
|
31
|
+
style?: any;
|
|
32
|
+
}>;
|
|
33
|
+
/** Panels in the strip */
|
|
34
|
+
panels: CartoonPanel[];
|
|
35
|
+
/** Grid layout: 'auto' | '1x2' | '2x1' | '2x2' | '1x3' | '3x1' | '2x3' | '3x2' */
|
|
36
|
+
layout?: string;
|
|
37
|
+
/** Theme name */
|
|
38
|
+
theme?: string;
|
|
39
|
+
/** Total width */
|
|
40
|
+
width?: number;
|
|
41
|
+
/** Total height */
|
|
42
|
+
height?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Render a speech bubble
|
|
46
|
+
*/
|
|
47
|
+
declare function speechBubble(x: number, y: number, text: string, colors: ThemeColors, type?: 'speech' | 'thought' | 'shout', tailDirection?: 'left' | 'right' | 'down', maxWidth?: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Generate a cartoon strip SVG
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateCartoonStrip(config: CartoonStripConfig): string;
|
|
52
|
+
export { speechBubble };
|
|
53
|
+
//# sourceMappingURL=cartoon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cartoon.d.ts","sourceRoot":"","sources":["../src/cartoon.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAoD,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAKjG,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4BAA4B;IAC5B,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,8BAA8B;IAC9B,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IAC3E,0BAA0B;IAC1B,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,iBAAS,YAAY,CACnB,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,IAAI,GAAE,QAAQ,GAAG,SAAS,GAAG,OAAkB,EAC/C,aAAa,GAAE,MAAM,GAAG,OAAO,GAAG,MAAe,EACjD,QAAQ,GAAE,MAAY,GACrB,MAAM,CAuGR;AAkHD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAqDvE;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
package/dist/cartoon.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cartoon strip generator with characters and speech bubbles
|
|
3
|
+
*/
|
|
4
|
+
import { getTheme } from './themes.js';
|
|
5
|
+
import { renderCharacter, createCharacter } from './characters.js';
|
|
6
|
+
import { escapeHtml } from './primitives.js';
|
|
7
|
+
const FONT = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
8
|
+
/**
|
|
9
|
+
* Render a speech bubble
|
|
10
|
+
*/
|
|
11
|
+
function speechBubble(x, y, text, colors, type = 'speech', tailDirection = 'down', maxWidth = 180) {
|
|
12
|
+
// Word wrap text
|
|
13
|
+
const words = text.split(' ');
|
|
14
|
+
const lines = [];
|
|
15
|
+
let currentLine = '';
|
|
16
|
+
const charsPerLine = Math.floor(maxWidth / 7);
|
|
17
|
+
for (const word of words) {
|
|
18
|
+
if ((currentLine + ' ' + word).trim().length <= charsPerLine) {
|
|
19
|
+
currentLine = (currentLine + ' ' + word).trim();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (currentLine)
|
|
23
|
+
lines.push(currentLine);
|
|
24
|
+
currentLine = word;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (currentLine)
|
|
28
|
+
lines.push(currentLine);
|
|
29
|
+
// Limit lines
|
|
30
|
+
if (lines.length > 4) {
|
|
31
|
+
lines.length = 4;
|
|
32
|
+
lines[3] = lines[3].substring(0, charsPerLine - 3) + '...';
|
|
33
|
+
}
|
|
34
|
+
const lineHeight = 16;
|
|
35
|
+
const padding = 12;
|
|
36
|
+
const bubbleWidth = Math.min(maxWidth, Math.max(...lines.map(l => l.length * 7)) + padding * 2);
|
|
37
|
+
const bubbleHeight = lines.length * lineHeight + padding * 2;
|
|
38
|
+
let bubblePath;
|
|
39
|
+
let tailPath;
|
|
40
|
+
if (type === 'thought') {
|
|
41
|
+
// Thought bubble with cloud shape
|
|
42
|
+
bubblePath = `
|
|
43
|
+
<ellipse cx="${x}" cy="${y}" rx="${bubbleWidth / 2}" ry="${bubbleHeight / 2}" fill="${colors.card}" stroke="${colors.border}" stroke-width="2"/>
|
|
44
|
+
`;
|
|
45
|
+
const tailX = tailDirection === 'left' ? x - bubbleWidth / 2 - 5 : tailDirection === 'right' ? x + bubbleWidth / 2 + 5 : x;
|
|
46
|
+
const tailY = y + bubbleHeight / 2 + 10;
|
|
47
|
+
tailPath = `
|
|
48
|
+
<circle cx="${tailX}" cy="${tailY}" r="6" fill="${colors.card}" stroke="${colors.border}" stroke-width="2"/>
|
|
49
|
+
<circle cx="${tailX + (tailDirection === 'left' ? -8 : tailDirection === 'right' ? 8 : 0)}" cy="${tailY + 12}" r="4" fill="${colors.card}" stroke="${colors.border}" stroke-width="2"/>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
else if (type === 'shout') {
|
|
53
|
+
// Spiky shout bubble
|
|
54
|
+
const spikes = 8;
|
|
55
|
+
const innerR = Math.min(bubbleWidth, bubbleHeight) / 2;
|
|
56
|
+
const outerR = innerR * 1.3;
|
|
57
|
+
let points = '';
|
|
58
|
+
for (let i = 0; i < spikes * 2; i++) {
|
|
59
|
+
const angle = (i * Math.PI) / spikes;
|
|
60
|
+
const r = i % 2 === 0 ? outerR : innerR;
|
|
61
|
+
const px = x + r * Math.cos(angle - Math.PI / 2);
|
|
62
|
+
const py = y + r * Math.sin(angle - Math.PI / 2) * (bubbleHeight / bubbleWidth);
|
|
63
|
+
points += `${px},${py} `;
|
|
64
|
+
}
|
|
65
|
+
bubblePath = `<polygon points="${points.trim()}" fill="${colors.card}" stroke="${colors.orange}" stroke-width="2"/>`;
|
|
66
|
+
tailPath = '';
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Regular speech bubble
|
|
70
|
+
const rx = 12;
|
|
71
|
+
bubblePath = `<rect x="${x - bubbleWidth / 2}" y="${y - bubbleHeight / 2}" width="${bubbleWidth}" height="${bubbleHeight}" rx="${rx}" fill="${colors.card}" stroke="${colors.border}" stroke-width="2"/>`;
|
|
72
|
+
// Tail pointing to speaker
|
|
73
|
+
const tailSize = 12;
|
|
74
|
+
let tx, ty, t1x, t1y, t2x, t2y;
|
|
75
|
+
if (tailDirection === 'left') {
|
|
76
|
+
tx = x - bubbleWidth / 2 - tailSize;
|
|
77
|
+
ty = y + bubbleHeight / 4;
|
|
78
|
+
t1x = x - bubbleWidth / 2;
|
|
79
|
+
t1y = y;
|
|
80
|
+
t2x = x - bubbleWidth / 2;
|
|
81
|
+
t2y = y + bubbleHeight / 4;
|
|
82
|
+
}
|
|
83
|
+
else if (tailDirection === 'right') {
|
|
84
|
+
tx = x + bubbleWidth / 2 + tailSize;
|
|
85
|
+
ty = y + bubbleHeight / 4;
|
|
86
|
+
t1x = x + bubbleWidth / 2;
|
|
87
|
+
t1y = y;
|
|
88
|
+
t2x = x + bubbleWidth / 2;
|
|
89
|
+
t2y = y + bubbleHeight / 4;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
tx = x;
|
|
93
|
+
ty = y + bubbleHeight / 2 + tailSize;
|
|
94
|
+
t1x = x - 8;
|
|
95
|
+
t1y = y + bubbleHeight / 2;
|
|
96
|
+
t2x = x + 8;
|
|
97
|
+
t2y = y + bubbleHeight / 2;
|
|
98
|
+
}
|
|
99
|
+
tailPath = `<polygon points="${t1x},${t1y} ${tx},${ty} ${t2x},${t2y}" fill="${colors.card}" stroke="${colors.border}" stroke-width="2"/>
|
|
100
|
+
<line x1="${t1x}" y1="${t1y}" x2="${t2x}" y2="${t2y}" stroke="${colors.card}" stroke-width="4"/>`;
|
|
101
|
+
}
|
|
102
|
+
const textContent = lines.map((line, i) => `<text x="${x}" y="${y - (lines.length - 1) * lineHeight / 2 + i * lineHeight + 5}" text-anchor="middle" fill="${colors.text}" font-size="12" font-family="${FONT}">${escapeHtml(line)}</text>`).join('');
|
|
103
|
+
return `
|
|
104
|
+
<g class="speech-bubble">
|
|
105
|
+
${bubblePath}
|
|
106
|
+
${tailPath}
|
|
107
|
+
${textContent}
|
|
108
|
+
</g>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Calculate grid layout dimensions
|
|
113
|
+
*/
|
|
114
|
+
function calculateLayout(panelCount, layout, totalWidth, totalHeight) {
|
|
115
|
+
let cols, rows;
|
|
116
|
+
if (layout === 'auto') {
|
|
117
|
+
if (panelCount <= 2) {
|
|
118
|
+
cols = panelCount;
|
|
119
|
+
rows = 1;
|
|
120
|
+
}
|
|
121
|
+
else if (panelCount <= 4) {
|
|
122
|
+
cols = 2;
|
|
123
|
+
rows = Math.ceil(panelCount / 2);
|
|
124
|
+
}
|
|
125
|
+
else if (panelCount <= 6) {
|
|
126
|
+
cols = 3;
|
|
127
|
+
rows = Math.ceil(panelCount / 3);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
cols = 4;
|
|
131
|
+
rows = Math.ceil(panelCount / 4);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const match = layout.match(/(\d+)x(\d+)/);
|
|
136
|
+
if (match) {
|
|
137
|
+
cols = parseInt(match[1]);
|
|
138
|
+
rows = parseInt(match[2]);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
cols = 2;
|
|
142
|
+
rows = Math.ceil(panelCount / 2);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const gap = 10;
|
|
146
|
+
const panelWidth = (totalWidth - gap * (cols + 1)) / cols;
|
|
147
|
+
const panelHeight = (totalHeight - gap * (rows + 1) - 40) / rows; // 40 for title
|
|
148
|
+
return { cols, rows, panelWidth, panelHeight };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Render a single panel
|
|
152
|
+
*/
|
|
153
|
+
function renderPanel(panel, characters, x, y, width, height, colors, panelIndex) {
|
|
154
|
+
const panelChars = panel.characters.map(id => characters.get(id)).filter(Boolean);
|
|
155
|
+
const charCount = panelChars.length;
|
|
156
|
+
// Panel background
|
|
157
|
+
let content = `
|
|
158
|
+
<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="8" fill="${colors.elevated}" stroke="${colors.border}" stroke-width="2"/>
|
|
159
|
+
`;
|
|
160
|
+
// Caption if present
|
|
161
|
+
if (panel.caption) {
|
|
162
|
+
content += `<text x="${x + width / 2}" y="${y + 18}" text-anchor="middle" fill="${colors.muted}" font-size="10" font-family="${FONT}">${escapeHtml(panel.caption)}</text>`;
|
|
163
|
+
}
|
|
164
|
+
// Position characters - adjusted for new larger character design
|
|
165
|
+
const charY = y + height - 100;
|
|
166
|
+
const charSpacing = width / (charCount + 1);
|
|
167
|
+
const charScale = Math.min(0.7, (height - 150) / 180); // Scale based on panel height
|
|
168
|
+
panelChars.forEach((char, i) => {
|
|
169
|
+
const charX = x + charSpacing * (i + 1);
|
|
170
|
+
const facing = i < charCount / 2 ? 'right' : 'left';
|
|
171
|
+
// Find emotion for this character in dialogue
|
|
172
|
+
const charDialogue = panel.dialogue.find(d => d.character === char.id);
|
|
173
|
+
const emotion = charDialogue?.emotion || 'neutral';
|
|
174
|
+
content += renderCharacter(charX, charY, char, emotion, charScale, facing);
|
|
175
|
+
});
|
|
176
|
+
// Render dialogue bubbles - positioned above characters
|
|
177
|
+
const dialogueStartY = y + (panel.caption ? 35 : 20);
|
|
178
|
+
const dialogueEndY = charY - 60 * charScale;
|
|
179
|
+
const bubbleSpacing = Math.min(70, (dialogueEndY - dialogueStartY) / Math.max(panel.dialogue.length, 1));
|
|
180
|
+
panel.dialogue.forEach((line, i) => {
|
|
181
|
+
const charIndex = panelChars.findIndex(c => c.id === line.character);
|
|
182
|
+
if (charIndex === -1)
|
|
183
|
+
return;
|
|
184
|
+
const charX = x + charSpacing * (charIndex + 1);
|
|
185
|
+
const bubbleX = charX;
|
|
186
|
+
const bubbleY = dialogueStartY + 25 + i * bubbleSpacing;
|
|
187
|
+
content += speechBubble(bubbleX, bubbleY, line.text, colors, line.type || 'speech', 'down', width * 0.65);
|
|
188
|
+
});
|
|
189
|
+
return content;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Generate a cartoon strip SVG
|
|
193
|
+
*/
|
|
194
|
+
export function generateCartoonStrip(config) {
|
|
195
|
+
const { title = '', characters: charDefs, panels, layout = 'auto', theme: themeName, width = 800, height = 600, } = config;
|
|
196
|
+
const theme = getTheme(themeName);
|
|
197
|
+
const colors = theme.colors;
|
|
198
|
+
// Create character instances
|
|
199
|
+
const characters = new Map();
|
|
200
|
+
for (const [id, def] of Object.entries(charDefs)) {
|
|
201
|
+
characters.set(id, createCharacter(id, def.name, def.preset || def.style || 'dev1'));
|
|
202
|
+
}
|
|
203
|
+
// Calculate layout
|
|
204
|
+
const { cols, rows, panelWidth, panelHeight } = calculateLayout(panels.length, layout, width, height);
|
|
205
|
+
const gap = 10;
|
|
206
|
+
// Build SVG
|
|
207
|
+
let content = '';
|
|
208
|
+
// Title
|
|
209
|
+
if (title) {
|
|
210
|
+
content += `<text x="${width / 2}" y="28" text-anchor="middle" fill="${colors.text}" font-size="16" font-weight="bold" font-family="${FONT}">${escapeHtml(title)}</text>`;
|
|
211
|
+
}
|
|
212
|
+
// Render panels
|
|
213
|
+
panels.forEach((panel, i) => {
|
|
214
|
+
const col = i % cols;
|
|
215
|
+
const row = Math.floor(i / cols);
|
|
216
|
+
const px = gap + col * (panelWidth + gap);
|
|
217
|
+
const py = (title ? 45 : gap) + row * (panelHeight + gap);
|
|
218
|
+
content += renderPanel(panel, characters, px, py, panelWidth, panelHeight, colors, i);
|
|
219
|
+
});
|
|
220
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
221
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
|
|
222
|
+
<defs>
|
|
223
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
224
|
+
<stop offset="0%" stop-color="${colors.bg}"/>
|
|
225
|
+
<stop offset="100%" stop-color="${colors.card}"/>
|
|
226
|
+
</linearGradient>
|
|
227
|
+
</defs>
|
|
228
|
+
<rect width="${width}" height="${height}" fill="url(#bg)"/>
|
|
229
|
+
${content}
|
|
230
|
+
</svg>`;
|
|
231
|
+
}
|
|
232
|
+
export { speechBubble };
|
|
233
|
+
//# sourceMappingURL=cartoon.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character definitions for cartoon strips
|
|
3
|
+
* Improved cartoon-style characters with better proportions and expressions
|
|
4
|
+
*/
|
|
5
|
+
export type Emotion = 'neutral' | 'happy' | 'sad' | 'angry' | 'surprised' | 'thinking' | 'confused' | 'excited' | 'worried';
|
|
6
|
+
export interface CharacterStyle {
|
|
7
|
+
/** Primary color (hair) */
|
|
8
|
+
primary: string;
|
|
9
|
+
/** Secondary color (shirt/clothing) */
|
|
10
|
+
secondary: string;
|
|
11
|
+
/** Skin tone */
|
|
12
|
+
skin: string;
|
|
13
|
+
/** Hair style */
|
|
14
|
+
hairStyle: 'short' | 'long' | 'bald' | 'spiky' | 'curly' | 'ponytail' | 'mohawk';
|
|
15
|
+
/** Accessory */
|
|
16
|
+
accessory: 'none' | 'glasses' | 'sunglasses' | 'hat' | 'headphones' | 'beanie';
|
|
17
|
+
/** Eye color */
|
|
18
|
+
eyeColor: string;
|
|
19
|
+
/** Shirt style */
|
|
20
|
+
shirtStyle: 'tshirt' | 'hoodie' | 'formal' | 'tank';
|
|
21
|
+
}
|
|
22
|
+
export interface Character {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
style: CharacterStyle;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Predefined character styles - diverse and visually distinct
|
|
29
|
+
*/
|
|
30
|
+
export declare const CHARACTER_PRESETS: Record<string, CharacterStyle>;
|
|
31
|
+
/**
|
|
32
|
+
* Render a character at position with emotion - completely redesigned
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderCharacter(x: number, y: number, character: Character, emotion?: Emotion, scale?: number, facing?: 'left' | 'right'): string;
|
|
35
|
+
/**
|
|
36
|
+
* Create a character from preset or custom style
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCharacter(id: string, name: string, presetOrStyle?: string | CharacterStyle): Character;
|
|
39
|
+
/**
|
|
40
|
+
* Get available character presets
|
|
41
|
+
*/
|
|
42
|
+
export declare function getCharacterPresets(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Get available emotions
|
|
45
|
+
*/
|
|
46
|
+
export declare function getEmotions(): Emotion[];
|
|
47
|
+
//# sourceMappingURL=characters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"characters.d.ts","sourceRoot":"","sources":["../src/characters.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,OAAO,GACf,SAAS,GACT,OAAO,GACP,KAAK,GACL,OAAO,GACP,WAAW,GACX,UAAU,GACV,UAAU,GACV,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IACjF,gBAAgB;IAChB,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,KAAK,GAAG,YAAY,GAAG,QAAQ,CAAC;IAC/E,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;CACrD;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,cAAc,CAAC;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAyE5D,CAAC;AAsRF;;GAEG;AACH,wBAAgB,eAAe,CAC7B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,OAAmB,EAC5B,KAAK,GAAE,MAAU,EACjB,MAAM,GAAE,MAAM,GAAG,OAAiB,GACjC,MAAM,CAkDR;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,aAAa,GAAE,MAAM,GAAG,cAAuB,GAC9C,SAAS,CAMX;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,OAAO,EAAE,CAEvC"}
|