diff --git a/CHANGELOG.md b/CHANGELOG.md index f417b5c..249aae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Notebook-style templates** - `notebook-cover` and `notebook-page` + (1080x1350) replicate the viral handwritten study-notes carousel format: + ruled paper, spiral binding, dashed callout boxes, and a cloud punchline. + Includes bundled handwritten fonts Kalam (Light/Regular/Bold) and Caveat + (Medium/Bold), drawn inline SVG pipeline icons (no emoji), shrink-to-fit + hero/chapter titles, and dynamic-height dashed boxes and punchline bubble. - **Multi-slide HTTP API** — `POST /api/render` now accepts `response_format: "json"` to render every slide of a carousel and return them diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 109a8e2..876de88 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -65,7 +65,7 @@ export default defineConfig({ text: 'Social Media', collapsed: true, items: [ - { text: 'Social Quote', link: '/templates/social-quote' }, { text: 'Dev Quote', link: '/templates/dev-quote' }, { text: 'Twitter/X Quote', link: '/templates/twitter-quote' }, { text: 'TikTok Quote', link: '/templates/tiktok-quote' }, { text: 'WhatsApp Chat', link: '/templates/whatsapp-chat' }, { text: 'Notes App', link: '/templates/notes-app' }, { text: 'Instagram Story', link: '/templates/instagram-story' }, { text: 'LinkedIn Card', link: '/templates/linkedin-card' }, { text: 'Pinterest Pin', link: '/templates/pinterest-pin' }, { text: 'Social Poll', link: '/templates/social-poll' }, { text: 'Tweet Embed', link: '/templates/tweet-embed' } + { text: 'Social Quote', link: '/templates/social-quote' }, { text: 'Dev Quote', link: '/templates/dev-quote' }, { text: 'Twitter/X Quote', link: '/templates/twitter-quote' }, { text: 'TikTok Quote', link: '/templates/tiktok-quote' }, { text: 'WhatsApp Chat', link: '/templates/whatsapp-chat' }, { text: 'Notes App', link: '/templates/notes-app' }, { text: 'Notebook Cover', link: '/templates/notebook-cover' }, { text: 'Notebook Page', link: '/templates/notebook-page' }, { text: 'Instagram Story', link: '/templates/instagram-story' }, { text: 'LinkedIn Card', link: '/templates/linkedin-card' }, { text: 'Pinterest Pin', link: '/templates/pinterest-pin' }, { text: 'Social Poll', link: '/templates/social-poll' }, { text: 'Tweet Embed', link: '/templates/tweet-embed' } ], }, { diff --git a/docs/templates/index.md b/docs/templates/index.md index 1a6be72..883e96d 100644 --- a/docs/templates/index.md +++ b/docs/templates/index.md @@ -773,6 +773,22 @@ Cosy ships with **148 templates** out of the box. Browse the gallery below, or u 1080×1350 + + + Notebook Cover +
+

Notebook Cover

+ 1080×1350 +
+
+ + + Notebook Page +
+

Notebook Page

+ 1080×1350 +
+
Rate Limit
diff --git a/docs/templates/notebook-cover.md b/docs/templates/notebook-cover.md new file mode 100644 index 0000000..8c3b31b --- /dev/null +++ b/docs/templates/notebook-cover.md @@ -0,0 +1,84 @@ +# Notebook Cover + +Notebook/study-notes carousel cover: handwritten hero, tagline banner, 6-step pipeline with inline SVG icons, dashed intro box, and a 9-row table of contents with dot leaders. Pairs with `notebook-page` for the inner slides. + +## Preview + +![Notebook Cover](https://s3.ajianaz.dev/hermes/codecoradev/cosy/preview/notebook-cover.png) + +## Dimensions + +- **Width:** 1080px +- **Height:** 1350px +- **Aspect Ratio:** 1080:1350 + +## Fonts + +- Kalam +- Caveat +- Inter + +## Brand Fields + +| Field | Type | Required | Max / Default | +|---|---|---|---| +| `accent_color` | text | - | #1e3a8a | +| `accent_color_end` | text | - | #6d28d9 | +| `bg_color` | text | - | #fdfcf5 | +| `bg_color_end` | text | - | #fdfcf5 | +| `bg_image` | image | - | - | +| `bg_image_opacity` | number | - | 0.15 | +| `logo` | image | - | - | +| `show_brand` | boolean | - | false | +| `brand_handle` | text | - | @ajianaz | +| `brand_name` | text | yes | ajianaz | + +## Slide Fields + +| Field | Type | Required | Max / Default | +|---|---|---|---| +| `header_handle` | text | yes | max 30 - Left header text (e.g. IG handle). | +| `page_label` | text | yes | max 12 - Right header label (e.g. 'cover'). | +| `hero_title` | text | yes | max 18 - Big display title (Caveat). | +| `hero_font_size` | number | - | Hero font size (140-170). | +| `hero_subtitle` | text | yes | max 46 - Uppercase subtitle under hero. | +| `tagline` | text | yes | max 60 - Italic banner line. | +| `icon1` | text | - | Pipeline icon 1: note/phone/brain/search/spark/rocket. | +| `icon2` | text | - | Pipeline icon 2. | +| `icon3` | text | - | Pipeline icon 3. | +| `icon4` | text | - | Pipeline icon 4. | +| `icon5` | text | - | Pipeline icon 5. | +| `icon6` | text | - | Pipeline icon 6. | +| `label1` | text | yes | max 14 - Pipeline label 1. | +| `label2` | text | yes | max 14 - Pipeline label 2. | +| `label3` | text | yes | max 14 - Pipeline label 3. | +| `label4` | text | yes | max 14 - Pipeline label 4. | +| `label5` | text | yes | max 14 - Pipeline label 5. | +| `label6` | text | yes | max 14 - Pipeline label 6. | +| `intro_lead` | text | yes | max 20 - Bold lead inside dashed intro box (purple). | +| `intro_text` | text | yes | max 150 - Intro sentence (wrapped). | +| `toc_title` | text | yes | max 24 - Table-of-contents heading. | +| `toc1` | text | yes | max 22 | +| `pg1` | text | yes | max 8 | +| `toc2` | text | yes | max 22 | +| `pg2` | text | yes | max 8 | +| `toc3` | text | yes | max 22 | +| `pg3` | text | yes | max 8 | +| `toc4` | text | yes | max 22 | +| `pg4` | text | yes | max 8 | +| `toc5` | text | yes | max 22 | +| `pg5` | text | yes | max 8 | +| `toc6` | text | yes | max 22 | +| `pg6` | text | yes | max 8 | +| `toc7` | text | yes | max 22 | +| `pg7` | text | yes | max 8 | +| `toc8` | text | yes | max 22 | +| `pg8` | text | yes | max 8 | +| `toc9` | text | yes | max 22 | +| `pg9` | text | yes | max 8 | + +## Usage + +```bash +cosy render --template notebook-cover --data input.json --output output.png +``` diff --git a/docs/templates/notebook-page.md b/docs/templates/notebook-page.md new file mode 100644 index 0000000..f8c9b9e --- /dev/null +++ b/docs/templates/notebook-page.md @@ -0,0 +1,59 @@ +# Notebook Page + +Notebook/study-notes content page: chapter title box, star bullets, dashed example box (optional), cloud punchline callout, optional second dashed box, and a bottom brand line. Pairs with `notebook-cover`. + +## Preview + +![Notebook Page](https://s3.ajianaz.dev/hermes/codecoradev/cosy/preview/notebook-page.png) + +## Dimensions + +- **Width:** 1080px +- **Height:** 1350px +- **Aspect Ratio:** 1080:1350 + +## Fonts + +- Kalam +- Caveat +- Inter + +## Brand Fields + +| Field | Type | Required | Max / Default | +|---|---|---|---| +| `accent_color` | text | - | #1e3a8a | +| `accent_color_end` | text | - | #6d28d9 | +| `bg_color` | text | - | #fdfcf5 | +| `bg_color_end` | text | - | #fdfcf5 | +| `bg_image` | image | - | - | +| `bg_image_opacity` | number | - | 0.15 | +| `logo` | image | - | - | +| `show_brand` | boolean | - | false | +| `brand_handle` | text | - | @ajianaz | +| `brand_name` | text | yes | ajianaz | + +## Slide Fields + +| Field | Type | Required | Max / Default | +|---|---|---|---| +| `header_handle` | text | yes | max 30 - Left header text (e.g. IG handle). | +| `page_label` | text | yes | max 12 - Right header label (e.g. 'Page 1'). | +| `chapter_title` | text | yes | max 30 - Chapter title inside the box. | +| `lead_text` | text | yes | max 140 - Lead sentence (wrapped). | +| `bullet1` | text | - | max 120 - Bullet point 1. | +| `bullet2` | text | - | max 120 - Bullet point 2. | +| `bullet3` | text | - | max 120 - Bullet point 3. | +| `box_title` | text | - | max 30 - Dashed box title (omit to hide box). | +| `box_text` | text | - | max 200 - Dashed box body (wrapped). | +| `box_color` | color | - | Dashed box border/title color (default accent). | +| `punchline` | text | yes | max 90 - Cloud callout punchline (wrapped). | +| `extra_title` | text | - | max 30 - Second dashed box title (omit to hide). | +| `extra_text` | text | - | max 180 - Second dashed box body (wrapped). | +| `footer_line` | text | - | max 60 - Bottom center line (brand/CTA). | + +## Usage + +```bash +cosy render --template notebook-page --data input.json --output output.png +``` diff --git a/src/assets/fonts/Caveat-Bold.ttf b/src/assets/fonts/Caveat-Bold.ttf new file mode 100644 index 0000000..d3a3f87 Binary files /dev/null and b/src/assets/fonts/Caveat-Bold.ttf differ diff --git a/src/assets/fonts/Caveat-Medium.ttf b/src/assets/fonts/Caveat-Medium.ttf new file mode 100644 index 0000000..d0ec399 Binary files /dev/null and b/src/assets/fonts/Caveat-Medium.ttf differ diff --git a/src/assets/fonts/Kalam-Bold.ttf b/src/assets/fonts/Kalam-Bold.ttf new file mode 100644 index 0000000..bedadb6 Binary files /dev/null and b/src/assets/fonts/Kalam-Bold.ttf differ diff --git a/src/assets/fonts/Kalam-Light.ttf b/src/assets/fonts/Kalam-Light.ttf new file mode 100644 index 0000000..600ba0a Binary files /dev/null and b/src/assets/fonts/Kalam-Light.ttf differ diff --git a/src/assets/fonts/Kalam-Regular.ttf b/src/assets/fonts/Kalam-Regular.ttf new file mode 100644 index 0000000..e170f29 Binary files /dev/null and b/src/assets/fonts/Kalam-Regular.ttf differ diff --git a/src/render.rs b/src/render.rs index 6519dec..eb46055 100644 --- a/src/render.rs +++ b/src/render.rs @@ -264,8 +264,39 @@ pub fn build_font_db(custom_dir: Option<&Path>) -> anyhow::Result + + + + + + + + + + + {% if bg_image_data_uri %} + + + {% endif %} + + + + + + + + {{ slide.header_handle }} + {{ slide.page_label }} + + + {% set hero_len = slide.hero_title | length %} + {% if hero_len > 10 %}{% set hfs = (1000 / hero_len) | round | int %}{% else %}{% set hfs = slide.hero_font_size | default(150) %}{% endif %} + {{ slide.hero_title }} + {% set slen = slide.hero_subtitle | length %} + {% if slen > 26 %}{% set sfs = (900 / slen) | round | int %}{% else %}{% set sfs = 34 %}{% endif %} + {{ slide.hero_subtitle }} + + + + {% set tlen = slide.tagline | length %} + {% if tlen > 46 %}{% set tfs = (1309 / tlen) | round | int %}{% else %}{% set tfs = 26 %}{% endif %} + {{ slide.tagline }} + + + {% macro pipe_icon(kind, cx, cy) %}{% if not kind %}{% set kind = 'note' %}{% endif %} + {% if kind == 'note' %} + {% elif kind == 'phone' %} + {% elif kind == 'brain' %} + {% elif kind == 'search' %} + {% elif kind == 'spark' %} + {% elif kind == 'rocket' %} + {% endif %} + {% endmacro %} + {% for i in range(6) %} + + {% set bx = 116 + i * 145 %} + + {{ pipe_icon(slide.icon1 if i == 0 else slide.icon2 if i == 1 else slide.icon3 if i == 2 else slide.icon4 if i == 3 else slide.icon5 if i == 4 else slide.icon6, bx + 64, 556) }} + {{ slide.label1 if i == 0 else slide.label2 if i == 1 else slide.label3 if i == 2 else slide.label4 if i == 3 else slide.label5 if i == 4 else slide.label6 }} + {% if i < 5 %}{% endif %} + + {% endfor %} + + + {{ slide.intro_lead }} + {% for ln in slide.intro_text|wordwrap(74)|split(' +') %}{{ ln }}{% endfor %} + + {{ slide.toc_title }} + {% for n in range(9) %} + {% set ly = 848 + n * 40 %} + {% if n == 0 %}{% set entry = slide.toc1 %}{% set pg = slide.pg1 %} + {% elif n == 1 %}{% set entry = slide.toc2 %}{% set pg = slide.pg2 %} + {% elif n == 2 %}{% set entry = slide.toc3 %}{% set pg = slide.pg3 %} + {% elif n == 3 %}{% set entry = slide.toc4 %}{% set pg = slide.pg4 %} + {% elif n == 4 %}{% set entry = slide.toc5 %}{% set pg = slide.pg5 %} + {% elif n == 5 %}{% set entry = slide.toc6 %}{% set pg = slide.pg6 %} + {% elif n == 6 %}{% set entry = slide.toc7 %}{% set pg = slide.pg7 %} + {% elif n == 7 %}{% set entry = slide.toc8 %}{% set pg = slide.pg8 %} + {% else %}{% set entry = slide.toc9 %}{% set pg = slide.pg9 %} + {% endif %} + {{ n + 1 }}. {{ entry }} + {{ pg }} + + {% endfor %} + \ No newline at end of file diff --git a/templates/notebook-page/defaults.json b/templates/notebook-page/defaults.json new file mode 100644 index 0000000..2534e40 --- /dev/null +++ b/templates/notebook-page/defaults.json @@ -0,0 +1,29 @@ +{ + "brand": { + "brand_name": "ajianaz", + "brand_handle": "@ajianaz", + "bg_color": "#fdfcf5", + "bg_color_end": "#fdfcf5", + "accent_color": "#1e3a8a", + "accent_color_end": "#6d28d9", + "show_brand": false + }, + "slides": [ + { + "header_handle": "IG: ajianaz", + "page_label": "Hal. 1", + "chapter_title": "1. MASALAH: IDE HILANG", + "lead_text": "Kita menulis ratusan catatan setiap bulan. Lalu kenapa tetap kehilangan ide?", + "bullet1": "Ide terbaik datang di luar meja kerja: di lift, jalan, antrean.", + "bullet2": "Kita buru-buru mengetik tanpa mikir kata kunci besok.", + "bullet3": "Tiga minggu kemudian: catatannya tidak ketemu.", + "box_title": "Contoh nyata", + "box_color": "#c2387d", + "box_text": "Kamu menulis 'ide paket harga klien kecil'. Tiga minggu kemudian mencari 'strategi pricing UMKM'. Beda kata, satu makna.", + "punchline": "Masalahnya bukan kamu pelupa. Masalahnya cara aplikasi mencari.", + "extra_title": "Cara mengujinya", + "extra_text": "Matikan internet, lalu cari catatanmu berdasarkan makna. Kalau tetap ketemu, berarti semua prosesnya benar-benar di perangkatmu.", + "footer_line": "uteke mobile - uteke.app" + } + ] +} diff --git a/templates/notebook-page/schema.json b/templates/notebook-page/schema.json new file mode 100644 index 0000000..30b4e02 --- /dev/null +++ b/templates/notebook-page/schema.json @@ -0,0 +1,154 @@ +{ + "id": "notebook-page", + "name": "Notebook Page", + "description": "Notebook/study-notes content page: chapter box, bullets, dashed box, cloud punchline (1080x1350).", + "dimensions": { + "width": 1080, + "height": 1350 + }, + "fonts": [ + "Kalam", + "Caveat", + "Inter" + ], + "brand_fields": { + "show_brand": { + "type": "boolean", + "required": false, + "default": false, + "description": "Show brand name/handle watermark at bottom. Default: false." + }, + "brand_name": { + "type": "text", + "required": true, + "max": 30 + }, + "brand_handle": { + "type": "text", + "required": false, + "max": 30 + }, + "bg_color": { + "type": "bg", + "required": false, + "default": "#fdfcf5", + "description": "Paper base color." + }, + "bg_color_end": { + "type": "bg", + "required": false, + "default": "#fdfcf5", + "description": "Second gradient stop (kept for brand-field uniformity; paper look uses solid color)." + }, + "accent_color": { + "type": "bg", + "required": false, + "default": "#1e3a8a", + "description": "Primary ink color." + }, + "accent_color_end": { + "type": "bg", + "required": false, + "default": "#6d28d9", + "description": "Secondary accent (purple callouts)." + }, + "bg_image": { + "type": "image", + "required": false + }, + "bg_image_opacity": { + "type": "number", + "required": false, + "default": 0.15 + }, + "logo": { + "type": "image", + "required": false + } + }, + "slide_fields": { + "header_handle": { + "type": "text", + "required": true, + "max": 30, + "description": "Left header text (e.g. IG handle)." + }, + "page_label": { + "type": "text", + "required": true, + "max": 12, + "description": "Right header label (e.g. 'Page 1')." + }, + "chapter_title": { + "type": "text", + "required": true, + "max": 30, + "description": "Chapter title inside the box." + }, + "lead_text": { + "type": "text", + "required": true, + "max": 140, + "description": "Lead sentence (wrapped)." + }, + "bullet1": { + "type": "text", + "required": false, + "max": 120, + "description": "Bullet point 1." + }, + "bullet2": { + "type": "text", + "required": false, + "max": 120, + "description": "Bullet point 2." + }, + "bullet3": { + "type": "text", + "required": false, + "max": 120, + "description": "Bullet point 3." + }, + "box_title": { + "type": "text", + "required": false, + "max": 30, + "description": "Dashed box title (omit to hide box)." + }, + "box_text": { + "type": "text", + "required": false, + "max": 200, + "description": "Dashed box body (wrapped)." + }, + "box_color": { + "type": "color", + "required": false, + "description": "Dashed box border/title color (default accent)." + }, + "punchline": { + "type": "text", + "required": true, + "max": 90, + "description": "Cloud callout punchline (wrapped)." + }, + "extra_title": { + "type": "text", + "required": false, + "max": 30, + "description": "Second dashed box title (omit to hide)." + }, + "extra_text": { + "type": "text", + "required": false, + "max": 180, + "description": "Second dashed box body (wrapped)." + }, + "footer_line": { + "type": "text", + "required": false, + "max": 60, + "description": "Bottom center line (brand/CTA)." + } + } +} diff --git a/templates/notebook-page/template.svg b/templates/notebook-page/template.svg new file mode 100644 index 0000000..a4f195c --- /dev/null +++ b/templates/notebook-page/template.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + {% if bg_image_data_uri %} + + + {% endif %} + + + + + + + + {{ slide.header_handle }} + {{ slide.page_label }} + + + + + {% set clen = slide.chapter_title | length %} + {% if clen > 24 %}{% set cfs = (620 / clen) | round | int %}{% else %}{% set cfs = 34 %}{% endif %} + {{ slide.chapter_title }} + + + {% for ln in slide.lead_text|wordwrap(56)|split(' +') %}{{ ln }}{% endfor %} + + {% for n in range(3) %} + {% set by = 352 + n * 84 %} + {% if n == 0 %}{% set b = slide.bullet1 %} + {% elif n == 1 %}{% set b = slide.bullet2 %} + {% else %}{% set b = slide.bullet3 %} + {% endif %} + {% if b %} + + {% for ln in b|wordwrap(48)|split('\n') %}{{ ln }}{% endfor %} + {% endif %} + {% endfor %} + + {% if slide.box_title or slide.box_text %} + {% set blines = slide.box_text|default('')|wordwrap(60)|split('\n') %} + {% set boxh = 74 + blines|length * 34 %} + + {{ slide.box_title }} + {% for ln in slide.box_text|default('')|wordwrap(60)|split('\n') %}{{ ln }}{% endfor %} + {% endif %} + + {% set plines = slide.punchline|wordwrap(42)|split('\n') %} + {% set bh = 92 + (plines | length - 1) * 36 %} + {% set bby = 792 %} + + + + + {% for ln in plines %}{{ ln }}{% endfor %} + + + {% if slide.extra_title %} + + {{ slide.extra_title }} + {% for ln in slide.extra_text|default('')|wordwrap(60)|split('\n') %}{{ ln }}{% endfor %} + {% endif %} + + {% for ln in slide.footer_line|wordwrap(60)|split('\n') %}{{ ln }}{% endfor %} + \ No newline at end of file