commit d8b5c2cd886add269ba07339319f895d66958078 Author: Jeff Emmett Date: Mon Dec 15 18:37:45 2025 -0500 feat: initial mycro-zine generator toolkit - Single-page print layout (2x4 grid) for 8-page zines - Prompt templates for AI content/image generation - Example Undernet zine pages - Support for US Letter and A4 paper sizes - CLI and programmatic API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a142f7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Output directory (generated files) +output/ + +# Environment +.env +.env.local + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +*.log +npm-debug.log* + +# Build artifacts +dist/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ffab95 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# MycroZine + +A toolkit for creating print-ready **mycro-zines** - 8-page mini folded zines that fit on a single 8.5" x 11" sheet. + +## What is a MycroZine? + +A mycro-zine is a tiny, foldable magazine made from a single sheet of paper. When folded correctly, it creates an 8-page booklet perfect for: +- Punk zines and manifestos +- Educational mini-guides +- Event programs +- DIY instructions +- Art projects + +## Features + +- **Single-page print layout**: All 8 pages arranged on one 8.5" x 11" sheet (2 cols x 4 rows) +- **High-resolution output**: 300 DPI for crisp printing +- **Prompt templates**: Ready-to-use prompts for AI content/image generation +- **Multiple styles**: punk-zine, minimal, collage, retro, academic +- **US Letter & A4 support**: Works with common paper sizes + +## Installation + +```bash +npm install +``` + +## Usage + +### CLI - Create Print Layout + +```bash +# Using example pages +npm run example + +# Or specify your own 8 pages +node src/layout.mjs page1.png page2.png page3.png page4.png page5.png page6.png page7.png page8.png + +# With custom output path +node src/layout.mjs page1.png ... page8.png --output my_zine_print.png +``` + +### Programmatic API + +```javascript +import { createPrintLayout } from 'mycro-zine'; + +// Create print-ready layout from 8 page images +await createPrintLayout({ + pages: [ + 'page1.png', 'page2.png', 'page3.png', 'page4.png', + 'page5.png', 'page6.png', 'page7.png', 'page8.png' + ], + outputPath: 'my_zine_print.png', + background: '#ffffff' +}); +``` + +### Prompt Templates (for AI generation) + +```javascript +import { getContentOutlinePrompt, getImagePrompt, STYLES, TONES } from 'mycro-zine/prompts'; + +// Generate content outline prompt +const outlinePrompt = getContentOutlinePrompt({ + topic: 'The Undernet', + style: 'punk-zine', + tone: 'rebellious', + sourceContent: 'Reference text here...' +}); + +// Generate image prompt for a page +const imagePrompt = getImagePrompt({ + pageNumber: 1, + zineTopic: 'The Undernet', + pageOutline: { + title: 'THE UNDERNET', + keyPoints: ['Own your data', 'Run local servers'], + hashtags: ['#DataSovereignty', '#Mycopunk'], + imagePrompt: 'Bold cover with mycelial network imagery...' + }, + style: 'punk-zine' +}); +``` + +## Print Layout + +The output is a single PNG image with all 8 pages arranged in reading order: + +``` +┌─────────────┬─────────────┐ +│ Page 1 │ Page 2 │ Row 1 +├─────────────┼─────────────┤ +│ Page 3 │ Page 4 │ Row 2 +├─────────────┼─────────────┤ +│ Page 5 │ Page 6 │ Row 3 +├─────────────┼─────────────┤ +│ Page 7 │ Page 8 │ Row 4 +└─────────────┴─────────────┘ + +Panel size: 4.25" x 2.75" (~10.8cm x 7cm) +Total: 8.5" x 11" at 300 DPI (2550 x 3300 pixels) +``` + +## Folding Instructions + +After printing, fold your zine: + +1. **Accordion fold**: Fold the paper in half horizontally (hamburger style) +2. **Fold again**: Fold in half vertically (hotdog style) +3. **One more fold**: Fold in half again +4. **Cut center**: Unfold completely, cut a slit along the center fold +5. **Push and fold**: Push through the center to create the booklet + +## Examples + +See the `examples/undernet/` directory for a complete 8-page zine about The Undernet project. + +## Styles + +| Style | Description | +|-------|-------------| +| `punk-zine` | Xerox texture, high contrast B&W, DIY collage, hand-drawn typography | +| `minimal` | Clean lines, white space, modern sans-serif, subtle gradients | +| `collage` | Layered imagery, mixed media textures, vintage photographs | +| `retro` | 1970s aesthetic, earth tones, groovy typography, halftone patterns | +| `academic` | Diagram-heavy, annotated illustrations, infographic elements | + +## Integration with Gemini MCP + +This library is designed to work with the [Gemini MCP Server](https://github.com/jeffemmett/gemini-mcp) for AI-powered content and image generation: + +1. Use `getContentOutlinePrompt()` with `gemini_generate` for zine planning +2. Use `getImagePrompt()` with `gemini_generate_image` for page creation +3. Use `createPrintLayout()` to assemble the final print-ready file + +## License + +MIT diff --git a/examples/undernet/undernet_zine_p1_cover.png b/examples/undernet/undernet_zine_p1_cover.png new file mode 100644 index 0000000..6e3101a Binary files /dev/null and b/examples/undernet/undernet_zine_p1_cover.png differ diff --git a/examples/undernet/undernet_zine_p2_what.png b/examples/undernet/undernet_zine_p2_what.png new file mode 100644 index 0000000..726e832 Binary files /dev/null and b/examples/undernet/undernet_zine_p2_what.png differ diff --git a/examples/undernet/undernet_zine_p3_metacelium.png b/examples/undernet/undernet_zine_p3_metacelium.png new file mode 100644 index 0000000..8aadef0 Binary files /dev/null and b/examples/undernet/undernet_zine_p3_metacelium.png differ diff --git a/examples/undernet/undernet_zine_p4_privacy.png b/examples/undernet/undernet_zine_p4_privacy.png new file mode 100644 index 0000000..8bfbe43 Binary files /dev/null and b/examples/undernet/undernet_zine_p4_privacy.png differ diff --git a/examples/undernet/undernet_zine_p5_threepunks.png b/examples/undernet/undernet_zine_p5_threepunks.png new file mode 100644 index 0000000..b9c1dd8 Binary files /dev/null and b/examples/undernet/undernet_zine_p5_threepunks.png differ diff --git a/examples/undernet/undernet_zine_p6_techstack.png b/examples/undernet/undernet_zine_p6_techstack.png new file mode 100644 index 0000000..0e06ca2 Binary files /dev/null and b/examples/undernet/undernet_zine_p6_techstack.png differ diff --git a/examples/undernet/undernet_zine_p7_philosophy.png b/examples/undernet/undernet_zine_p7_philosophy.png new file mode 100644 index 0000000..8f737b8 Binary files /dev/null and b/examples/undernet/undernet_zine_p7_philosophy.png differ diff --git a/examples/undernet/undernet_zine_p8_cta.png b/examples/undernet/undernet_zine_p8_cta.png new file mode 100644 index 0000000..eafd542 Binary files /dev/null and b/examples/undernet/undernet_zine_p8_cta.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2edcb13 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,566 @@ +{ + "name": "mycro-zine", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mycro-zine", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "sharp": "^0.34.5" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6ef507e --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "mycro-zine", + "version": "1.0.0", + "description": "8-page mini-zine generator utilities for print-ready zine layouts", + "type": "module", + "main": "src/index.mjs", + "exports": { + ".": "./src/index.mjs", + "./layout": "./src/layout.mjs", + "./prompts": "./src/prompts.mjs" + }, + "scripts": { + "layout": "node src/layout.mjs", + "example": "node src/layout.mjs examples/undernet/undernet_zine_p1_cover.png examples/undernet/undernet_zine_p2_what.png examples/undernet/undernet_zine_p3_metacelium.png examples/undernet/undernet_zine_p4_privacy.png examples/undernet/undernet_zine_p5_threepunks.png examples/undernet/undernet_zine_p6_techstack.png examples/undernet/undernet_zine_p7_philosophy.png examples/undernet/undernet_zine_p8_cta.png" + }, + "keywords": [ + "zine", + "mycrozine", + "mini-zine", + "print", + "layout", + "punk", + "diy", + "generator" + ], + "author": "Jeff Emmett", + "license": "MIT", + "dependencies": { + "sharp": "^0.34.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "git@gitea.jeffemmett.com:jeffemmett/mycro-zine.git" + } +} diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000..1ef610e --- /dev/null +++ b/src/index.mjs @@ -0,0 +1,130 @@ +/** + * MycroZine - 8-page mini-zine generator utilities + * + * A toolkit for creating print-ready mycro-zines (8-page mini folded zines). + * + * @module mycro-zine + */ + +export { createPrintLayout } from './layout.mjs'; +export { + STYLES, + TONES, + PAGE_TEMPLATES, + ZINE_STRUCTURES, + getContentOutlinePrompt, + getImagePrompt, + getIdeationPrompt +} from './prompts.mjs'; + +/** + * Zine configuration defaults + */ +export const DEFAULTS = { + style: 'punk-zine', + tone: 'rebellious', + paperFormat: 'letter', // US Letter 8.5" x 11" + dpi: 300, + pageCount: 8 +}; + +/** + * Page dimensions in pixels at 300 DPI + */ +export const DIMENSIONS = { + letter: { + width: 2550, // 8.5" x 300 + height: 3300, // 11" x 300 + panelWidth: 1275, // width / 2 cols + panelHeight: 825, // height / 4 rows + panelCols: 2, + panelRows: 4 + }, + a4: { + width: 2480, // 210mm at 300 DPI + height: 3508, // 297mm at 300 DPI + panelWidth: 1240, + panelHeight: 877, + panelCols: 2, + panelRows: 4 + } +}; + +/** + * Validate a zine configuration + * + * @param {Object} config - Zine configuration to validate + * @returns {{ valid: boolean, errors: string[] }} + */ +export function validateConfig(config) { + const errors = []; + + if (!config.topic || typeof config.topic !== 'string') { + errors.push('Topic is required and must be a string'); + } + + if (config.style && !['punk-zine', 'minimal', 'collage', 'retro', 'academic'].includes(config.style)) { + errors.push(`Invalid style: ${config.style}`); + } + + if (config.tone && !['rebellious', 'playful', 'informative', 'poetic'].includes(config.tone)) { + errors.push(`Invalid tone: ${config.tone}`); + } + + if (config.paperFormat && !['letter', 'a4'].includes(config.paperFormat)) { + errors.push(`Invalid paper format: ${config.paperFormat}`); + } + + if (config.pages && (!Array.isArray(config.pages) || config.pages.length !== 8)) { + errors.push('Pages must be an array of exactly 8 items'); + } + + return { + valid: errors.length === 0, + errors + }; +} + +/** + * Create a new zine configuration + * + * @param {Object} options + * @param {string} options.topic - Main topic/theme + * @param {string} [options.title] - Zine title (generated from topic if not provided) + * @param {string} [options.style='punk-zine'] - Visual style + * @param {string} [options.tone='rebellious'] - Content tone + * @param {string} [options.paperFormat='letter'] - Paper format + * @param {string[]} [options.sourceUrls] - Reference URLs + * @returns {Object} Zine configuration object + */ +export function createZineConfig(options) { + const { + topic, + title = topic.toUpperCase(), + style = DEFAULTS.style, + tone = DEFAULTS.tone, + paperFormat = DEFAULTS.paperFormat, + sourceUrls = [] + } = options; + + const config = { + id: `zine_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + topic, + title, + style, + tone, + paperFormat, + sourceUrls, + createdAt: Date.now(), + pages: [], + outline: null, + status: 'draft' + }; + + const validation = validateConfig(config); + if (!validation.valid) { + throw new Error(`Invalid config: ${validation.errors.join(', ')}`); + } + + return config; +} diff --git a/src/layout.mjs b/src/layout.mjs new file mode 100644 index 0000000..5f0dcb6 --- /dev/null +++ b/src/layout.mjs @@ -0,0 +1,159 @@ +/** + * MycroZine Layout Generator + * + * Creates a print-ready layout with all 8 pages on a single 8.5" x 11" sheet. + * Layout: 2 columns x 4 rows (reading order: left-to-right, top-to-bottom) + * + * Panel size: 4.25" x 2.75" (~10.8cm x 7cm) + */ + +import sharp from 'sharp'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs/promises'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// 8.5" x 11" at 300 DPI = 2550 x 3300 pixels +const PAGE_WIDTH = 2550; +const PAGE_HEIGHT = 3300; + +// Layout configuration: 2 columns x 4 rows +const COLS = 2; +const ROWS = 4; +const PANEL_WIDTH = Math.floor(PAGE_WIDTH / COLS); // 1275 pixels +const PANEL_HEIGHT = Math.floor(PAGE_HEIGHT / ROWS); // 825 pixels + +/** + * Create a print-ready zine layout from 8 page images + * + * @param {Object} options - Layout options + * @param {string[]} options.pages - Array of 8 page image paths (in order) + * @param {string} [options.outputPath] - Output file path (default: output/mycrozine_print.png) + * @param {string} [options.background] - Background color (default: '#ffffff') + * @returns {Promise} - Path to generated print layout + */ +export async function createPrintLayout(options) { + const { + pages, + outputPath = path.join(__dirname, '..', 'output', 'mycrozine_print.png'), + background = '#ffffff' + } = options; + + if (!pages || pages.length !== 8) { + throw new Error('Exactly 8 page images are required'); + } + + // Ensure output directory exists + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + + // Load and resize all pages to panel size + const resizedPages = await Promise.all( + pages.map(async (pagePath) => { + return sharp(pagePath) + .resize(PANEL_WIDTH, PANEL_HEIGHT, { + fit: 'contain', + background + }) + .toBuffer(); + }) + ); + + // Build composite array for all 8 pages in reading order + const compositeImages = []; + for (let row = 0; row < ROWS; row++) { + for (let col = 0; col < COLS; col++) { + const pageIndex = row * COLS + col; // 0-7 + compositeImages.push({ + input: resizedPages[pageIndex], + left: col * PANEL_WIDTH, + top: row * PANEL_HEIGHT + }); + } + } + + // Create the final composite image + await sharp({ + create: { + width: PAGE_WIDTH, + height: PAGE_HEIGHT, + channels: 3, + background + } + }) + .composite(compositeImages) + .png() + .toFile(outputPath); + + console.log(`Created print layout: ${outputPath}`); + console.log(` Dimensions: ${PAGE_WIDTH}x${PAGE_HEIGHT} pixels (8.5"x11" @ 300 DPI)`); + console.log(` Panel size: ${PANEL_WIDTH}x${PANEL_HEIGHT} pixels (${COLS}x${ROWS} grid)`); + + return outputPath; +} + +/** + * CLI entry point + * Usage: node layout.mjs ... [--output ] + */ +async function main() { + const args = process.argv.slice(2); + + // Parse arguments + let pages = []; + let outputPath = null; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--output' || args[i] === '-o') { + outputPath = args[++i]; + } else if (!args[i].startsWith('-')) { + pages.push(args[i]); + } + } + + if (pages.length === 0) { + // Default: look for undernet_zine_p*.png in examples + const exampleDir = path.join(__dirname, '..', 'examples', 'undernet'); + try { + const files = await fs.readdir(exampleDir); + pages = files + .filter(f => f.match(/undernet_zine_p\d\.png/)) + .sort() + .map(f => path.join(exampleDir, f)); + + if (pages.length === 0) { + console.log('No page images found. Usage:'); + console.log(' node layout.mjs page1.png page2.png ... page8.png [--output path]'); + console.log(' Or place 8 pages named undernet_zine_p1.png through p8.png in examples/undernet/'); + process.exit(1); + } + } catch (e) { + console.log('Usage: node layout.mjs page1.png page2.png ... page8.png [--output path]'); + process.exit(1); + } + } + + if (pages.length !== 8) { + console.error(`Error: Expected 8 pages, got ${pages.length}`); + process.exit(1); + } + + const options = { pages }; + if (outputPath) { + options.outputPath = outputPath; + } + + try { + await createPrintLayout(options); + } catch (error) { + console.error('Error creating layout:', error.message); + process.exit(1); + } +} + +// Run CLI if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export default createPrintLayout; diff --git a/src/prompts.mjs b/src/prompts.mjs new file mode 100644 index 0000000..4becbad --- /dev/null +++ b/src/prompts.mjs @@ -0,0 +1,207 @@ +/** + * MycroZine Prompt Templates + * + * Templates for generating zine content outlines and image prompts. + * Designed for use with Gemini MCP tools. + */ + +/** + * Available zine styles + */ +export const STYLES = { + 'punk-zine': 'xerox texture, high contrast black and white with accent color highlights, DIY cut-and-paste collage, hand-drawn typography, rough edges, rebellious feel', + 'minimal': 'clean lines, lots of white space, simple geometric shapes, modern sans-serif typography, subtle gradients', + 'collage': 'layered imagery, mixed media textures, overlapping elements, vintage photographs, torn paper edges', + 'retro': '1970s aesthetic, warm earth tones, groovy typography, halftone patterns, nostalgic imagery', + 'academic': 'diagram-heavy, annotated illustrations, serif typography, reference-style layout, infographic elements' +}; + +/** + * Available tones + */ +export const TONES = { + 'rebellious': 'defiant, anti-establishment, punk attitude, call to action, questioning authority', + 'playful': 'whimsical, fun, light-hearted, humorous, engaging, accessible', + 'informative': 'educational, clear explanations, factual, well-structured, easy to understand', + 'poetic': 'lyrical, metaphorical, evocative imagery, emotional resonance, artistic expression' +}; + +/** + * Generate a content outline prompt for an 8-page zine + * + * @param {Object} options + * @param {string} options.topic - Main topic/theme + * @param {string} [options.style='punk-zine'] - Visual style + * @param {string} [options.tone='rebellious'] - Tone of content + * @param {string} [options.sourceContent] - Optional reference content + * @returns {string} Prompt for content outline generation + */ +export function getContentOutlinePrompt({ topic, style = 'punk-zine', tone = 'rebellious', sourceContent = null }) { + return `You are creating an 8-page mycro-zine (mini folded zine) on the topic: ${topic} + +Style: ${style} | Tone: ${tone} + +${sourceContent ? `Reference content:\n${sourceContent}\n` : ''} + +Generate a JSON outline for 8 pages: + +Page 1 (Cover): Bold title, subtitle, visual hook +Pages 2-7 (Content): Key concepts with emoji-fied, memetic explanations, hashtags +Page 8 (CTA): Call-to-action with QR code placeholders + +For each page provide: +- pageNumber (1-8) +- type: "cover" | "content" | "cta" +- title: Bold headline +- subtitle: (optional) Supporting text +- keyPoints: Array of 2-4 key points with emojis +- hashtags: 2-3 relevant hashtags +- imagePrompt: Detailed prompt for ${style} style image generation + +Output as JSON array.`; +} + +/** + * Generate an image prompt for a zine page + * + * @param {Object} options + * @param {number} options.pageNumber - Page number (1-8) + * @param {string} options.zineTopic - Overall zine topic + * @param {Object} options.pageOutline - Page outline from content generation + * @param {string} [options.style='punk-zine'] - Visual style + * @param {string} [options.feedback] - User feedback to incorporate + * @returns {string} Prompt for image generation + */ +export function getImagePrompt({ pageNumber, zineTopic, pageOutline, style = 'punk-zine', feedback = null }) { + const styleDesc = STYLES[style] || STYLES['punk-zine']; + + let prompt = `Punk zine page ${pageNumber}/8 for "${zineTopic}". + +${pageOutline.imagePrompt || ''} + +Style: ${styleDesc} + +Include text elements: ${pageOutline.title} +${pageOutline.keyPoints ? pageOutline.keyPoints.join(', ') : ''} +${pageOutline.hashtags ? `Hashtags: ${pageOutline.hashtags.join(' ')}` : ''}`; + + if (feedback) { + prompt += `\n\nUser feedback to incorporate: ${feedback}`; + } + + return prompt; +} + +/** + * Generate an ideation prompt to start zine planning + * + * @param {string} topic - Topic to brainstorm about + * @param {string} [style='punk-zine'] - Visual style preference + * @returns {string} Prompt for ideation brainstorming + */ +export function getIdeationPrompt(topic, style = 'punk-zine') { + return `Let's create an 8-page mycro-zine about "${topic}" in ${style} style. + +This is a mini folded zine format - think punk, DIY, memetic, shareable. + +Help me brainstorm: +1. What are the key concepts to cover? +2. What's the narrative arc (intro → core content → call to action)? +3. What visual metaphors or imagery would work well? +4. What hashtags and slogans would resonate? +5. Any source URLs or references to pull from? + +Let's iterate on this before generating the actual pages.`; +} + +/** + * Page templates for common zine structures + */ +export const PAGE_TEMPLATES = { + cover: { + type: 'cover', + description: 'Bold title, eye-catching visual, sets the tone', + elements: ['title', 'subtitle', 'visual hook', 'issue number (optional)'] + }, + intro: { + type: 'content', + description: 'What is this about? Hook the reader', + elements: ['question or statement', 'brief explanation', 'why it matters'] + }, + concept: { + type: 'content', + description: 'Explain a key concept', + elements: ['concept name', 'visual metaphor', 'key points with emojis', 'hashtag'] + }, + comparison: { + type: 'content', + description: 'Compare/contrast two things', + elements: ['side by side layout', 'pros/cons', 'clear distinction'] + }, + process: { + type: 'content', + description: 'Step-by-step or flow', + elements: ['numbered steps', 'arrows/flow', 'simple icons'] + }, + manifesto: { + type: 'content', + description: 'Statement of values/beliefs', + elements: ['bold statements', 'call to action language', 'emotive'] + }, + resources: { + type: 'content', + description: 'Links, QR codes, further reading', + elements: ['QR codes', 'URLs', 'social handles', 'book/article references'] + }, + cta: { + type: 'cta', + description: 'Call to action - what should reader do?', + elements: ['action items', 'QR code', 'community links', 'hashtag', 'share prompt'] + } +}; + +/** + * Suggested 8-page structures + */ +export const ZINE_STRUCTURES = { + educational: [ + 'cover', + 'intro', + 'concept', + 'concept', + 'concept', + 'process', + 'resources', + 'cta' + ], + manifesto: [ + 'cover', + 'manifesto', + 'concept', + 'concept', + 'comparison', + 'manifesto', + 'resources', + 'cta' + ], + howto: [ + 'cover', + 'intro', + 'process', + 'process', + 'process', + 'concept', + 'resources', + 'cta' + ] +}; + +export default { + STYLES, + TONES, + PAGE_TEMPLATES, + ZINE_STRUCTURES, + getContentOutlinePrompt, + getImagePrompt, + getIdeationPrompt +};