diff --git a/docs/authoring/animations.md b/docs/authoring/animations.md new file mode 100644 index 00000000..3658a65f --- /dev/null +++ b/docs/authoring/animations.md @@ -0,0 +1,130 @@ +# Animations + +An animation reveals, hides, or emphasizes a single element as you advance through a slide. +Each one targets an element by its `id`, with no leading `#`: + +```python +from inkflow import Slide, animations + +Slide( + "title", + animations=[ + animations.FadeIn("headline"), + animations.FadeIn("subtitle"), + animations.FadeIn("byline"), + ], +) +``` + +The three fades take one keypress each. +*When* a cue fires is decided by its [trigger](steps.md#animation-triggers), +and this page is about *what* it does. + +Elements with no animation declared start **visible**. +An element targeted by an entrance animation starts **hidden** +and appears when its step arrives. +Stepping backward plays each animation in reverse. + +## The built-in types + +Every type accepts `duration`, `easing`, `delay` and `iterations` as keyword arguments. +`element` and `trigger` are the two positional slots. +The [animations reference](../reference/animations.md) has the full signatures. + +| Class | Effect | Starts | +|---|---|---| +| `FadeIn` | Opacity 0 to 1, with a subtle upward drift | Hidden | +| `FadeOut` | Opacity 1 to 0 | Visible | +| `Bounce` | Springs up into place from just below (`distance`) | Hidden | +| `SlideIn` / `SlideOut` | Travels from or to an edge (`direction`, `distance`) | Hidden / Visible | +| `ZoomIn` / `ZoomOut` | Scales into or out of place (`scale`) | Hidden / Visible | +| `Highlight` | Pulses a glow (`color`, `iterations`) without hiding | Visible | + +```python +from inkflow import Direction, animations + +animations.SlideIn("box", direction=Direction.LEFT, duration=0.6) +animations.ZoomIn("logo", scale=0.6) +animations.Highlight("total", color="#cba6f7", iterations=2) +``` + +## Enter, exit, emphasis + +Every type has a **kind**, fixed by the semantic base it subclasses: + +- **`Enter`** reveals the element and leaves it visible. +- **`Exit`** hides it and leaves it hidden. +- **`Emphasis`** fires momentarily without changing visibility. + +The kind is what lets one element carry several cues across a slide. +It can enter, be emphasized, leave, and come back, each on its own step: + +```python +animations = [ + animations.FadeIn("hero"), # step 1: enters + animations.Highlight("hero"), # step 2: emphasized + animations.SlideOut("hero", direction=Direction.DOWN), # step 3: exits + animations.Bounce("hero"), # step 4: returns +] +``` + +At any step the element is shown or hidden by its most recent enter or exit, +while emphasis cues just pulse. + +Two enters, or two exits, on one element with no opposing cue between them +is almost always a mistake. +inkflow warns about it when it builds the slide. + +## Playing a video + +`PlayVideo` is a cue with no animation of its own. +It starts a [`Video`](slides.md#video-playback) on a step +instead of on load, targeting the video's zone key: + +```python +from inkflow import Slide, Video, animations + +Slide( + "media-right", + zones={"media": Video("assets/demo.mp4")}, + animations=[animations.PlayVideo("media")], +) +``` + +It takes a trigger like any other cue. + +## Writing your own + +A custom animation is a dataclass and a `@keyframes` rule. +No JavaScript is involved. + +Subclass one of the semantic bases and name the keyframes `anim-`, +where the slug is the kebab-cased class name (`Glow` becomes `anim-glow`): + +```python +from dataclasses import dataclass +from inkflow import animations + + +@dataclass +class Glow(animations.Emphasis): + intensity: float = 1.0 +``` + +```css +/* styles.css, next to deck.py */ +@keyframes anim-glow { + 50% { + filter: drop-shadow(0 0 calc(8px * var(--anim-intensity)) var(--inkflow-accent)); + } +} +``` + +The step engine reads the keyframes and drives them. +Any extra field on the class is substituted wherever it appears +as `var(--anim-)`, so `intensity=2.0` on one cue +and `intensity=0.5` on another reuse the same rule. + +A `styles.css` next to `deck.py` is loaded automatically. +Your class is usable straight away, in `deck.py` +and as a [`type=`](steps.md#choosing-the-animation-with-type) in a Markdown reveal. diff --git a/docs/authoring/inkscape.md b/docs/authoring/inkscape.md new file mode 100644 index 00000000..c487e38d --- /dev/null +++ b/docs/authoring/inkscape.md @@ -0,0 +1,216 @@ +# Working in Inkscape + +inkflow never opens your editor and never rewrites your SVGs while serving. +That keeps the pipeline simple, but it leaves a few gaps +between what Inkscape shows you and what the browser will show. + +Four commands close them. +All of them are optional, and all of them are safe to re-run. + +| Command | Closes the gap between | +|---|---| +| [`inkflow sync`](#previewing-the-full-slide-sync) | a bare slide file and the composed slide | +| [`inkflow label2id`](#naming-elements-label2id) | Inkscape's labels and SVG ids | +| [`inkflow colorize`](#theme-colors-in-the-editor-colorize-and-palette) | hardcoded hex fills and theme tokens | +| [`inkflow verify`](#checking-a-deck-verify) | a deck that looks fine and one that is | + +## Previewing the full slide (`sync`) + +A slide that inherits a [layout](../design/layouts.md) is mostly empty on its own. +The background, the frame and the zone positions all live in its ancestors, +and the [chrome](../design/overlays.md) lives in the overlays. +Open the file in Inkscape and you see none of it. + +`inkflow sync` writes them in as locked layers: + +```bash +inkflow sync +``` + +Bottom to top, a synced slide holds its ancestor chain, its own content, then the overlays. +You can see how much room the footer needs and where the content zone actually sits. + +These layers are authoring reference only. +The pipeline strips them before serving, so they never reach the browser. + +To find stale files without rewriting anything: + +```bash +inkflow sync --check +``` + +It exits 1 if any file needs updating, which makes it usable in CI. + +### Which overlays a file previews + +`sync` works on files, but overlays are declared on slides, +and one layout can back many slides that disagree about their chrome. +The answer is resolved in three steps: + +1. An explicit `inkflow:preview-overlays` attribute on the file wins. + Space-separated names, or `""` for none. +2. Otherwise, what every slide backed by this file agrees on. +3. Otherwise the deck default. + +`sync` prints which rule fired for each file, so the third is never silent: + +``` + Injected slides/intro.svg (1 overlay layer, slides agree) + Injected layouts/content.svg (1 overlay layer, deck default overlays) + Injected overlays/footer.svg (overlay file) +``` + +The third rule is a guess, and it leans toward *showing* chrome. +The question you are answering in Inkscape is how much room to leave, +so a preview with chrome a slide will not have costs you some empty space, +while the reverse causes overlap. + +When the guess is wrong for a file, pin it: + +```xml + +``` + +### Drawing an overlay + +An overlay file gets no chrome of its own, +otherwise `sync` would stamp the deck's footer onto the footer you are drawing. + +It can name a **backdrop** instead: something drawn behind it purely as reference, +so you are positioning against a real slide rather than a checkerboard. + +```xml + + +``` + +`inkflow:preview` takes a layout name, any of the +[prefixes](../design/layouts.md#finding-a-layout-by-name), or a relative path. +A path is the useful form for a deck of hand-drawn SVGs with no layouts at all, +where the honest backdrop is an actual slide: + +```xml + inkflow:preview="../slides/01-title.svg" +``` + +That slide's own layout chain comes along with it. + +A backdrop is a preview choice, not a structural claim. +Picking `content` while the overlay lands on `two-cols` at runtime is fine. + +Without the attribute an overlay previews against nothing, and `sync` says so: + +``` + Injected overlays/footer.svg (overlay file, no backdrop) + Injected overlays/logo.svg (overlay file, backdrop: content) +``` + +A file counts as an overlay when it lives in an `overlays/` directory, +or when the deck references it as one. + +### Working on a theme + +Theme files have no `deck.py` to consult. +Use `--no-deck`: + +```bash +inkflow sync --no-deck layouts/*.svg +``` + +With no deck there is no slide-to-overlay mapping to derive, +so overlay previews come from `inkflow:preview-overlays` alone +and everything else is synced without chrome. +A theme overlay still gets whatever backdrop it names. + +`local:` and `theme:` references need a project context, +so using them with `--no-deck` is an immediate error. + +## Naming elements (`label2id`) + +Animations and [morph](morph.md) match elements by `id`. +Inkscape's Layers & Objects panel edits an element's *label* (`inkscape:label`), +which is not the same field. +Setting an `id` means opening the XML editor for every element. + +`label2id` promotes every label to the `id`: + +```bash +inkflow label2id slides/*.svg # rewrite in place +inkflow label2id -n slides/three.svg # preview, write nothing +``` + +Name things in the panel as you draw, run it once, then wire up `deck.py`. + +A label that is already a valid id is used verbatim. +Anything else is slugified: spaces become hyphens, accents and symbols are dropped. +Labels are allowed to repeat but ids are not, +so a clash is reported and skipped rather than overwriting an existing id. +Elements inside the locked preview layers are left alone. + +## Theme colors in the editor (`colorize` and `palette`) + +Inkscape cannot read CSS custom properties, +so an element painted with a [semantic class](../design/themes.md#svg-element-utility-classes) +appears unstyled in the editor without help. + +```bash +# 1. Install the theme's palette as Inkscape swatches, once per machine +inkflow palette --deck deck.py > ~/.config/inkscape/palettes/inkflow.gpl + +# 2. Convert hardcoded hex fills and strokes into semantic classes +inkflow colorize slides/*.svg + +# 3. Refresh the editor preview +inkflow sync +``` + +Step 3 injects hex fallbacks that Inkscape can render. +They are stripped at serve time and never reach the browser, +so the slide still follows the live theme and its light/dark switch. + +`inkflow palette` derives the swatches from the active theme, +so a custom theme exports its own colors. + +## Checking a deck (`verify`) + +`inkflow verify` checks a deck before you present it, +printing one line per slide: + +```bash +inkflow verify +inkflow verify --strict # exit 1 on warnings too +``` + +**Errors** are things that will not render: +a missing SVG, `.md`, notes file or media file, +a zone id or animation element id that is not in the composed slide, +or an overlay that paints an opaque full-canvas rect and would hide the deck. + +**Warnings** are things that are probably wrong: +animation steps that are not contiguous from 1, +a zone id declared twice after composition, +or layout layers that are stale and need `inkflow sync`. + +Hidden slides (`visible=False`) are skipped unless you pass `--all`. + +## Keeping SVGs clean in git + +Inkscape stores viewport position, zoom level and window size inside the file, +so every save produces a diff even when nothing visual changed. + +`inkflow setup-git` installs a pre-commit hook that strips that metadata +from staged SVGs, plus a diff driver so `git diff` and GitHub show only visual changes. +[`inkflow init`](../getting-started.md#git-integration) does this for new projects. + +To clean files committed before the hook was in place: + +```bash +inkflow clean slides/*.svg +``` diff --git a/docs/authoring/markdown.md b/docs/authoring/markdown.md new file mode 100644 index 00000000..e3f32d21 --- /dev/null +++ b/docs/authoring/markdown.md @@ -0,0 +1,205 @@ +# Markdown content + +Pointing `md=` at a Markdown file fills a slide's [zones](slides.md#zones) with its content: + +```python +from inkflow import Deck, Slide + + +def main() -> Deck: + return Deck( + slides=[ + Slide("content", md="intro"), + ] + ) +``` + +`md` takes the same kind of reference as `Slide.src`. +A bare name like `"intro"` resolves to `slides/intro.md`, +or you can write out a full path. +`Inline("# Title\n\nBody")` passes Markdown directly instead of reading a file. + +Here `"content"` happens to be a [built-in layout](../design/layouts.md#built-in-layouts), +but nothing about `md=` requires one. +It fills whatever zones the referenced SVG defines. + +## Where the content goes + +inkflow routes a Markdown file into zones in two ways. + +### Automatic + +With no markers at all: + +- a leading `# H1` goes to `zone-title` +- an `## H2` immediately after it goes to `zone-subtitle` +- everything else goes to the [default zone](../design/layouts.md#the-default-zone) + +```markdown +# My slide title +## Optional subtitle + +The body content goes here. +``` + +### Explicit markers + +`::zone-name::` routes everything after it to that zone, +until the next marker: + +```markdown +::left:: + +## Left column + +Content for the left side. + +::right:: + +## Right column + +Content for the right side. +``` + +Explicit markers always win over the automatic routing. + +## Alignment inside a zone + +Zone markers take optional `key=value` parameters: + +```markdown +::content align=center valign=center padding=60:: + +Horizontally and vertically centered, with 60 units of padding. +``` + +| Parameter | Values | Controls | +|---|---|---| +| `align` | `left`, `center`, `right`, `justify` | Horizontal text alignment | +| `valign` | `top`, `center`, `bottom` | Vertical position of the content block | +| `padding` | number, in SVG user units | Inner spacing on all sides | + +All three are optional and combine freely. + +These are the most specific of three layers. +A marker parameter beats a CSS variable set in the layout SVG, +which beats the built-in default (`left`, `top`, `0`): + +```css +/* in the layout SVG's