- engine: add animation.painter (Alt+Shift+C) and animation.painterApply (Alt+Shift+V) supporting single-use and sticky modes
- tools: apply animation painter on canvas shape click
- edit: disarm animation painter on deselect (Escape)
- ui-egui: add Animation Painter button in Advanced Animation ribbon group
- tests: unit tests for single and sticky animation painting
- docs/parity: mark Animation Painter as done (breadth parity to 80%)
- contributors: add Johannesgauss to contributors/people.toml
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Three causes, each making links look like they "don't save":
- With the caret at a word boundary (end of text, empty paragraph),
insert.hyperlink formatted an empty range and changed nothing. It now
inserts the address (or the slide's title, or `text`) as linked text, as
PowerPoint does. Typing right after a link no longer extends it.
- The slide show ignored hyperlinks and shape actions. A click on one now
follows it (ShowState::follow): slides, next/previous/first/last, Last
Slide Viewed, End Show, and URLs; the pointer turns into a hand over links.
- The desktop app was built without eframe's `links` feature, so
ctx.open_url did nothing natively (also the ribbon's app page link).
Enabled as in DesignCraft, EffectCraft and PrintCraft.
Saving was fine: links round-trip through .deckcraft and .pptx.
generated with Claude Code
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pictures): animated GIFs play, with pause (media.gifPlay)
GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(pictures): animated GIFs play, with pause (media.gifPlay)
GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command.
Also adds my entry to contributors/people.toml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Animated GIFs: cap frames (1000), canvas (4096²), per-GIF (64 MB) and cache (192 MB) memory
A few MB of hostile GIF could decode to millions of tiny frames or a 100 Mpx canvas per frame,
and eight cached GIFs could hold 2 GB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
It ran file.new, so each click silently added another blank presentation
and looked like it did nothing. Home is now a UI command (app.home) that
shows the start screen over the open presentations, with a "Back to …"
button; Escape or Home again goes back. Creating or opening a presentation
from it leaves it. While it's open, keys don't reach the hidden
presentation (only app shortcuts and Escape). ui.inspect reports `home`.
generated with Claude Code
egui::Popup::menu closes on any click inside the popup, so the "More
Colors… #" field of every colour drop-down closed the menu before a value
could be typed. The colour drop-downs now open through
widgets::color_popup (CloseOnClickOutside): picking a colour still closes
them (color_grid calls ui.close()), and the other buttons in them
(Gradient…, the Weight, Dashes and Arrows submenus of Shape Outline) close
them explicitly.
generated with Claude Code
* Morph: a changed geometry morphs its outline (a circle turns into a star)
Paired shapes that differ only in geometry (another preset, other adjust
values, a custom path) and fill colour no longer cross-fade: one shape moves
and its outline turns from the old geometry into the new one.
- geom::morph aligns two outlines the way manim does: the same number of
contours, the same number of cubic curves per contour pair, then every
point moves in a straight line. Closed contours are turned and reversed so
the points travel least (a triangle does not twist into a circle), extra
points go to the longest curves, and a sub-path without partner grows out
of (or shrinks into) a point, so a cube's faces appear and a donut's hole
closes. Fill mode, outline and fill rule switch at the midpoint.
- anim::morph_frame lists such shapes in MorphFrame::paths; the renderer
draws them with the morphed geometry (PathMorph, a new render_blend
argument next to TextMorph).
Ported from the path morph in alexplain (MIT, same author).
generated with Claude Code
* Morph: differing looks of an outline cross-fade instead of switching
When a sub-path pair differs in fill mode, outline or fill rule, the two
looks no longer switch at the midpoint. A fill or outline only one side has
fades out (or in), so a filled shape turning into a line loses its fill
gradually. When both fill, the even-odd side lies opaque underneath and the
other fades over it, so a donut's hole fills smoothly while it shrinks.
generated with Claude Code
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Flips mirror a shape's geometry, never its text. The text was drawn with the shape's full
transform, so every label on a flipped arrow came out mirrored (flipH) or mirrored upside down
(flipV). As in PowerPoint, the text rect keeps its flipped place and the text is mirrored back
about the rect's centre: flipH text stays upright, flipV (or both flips) turns it 180°, rotation
still applies, and the same holds for shapes inside flipped groups.
render::text_transform is that transform (plus vertical-text rotation). Drawing, the PDF text
layer and link boxes, morph text, the editing caret and selection, and click-to-place-the-caret
all use it, so a click lands on the character drawn under it.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On a slide change the show kept a clone of the old slide's texture handle
as the transition's "old" picture, then rendered the new slide into that
same texture with set(). Old and new were the same image, so every
transition (except Morph, which renders into its own texture) blended the
new slide with itself and looked like an instant cut. The old texture is
now taken out of show.tex, so the new slide gets a texture of its own.
generated with Claude Code
* Sprint 1: bidi and Arabic typography engine
fonts: ISO 15924 script and a:latin/a:ea/a:cs slot classification;
shape_range shapes an item with the whole paragraph as HarfRust
pre/post context, explicit script/language/direction, glyphs kept in
visual order; per-grapheme-cluster fallback (slot face, theme script
font, OS cascade families, bundled craft-fonts face); GDEF ligature
carets.
text: UAX #9 levels for the whole paragraph (a:pPr/@rtl), itemization
by level, script, face and shaping style (paint-only changes no longer
split words), logical line breaking at cluster boundaries, per-line
L1/L2 reordering (L4 mirroring by the shaper); RTL start edge and
bullets, justLow kashida; caret Affinity, logical/visual movement,
bidi hit testing and selection as the union of visual boxes.
model/pptx: theme a:font script fonts and algn="justLow" round-trip.
engine: format.rtl (Right-to-Left Text Direction), justifyLow align.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fonts: the bundled script face comes before installed fonts; no Microsoft family names
Arabic, Hebrew, Thai and Devanagari clusters try the craft-fonts face first, so every machine
draws the same, then openly licensed or OS-native installed families, then any installed face.
Installed Microsoft faces are still used through that scan, but not named (AGENTS.md §1.1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tayeb <tayeb@192.168.11.179>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Tables: rows grow to fit their text, cell text in the PDF text layer (#13)
A stored row height (a:tr/@h) is a minimum, as in PowerPoint: each row grows so its cells' text fits. Tables from pandoc or python-pptx store h="0" and drew every row on top of each other. Cell editing (click to edit, cell outline) uses the same heights, and the PDF text layer now includes table cell text.
* Tables: a grown table's box covers its drawn rows; resize scales from drawn heights
Handles, hit tests and marquee used the stored frame, so rows grown to fit their text drew
below the selection box (a file with h=0 rows had a 0 pt tall box). Resizing divided by the
stored row total, which is ~0 for those files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Morph: shapes move, resize and blend in the slide show
Morph used to fall back to a plain crossfade in the show: morph_pairs and
morph_xfrm existed, but nothing rendered frames with them.
- anim: morph_frame builds a frame from two slides. Paired shapes move along
morph_xfrm (eased); a pure fill colour change blends, other content changes
cross-fade on the same moving box. Unpaired shapes fade out/in, shapes with
entrance animations or hidden media stay out of the frame.
- render: render_blend draws such a frame in one pass: both backdrops
(background, master and layout graphics) cross-fade, every shape resolves
against the slide it comes from, so placeholders keep their own layout.
Renderer::slide is split into canvas/fields/backdrop to share the code.
- ui-egui: the show renders morph frames with these, crossfade otherwise.
generated with Claude Code
* Contributors: add my own entry (alexanderalber)
generated with Claude Code
* Morph: Words and Characters options morph text token by token
The transition option (objects/words/characters) was stored and round-tripped
but ignored by the show. With Words or Characters, paired shapes whose text
differs now morph their text instead of cross-fading it:
- text: GlyphRun records per glyph the paragraph character it draws, so
glyphs can be grouped into words or characters.
- anim: morph_frame reads the option and lists such pairs in MorphFrame::text.
- render: morph_text draws them. Shared words (or characters) travel from their
place in the old text to their place in the new one and grow or shrink to the
new size; a different look cross-fades on the way. The remaining tokens fade
while following a scale and shift fitted to the shared ones, so a line moves
and rearranges as one. Rotated text falls back to a cross-fade.
draw_layout_with's glyph drawing moved into draw_glyph for reuse.
- ui-egui: the show passes the pairs to render_blend.
generated with Claude Code
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The step clock ran before the frame's input actions were applied. A click
that started a step (PlayStep) was timed against the old step_start (when the
slide was entered), so after waiting longer than the step's duration the step
finished in the same frame: everything appeared at once, without animation.
Input actions are now applied before the clock is read.
generated with Claude Code
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Adds the craftrules standard (standards/contributors.md) to DeckCraft's About window:
tabs About · Contributors · Models. contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as
static tables. Each contributor line: @username, opt-in display/real name, PRs, commits,
lines +/−/Δ, binary assets +/−, first/last commit. Sortable by every column; alphabetical
ignores case and the leading @. The control channel's ui.dialog.open now honours its
documented `fields` param (e.g. About {"tab": "contributors"}).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- crates/media: probe (duration, rate/channels, size/fps; WMA/WMV recognised), audio decode to
f32 PCM (MP3, AAC M4A/ADTS, ALAC, WAV, AIFF, CAF, FLAC, Vorbis via symphonia; Opus with our
decoder in Ogg/WebM/MKV), video decode (MP4/MOV/WebM/MKV) with YUV->RGBA, poster frames,
trim/fade/volume, Player mixer + per-voice clock (device-clocked or wall-clocked), VideoFeed.
- Codec crates ported from FilmCraft: bitstream, h264, hevc, vp9, av1, isobmff, matroska, ogg, opus.
- insert.audio/insert.video probe and store duration/size and a poster frame; unplayable media
is embedded with a warning. New media.play/pause/toggle/stop/seek/info/posterFrame.
- UI: MediaHost (cpal output injected by the desktop app), control bar under media, video frames
on canvas and in the show; autoplay, click to play, loop, rewind, across slides, hide, full screen.
- Tests with in-code WAV/AIFF and CC0 self-generated fixtures (make.sh, ATTRIBUTION.md rows).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- apps/slidecraft-web: SlideApp in the browser via eframe's web runner (trunk; WebGPU with
WebGL2 fallback). Opens the sample deck (?blank for empty, ?show to present, ?webgl to force
WebGL2). Open/Insert use the browser file picker through Services::open_async + inbox;
Save/Export download; dropped files are read asynchronously.
- ui-egui: native-only synchronous drop handling (wasm uses the inbox), real clock on wasm
(js_sys::Date::now), About shows build commit/date when SLIDECRAFT_BUILD_SHA/DATE are set.
- .github/workflows/release.yml (push to `release` / workflow_dispatch): macOS universal
signed+notarized dmg, Windows x64/x86 signed msi + portable zip, Linux AppImage/deb/rpm/tar.gz
(x86_64, aarch64), Flatpak bundle, FreeBSD tar.gz (vmactions), web zip; draft GitHub Release
"SlideCraft v<version>" from [workspace.package] version. Same secret names as DesignCraft.
- freebsd.yml and packaging-lint.yml CI, ported from DesignCraft.
- packaging/: macOS (Info.plist, entitlements, cert import), Windows (WiX with new UpgradeCode,
.slidecraft ProgId, Open-with for pptx), Linux (.desktop, AppStream, MIME, nfpm, Flatpak),
FreeBSD and web scripts; shared setup in packaging/common.sh.
- apps/slidecraft/build.rs embeds icon + VERSIONINFO on Windows; slidecraft.icns/.ico generated
from the committed PNGs by packaging/icons.sh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>