diff --git a/packages/preview/black-angular-frame/0.1.0/LICENSE b/packages/preview/black-angular-frame/0.1.0/LICENSE new file mode 100644 index 0000000000..82eca67b4d --- /dev/null +++ b/packages/preview/black-angular-frame/0.1.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Miguel Montes (@miguelm7654) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/preview/black-angular-frame/0.1.0/README.md b/packages/preview/black-angular-frame/0.1.0/README.md new file mode 100644 index 0000000000..9342cb71ec --- /dev/null +++ b/packages/preview/black-angular-frame/0.1.0/README.md @@ -0,0 +1,308 @@ +# black-angular-frame + +A Typst presentation template with a square, minimal, academic design language: a solid navigation bar on top, a two-level footer with page number, a tinted title strip for regular content slides, and compact box environments without wasted whitespace. + +![black-angular-frame thumbnail](thumbnail.png) + +--- + +## Quick start + +```typst +#import "@preview/black-angular-frame:0.1.0": * + +#let presentation-config = ( + title: "My Presentation", + subtitle: "An optional subtitle", + authors: "Alice, Bob", + institution: "Institution Name", + date: "May 2026", + final-message: "Thank you for your attention", + TOC: true, +) + +#show: black-angular-frame.with(config: presentation-config) + +#new-section("Introduction") + +#slide(title: "First slide")[ + - Bullet one + - Bullet two +] + +#thank-you-slide +``` + +Initialize from the template after publication with: + +```bash +typst init @preview/black-angular-frame:0.1.0 +``` + +For local development in this repository, compile the demo with: + +```bash +typst compile example.typ example.pdf --font-path assets/fonts +``` + +The source file is `example.typ` and the output path should be exactly `example.pdf`; this repository keeps only that generated PDF. + +The local demo imports `black-angular-frame.typ` directly and uses the fonts in `assets/fonts/`. If you use VS Code + Tinymist, configure the font path manually if you want IBM Plex to be discovered from `assets/fonts/`. + +--- + +## Template configuration + +Pass configuration through `#show: black-angular-frame.with(config: presentation-config)`. + +| Name | Expected value | Default | Description | +|------|----------------|---------|-------------| +| `title` | String | `""` | Presentation title. | +| `subtitle` | String | `""` | Presentation subtitle. | +| `authors` | String | `""` | Author line shown on the cover and footer. | +| `institution` | String | `""` | Institution shown on the cover and footer. | +| `date` | String | `""` | Date shown on the cover. | +| `final-message` | String | `""` | Message shown on the last slide. | +| `primary-color` | Color | `rgb("#1C1C1C")` | Main bars, highlights, numbering, and accents. | +| `secondary-color` | Color | `rgb("#D9D9D9")` | Secondary header and footer bands. | +| `background-color` | Color | `rgb("#FFFFFF")` | Slide background color. | +| `font-color` | Color | `luma(20)` | Default body text color. | +| `header-font-color-1` | Color | `_muted-nav(primary-color)` | Inactive text in the primary header band and text in the lower footer band. | +| `header-font-color-2` | Color | `primary-color` | Text in the secondary header and footer bands. | +| `header-font-color-1-highlight` | Color | `rgb("#FFFFFF")` | Active text in the primary header band. | +| `content-center` | Float 0-1 | `0.3` | Vertical position used to center content; `0` starts at the top, `1` at the bottom. | +| `content-upper-padding` | Float 0-1 | `0.05` | Top proportion of the available content area kept empty. | +| `content-lower-padding` | Float 0-1 | `0.05` | Bottom proportion of the available content area kept empty. | +| `logos` | `array[content]` | `()` | Logo images or custom content shown on the cover. | +| `TOC` | Bool | `true` | Whether to add the table of contents slide with links to each section's divider or first slide. | + +The template still accepts the previous parameter names (`title-color`, `bg-color`, `cover-images`, `toc`, and related aliases) for existing documents, but new presentations should use the `config` object. + +For cover logos, pass Typst content rather than path strings, for example `logos: (image("assets/logo.png", height: 45pt),)`. This keeps image paths resolved from the user document instead of from the package internals. + +--- + +## Fonts + +The template uses IBM Plex when available, with Liberation and DejaVu fallbacks. For local compilation with IBM Plex, install the fonts on your system or pass them with `--font-path`. + +This repository keeps IBM Plex files in `assets/fonts/` so `example.typ` can compile locally with the intended typography. The package manifest excludes `assets/fonts/**` from the Typst Universe bundle, because Universe packages must not ship font files. + +| Role | Font | +|------|------| +| Body text | IBM Plex Serif | +| Titles and UI elements | IBM Plex Sans | +| Code and verbatim | IBM Plex Mono | + +Fallback chains include Liberation and DejaVu, so the template works even when IBM Plex is unavailable. + +--- + +## Slide functions + +### `#slide(title: "...", body)` + +Standard content slide. Content is vertically centred in the available area. + +```typst +#slide(title: "My Slide")[ + Content goes here. +] +``` + +### `#new-section("Name", slide-title: auto)` + +Registers a section, increments the section counter, resets figure and theorem counters, updates the navigation bar label, and automatically renders the section intro slide. Use `slide-title:` when the intro slide should display a different title from the navigation label. + +### `#section-slide("Name")` + +Renders an extra full-page section divider manually. This is no longer needed for the normal `#new-section(...)` workflow. + +### `#new-subsection("Name")` + +Registers a subsection in the TOC without rendering a divider slide. + +### `#thank-you-slide` + +A centered italic final-message slide. Configure the displayed text with `final-message`. No parentheses — this is a content value, not a function call. + +--- + +## Layout helpers + +### `#two-col(left, right, left-width: 48%, gutter: 4%)` + +Splits the slide into two columns. + +```typst +#two-col( + [Left column content], + [Right column content], + left-width: 55%, +) +``` + +--- + +## Figures + +### `#fs-figure(caption: [...], body)` + +Numbered figure. The counter resets at each `#new-section`. Reference by number in surrounding text. + +```typst +#fs-figure(caption: [A diagram showing the architecture.])[ + #image("diagram.svg", width: 80%) +] +``` + +--- + +## Tables + +### `#fs-table-cell(body, fill: ..., stroke: ..., pos: ...)` + +Cell helper for tables built with `grid`. It applies the template table font, IBM Plex Sans by default. + +```typst +#fs-table-cell(fill: luma(248), stroke: luma(200) + 0.6pt, pos: center)[88.9] +``` + +--- + +## Theorem-style boxes + +All boxes display a colored header with `Kind N.M` (section.number) and an optional name panel. + +| Function | Default color | +|----------|---------------| +| `#theorem(name: "...", body)` | `blue.darken(50%)` | +| `#lemma(name: "...", body)` | `blue.darken(30%)` | +| `#corollary(name: "...", body)` | `blue.darken(40%)` | +| `#proposition(name: "...", body)` | `teal.darken(30%)` | +| `#definition(name: "...", body)` | `purple.darken(20%)` | +| `#example(name: "...", body)` | `green.darken(30%)` | +| `#exercise(name: "...", body)` | `orange.darken(20%)` | +| `#remark(name: "...", body)` | `luma(90)` | +| `#proof(body)` | left-border style | +| `#fs-box("kind", name: "...", color: ..., body)` | any color | + +The `color:` parameter can be overridden on any box. `#fs-box` accepts any string as the kind label. + +```typst +#theorem(name: "Banach Fixed-Point Theorem")[ + Let $(M, d)$ be a complete metric space and $f$ a contraction. Then $f$ has a unique fixed point. +] + +#fs-box("warning", name: "Careful!", color: red.darken(20%))[ + Do not confuse contractions with nonexpansive maps. +] +``` + +## Code and pseudo-code boxes + +Code fragments and pseudo-code can use the same framed header style as theorem boxes, with a `type` label and optional `title`. + +| Function | Purpose | +|----------|---------| +| `#code-box("...", type: "...", title: "...", lang: "...")` | Source code with optional syntax highlighting | +| `#pseudo-code("...", type: "...", title: "...")` | Pseudo-code with the same box chrome | + +```typst +#code-box( + "def train_step(x):\n return model(x)", + type: "Source Code", + title: "Python", + lang: "python", +) + +#pseudo-code( + "for t <- 1 to T\n update theta\nend", + title: "Training Loop", +) +``` + +--- + +## Example slides (`example.typ`) + +The file `example.typ` is a complete demo presentation covering all template features. Below is a summary of each section. + +### Section 1 — Configuration + +- Template configuration table with name, expected value, default, and description. +- Exact Typst code used by the example presentation to import and configure the template. + +### Section 2 — Typography + +- Side-by-side comparison of the three IBM Plex families (Serif, Sans, Mono) with representative weight and style variants. +- Size scale from 8 pt to 18 pt; semantic use of bold, italic, color, and underline. + +### Section 3 — Lists & Enumerations + +- Unordered bullet list (3 levels) and ordered enumeration (3 levels) side by side in two columns. + +### Section 4 — Figures + +- Two numbered figures with placeholder rectangles and captions. Explains how to replace placeholders with real images. + +### Section 5 — Layouts + +- A two-column layout with explanatory text on the left and a content block on the right. + +### Section 6 — Code Blocks + +- Python source code and pseudo-code side by side, both rendered as framed boxes with a theorem-style `type | title` header. + +### Section 7 — Tables + +- Paper-style booktabs table (horizontal rules only) and grid-style table (full borders, colored header, alternating row shading) side by side. + +### Section 8 — Diagrams & Charts + +- Two block diagrams — a Transformer encoder block (matching the original color scheme: orange FFN, blue Add & Norm, green attention) and a state-space system block diagram. +- A linear-algebra kernel/image decomposition diagram and a three-state Markov chain transition graph with labeled arcs. +- A line chart of model accuracy vs. epoch (data from `assets/curves.csv`) and a grouped bar histogram of test scores by group and year (2020–2024). + +### Section 9 — Theorem-style Boxes + +- `#definition`, `#theorem`, `#lemma`, `#corollary`, and `#proof` environments demonstrated with the Banach fixed-point theorem and Picard–Lindelöf corollary. +- `#example`, `#exercise`, `#proposition`, and three `#fs-box` calls with custom kind labels ("note", "warning", "custom") and custom colors. + +--- + +## Repository layout + +```text +black-angular-frame/ +├── typst.toml # Package manifest +├── black-angular-frame.typ # Package entrypoint +├── template/ +│ ├── main.typ # typst init starter file +│ └── assets/ # Starter assets copied by typst init +│ ├── typst-logo.png # Demo logo asset +│ └── github-logo.png # Demo logo asset +├── thumbnail.png # Universe thumbnail +├── example.typ # Full local demo presentation +├── example.pdf # Pre-compiled local demo PDF +├── assets/ +│ ├── typst-logo.png # Local demo logo asset +│ ├── github-logo.png # Local demo logo asset +│ ├── fonts/ # IBM Plex Serif / Sans / Mono TTF files +│ └── curves.csv # Sample data for the line chart slide +└── README.md +``` + +The Typst Universe bundle is controlled by `exclude` in `typst.toml`. The local demo, generated PDFs, local fonts, Tinymist lockfile, and temporary files stay in the repository but are excluded from the published package archive. + +--- + +## Acknowledgements + +This template was visually inspired by academic Beamer themes, especially UChicago-style slide layouts. It is not affiliated with or endorsed by the University of Chicago. No University of Chicago logos or brand assets are included. + +--- + +## License + +The template code is released under the MIT License. The IBM Plex fonts kept for local demo builds are distributed under the SIL Open Font License 1.1; see `assets/fonts/OFL-1.1.txt`. Font files are excluded from the Typst Universe package bundle. diff --git a/packages/preview/black-angular-frame/0.1.0/black-angular-frame.typ b/packages/preview/black-angular-frame/0.1.0/black-angular-frame.typ new file mode 100644 index 0000000000..3b3a3bc941 --- /dev/null +++ b/packages/preview/black-angular-frame/0.1.0/black-angular-frame.typ @@ -0,0 +1,1275 @@ +// ============================================================ +// black-angular-frame -- Typst presentation template +// Formal academic presentation theme for Typst +// Usage: +// #import "black-angular-frame.typ": * +// #show: black-angular-frame.with(config: (title: "My Talk", ...)) +// #slide(title: "First")[...] +// ============================================================ + +// ---- Internal state ---------------------------------------- +#let _fs-tc = state("_fs-tc", rgb("#1C1C1C")) +#let _fs-sc = state("_fs-sc", rgb("#D9D9D9")) +#let _fs-bg = state("_fs-bg", white) +#let _fs-w = state("_fs-w", 254mm) +#let _fs-h = state("_fs-h", 143mm) +#let _fs-ft = state("_fs-ft", none) +#let _fs-fst = state("_fs-fst", none) +#let _fs-title = state("_fs-title", "") +#let _fs-subt = state("_fs-subt", "") +#let _fs-inst = state("_fs-inst", none) +#let _fs-auth = state("_fs-auth", none) +#let _fs-fc = state("_fs-fc", luma(20)) +#let _fs-hfc1 = state("_fs-hfc1", rgb("#999999")) +#let _fs-hfc2 = state("_fs-hfc2", rgb("#1C1C1C")) +#let _fs-hfc1h = state("_fs-hfc1h", white) +#let _fs-final = state("_fs-final", "") +#let _fs-cctr = state("_fs-cctr", 0.3) +#let _fs-cupad = state("_fs-cupad", 0.05) +#let _fs-clpad = state("_fs-clpad", 0.05) +#let _fs-pages = state("_fs-pages", ()) +#let _fs-sec-targets = state("_fs-sec-targets", ()) + +#let _sec-ctr = counter("_fs-sec") +#let _fig-ctr = counter("_fs-fig") +#let _cur-sec = state("_fs-cursec", "") +#let _toc-data = state("_fs-toc", ()) + +#let _thm-ctrs = ( + theorem: counter("_fs-thm"), + lemma: counter("_fs-lem"), + corollary: counter("_fs-cor"), + proposition: counter("_fs-prp"), + definition: counter("_fs-def"), + example: counter("_fs-exa"), + exercise: counter("_fs-exr"), + remark: counter("_fs-rmk"), +) + +// ---- Color helpers ----------------------------------------- +#let _lmix(c, pct) = color.mix((c, 100% - pct), (white, pct)) +#let _title-bg(c) = _lmix(c, 88%) +#let _sub-hdr(c) = _lmix(c, 40%) +#let _soft-rule(c) = _lmix(c, 72%) +#let _muted-nav(c) = color.mix((white, 55%), (c, 45%)) + +// ---- Font stacks ------------------------------------------- +#let _sans = ("IBM Plex Sans", "Liberation Sans", "DejaVu Sans") +#let _serif = ("IBM Plex Serif", "Liberation Serif", "DejaVu Serif") +#let _mono = ("IBM Plex Mono", "Liberation Mono", "DejaVu Sans Mono") + +#let _slide-x-margin = 22.4pt +#let _single-col-x-margin = 2 * _slide-x-margin +#let _nav-compact-threshold = 4 +#let _cover-image-gap = 18pt + +#let _clamp01(value) = { + if value < 0 { 0 } else if value > 1 { 1 } else { value } +} + +// ============================================================ +// Shared chrome +// ============================================================ + +#let _single-line(content) = { + if content == none { none } else if type(content) == str { content.replace("\n", " ") } else { content } +} + +#let _fit-ellipsis(value, remaining, width, text-width, suffix: "...") = { + if remaining <= 0 { suffix } else { + let omitted = value.len() - remaining + let candidate = if omitted < suffix.len() { + value.slice(0, remaining) + value.slice(remaining) + } else { + value.slice(0, remaining) + suffix + } + if text-width(candidate) <= width { + candidate + } else { + _fit-ellipsis(value, remaining - 1, width, text-width, suffix: suffix) + } + } +} + +#let _ellipsis-text( + content, + width, + font: _sans, + size: 10pt, + fill: black, + reserve: 8pt, +) = context { + let effective-width = calc.max(0pt, width - reserve) + let text-width = value => measure(text(font: font, size: size, fill: fill, value)).width + let value = _single-line(content) + + if value == none or type(value) != str or text-width(value) <= effective-width { + text(font: font, size: size, fill: fill, value) + } else { + let suffix = "..." + text( + font: font, + size: size, + fill: fill, + _fit-ellipsis(value, value.len(), effective-width, text-width, suffix: suffix), + ) + } +} + +#let _nav-dot(active: false, color, inactive-fill: auto, active-fill: white) = box( + width: 6.2pt, + height: 6.2pt, + inset: 0pt, + align(center + horizon, circle( + radius: 2pt, + fill: if active { active-fill } else { none }, + stroke: if active { none } else { (if inactive-fill == auto { _muted-nav(color) } else { inactive-fill }) + 0.7pt }, + )), +) + +#let _cover-image-item(logo, height: 45pt) = { + if type(logo) == str { + image(logo, height: height) + } else { + logo + } +} + +#let _cover-image-row(logos, height: 45pt) = { + grid( + columns: logos.map(_ => auto), + column-gutter: _cover-image-gap, + align: horizon, + ..logos.map(logo => _cover-image-item(logo, height: height)), + ) +} + +#let _register-page(section: none, intro: false) = { + _fs-pages.update(p => p + ((section: section, intro: intro),)) +} + +#let _register-section-target(title, loc) = { + if title != none and title != "" { + _fs-sec-targets.update(t => { + if t.find(entry => entry.title == title) == none { + t + ((title: title, loc: loc),) + } else { + t + } + }) + } +} + +#let _header-band( + w, + section, + slide-title: none, + primary, + secondary, + header-font-color-1: auto, + header-font-color-2: auto, + header-font-color-1-highlight: white, +) = context { + let hfc1 = if header-font-color-1 == auto { _muted-nav(primary) } else { header-font-color-1 } + let hfc2 = if header-font-color-2 == auto { primary } else { header-font-color-2 } + let titles = _toc-data.final().filter(e => e.kind == "section").map(e => e.title) + let sec-targets = _fs-sec-targets.final() + let pages = _fs-pages.final() + let page-no = counter(page).get().first() + let nav-margin = 22.4pt + let nav-top = 3.425pt + let nav-bottom = 2.025pt + let nav-item-x-inset = 0.4pt + let active-dot-index = { + if ( + page-no <= pages.len() + and section != none + and section != "" + and not pages.at(page-no - 1, default: (intro: false)).intro + ) { + let seen = 0 + for (idx, entry) in pages.enumerate() { + if entry.section == section and not entry.intro { + seen += 1 + } + if idx + 1 == page-no { break } + } + if seen == 0 { none } else { seen } + } else { + none + } + } + let nav-slide-count(title) = { + calc.max(1, pages.filter(entry => entry.section == title and not entry.intro).len()) + } + let nav-progress-label(title) = { + let slide-count = nav-slide-count(title) + let current-page = pages.at(page-no - 1, default: (intro: false)) + let is-current-section = section == title + let is-section-slide = is-current-section and active-dot-index != none + if is-section-slide { + str(active-dot-index) + "/" + str(slide-count) + } else { + str(slide-count) + } + } + let nav-progress-width(title) = { + let slide-count = nav-slide-count(title) + if slide-count > _nav-compact-threshold { + let label-width = measure(text(font: _sans, size: 6.8pt, nav-progress-label(title))).width + 6.2pt + 1.6pt + label-width + } else { + 6.2pt * slide-count + 1.4pt * calc.max(0, slide-count - 1) + } + } + let nav-title-width(title, item-width) = { + let inner-width = calc.max(0pt, item-width - 2 * nav-item-x-inset) + let value = _single-line(title) + if value == none or type(value) != str { + 0pt + } else { + let raw-width = measure(text(font: _sans, size: 7.1pt, value)).width + calc.min(raw-width, inner-width) + } + } + let nav-visible-width(title, item-width) = { + calc.max(nav-title-width(title, item-width), nav-progress-width(title)) + } + let nav-item(title, item-width) = { + let slide-count = nav-slide-count(title) + let target = sec-targets.rev().find(entry => entry.title == title) + block( + width: item-width, + inset: (left: nav-item-x-inset, right: nav-item-x-inset, top: 1.3pt, bottom: 1.1pt), + { + layout(size => { + let title-fill = if section == title { header-font-color-1-highlight } else { hfc1 } + let current-page = pages.at(page-no - 1, default: (intro: false)) + let is-current-section = section == title + let is-section-intro = is-current-section and current-page.intro + let is-section-slide = is-current-section and active-dot-index != none + let use-compact-progress = slide-count > _nav-compact-threshold + let progress-label = nav-progress-label(title) + let progress-fill = if is-current-section { header-font-color-1-highlight } else { hfc1 } + + let title-content = if target != none { + link(target.at("loc"))[ + #_ellipsis-text( + title, + size.width, + font: _sans, + size: 7.1pt, + fill: title-fill, + ) + ] + } else { + _ellipsis-text( + title, + size.width, + font: _sans, + size: 7.1pt, + fill: title-fill, + ) + } + + let progress-content = if use-compact-progress { + grid( + columns: (auto, auto), + column-gutter: 1.6pt, + align: horizon, + _nav-dot( + active: is-section-slide or is-section-intro, + primary, + inactive-fill: hfc1, + active-fill: header-font-color-1-highlight, + ), + move( + dy: -0.2pt, + text(font: _sans, fill: progress-fill, size: 6.8pt, progress-label), + ), + ) + } else { + box[ + #for dot-idx in range(slide-count) { + _nav-dot( + active: is-section-slide and active-dot-index == dot-idx + 1, + primary, + inactive-fill: hfc1, + active-fill: header-font-color-1-highlight, + ) + if dot-idx + 1 < slide-count { h(1.4pt) } + } + ] + } + + grid( + columns: (1fr,), + rows: (8.6pt, 6.2pt), + row-gutter: 0pt, + block( + width: 100%, + height: 8.6pt, + align(left + horizon, title-content), + ), + block( + width: 100%, + height: 6.2pt, + align(left + horizon, move(dy: 2pt, progress-content)), + ), + ) + }) + }, + ) + } + stack( + dir: ttb, + spacing: 0pt, + block( + width: w, + height: 24.75pt, + fill: primary, + inset: (x: 0pt, y: 0pt), + { + if titles != () { + layout(size => { + let usable-width = size.width - 2 * nav-margin + let positions = if titles.len() == 1 { + (0.5,) + } else if titles.len() == 2 { + (1 / 3, 2 / 3) + } else if titles.len() == 3 { + (1 / 4, 1 / 2, 3 / 4) + } else { + range(titles.len()).map(idx => (idx + 0.5) / titles.len()) + } + let item-width = if titles.len() == 1 { + calc.min(usable-width, 260pt) + } else if titles.len() == 2 { + calc.min(usable-width / 3, 210pt) + } else if titles.len() == 3 { + calc.min(usable-width / 4, 160pt) + } else { + usable-width / titles.len() + } + let relative-lefts = positions.map(pos => usable-width * pos - item-width / 2) + let first-left = relative-lefts.at(0) + let last-left = relative-lefts.at(relative-lefts.len() - 1) + let last-title = titles.at(titles.len() - 1) + let last-visible-width = nav-visible-width(last-title, item-width) + let centering-shift = ( + ( + usable-width - first-left - last-left - last-visible-width - 2 * nav-item-x-inset + ) + / 2 + ) + for (idx, title) in titles.enumerate() { + place( + top + left, + dx: nav-margin + centering-shift + relative-lefts.at(idx), + dy: nav-top, + nav-item(title, item-width), + ) + } + }) + } + }, + ), + if slide-title != none { + block( + width: w, + height: 25.5pt, + fill: secondary, + inset: (left: _single-col-x-margin, right: _single-col-x-margin, top: 0pt, bottom: 0pt), + block( + width: 100%, + height: 100%, + align(horizon, align(left, text(font: _sans, fill: hfc2, size: 14pt, slide-title))), + ), + ) + }, + ) +} +#let _footer-band( + w, + left-top, + right-top, + left-bottom, + page-no, + primary, + secondary, + header-font-color-1: auto, + header-font-color-2: auto, + header-font-color-1-highlight: white, +) = { + let hfc1 = if header-font-color-1 == auto { _muted-nav(primary) } else { header-font-color-1 } + let hfc2 = if header-font-color-2 == auto { primary } else { header-font-color-2 } + stack( + dir: ttb, + spacing: 0pt, + block(width: w, height: 14.1pt, fill: secondary, inset: (left: 22.4pt, right: 22.4pt, top: 0pt, bottom: 0pt), { + grid( + columns: (1fr, 36%), + column-gutter: 10pt, + block( + width: 100%, + height: 100%, + align(horizon, align(left, if _single-line(left-top) != none { + text(font: _sans, fill: hfc2, size: 7pt, _single-line(left-top)) + } else { [] })), + ), + block( + width: 100%, + height: 100%, + align(horizon, align(right, if _single-line(right-top) != none { + text(font: _sans, fill: hfc2, size: 7pt, _single-line(right-top)) + } else { [] })), + ), + ) + }), + block(width: w, height: 15.51pt, fill: primary, inset: (left: 22.4pt, right: 22.4pt, top: 0pt, bottom: 0pt), { + grid( + columns: (1fr, 36%), + column-gutter: 10pt, + block( + width: 100%, + height: 100%, + align(horizon, align(left, if _single-line(left-bottom) != none { + move(dy: -1pt, text(font: _sans, fill: hfc1, size: 7pt, _single-line(left-bottom))) + } else { [] })), + ), + block( + width: 100%, + height: 100%, + align(horizon, align(right, move(dy: -1pt, text( + font: _sans, + fill: hfc1, + size: 6.8pt, + _single-line(page-no), + )))), + ), + ) + }), + ) +} + +#let _slide-body-area(w, h, body) = context { + let center = _clamp01(_fs-cctr.get()) + let upper = _clamp01(_fs-cupad.get()) + let lower = _clamp01(_fs-clpad.get()) + let usable-ratio = calc.max(0, 1 - upper - lower) + + block( + width: w, + height: h, + inset: (x: _single-col-x-margin, y: 0pt), + clip: true, + layout(size => { + let content-width = size.width + let inner-top = size.height * upper + let inner-height = size.height * usable-ratio + let content = block(width: content-width, body) + let measured = measure(content) + let free-height = calc.max(0pt, inner-height - measured.height) + let dy = inner-top + if center <= 0 { 0pt } else { free-height * center } + place(top + left, dy: dy, content) + }), + ) +} + +// ============================================================ +// Public API -- layout helpers +// ============================================================ + +/// Two-column layout inside a slide. +/// #two-col([left content], [right content], left-width: 50%) +#let two-col(left, right, gutter: 22.4pt, left-width: 48%) = { + block( + width: 100%, + grid( + columns: (left-width, 1fr), + column-gutter: gutter, + grid.cell(align: top + start)[#left], + grid.cell(align: top + start)[#right], + ), + ) +} + +// ============================================================ +// Figure +// ============================================================ + +#let _visual-y-margin = 15.75pt +#let _caption-gap = 2.3pt +#let _diagram-caption-gap = _caption-gap +#let _table-caption-gap = 6pt +#let _table-after-caption-margin = _visual-y-margin + +/// Generic visual block without a caption. +#let fs-visual(body) = block( + width: 100%, + above: _visual-y-margin, + below: _visual-y-margin, + align(center, body), +) + +/// Cell helper for grid-built tables. Uses the template's table font. +#let fs-table-cell(body, fill: none, stroke: none, pos: left, inset: (x: 5pt, y: 4pt)) = block( + width: 100%, + fill: fill, + stroke: stroke, + inset: inset, + align(pos, text(font: _sans, body)), +) + +/// Numbered figure with optional italic caption. +/// Counters reset per section. +#let fs-figure(body, caption: none, caption-gap: _caption-gap) = { + _fig-ctr.step() + block(above: _visual-y-margin, below: _visual-y-margin, spacing: 0pt, width: 100%, { + align(center, body) + if caption != none { + v(caption-gap) + align(center, context text(size: 0.72em, style: "italic", [*Figure #_fig-ctr.display().* #caption])) + } + }) +} + +/// Diagram figure with template-level caption spacing. +#let fs-diagram(body, caption: none) = fs-figure( + caption: caption, + caption-gap: _diagram-caption-gap, + body, +) + +// ============================================================ +// Theorem-style boxes +// ============================================================ +#let _panel-box(type-label, title, color, body, body-fill: none, width: 100%) = block( + width: width, + breakable: false, + above: _visual-y-margin, + below: _visual-y-margin, + stroke: color + 0.8pt, + fill: body-fill, + inset: 0pt, + spacing: 0pt, + align(top + left, stack( + dir: ttb, + spacing: 0pt, + if title != none { + grid( + columns: (auto, 1fr), + column-gutter: 0pt, + block( + fill: color, + inset: (left: 5pt, right: 5pt, top: 3.5pt, bottom: 5.3pt), + text(font: _sans, fill: white, weight: "bold", size: 0.80em, type-label), + ), + block( + fill: _sub-hdr(color), + inset: (left: 5pt, right: 5pt, top: 3.5pt, bottom: 5.3pt), + width: 100%, + text(font: _sans, fill: white, size: 0.80em, title), + ), + ) + } else { + block( + width: 100%, + fill: color, + inset: (left: 5pt, right: 5pt, top: 3.5pt, bottom: 5.3pt), + text(font: _sans, fill: white, weight: "bold", size: 0.80em, type-label), + ) + }, + if body-fill != none { + block(width: 100%, height: 1.2pt, fill: body-fill) + }, + block( + width: 100%, + inset: (left: 5pt, right: 5pt, top: 5pt, bottom: 7pt), + fill: body-fill, + align(left, body), + ), + )), +) + +#let _tbox(kind, name, color, body, width: 100%) = { + let ctr = _thm-ctrs.at(kind, default: _thm-ctrs.theorem) + ctr.step() + let cap = upper(kind.first()) + kind.slice(1) + context { + let sec = _sec-ctr.get().first() + let num = ctr.get().first() + _panel-box([#cap #sec.#num], name, color, body, width: width) + } +} + +/// Theorem box +#let theorem(name: none, color: blue.darken(50%), width: 100%, body) = { + _tbox("theorem", name, color, body, width: width) +} +/// Lemma box +#let lemma(name: none, color: blue.darken(30%), width: 100%, body) = { _tbox("lemma", name, color, body, width: width) } +/// Corollary box +#let corollary(name: none, color: blue.darken(40%), width: 100%, body) = { + _tbox("corollary", name, color, body, width: width) +} +/// Proposition box +#let proposition(name: none, color: teal.darken(30%), width: 100%, body) = { + _tbox("proposition", name, color, body, width: width) +} +/// Definition box +#let definition(name: none, color: purple.darken(20%), width: 100%, body) = { + _tbox("definition", name, color, body, width: width) +} +/// Example box +#let example(name: none, color: green.darken(30%), width: 100%, body) = { + _tbox("example", name, color, body, width: width) +} +/// Exercise box +#let exercise(name: none, color: orange.darken(20%), width: 100%, body) = { + _tbox("exercise", name, color, body, width: width) +} +/// Remark box +#let remark(name: none, color: luma(90), width: 100%, body) = { _tbox("remark", name, color, body, width: width) } + +/// Generic custom box -- any kind label and color. +/// #fs-box("warning", name: "Watch out", color: red)[...] +#let fs-box(kind, name: none, color: blue.darken(50%), width: 100%, body) = { + _tbox(kind, name, color, body, width: width) +} + +/// Full-width separator for use inside theorem-style boxes. +#let box-separator(label, color: luma(80)) = { + v(5pt) + move( + dx: -5pt, + block( + width: 100% + 10pt, + fill: color, + inset: (x: 5pt, y: 3pt), + text(font: _sans, fill: white, weight: "bold", size: 0.80em, label), + ), + ) + v(5pt) +} + +/// Display equation with the same vertical rhythm as visual boxes. +#let fs-equation(body) = block( + width: 100%, + above: _visual-y-margin, + below: _visual-y-margin, + align(center, body), +) + +/// Proof environment with QED box. +#let proof(width: 100%, body) = { + block( + width: width, + inset: (x: 6pt, y: 5pt), + above: _visual-y-margin, + below: _visual-y-margin, + stroke: (left: luma(80) + 2pt), + spacing: 0.4em, + align(left, text(style: "italic", [_Proof._ ]) + body + h(1fr) + box(width: 6pt, height: 6pt, fill: luma(80))), + ) +} + +// ============================================================ +// Code and pseudo-code boxes +// ============================================================ + +/// Generic code-style box with theorem-like header. +/// #code-box("print('hi')", type: "Code", title: "Python", lang: "python") +#let code-box( + code, + type: "Code", + title: none, + lang: none, + color: blue.darken(50%), + fill: luma(245), + text-size: 8pt, +) = { + _panel-box( + type, + title, + color, + block( + width: 100%, + inset: 0pt, + spacing: 0pt, + move( + dy: -1.6pt, + text( + font: _mono, + size: text-size, + if lang == none { raw(code) } else { raw(lang: lang, code) }, + ), + ), + ), + body-fill: fill, + ) +} + +/// Pseudo-code box with theorem-like header. +/// #pseudo-code("for i <- 1 to n", title: "Mini-Batch SGD") +#let pseudo-code( + code, + type: "Pseudo-code", + title: none, + color: teal.darken(30%), + fill: luma(252), + text-size: 8pt, +) = { + code-box( + code, + type: type, + title: title, + color: color, + fill: fill, + text-size: text-size, + ) +} + +// ============================================================ +// Section management +// ============================================================ + +/// Register a subsection in the TOC (does not create a slide). +#let new-subsection(title) = { + _toc-data.update(t => t + ((kind: "sub", title: title),)) +} + +// ============================================================ +// Slide functions +// ============================================================ + +/// Standard content slide. +/// #slide(title: "My Slide")[Content] +#let slide(title: none, section: auto, show-title-band: auto, body) = { + context { + let tc = _fs-tc.get() + let sc = _fs-sc.get() + let bg = _fs-bg.get() + let w = _fs-w.get() + let h = _fs-h.get() + let ft = _fs-ft.get() + let auth = _fs-auth.get() + let inst = _fs-inst.get() + let hfc1 = _fs-hfc1.get() + let hfc2 = _fs-hfc2.get() + let hfc1h = _fs-hfc1h.get() + let cur = _cur-sec.get() + let sec = if section == auto { cur } else { section } + let header-title = if show-title-band == auto { + if sec == none or sec == "" { none } else { title } + } else if show-title-band and title != none { + title + } else { + none + } + + let header-h = if header-title == none { 24.75pt } else { 50.25pt } + let footer-h = 29.61pt + let avail = h - header-h - footer-h + let slide-loc = here() + _register-section-target(sec, slide-loc) + _register-page(section: sec) + + page( + width: w, + height: h, + margin: 0pt, + background: rect(width: 100%, height: 100%, fill: bg), + header: none, + footer: none, + { + place( + top + left, + float: false, + _header-band( + w, + if sec == none { "" } else { sec }, + slide-title: header-title, + tc, + sc, + header-font-color-1: hfc1, + header-font-color-2: hfc2, + header-font-color-1-highlight: hfc1h, + ), + ) + place( + bottom + left, + float: false, + _footer-band( + w, + auth, + inst, + ft, + counter(page).display() + " / " + str(counter(page).final().first()), + tc, + sc, + header-font-color-1: hfc1, + header-font-color-2: hfc2, + header-font-color-1-highlight: hfc1h, + ), + ) + place( + top + left, + dy: header-h, + float: false, + _slide-body-area(w, avail, body), + ) + }, + ) + } +} + +/// Section divider slide (typically called right after new-section). +#let section-slide(sec-title) = { + context { + let tc = _fs-tc.get() + let sc = _fs-sc.get() + let bg = _fs-bg.get() + let w = _fs-w.get() + let h = _fs-h.get() + let ft = _fs-ft.get() + let auth = _fs-auth.get() + let inst = _fs-inst.get() + let hfc1 = _fs-hfc1.get() + let hfc2 = _fs-hfc2.get() + let hfc1h = _fs-hfc1h.get() + let nav-sec = { + let cur = _cur-sec.get() + if cur == none or cur == "" { sec-title } else { cur } + } + let sec-loc = here() + _register-page(section: nav-sec, intro: true) + _register-section-target(nav-sec, sec-loc) + + page( + width: w, + height: h, + margin: 0pt, + background: rect(width: 100%, height: 100%, fill: bg), + header: none, + footer: none, + { + place( + top + left, + float: false, + _header-band( + w, + nav-sec, + tc, + sc, + header-font-color-1: hfc1, + header-font-color-2: hfc2, + header-font-color-1-highlight: hfc1h, + ), + ) + place( + bottom + left, + float: false, + _footer-band( + w, + auth, + inst, + ft, + counter(page).display() + " / " + str(counter(page).final().first()), + tc, + sc, + header-font-color-1: hfc1, + header-font-color-2: hfc2, + header-font-color-1-highlight: hfc1h, + ), + ) + block( + width: w, + height: h - 24.75pt - 29.61pt, + align(center + horizon, block( + width: 65%, + stroke: tc + 0.8pt, + inset: 0pt, + { + block( + width: 100%, + fill: tc, + inset: (x: 12pt, y: 4pt), + context { + let n = _sec-ctr.get().first() + text(font: _sans, fill: hfc1h, size: 8pt, [Section #n]) + }, + ) + block( + width: 100%, + height: 38pt, + inset: (x: 12pt, y: 0pt), + align(horizon, move(dy: -3pt, text(font: _sans, size: 15pt, weight: "bold", fill: tc, sec-title))), + ) + }, + )), + ) + }, + ) + } +} + +/// Declare a new section and render its intro slide. +#let new-section(title, slide-title: auto) = { + _sec-ctr.step() + for (_, c) in _thm-ctrs { c.update(0) } + _fig-ctr.update(0) + _cur-sec.update(title) + _toc-data.update(t => t + ((kind: "section", title: title),)) + section-slide(if slide-title == auto { title } else { slide-title }) +} + +/// Final message slide. +#let final-slide = context { + let tc = _fs-tc.get() + let sc = _fs-sc.get() + let bg = _fs-bg.get() + let w = _fs-w.get() + let h = _fs-h.get() + let ft = _fs-ft.get() + let auth = _fs-auth.get() + let inst = _fs-inst.get() + let hfc1 = _fs-hfc1.get() + let hfc2 = _fs-hfc2.get() + let hfc1h = _fs-hfc1h.get() + let final-message = _fs-final.get() + _register-page() + + page( + width: w, + height: h, + margin: 0pt, + background: rect(width: 100%, height: 100%, fill: bg), + header: none, + footer: none, + { + place( + top + left, + float: false, + _header-band( + w, + "", + tc, + sc, + header-font-color-1: hfc1, + header-font-color-2: hfc2, + header-font-color-1-highlight: hfc1h, + ), + ) + place( + bottom + left, + float: false, + _footer-band( + w, + auth, + inst, + ft, + counter(page).display() + " / " + str(counter(page).final().first()), + tc, + sc, + header-font-color-1: hfc1, + header-font-color-2: hfc2, + header-font-color-1-highlight: hfc1h, + ), + ) + block( + width: w, + height: h - 24.75pt - 29.61pt, + align(center + horizon, text(font: _serif, size: 24pt, style: "italic", fill: tc, final-message)), + ) + }, + ) +} + +// ============================================================ +// Main entry point -- use via: +// #show: black-angular-frame.with(config: (title: "...", ...)) +// ============================================================ +#let black-angular-frame( + config: (:), + title: none, + subtitle: none, + institution: none, + institute: none, + date: none, + authors: none, + final-message: none, + primary-color: none, + secondary-color: none, + background-color: none, + font-color: none, + header-font-color-1: none, + header-font-color-2: none, + header-font-color-1-highlight: none, + content-center: none, + content-upper-padding: none, + content-lower-padding: none, + logos: none, + ratio: 16 / 9, + title-color: none, + bg-color: none, + toc: none, + footer-title: auto, + footer-subtitle: auto, + logo: none, + cover-images: none, + cover-image-height: 45pt, + body, +) = { + let cfg-title = config.at("title", default: if title == none { "" } else { title }) + let cfg-subtitle = config.at("subtitle", default: if subtitle == none { "" } else { subtitle }) + let cfg-authors = config.at("authors", default: if authors == none { "" } else { authors }) + let cfg-institution = config.at( + "institution", + default: if institution != none { institution } else if institute != none { institute } else { "" }, + ) + let cfg-date = config.at("date", default: if date == none { "" } else { date }) + let cfg-final-message = config.at( + "final-message", + default: if final-message == none { "" } else { final-message }, + ) + let cfg-primary = config.at( + "primary-color", + default: if primary-color != none { primary-color } else if title-color != none { title-color } else { + rgb("#1C1C1C") + }, + ) + let cfg-secondary = config.at( + "secondary-color", + default: if secondary-color == none { rgb("#D9D9D9") } else { secondary-color }, + ) + let cfg-background = config.at( + "background-color", + default: if background-color != none { background-color } else if bg-color != none { bg-color } else { + rgb("#FFFFFF") + }, + ) + let cfg-font-color = config.at( + "font-color", + default: if font-color == none { luma(20) } else { font-color }, + ) + let cfg-hfc1 = config.at( + "header-font-color-1", + default: if header-font-color-1 == none { _muted-nav(cfg-primary) } else { header-font-color-1 }, + ) + let cfg-hfc2 = config.at( + "header-font-color-2", + default: if header-font-color-2 == none { cfg-primary } else { header-font-color-2 }, + ) + let cfg-hfc1h = config.at( + "header-font-color-1-highlight", + default: if header-font-color-1-highlight == none { white } else { header-font-color-1-highlight }, + ) + let cfg-content-center = config.at( + "content-center", + default: if content-center == none { 0.3 } else { content-center }, + ) + let cfg-content-upper-padding = config.at( + "content-upper-padding", + default: if content-upper-padding == none { 0.05 } else { content-upper-padding }, + ) + let cfg-content-lower-padding = config.at( + "content-lower-padding", + default: if content-lower-padding == none { 0.05 } else { content-lower-padding }, + ) + let cfg-logos = config.at( + "logos", + default: if logos != none { logos } else if cover-images != none { cover-images } else if logo != none { + (logo,) + } else { () }, + ) + let cfg-toc = config.at("TOC", default: if toc == none { true } else { toc }) + + let ft = if footer-title == auto { cfg-title } else { footer-title } + let fst = if footer-subtitle == auto { cfg-subtitle } else { footer-subtitle } + let inst = if cfg-institution == "" { none } else { cfg-institution } + let subt = if cfg-subtitle == "" { none } else { cfg-subtitle } + let date-text = if cfg-date == "" { none } else { cfg-date } + let cover-imgs = if cfg-logos == none { + () + } else if type(cfg-logos) == array { + cfg-logos + } else { + (cfg-logos,) + } + let auth = if cfg-authors != "" and cfg-authors != none { + let al = if type(cfg-authors) == array { cfg-authors } else { (cfg-authors,) } + al.join([, ]) + } else { none } + + let w = if ratio > (16 / 9 - 0.01) { 254mm } else { 190mm } + let h = w / ratio + + // Push config into state so slide() can read it contextually + _fs-tc.update(cfg-primary) + _fs-sc.update(cfg-secondary) + _fs-bg.update(cfg-background) + _fs-w.update(w) + _fs-h.update(h) + _fs-ft.update(ft) + _fs-fst.update(fst) + _fs-title.update(cfg-title) + _fs-subt.update(cfg-subtitle) + _fs-inst.update(inst) + _fs-auth.update(auth) + _fs-fc.update(cfg-font-color) + _fs-hfc1.update(cfg-hfc1) + _fs-hfc2.update(cfg-hfc2) + _fs-hfc1h.update(cfg-hfc1h) + _fs-final.update(cfg-final-message) + _fs-cctr.update(cfg-content-center) + _fs-cupad.update(cfg-content-upper-padding) + _fs-clpad.update(cfg-content-lower-padding) + + // Global text defaults + set text(font: _serif, size: 10.5pt, fill: cfg-font-color) + set figure(gap: _caption-gap) + show figure.where(kind: table): it => block( + above: _visual-y-margin, + below: _table-after-caption-margin, + { + set figure(gap: _table-caption-gap) + it + }, + ) + show figure.caption.where(kind: table): it => { + v(_table-caption-gap) + align(center, text(size: 0.72em, style: "italic", [*#it.supplement #it.counter.display().* #it.body])) + } + show table.cell: set text(font: _sans) + show table: it => figure(kind: table, caption: [Table caption.], gap: _table-caption-gap, it) + set par(justify: true, leading: 0.55em, spacing: 0.65em) + set list(indent: 6pt) + set enum(indent: 6pt) + set page(width: w, height: h, margin: 0pt, header: none, footer: none) + + // ---- Title slide ----------------------------------------- + page( + width: w, + height: h, + margin: 0pt, + background: rect(width: 100%, height: 100%, fill: cfg-background), + header: none, + footer: none, + { + _register-page() + place( + top + left, + float: false, + _header-band( + w, + "", + cfg-primary, + cfg-secondary, + header-font-color-1: cfg-hfc1, + header-font-color-2: cfg-hfc2, + header-font-color-1-highlight: cfg-hfc1h, + ), + ) + place( + bottom + left, + float: false, + _footer-band( + w, + auth, + inst, + ft, + context counter(page).display() + " / " + str(counter(page).final().first()), + cfg-primary, + cfg-secondary, + header-font-color-1: cfg-hfc1, + header-font-color-2: cfg-hfc2, + header-font-color-1-highlight: cfg-hfc1h, + ), + ) + block( + width: w, + height: h - 24.75pt - 29.61pt, + align(center + horizon, { + block( + width: 84%, + fill: cfg-primary, + inset: (x: 14pt, y: 10pt), + align(center + horizon, { + set text(font: _sans, fill: cfg-hfc1h) + text(size: 19pt, cfg-title) + if subt != none { + linebreak() + v(4pt) + text(size: 11pt, subt) + } + }), + ) + v(12pt) + if auth != none { + text(font: _sans, size: 11pt, auth) + linebreak() + v(6pt) + } + if inst != none { + text(font: _sans, size: 9.5pt, inst) + linebreak() + v(6pt) + } + if date-text != none { + text(font: _sans, size: 10pt, date-text) + linebreak() + } + v(26pt) + if cover-imgs != () { + _cover-image-row(cover-imgs, height: cover-image-height) + } + }), + ) + }, + ) + + // ---- TOC slide ------------------------------------------- + if cfg-toc { + slide(title: "Table of Contents", section: none, show-title-band: false, { + context { + let entries = _toc-data.final() + let sec-targets = _fs-sec-targets.final() + let sec-n = 0 + text(font: _sans, size: 14pt, weight: "bold", fill: cfg-primary, [Table of Contents]) + v(8pt) + for e in entries { + if e.kind == "section" { + sec-n += 1 + let target = sec-targets.rev().find(entry => entry.title == e.title) + let number-cell = box( + width: 16pt, + height: 16pt, + fill: cfg-primary, + align(center + horizon, text(font: _sans, fill: cfg-hfc1h, size: 8pt, weight: "bold", str(sec-n))), + ) + let title-cell = align(left + horizon, text( + font: _sans, + size: 10pt, + weight: "bold", + fill: cfg-primary, + e.title, + )) + v(3pt) + grid( + columns: (18pt, 1fr), + column-gutter: 4pt, + if target != none { link(target.at("loc"))[#number-cell] } else { number-cell }, + if target != none { link(target.at("loc"))[#title-cell] } else { title-cell }, + ) + } else { + pad(left: 22pt, text(font: _sans, size: 9pt, fill: luma(40), "- " + e.title)) + } + } + } + }) + } + + // ---- User content ---------------------------------------- + body +} diff --git a/packages/preview/black-angular-frame/0.1.0/template/assets/curves.csv b/packages/preview/black-angular-frame/0.1.0/template/assets/curves.csv new file mode 100644 index 0000000000..00d583ed4a --- /dev/null +++ b/packages/preview/black-angular-frame/0.1.0/template/assets/curves.csv @@ -0,0 +1,12 @@ +x,model_a,model_b,baseline +0.0,0.52,0.48,0.50 +0.5,0.57,0.51,0.50 +1.0,0.63,0.55,0.50 +1.5,0.70,0.60,0.50 +2.0,0.76,0.65,0.50 +2.5,0.81,0.69,0.50 +3.0,0.85,0.73,0.50 +3.5,0.88,0.76,0.50 +4.0,0.90,0.79,0.50 +4.5,0.91,0.81,0.50 +5.0,0.92,0.83,0.50 diff --git a/packages/preview/black-angular-frame/0.1.0/template/assets/github-logo.png b/packages/preview/black-angular-frame/0.1.0/template/assets/github-logo.png new file mode 100644 index 0000000000..34966ac655 Binary files /dev/null and b/packages/preview/black-angular-frame/0.1.0/template/assets/github-logo.png differ diff --git a/packages/preview/black-angular-frame/0.1.0/template/assets/typst-logo.png b/packages/preview/black-angular-frame/0.1.0/template/assets/typst-logo.png new file mode 100644 index 0000000000..773232fdf6 Binary files /dev/null and b/packages/preview/black-angular-frame/0.1.0/template/assets/typst-logo.png differ diff --git a/packages/preview/black-angular-frame/0.1.0/template/main.typ b/packages/preview/black-angular-frame/0.1.0/template/main.typ new file mode 100644 index 0000000000..1b30d5bc01 --- /dev/null +++ b/packages/preview/black-angular-frame/0.1.0/template/main.typ @@ -0,0 +1,1796 @@ +// ============================================================ +// black-angular-frame -- Example / Demo Presentation +// This file demonstrates all features of the template. +// ============================================================ + +#import "@preview/black-angular-frame:0.1.0": * + +#let presentation-config = ( + title: "Black Angular Frame", + subtitle: "A Typst Template for Academic Presentations", + authors: "Author One, Author Two", + institution: "Institution Name", + date: "May 2026", + final-message: "Thank you for your attention", + primary-color: rgb("#1C1C1C"), + secondary-color: rgb("#D9D9D9"), + background-color: rgb("#FFFFFF"), + font-color: luma(20), + header-font-color-1: rgb("#999999"), + header-font-color-2: rgb("#1C1C1C"), + header-font-color-1-highlight: rgb("#FFFFFF"), + content-center: 0.3, + content-upper-padding: 0.05, + content-lower-padding: 0.05, + logos: ( + image("assets/typst-logo.png", height: 45pt), + image("assets/github-logo.png", height: 45pt), + ), + TOC: true, +) + +#show: black-angular-frame.with(config: presentation-config) + +// ============================================================ +// CONFIGURATION +// ============================================================ +#new-section("Configuration") + +#slide(title: "Template Configuration")[ + The template is configured from a single Typst dictionary. The table below lists the keys that can be passed through `config`, the expected value shape, and the defaults used when a key is omitted. + + #v(3pt) + + #let cell(body, fill: white, pos: left, weight: "regular") = fs-table-cell( + fill: fill, + stroke: luma(200) + 0.45pt, + pos: pos, + inset: (x: 3pt, y: 2pt), + )[ + #text(font: "IBM Plex Sans", size: 5.6pt, weight: weight, body) + ] + + #grid( + columns: (23%, 20%, 20%, 37%), + cell(fill: rgb("#1C1C1C"), weight: "bold")[#text(fill: white)[Name]], + cell(fill: rgb("#1C1C1C"), weight: "bold")[#text(fill: white)[Expected value]], + cell(fill: rgb("#1C1C1C"), weight: "bold")[#text(fill: white)[Default]], + cell(fill: rgb("#1C1C1C"), weight: "bold")[#text(fill: white)[Description]], + + cell[`title`], cell[String], cell[`""`], cell[Presentation title.], + cell[`subtitle`], cell[String], cell[`""`], cell[Presentation subtitle.], + cell[`authors`], cell[String], cell[`""`], cell[Author line shown on the cover and footer.], + cell[`institution`], cell[String], cell[`""`], cell[Institution shown on the cover and footer.], + cell[`date`], cell[String], cell[`""`], cell[Date shown on the cover.], + cell[`final-message`], cell[String], cell[`""`], cell[Message shown on the last slide.], + cell[`primary-color`], cell[Color], cell[`rgb("#1C1C1C")`], cell[Main bars, highlights, numbering, and accents.], + cell[`secondary-color`], cell[Color], cell[`rgb("#D9D9D9")`], cell[Secondary header and footer bands.], + cell[`background-color`], cell[Color], cell[`rgb("#FFFFFF")`], cell[Slide background color.], + cell[`font-color`], cell[Color], cell[`luma(20)`], cell[Default body text color.], + cell[`header-font-color-1`], + cell[Color], + cell[`_muted-nav(primary-color)`], + cell[Inactive text in the primary header band and text in the lower footer band.], + cell[`header-font-color-2`], + cell[Color], + cell[`primary-color`], + cell[Text in the secondary header and footer bands.], + cell[`header-font-color-1-highlight`], + cell[Color], + cell[`rgb("#FFFFFF")`], + cell[Active text in the primary header band.], + cell[`content-center`], + cell[Float 0-1], + cell[`0.3`], + cell[Vertical position used to center content; 0 starts at the top, 1 at the bottom.], + cell[`content-upper-padding`], + cell[Float 0-1], + cell[`0.05`], + cell[Top proportion of the available content area kept empty.], + cell[`content-lower-padding`], + cell[Float 0-1], + cell[`0.05`], + cell[Bottom proportion of the available content area kept empty.], + cell[`logos`], cell[`array[content]`], cell[`()`], cell[Logo images or custom content shown on the cover.], + cell[`TOC`], cell[Bool], cell[`true`], cell[Whether to add the table of contents slide with section links.], + ) + + #v(3pt) + These names are intentionally presentation-level settings, so changing the theme does not require editing the template internals. +] + +#slide(title: "Template Configuration Code")[ + The example presentation stores its theme and metadata in `presentation-config`, then passes that dictionary to the template with `black-angular-frame.with`. + + #v(3pt) + + #code-box( + "#import \"@preview/black-angular-frame:0.1.0\": * + +#let presentation-config = ( + title: \"Black Angular Frame\", + subtitle: \"A Typst Template for Academic Presentations\", + authors: \"Author One, Author Two\", + institution: \"Institution Name\", + date: \"May 2026\", + final-message: \"Thank you for your attention\", + primary-color: rgb(\"#1C1C1C\"), + secondary-color: rgb(\"#D9D9D9\"), + background-color: rgb(\"#FFFFFF\"), + font-color: luma(20), + header-font-color-1: rgb(\"#999999\"), + header-font-color-2: rgb(\"#1C1C1C\"), + header-font-color-1-highlight: rgb(\"#FFFFFF\"), + content-center: 0.3, + content-upper-padding: 0.05, + content-lower-padding: 0.05, + logos: (\n image(\"assets/typst-logo.png\", height: 45pt),\n image(\"assets/github-logo.png\", height: 45pt),\n ), + TOC: true, +) + +#show: black-angular-frame.with(config: presentation-config)", + type: "Typst", + title: "Import and configure the template", + lang: "typst", + color: luma(90), + fill: luma(248), + text-size: 5.6pt, + ) + + #v(3pt) + The rest of the document can focus on sections and slides while the template reads these values for the cover, navigation, footer, body text, logos, TOC, and final slide. +] + +// ============================================================ +// SECTION 1 -- TYPOGRAPHY +// ============================================================ +#new-section("Typography") + +#slide(title: "Default Fonts")[ + The template uses three IBM Plex families when available, with standard fallback fonts if they are not installed: + + #v(5pt) + #grid( + columns: (1fr, 1fr, 1fr), + column-gutter: 5pt, + block(stroke: blue.darken(50%) + 0.6pt, inset: 7pt, width: 100%, text(font: "IBM Plex Serif", size: 11.5pt)[ + *IBM Plex Serif* \ + body text \ + Regular, _italic_, \ + *bold*, _*bold-italic*_ + ]), + block(stroke: blue.darken(50%) + 0.6pt, inset: 7pt, width: 100%, text(font: "IBM Plex Sans", size: 11.5pt)[ + *IBM Plex Sans* \ + titles & headings \ + Regular, _italic_, \ + *bold*, _*bold-italic*_ + ]), + block(stroke: blue.darken(50%) + 0.6pt, inset: 7pt, width: 100%, text(font: "IBM Plex Mono", size: 11.5pt)[ + *IBM Plex Mono* \ + code & verbatim \ + Regular, _italic_, \ + *bold* + ]), + ) + #v(6pt) + Inline styling: #text(weight: "bold")[bold], #text(style: "italic")[italic], #text(fill: blue.darken(50%))[colored], #underline[underlined], #text(size: 13pt)[large (13 pt)], #text(size: 7.5pt)[small (7.5 pt)]. +] + +#slide(title: "Text Sizing and Semantic Emphasis")[ + #v(2pt) + #text(size: 18pt, weight: "bold", fill: blue.darken(50%))[Headline -- 18 pt bold] \ + #text(size: 14pt, weight: "bold")[Subheading -- 14 pt bold] \ + #text(size: 12pt)[Section heading -- 12 pt regular] \ + #text(size: 10pt)[Body text -- 10 pt (default)] \ + #text(size: 8pt, fill: luma(60))[Caption / footnote -- 8 pt muted] + + #v(8pt) + Semantic use: #text(style: "italic")[italics for emphasis], #text(weight: "bold")[bold for key terms], #text(fill: red.darken(20%))[red for warnings], #text(fill: green.darken(25%))[green for results]. Combine for #text(style: "italic", weight: "bold", fill: blue.darken(50%))[critical highlighted points]. + + #v(4pt) + Change the global accent via `primary-color`, the secondary bands via `secondary-color`, and the page fill via `background-color`. These parameters propagate through the navigation bar, footer, section dividers, TOC numbering squares, and theorem environments. +] + +// ============================================================ +// SECTION 2 -- LISTS AND ENUMERATIONS +// ============================================================ +#new-section("Lists & Enumerations") + +#slide(title: "Bullet Points and Nested Lists")[ + #two-col( + [ + *Unordered list (three levels):* + - First top-level item + - Nested child A + - Nested child B + - Deeply nested + - Another deep item + - Second top-level item + - Another child + - Third top-level item + ], + [ + *Ordered enumeration (three levels):* + + Step one: initialise + + Sub-step 1a + + Sub-step 1b + + Step two: process data + + Sub-step 2a + + Detail 2a-i + + Step three: evaluate output + + Step four: report results + ], + ) + + #v(5pt) + Typst automatically styles bullet symbols and numerals at each nesting depth. Ordered and unordered lists can be freely mixed at any level. +] + +// ============================================================ +// SECTION 3 -- FIGURES +// ============================================================ +#new-section("Figures") + +#slide(title: "Inserting and Referencing Figures")[ + #two-col( + [ + Figures use `#fs-figure(caption: [...])`. The counter resets per section; reference figures by their auto-assigned number. Longer surrounding paragraphs make it easier to inspect the vertical rhythm before and after visual material. + + #v(5pt) + As shown in *Figure 1*, a colored rectangle acts as a placeholder. In practice pass `image("diagram.svg")` or any Typst content as the figure body. This text intentionally spans multiple lines so the figure margins can be judged against realistic prose. + + #v(4pt) + *Figure 2* illustrates that captions appear italic below the figure, numbered automatically within the current section. The paragraph below the visual block should feel close enough to belong to the same slide, but not so close that the caption looks cramped. + ], + [ + The first placeholder represents a diagram or image inserted into the slide flow. A few lines of prose above it help show how the template separates ordinary text from framed visual content. + + #fs-figure(caption: [Placeholder -- replace with `image("diagram.svg")`.])[ + #rect( + width: 100%, + height: 62pt, + fill: blue.darken(50%).lighten(88%), + stroke: blue.darken(50%) + 0.8pt, + align(center + horizon, text(fill: blue.darken(50%), size: 9pt)[Diagram / Image here]), + ) + ] + #fs-figure(caption: [Second figure -- captions are italic, automatically numbered.])[ + #rect( + width: 100%, + height: 38pt, + fill: luma(240), + stroke: luma(190) + 0.6pt, + align(center + horizon, text(fill: luma(80), size: 9pt)[Another placeholder]), + ) + ] + + After the second figure, this short paragraph checks the lower margin beneath a caption. It should read as a continuation of the slide narrative rather than as text accidentally attached to the figure. + ], + ) +] + +#slide(title: "Full-Width and Fractional-Width Figures (With Captions)")[ + #two-col( + [ + A figure can expand to the full width of its column when the content should dominate the layout. This is useful for diagrams, screenshots, or images that need as much horizontal room as possible. + + #fs-figure(caption: [Full-width placeholder figure spanning the whole column.])[ + #rect( + width: 100%, + height: 60pt, + fill: blue.darken(50%).lighten(88%), + stroke: blue.darken(50%) + 0.8pt, + align(center + horizon, text(fill: blue.darken(50%), size: 9pt)[Full-width figure]), + ) + ] + + The caption should stay centered under the figure even when the visual takes the entire available width of the column. + ], + [ + Smaller visuals often read better when they keep some white space around them. A fractional-width figure makes that possible while still preserving the same numbering and caption behavior. + + #fs-figure(caption: [Fractional-width placeholder figure centered inside the column.])[ + #align(center, rect( + width: 68%, + height: 60pt, + fill: luma(240), + stroke: luma(190) + 0.6pt, + align(center + horizon, text(fill: luma(80), size: 9pt)[Fractional-width figure]), + )) + ] + + This example checks that a narrower figure still aligns cleanly in the column and that the caption feels attached to the centered image rather than to the whole column width. + ], + ) +] + +#slide(title: "Full-Width and Fractional-Width Figures (No Captions)")[ + #two-col( + [ + The same pair can also be shown without captions when the slide is purely illustrative and the surrounding prose already provides enough context for the audience. + + #fs-visual[ + #rect( + width: 100%, + height: 60pt, + fill: blue.darken(50%).lighten(88%), + stroke: blue.darken(50%) + 0.8pt, + align(center + horizon, text(fill: blue.darken(50%), size: 9pt)[Full-width figure]), + ) + ] + + Without a caption, the lower margin should still separate the figure from the next paragraph and keep the slide from feeling cramped. + ], + [ + The fractional-width version below uses the same visual content but keeps the narrower footprint. This lets us compare centered image placement with and without the caption layer. + + #fs-visual[ + #rect( + width: 68%, + height: 60pt, + fill: luma(240), + stroke: luma(190) + 0.6pt, + align(center + horizon, text(fill: luma(80), size: 9pt)[Fractional-width figure]), + ) + ] + + The surrounding text remains multi-line on purpose so we can judge the spacing around a centered narrow figure in the same way as we do for captioned figures. + ], + ) +] + +// ============================================================ +// SECTION 4 -- TWO-COLUMN LAYOUT +// ============================================================ +#new-section("Layouts", slide-title: "Two-Column Layouts") + +#slide(title: "Two-Column Layout")[ + #two-col( + left-width: 50%, + [ + == Left column + + The `#two-col(left, right)` helper builds a two-column `grid`. Parameters: + - `left-width` -- fraction of slide width (default 48%) + - `gutter` -- gap between columns (default 4%) + + #v(4pt) + Works well for: text + figure, code + output, comparative tables, side-by-side theorem boxes. + ], + [ + == Right column + + Any Typst content fits inside a column, including nested `two-col` calls, theorem boxes, figures, and tables. + + #fs-visual[ + #rect( + width: 100%, + height: 58pt, + fill: luma(245), + stroke: luma(200) + 0.6pt, + align(center + horizon, text(size: 9pt, fill: luma(60))[Arbitrary block inside a column]), + ) + ] + ], + ) +] + +// ============================================================ +// SECTION 5 -- CODE BLOCKS +// ============================================================ +#new-section("Code Blocks", slide-title: "Source Code & Pseudo-code") + +#slide(title: "Source Code and Pseudo-code Side by Side")[ + #two-col( + [ + #code-box( + "def softmax(x): + e = np.exp(x - x.max(axis=-1, keepdims=True)) + return e / e.sum(axis=-1, keepdims=True) + +def cross_entropy(logits, labels): + probs = softmax(logits) + n = labels.shape[0] + log_p = np.log(probs[range(n), labels]) + return -log_p.mean()", + type: "Source Code", + title: "Python", + lang: "python", + color: luma(110), + fill: luma(245), + ) + Uses `IBM Plex Mono` on a light grey background. Pass a language name to `#code-box(..., lang: "...")` for syntax highlighting. + ], + [ + #pseudo-code( + "Algorithm: Mini-Batch SGD +Input: loss L, data D, lr eta, T, B +------------------------------------- +for t = 1 to T do + B_t <- sample B examples from D + g <- grad_theta L(theta; B_t) + theta <- theta - eta * g +end for +return theta", + title: "Mini-Batch SGD", + ) + Pseudo-code now uses the same framed box language as the theorem environments, with a `type` label and `title`. + ], + ) +] + +// ============================================================ +// SECTION 6 -- TABLES +// ============================================================ +#new-section("Tables") + +#slide(title: "Paper-style and Grid-style Tables (No Captions)")[ + #two-col( + [ + *Paper style* (booktabs-like -- horizontal rules only) + + This table is introduced by a short paragraph rather than a single label. The extra prose makes the spacing above the table visible in a realistic slide, where a table usually follows a sentence or two of setup. + + #let paper-cell(body, pos: left, header: false, model-col: false, first-data: false) = grid.cell( + stroke: ( + bottom: if header { 0.6pt } else { none }, + right: if model-col { 0.6pt } else { none }, + ), + inset: ( + left: 5pt, + right: 5pt, + top: if first-data { 6pt } else { 4pt }, + bottom: if header { 7pt } else { 4pt }, + ), + align: pos, + text(font: "IBM Plex Serif", body), + ) + #fs-visual[ + #block(width: 100%, { + line(length: 100%, stroke: 0.9pt) + grid( + columns: (28%, 24%, 24%, 24%), + paper-cell(header: true, model-col: true)[*Method*], + paper-cell(header: true, pos: center)[*Acc. (%)*], + paper-cell(header: true, pos: center)[*F#sub[1]*], + paper-cell(header: true, pos: center)[*AUC*], + + paper-cell(model-col: true, first-data: true)[Baseline], + paper-cell(pos: center, first-data: true)[72.3], + paper-cell(pos: center, first-data: true)[0.701], + paper-cell(pos: center, first-data: true)[0.743], + + paper-cell(model-col: true)[Model A], + paper-cell(pos: center)[81.5], + paper-cell(pos: center)[0.803], + paper-cell(pos: center)[0.851], + + paper-cell(model-col: true)[Model B], + paper-cell(pos: center)[*88.9*], + paper-cell(pos: center)[*0.876*], + paper-cell(pos: center)[*0.903*], + ) + line(length: 100%, stroke: 0.9pt) + }) + ] + Classic academic style: only top and bottom rules, no vertical lines. The text after the table deliberately runs for a couple of lines so the lower margin can be compared with the upper margin. + ], + [ + *Grid style* (full borders, colored header, alternating rows) + + The grid version is meant for dense numeric summaries or dashboard-like reporting. A longer lead-in makes it easier to see whether the table feels attached to the explanation or floats too far away. + + #let grid-cell(body, fill: white, stroke: luma(200) + 0.6pt, pos: center) = fs-table-cell( + fill: fill, + stroke: stroke, + pos: pos, + body, + ) + #fs-visual[ + #block(width: 100%, grid( + columns: (28%, 24%, 24%, 24%), + grid-cell(fill: blue.darken(50%), pos: left)[#text(fill: white)[Method]], + grid-cell(fill: blue.darken(50%))[#text(fill: white)[Acc. (%)]], + grid-cell(fill: blue.darken(50%))[#text(fill: white)[F#sub[1]]], + grid-cell(fill: blue.darken(50%))[#text(fill: white)[AUC]], + + grid-cell(fill: luma(248), pos: left)[Baseline], + grid-cell(fill: luma(248))[72.3], + grid-cell(fill: luma(248))[0.701], + grid-cell(fill: luma(248))[0.743], + + grid-cell(fill: white, pos: left)[Model A], + grid-cell(fill: white)[81.5], + grid-cell(fill: white)[0.803], + grid-cell(fill: white)[0.851], + + grid-cell(fill: luma(248), pos: left)[Model B], + grid-cell(fill: luma(248))[*88.9*], + grid-cell(fill: luma(248))[*0.876*], + grid-cell(fill: luma(248))[*0.903*], + )) + ] + Dashboard style: solid grid, alternating row shading. This closing note also spans multiple lines, which helps reveal whether the table block leaves enough room before normal prose resumes. + ], + ) +] + +#slide(title: "Paper-style and Grid-style Tables (With Captions)")[ + #two-col( + [ + *Paper style* (booktabs-like -- horizontal rules only) + + Captions are useful when the table needs to be referenced later in the talk or connected to a source. This paragraph gives the captioned table enough surrounding prose to test both the top margin and the caption spacing. + + #let paper-cell(body, pos: left, header: false, model-col: false, first-data: false) = grid.cell( + stroke: ( + bottom: if header { 0.6pt } else { none }, + right: if model-col { 0.6pt } else { none }, + ), + inset: ( + left: 5pt, + right: 5pt, + top: if first-data { 6pt } else { 4pt }, + bottom: if header { 7pt } else { 4pt }, + ), + align: pos, + text(font: "IBM Plex Serif", body), + ) + #figure( + kind: table, + caption: [Placeholder caption for the paper-style table.], + { + block(width: 100%, { + line(length: 100%, stroke: 0.9pt) + grid( + columns: (28%, 24%, 24%, 24%), + paper-cell(header: true, model-col: true)[*Method*], + paper-cell(header: true, pos: center)[*Acc. (%)*], + paper-cell(header: true, pos: center)[*F#sub[1]*], + paper-cell(header: true, pos: center)[*AUC*], + + paper-cell(model-col: true, first-data: true)[Baseline], + paper-cell(pos: center, first-data: true)[72.3], + paper-cell(pos: center, first-data: true)[0.701], + paper-cell(pos: center, first-data: true)[0.743], + + paper-cell(model-col: true)[Model A], + paper-cell(pos: center)[81.5], + paper-cell(pos: center)[0.803], + paper-cell(pos: center)[0.851], + + paper-cell(model-col: true)[Model B], + paper-cell(pos: center)[*88.9*], + paper-cell(pos: center)[*0.876*], + paper-cell(pos: center)[*0.903*], + ) + line(length: 100%, stroke: 0.9pt) + }) + }, + ) + Classic academic style: only top and bottom rules, no vertical lines. With a caption present, the paragraph after the table should sit beneath the full table block rather than feeling glued to the caption. + ], + [ + *Grid style* (full borders, colored header, alternating rows) + + The captioned grid table shows how a more operational table behaves inside the same layout. The text before it is intentionally longer so vertical spacing is visible without relying on empty slide area. + + #let grid-cell(body, fill: white, stroke: luma(200) + 0.6pt, pos: center) = fs-table-cell( + fill: fill, + stroke: stroke, + pos: pos, + body, + ) + #figure( + kind: table, + caption: [Placeholder caption for the grid-style table.], + { + block(width: 100%, grid( + columns: (28%, 24%, 24%, 24%), + grid-cell(fill: blue.darken(50%), pos: left)[#text(fill: white)[Method]], + grid-cell(fill: blue.darken(50%))[#text(fill: white)[Acc. (%)]], + grid-cell(fill: blue.darken(50%))[#text(fill: white)[F#sub[1]]], + grid-cell(fill: blue.darken(50%))[#text(fill: white)[AUC]], + + grid-cell(fill: luma(248), pos: left)[Baseline], + grid-cell(fill: luma(248))[72.3], + grid-cell(fill: luma(248))[0.701], + grid-cell(fill: luma(248))[0.743], + + grid-cell(fill: white, pos: left)[Model A], + grid-cell(fill: white)[81.5], + grid-cell(fill: white)[0.803], + grid-cell(fill: white)[0.851], + + grid-cell(fill: luma(248), pos: left)[Model B], + grid-cell(fill: luma(248))[*88.9*], + grid-cell(fill: luma(248))[*0.876*], + grid-cell(fill: luma(248))[*0.903*], + )) + }, + ) + Dashboard style: solid grid, alternating row shading. This final description should have comfortable breathing room after the caption while still reading as part of the same explanatory unit. + ], + ) +] + +// ============================================================ +// SECTION 7 -- DIAGRAMS AND CHARTS (three slides) +// ============================================================ +#new-section("Diagrams & Charts") + +// ---- Diagram drawing helpers ------------------------------- +#let _arrow-head(x, y, dir: "r", color: luma(25)) = { + if dir == "r" { + place(top + left, dx: x - 6pt, dy: y - 3pt, polygon((0pt, 0pt), (6pt, 3pt), (0pt, 6pt), fill: color)) + } else if dir == "l" { + place(top + left, dx: x, dy: y - 3pt, polygon((0pt, 3pt), (6pt, 0pt), (6pt, 6pt), fill: color)) + } else if dir == "d" { + place(top + left, dx: x - 3pt, dy: y - 6pt, polygon((0pt, 0pt), (6pt, 0pt), (3pt, 6pt), fill: color)) + } else if dir == "u" { + place(top + left, dx: x - 3pt, dy: y, polygon((0pt, 6pt), (3pt, 0pt), (6pt, 6pt), fill: color)) + } else if dir == "dr" { + place(top + left, dx: x - 6pt, dy: y - 6pt, polygon((6pt, 6pt), (1pt, 4pt), (4pt, 1pt), fill: color)) + } else if dir == "dl" { + place(top + left, dx: x, dy: y - 6pt, polygon((0pt, 6pt), (5pt, 4pt), (2pt, 1pt), fill: color)) + } else if dir == "ur" { + place(top + left, dx: x - 6pt, dy: y, polygon((6pt, 0pt), (1pt, 2pt), (4pt, 5pt), fill: color)) + } else { + place(top + left, dx: x, dy: y, polygon((0pt, 0pt), (5pt, 2pt), (2pt, 5pt), fill: color)) + } +} + +#let _arr-r(x1, y, x2, color: luma(25), weight: 0.8pt, label: none, label-dy: -8pt) = { + place(top + left, line(start: (x1, y), end: (x2 - 5pt, y), stroke: color + weight)) + _arrow-head(x2, y, dir: "r", color: color) + if label != none { + place(top + left, dx: (x1 + x2) / 2 - 6pt, dy: y + label-dy, text(size: 6.4pt, fill: color, label)) + } +} + +#let _arr-l(x1, y, x2, color: luma(25), weight: 0.8pt, label: none, label-dy: -8pt) = { + place(top + left, line(start: (x1, y), end: (x2 + 5pt, y), stroke: color + weight)) + _arrow-head(x2, y, dir: "l", color: color) + if label != none { + place(top + left, dx: (x1 + x2) / 2 - 6pt, dy: y + label-dy, text(size: 6.4pt, fill: color, label)) + } +} + +#let _arr-v(x, y1, y2, color: luma(25), weight: 0.8pt, label: none, label-dx: 4pt) = { + if y2 > y1 { + place(top + left, line(start: (x, y1), end: (x, y2 - 5pt), stroke: color + weight)) + _arrow-head(x, y2, dir: "d", color: color) + } else { + place(top + left, line(start: (x, y1), end: (x, y2 + 5pt), stroke: color + weight)) + _arrow-head(x, y2, dir: "u", color: color) + } + if label != none { + place(top + left, dx: x + label-dx, dy: (y1 + y2) / 2 - 4pt, text(size: 6.4pt, fill: color, label)) + } +} + +#let _arr-diag(x1, y1, x2, y2, dir, color: luma(25), weight: 0.8pt, label: none, label-dx: 0pt, label-dy: 0pt) = { + place(top + left, line(start: (x1, y1), end: (x2, y2), stroke: color + weight)) + _arrow-head(x2, y2, dir: dir, color: color) + if label != none { + place(top + left, dx: (x1 + x2) / 2 + label-dx, dy: (y1 + y2) / 2 + label-dy, text(size: 6.4pt, fill: color, label)) + } +} + +#let _diagram-block(label, w: 52pt, h: 18pt, fill: luma(245), stroke: luma(25), text-size: 6.6pt, radius: 2pt) = box( + width: w, + height: h, + fill: fill, + stroke: stroke + 0.7pt, + radius: radius, + align(center + horizon, text(size: text-size, fill: luma(15), label)), +) + +#let transformer-diagram = align(center, { + let W = 278pt + let H = 232pt + let ink = luma(15) + let block-w = 54pt + let frame-w = 90pt + let frame-pad-x = (frame-w - block-w) / 2 + let enc-x = 30pt + let dec-x = 154pt + let label-size = 5.7pt + let small-size = 4.9pt + let arrow-weight = 0.9pt + let node-r = 3.5pt + let positional-center-y = 180pt + let input-sum-y = positional-center-y + let nx-y = 118pt + let decoder-bottom-label-width = 94pt + let pink = rgb("#F9DCDD") + let peach = rgb("#FFE3B8") + let bluefill = rgb("#C5E8F5") + let normfill = rgb("#F3F5C2") + let greenfill = rgb("#D8F0D9") + let violet = rgb("#E4E7F8") + + let encoder = ( + x: enc-x, + frame-y: 73pt, + frame-h: 100pt, + stack-x: enc-x + frame-pad-x, + nx-x: enc-x - 25pt, + nx-y: nx-y, + pos-side: "left", + pos-label-x: -15pt, + pos-circle-x: 49pt, + emb-label: [Input\ Embedding], + bottom-label: [Inputs], + bottom-label-x: enc-x + 8pt, + bottom-label-width: 74pt, + ) + let decoder = ( + x: dec-x, + frame-y: 41pt, + frame-h: 132pt, + stack-x: dec-x + frame-pad-x, + nx-x: dec-x + frame-w + 8pt, + nx-y: nx-y, + pos-side: "right", + pos-label-x: 229pt, + pos-circle-x: 224pt, + emb-label: [Output\ Embedding], + bottom-label: [Outputs (shifted right)], + bottom-label-x: dec-x + frame-pad-x + block-w / 2 - decoder-bottom-label-width / 2, + bottom-label-width: decoder-bottom-label-width, + ) + + let cx(col) = col.stack-x + block-w / 2 + let cy(layer) = layer.y + layer.h / 2 + + let diagram-text(x, y, body, size: label-size, width: auto, align-pos: center) = place( + top + left, + dx: x, + dy: y, + block(width: width, align(align-pos, text(size: size, fill: ink, body))), + ) + let diagram-text-centered-on-y(x, center-y, body, size: label-size, width: auto, align-pos: center) = context { + let label = block(width: width, align(align-pos, text(size: size, fill: ink, body))) + place(top + left, dx: x, dy: center-y - measure(label).height / 2, label) + } + let block-at(x, y, label, fill, h: 16pt, size: label-size) = place( + top + left, + dx: x, + dy: y, + _diagram-block(label, w: block-w, h: h, fill: fill, stroke: ink, text-size: size, radius: 2.2pt), + ) + let frame(col) = place( + top + left, + dx: col.x, + dy: col.frame-y, + block( + width: frame-w, + height: col.frame-h, + stroke: ink + 1.3pt, + radius: 7pt, + fill: luma(250), + ), + ) + let plus(x, y) = { + let r = node-r + let d = 2.45pt + place(top + left, dx: x - r, dy: y - r, circle(radius: r, stroke: ink + 0.85pt, fill: white)) + place(top + left, line(start: (x - d, y), end: (x + d, y), stroke: ink + 0.65pt)) + place(top + left, line(start: (x, y - d), end: (x, y + d), stroke: ink + 0.65pt)) + } + let pos-signal(x, y) = { + let r = node-r + place(top + left, dx: x - r, dy: y - r, circle(radius: r, stroke: ink + 0.9pt, fill: white)) + place(top + left, curve( + stroke: ink + 0.65pt, + fill: none, + curve.move((x - 2.2pt, y + 1.1pt)), + curve.cubic((x - 1.2pt, y + 1.1pt), (x - 1.1pt, y - 1.1pt), (x, y)), + curve.cubic((x + 1.1pt, y + 1.1pt), (x + 1.2pt, y - 1.1pt), (x + 2.2pt, y - 1.1pt)), + )) + } + let poly(points, weight: arrow-weight) = { + for i in range(points.len() - 1) { + let a = points.at(i) + let b = points.at(i + 1) + place(top + left, line(start: a, end: b, stroke: ink + weight)) + } + } + let arrow-head(x, y, dir: "r") = { + let len = 2.4pt + let half = 1.5pt + if dir == "r" { + place(top + left, polygon((x, y), (x - len, y - half), (x - len, y + half), fill: ink)) + } else if dir == "l" { + place(top + left, polygon((x, y), (x + len, y - half), (x + len, y + half), fill: ink)) + } else if dir == "d" { + place(top + left, polygon((x, y), (x - half, y - len), (x + half, y - len), fill: ink)) + } else { + place(top + left, polygon((x, y), (x - half, y + len), (x + half, y + len), fill: ink)) + } + } + let arrow-poly(points, dir: "r") = { + poly(points) + let end = points.at(points.len() - 1) + arrow-head(end.at(0), end.at(1), dir: dir) + } + let arr-v(x, y1, y2, weight: arrow-weight) = { + let len = 2.4pt + if y2 > y1 { + place(top + left, line(start: (x, y1), end: (x, y2 - len), stroke: ink + weight)) + arrow-head(x, y2, dir: "d") + } else { + place(top + left, line(start: (x, y1), end: (x, y2 + len), stroke: ink + weight)) + arrow-head(x, y2, dir: "u") + } + } + let attention-fork(layer, gap: 6pt) = { + let xs = (layer.x + 9pt, layer.x + block-w / 2, layer.x + block-w - 9pt) + let y = layer.y + layer.h + gap + place(top + left, line(start: (xs.first(), y), end: (xs.last(), y), stroke: ink + arrow-weight)) + for x in xs { + arr-v(x, y, layer.y + layer.h) + } + } + let branch-y(start, end, pct: 0.3) = start + pct * (end - start) + let residual(col, layer, norm, branch-from, branch-to, side: "left", branch-pct: 0.3) = { + let bus = if side == "left" { col.x + 6pt } else { col.x + frame-w - 6pt } + let by = branch-y(branch-from, branch-to, pct: branch-pct) + let from = (cx(col), by) + let mid = (bus, by) + let into = if side == "left" { + (norm.x, cy(norm)) + } else { + (norm.x + block-w, cy(norm)) + } + arrow-poly((from, mid, (bus, cy(norm)), into), dir: if side == "left" { "r" } else { "l" }) + } + let column-bottom(col) = { + let emb-y = 188pt + let emb-h = 14pt + let plus-y = positional-center-y + block-at(col.stack-x, emb-y, col.emb-label, pink, h: emb-h, size: 4.2pt) + plus(cx(col), plus-y) + arr-v(cx(col), emb-y + emb-h + 10pt, emb-y + emb-h) + arr-v(cx(col), emb-y, plus-y + 3.5pt) + diagram-text(col.bottom-label-x, 221pt, col.bottom-label, size: 7.2pt, width: col.bottom-label-width) + diagram-text-centered-on-y(col.pos-label-x, positional-center-y, [Positional Embedding], size: 5.0pt, width: 60pt) + pos-signal(col.pos-circle-x, plus-y) + if col.pos-side == "left" { + place(top + left, line( + start: (col.pos-circle-x + node-r, plus-y), + end: (cx(col) - node-r, plus-y), + stroke: ink + arrow-weight, + )) + } else { + place(top + left, line( + start: (cx(col) + node-r, plus-y), + end: (col.pos-circle-x - node-r, plus-y), + stroke: ink + arrow-weight, + )) + } + } + let sum-to-attention(col, layer, gap: 6pt) = place( + top + left, + line( + start: (cx(col), input-sum-y - 3.5pt), + end: (cx(col), layer.y + layer.h + gap), + stroke: ink + arrow-weight, + ), + ) + let lower-attn-shift = 3pt + + let enc-layers = ( + ( + key: "attn", + x: encoder.stack-x, + y: 138pt + lower-attn-shift, + h: 16pt, + label: [Multi-Head\ Attention], + fill: peach, + size: 4.5pt, + ), + ( + key: "norm1", + x: encoder.stack-x, + y: 123pt + lower-attn-shift, + h: 9pt, + label: [Add & Norm], + fill: normfill, + size: 4.8pt, + ), + (key: "ff", x: encoder.stack-x, y: 92pt, h: 15pt, label: [Feed\ Forward], fill: bluefill, size: 4.7pt), + (key: "norm2", x: encoder.stack-x, y: 77pt, h: 9pt, label: [Add & Norm], fill: normfill, size: 4.8pt), + ) + let dec-layers = ( + ( + key: "masked", + x: decoder.stack-x, + y: 139pt + lower-attn-shift, + h: 18pt, + label: [Masked\ Multi-Head\ Attention], + fill: peach, + size: 4.0pt, + ), + ( + key: "norm1", + x: decoder.stack-x, + y: 125pt + lower-attn-shift, + h: 9pt, + label: [Add & Norm], + fill: normfill, + size: 4.8pt, + ), + (key: "cross", x: decoder.stack-x, y: 98pt, h: 14pt, label: [Multi-Head\ Attention], fill: peach, size: 4.2pt), + (key: "norm2", x: decoder.stack-x, y: 84pt, h: 9pt, label: [Add & Norm], fill: normfill, size: 4.8pt), + (key: "ff", x: decoder.stack-x, y: 59pt, h: 14pt, label: [Feed\ Forward], fill: bluefill, size: 4.4pt), + (key: "norm3", x: decoder.stack-x, y: 45pt, h: 9pt, label: [Add & Norm], fill: normfill, size: 4.8pt), + (key: "linear", x: decoder.stack-x, y: 27pt, h: 9pt, label: [Linear], fill: violet, size: 4.9pt), + (key: "softmax", x: decoder.stack-x, y: 15.5pt, h: 9pt, label: [Softmax], fill: greenfill, size: 4.9pt), + ) + let enc(key) = enc-layers.find(layer => layer.key == key) + let dec(key) = dec-layers.find(layer => layer.key == key) + + box(width: W, height: H, { + frame(encoder) + frame(decoder) + diagram-text(encoder.nx-x, encoder.nx-y, [N×], size: 12pt) + diagram-text(decoder.nx-x, decoder.nx-y, [N×], size: 12pt) + diagram-text(decoder.stack-x - 7pt, 0pt, [Output Probabilities], size: 7.2pt, width: block-w + 14pt) + + column-bottom(encoder) + column-bottom(decoder) + + for layer in enc-layers { + block-at(layer.x, layer.y, layer.label, layer.fill, h: layer.h, size: layer.size) + } + for layer in dec-layers { + block-at(layer.x, layer.y, layer.label, layer.fill, h: layer.h, size: layer.size) + } + + attention-fork(enc("attn")) + attention-fork(dec("masked")) + sum-to-attention(encoder, enc("attn")) + sum-to-attention(decoder, dec("masked")) + + residual( + encoder, + enc("attn"), + enc("norm1"), + input-sum-y - 3.5pt, + enc("attn").y + enc("attn").h + 6pt, + side: "left", + branch-pct: 0.65, + ) + residual(encoder, enc("ff"), enc("norm2"), enc("norm1").y, enc("ff").y + enc("ff").h, side: "left", branch-pct: 0.5) + residual( + decoder, + dec("masked"), + dec("norm1"), + input-sum-y - 3.5pt, + dec("masked").y + dec("masked").h + 6pt, + side: "right", + branch-pct: 0.65, + ) + residual(decoder, dec("cross"), dec("norm2"), dec("norm1").y, dec("cross").y + dec("cross").h, side: "right") + residual( + decoder, + dec("ff"), + dec("norm3"), + dec("norm2").y, + dec("ff").y + dec("ff").h, + side: "right", + branch-pct: 0.5, + ) + + arr-v(cx(encoder), enc("attn").y, enc("norm1").y + enc("norm1").h) + arr-v(cx(encoder), enc("norm1").y, enc("ff").y + enc("ff").h) + arr-v(cx(encoder), enc("ff").y, enc("norm2").y + enc("norm2").h) + arr-v(cx(decoder), dec("masked").y, dec("norm1").y + dec("norm1").h) + let cross-input-xs = ( + dec("cross").x + 9pt, + dec("cross").x + block-w / 2, + dec("cross").x + block-w - 9pt, + ) + let cross-right-route-y = branch-y(dec("norm1").y, dec("cross").y + dec("cross").h, pct: 0.3) - 4pt + arrow-poly( + ( + (cx(decoder), dec("norm1").y), + (cx(decoder), cross-right-route-y), + (cross-input-xs.last(), cross-right-route-y), + (cross-input-xs.last(), dec("cross").y + dec("cross").h), + ), + dir: "u", + ) + arr-v(cx(decoder), dec("cross").y, dec("norm2").y + dec("norm2").h) + arr-v(cx(decoder), dec("norm2").y, dec("ff").y + dec("ff").h) + arr-v(cx(decoder), dec("ff").y, dec("norm3").y + dec("norm3").h) + arr-v(cx(decoder), dec("norm3").y, dec("linear").y + dec("linear").h) + arr-v(cx(decoder), dec("linear").y, dec("softmax").y + dec("softmax").h) + arr-v(cx(decoder), dec("softmax").y, 10pt) + + let enc-out-x = cx(encoder) + let enc-out-y = enc("norm2").y + let enc-route-y = encoder.frame-y - 5pt + let enc-dec-route-x = (encoder.x + frame-w + decoder.x) / 2 + let cross-in-y = dec("cross").y + dec("cross").h + 5pt + poly(( + (enc-out-x, enc-out-y), + (enc-out-x, enc-route-y), + (enc-dec-route-x, enc-route-y), + (enc-dec-route-x, cross-in-y), + (cross-input-xs.at(1), cross-in-y), + )) + arr-v(cross-input-xs.at(1), cross-in-y, dec("cross").y + dec("cross").h) + arrow-poly( + ( + (enc-dec-route-x, cross-in-y), + (cross-input-xs.at(0), cross-in-y), + (cross-input-xs.at(0), dec("cross").y + dec("cross").h), + ), + dir: "u", + ) + }) +}) + + +#let closed-loop-diagram = align(center, { + let W = 248pt + let H = 112pt + let ink = luma(0) + let stroke = ink + 1.0pt + let label(x, y, body, size: 7.2pt) = place(top + left, dx: x, dy: y, text(size: size, fill: ink, body)) + let arrow-r(x1, y, x2) = { + place(top + left, line(start: (x1, y), end: (x2 - 5pt, y), stroke: stroke)) + _arrow-head(x2, y, dir: "r", color: ink) + } + let arrow-l(x1, y, x2) = { + place(top + left, line(start: (x1, y), end: (x2 + 5pt, y), stroke: stroke)) + _arrow-head(x2, y, dir: "l", color: ink) + } + let arrow-u(x, y1, y2) = { + place(top + left, line(start: (x, y1), end: (x, y2 + 5pt), stroke: stroke)) + _arrow-head(x, y2, dir: "u", color: ink) + } + let arrow-d(x, y1, y2) = { + place(top + left, line(start: (x, y1), end: (x, y2 - 5pt), stroke: stroke)) + _arrow-head(x, y2, dir: "d", color: ink) + } + let labeled-arrow(x1, y1, x2, y2, body, label-dx: 0pt, label-dy: 0pt, label-size: 8pt) = { + if y1 == y2 and x2 > x1 { + arrow-r(x1, y1, x2) + } else if y1 == y2 and x2 < x1 { + arrow-l(x1, y1, x2) + } else if x1 == x2 and y2 > y1 { + arrow-d(x1, y1, y2) + } else if x1 == x2 and y2 < y1 { + arrow-u(x1, y1, y2) + } else { + place(top + left, line(start: (x1, y1), end: (x2, y2), stroke: stroke)) + } + if body != none { + label((x1 + x2) / 2 + label-dx, (y1 + y2) / 2 + label-dy, body, size: label-size) + } + } + + + let sys-box(x, y, label, w: 26pt, h: 26pt) = { + place( + top + left, + dx: x, + dy: y, + box( + width: w, + height: h, + fill: white, + stroke: ink + 1.2pt, + align(center + horizon, text(size: 8pt, fill: ink, label)), + ), + ) + } + + let sum-sign(x, y, body) = place( + top + left, + dx: x, + dy: y, + box(width: 5.5pt, height: 5.5pt, align(center + horizon, text(size: 5.8pt, fill: ink, body))), + ) + let sum-node(x, y, kind: "feedback") = { + let r = 10pt + let d = 7.1pt + let signs = if kind == "disturbance" { + ((-8.6pt, -3.0pt, [+]), (-2.8pt, -8.6pt, [+])) + } else { + ((-8.6pt, -3.0pt, [+]), (-2.8pt, 3.2pt, [−])) + } + place(top + left, dx: x - r, dy: y - r, circle(radius: r, stroke: ink + 1.0pt, fill: white)) + place(top + left, line(start: (x - d, y - d), end: (x + d, y + d), stroke: ink + 0.65pt)) + place(top + left, line(start: (x - d, y + d), end: (x + d, y - d), stroke: ink + 0.65pt)) + for (sx, sy, sign) in signs { + sum-sign(x + sx, y + sy, sign) + } + } + box(width: W, height: H, { + let y = 42pt + let s1 = 48pt + let s2 = 154pt + let low = 85pt + let arrow-len = 30pt + let disturbance-arrow-len = 20pt + let node-r = 10pt + let box-w = 26pt + let k-x = s1 + node-r + arrow-len + let g-x = s2 + node-r + arrow-len + let branch = g-x + box-w + arrow-len / 2 + let h-x = 144pt + + sum-node(s1, y) + label(k-x - 4pt, 17pt, [Controller], size: 7pt) + + sys-box(k-x, 29pt, [$K(z)$]) + // sys-box(50pt, 29pt, [$K(z)$]) + + sum-node(s2, y, kind: "disturbance") + label(g-x - 11pt, 17pt, [Target System], size: 7pt) + sys-box(g-x, 29pt, [$G(z)$]) + sys-box(h-x, 72pt, [$H(z)$]) + label(h-x - 7pt, 103pt, [Transducer], size: 7pt) + + labeled-arrow(s1 - node-r - arrow-len, y, s1 - node-r, y, [$R(z)$], label-dx: -10pt, label-dy: -10pt) + labeled-arrow(s1 + node-r, y, k-x, y, [$E(z)$], label-dx: -10pt, label-dy: -10pt) + labeled-arrow(k-x + box-w, y, s2 - node-r, y, [$U(z)$], label-dx: -10pt, label-dy: -10pt) + labeled-arrow(s2 + node-r, y, g-x, y, [$V(z)$], label-dx: -10pt, label-dy: -10pt) + labeled-arrow(g-x + box-w, y, g-x + box-w + arrow-len, y, [$Y(z)$], label-dx: -10pt, label-dy: -10pt) + labeled-arrow(s2, y - node-r - disturbance-arrow-len, s2, y - node-r, [$D(z)$], label-dx: -9pt, label-dy: -21pt) + place(top + left, line(start: (branch, y), end: (branch, low), stroke: stroke)) + arrow-l(branch, low, h-x + box-w) + place(top + left, line(start: (h-x, low), end: (s1, low), stroke: stroke)) + arrow-u(s1, low, 52pt) + label(94pt, 90pt, [$W(z)$], size: 8pt) + }) +}) + + + +#let kernel-image-diagram = align(center, { + let W = 220pt + let H = 86pt + let ink = luma(15) + let label-size = 9pt + let node(x, y, w, h, body, size) = place( + top + left, + dx: x - w / 2, + dy: y - h / 2, + box(width: w, height: h, align(center + horizon, text(size: size, fill: ink, body))), + ) + let arrow-r(x1, y, x2, body, label-dx: 0pt, label-dy: -13pt) = { + place(top + left, line(start: (x1, y), end: (x2 - 5pt, y), stroke: ink + 0.8pt)) + _arrow-head(x2, y, dir: "r", color: ink) + place(top + left, dx: (x1 + x2) / 2 + label-dx, dy: y + label-dy, text(size: label-size, fill: ink, body)) + } + let arrow-v(x, y1, y2, body, label-dx: 7pt, label-dy: -4pt) = { + if y2 > y1 { + place(top + left, line(start: (x, y1), end: (x, y2 - 5pt), stroke: ink + 0.8pt)) + _arrow-head(x, y2, dir: "d", color: ink) + } else { + place(top + left, line(start: (x, y1), end: (x, y2 + 5pt), stroke: ink + 0.8pt)) + _arrow-head(x, y2, dir: "u", color: ink) + } + place(top + left, dx: x + label-dx, dy: (y1 + y2) / 2 + label-dy, text(size: label-size, fill: ink, body)) + } + box(width: W, height: H, { + let top-y = 20pt + let bot-y = 68pt + let left-x = 66pt + let right-x = 162pt + let g-w = 24pt + let gp-w = 30pt + let q-w = 92pt + let im-w = 58pt + let g-arrow-w = 20pt + let gp-arrow-w = 24pt + let q-arrow-w = 74pt + let im-arrow-w = 42pt + let node-h = 18pt + + node(left-x, top-y, g-w, node-h, [$G$], 18pt) + node(right-x, top-y, gp-w, node-h, [$G'$], 18pt) + node(left-x, bot-y, q-w, node-h, [$G slash ker phi$], 16pt) + node(right-x, bot-y, im-w, node-h, [$im phi$], 16pt) + + arrow-r(left-x + g-arrow-w / 2 + 2pt, top-y, right-x - gp-arrow-w / 2 + 1pt, [$phi$], label-dx: -4pt) + arrow-v(left-x, top-y + node-h / 2 + 2pt, bot-y - node-h / 2 + 1pt, [$-$], label-dx: -13pt) + arrow-r( + left-x + q-arrow-w / 2 + 2pt, + bot-y, + right-x - im-arrow-w / 2 + 1pt, + [$overline(phi)$], + label-dx: -8pt, + label-dy: 5pt, + ) + arrow-v(right-x, bot-y - node-h / 2 + 1pt, top-y + node-h / 2 + 2pt, [inc], label-dx: 7pt) + }) +}) + +#let weighted-transition-graph = align(center, { + let W = 176pt + let H = 162pt + let ink = luma(25) + let edge = rgb("#C66A00") + let node-r = 13pt + let nodes = ( + P: (x: 17pt, y: 99pt), + B: (x: 64pt, y: 58pt), + D: (x: 110pt, y: 17pt), + C: (x: 110pt, y: 98pt), + M: (x: 64pt, y: 139pt), + L: (x: 158pt, y: 139pt), + ) + let pos(name) = nodes.at(name) + let arrow-dir(dx, dy) = { + let ax = calc.abs(dx) + let ay = calc.abs(dy) + if ax < ay * 0.45 { + if dy > 0pt { "d" } else { "u" } + } else if ay < ax * 0.45 { + if dx > 0pt { "r" } else { "l" } + } else if dx > 0pt and dy > 0pt { + "dr" + } else if dx < 0pt and dy > 0pt { + "dl" + } else if dx > 0pt and dy < 0pt { + "ur" + } else { + "ul" + } + } + let node(x, y, short) = { + place(top + left, dx: x - node-r, dy: y - node-r, box( + width: 2 * node-r, + height: 2 * node-r, + radius: node-r, + stroke: ink + 0.7pt, + fill: white, + align(center + horizon, text(size: 8.8pt, style: "italic", short)), + )) + } + let node-by-name(name, body) = { + let p = pos(name) + node(p.x, p.y, body) + } + let label-at(x, y, body) = { + place(top + left, dx: x - 7.5pt, dy: y - 5pt, box( + width: 15pt, + height: 10pt, + fill: white, + inset: 0pt, + align(center + horizon, text(size: 7.6pt, fill: ink, body)), + )) + } + let edge-line(a, b, label: none, label-dx: 0pt, label-dy: 0pt, both: false) = { + let pa = pos(a) + let pb = pos(b) + let dx = pb.x - pa.x + let dy = pb.y - pa.y + let ndx = dx / 1pt + let ndy = dy / 1pt + let len = calc.sqrt(ndx * ndx + ndy * ndy) + let ux = ndx / len + let uy = ndy / len + let sx = pa.x + ux * (node-r + 1pt) + let sy = pa.y + uy * (node-r + 1pt) + let ex = pb.x - ux * (node-r + 1pt) + let ey = pb.y - uy * (node-r + 1pt) + + place(top + left, line(start: (sx, sy), end: (ex, ey), stroke: edge + 0.85pt)) + _arrow-head(ex, ey, dir: arrow-dir(dx, dy), color: edge) + if both { + _arrow-head(sx, sy, dir: arrow-dir(-dx, -dy), color: edge) + } + if label != none { + label-at((sx + ex) / 2 + label-dx, (sy + ey) / 2 + label-dy, label) + } + } + let edge-curve(a, b, label: none, label-dx: 0pt, label-dy: 0pt) = { + let pa = pos(a) + let pb = pos(b) + let c1x = W + 18pt + let c1y = H - 42pt + let c2x = W + 8pt + let c2y = 22pt + let sdx = (c1x - pa.x) / 1pt + let sdy = (c1y - pa.y) / 1pt + let slen = calc.sqrt(sdx * sdx + sdy * sdy) + let sx = pa.x + sdx / slen * (node-r + 1pt) + let sy = pa.y + sdy / slen * (node-r + 1pt) + let edx = (c2x - pb.x) / 1pt + let edy = (c2y - pb.y) / 1pt + let elen = calc.sqrt(edx * edx + edy * edy) + let ex = pb.x + edx / elen * (node-r + 1pt) + let ey = pb.y + edy / elen * (node-r + 1pt) + + place(top + left, curve( + stroke: edge + 0.85pt, + fill: none, + curve.move((sx, sy)), + curve.cubic((c1x, c1y), (c2x, c2y), (ex, ey)), + )) + _arrow-head(ex, ey, dir: "l", color: edge) + if label != none { + let mx = 0.125 * sx + 0.375 * c1x + 0.375 * c2x + 0.125 * ex + let my = 0.125 * sy + 0.375 * c1y + 0.375 * c2y + 0.125 * ey + label-at(mx + label-dx, my + label-dy, label) + } + } + box(width: W, height: H, { + edge-line("D", "B", label: [10]) + edge-line("B", "P", label: [10]) + edge-line("P", "M", label: [4]) + edge-line("B", "M", label: [5], both: true) + edge-line("C", "B", label: [3]) + edge-line("M", "C", label: [9]) + edge-line("D", "C", label: [4], both: true) + edge-line("L", "M", label: [10]) + edge-curve("L", "D", label: [10]) + + node-by-name("P", [$P$]) + node-by-name("B", [$B$]) + node-by-name("D", [$D$]) + node-by-name("C", [$C$]) + node-by-name("M", [$M$]) + node-by-name("L", [$L$]) + }) +}) + +// ---- Chart slide 1: Transformer + Dynamic system ----------- +#slide(title: "Block Diagrams")[ + #two-col( + left-width: 49%, + [ + Transformer encoder-decoder stack with attention, feed-forward blocks, residual paths, and layer normalisation. + + #fs-diagram(caption: [Transformer encoder-decoder block diagram (Vaswani et al., 2017).])[ + #transformer-diagram + ] + ], + [ + A closed-loop controller compares the reference signal with the measured output, drives the plant, and routes the response through a feedback transducer. + + #fs-diagram( + caption: [Closed-loop control system with controller, plant, disturbance input, and feedback transducer.], + )[ + #closed-loop-diagram + ] + The disturbance $D(z)$ enters before the plant, while $H(z)$ shapes the measured feedback signal $W(z)$ returned to the summing junction. + ], + ) +] + +// ---- Chart slide 2: Linear algebra + Graph ----------------- +#slide(title: "Linear Algebra Diagram and Transition Graph")[ + #two-col( + left-width: 49%, + [ + For a homomorphism $phi: G -> G'$, the *kernel* determines the quotient $G slash ker phi$, while the *image* is the subgroup of reachable outputs in $G'$. + + #fs-diagram(caption: [Kernel-image decomposition for a homomorphism $phi: G -> G'$.])[ + #kernel-image-diagram + ] + The induced map $overline(phi)$ sends cosets modulo $ker phi$ onto $im phi$, and the inclusion embeds that image back into $G'$. + ], + [ + A weighted directed graph encodes reachable states as nodes and transition costs as labels on the arcs. This version keeps the notation compact to match the reference diagram. + + #fs-diagram(caption: [Weighted directed transition graph; edge labels denote transition costs.])[ + #weighted-transition-graph + ] + Parallel and long-range transitions are shown with separate arrows, making bidirectional moves and high-cost paths visible at a glance. + ], + ) +] + +// ---- Chart slide 3: Line chart + Histogram ----------------- +#let accuracy-chart = align(center, { + let W = 248pt + let H = 126pt + let pl = 32pt + let pr = 13pt + let pt = 21pt + let pb = 24pt + let iw = W - pl - pr + let ih = H - pt - pb + let xs = (0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0) + let ya = (0.52, 0.57, 0.63, 0.70, 0.76, 0.81, 0.85, 0.88, 0.90, 0.91, 0.92) + let yb = (0.48, 0.51, 0.55, 0.60, 0.65, 0.69, 0.73, 0.76, 0.79, 0.81, 0.83) + let yc = (0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50) + let xmin = 0.0 + let xmax = 5.0 + let ymin = 0.44 + let ymax = 0.96 + let px(xi) = pl + (xi - xmin) / (xmax - xmin) * iw + let py(yi) = pt + (ymax - yi) / (ymax - ymin) * ih + let tick-label(x, y, width, body, size: 5.4pt, fill: luma(70)) = { + place(top + left, dx: x - width / 2, dy: y - 4.5pt, box( + width: width, + height: 9pt, + inset: 0pt, + align(center + horizon, text(size: size, fill: fill, body)), + )) + } + let line-legend-item(x, y, col, body, text-width: 18pt) = { + place(top + left, line(start: (x, y), end: (x + 9pt, y), stroke: col + 1.1pt)) + place(top + left, dx: x + 12pt, dy: y - 6.5pt, box( + width: text-width, + height: 13pt, + inset: 0pt, + align(left + horizon, text(size: 5.3pt, fill: luma(45), body)), + )) + } + let mkpath(ys, col) = { + let pts = xs.zip(ys).map(p => (px(p.first()), py(p.last()))) + curve( + stroke: col + 1.35pt, + fill: none, + curve.move(pts.first()), + ..pts.slice(1).map(curve.line), + ) + } + let markers(ys, col) = { + for (xv, yv) in xs.zip(ys) { + place(top + left, dx: px(xv) - 1.6pt, dy: py(yv) - 1.6pt, circle(radius: 1.6pt, fill: white, stroke: col + 0.7pt)) + } + } + box(width: W, height: H, stroke: luma(180) + 0.45pt, fill: luma(252), { + place(top + left, dx: 8pt, dy: 5pt, text(size: 6.8pt, weight: "bold", fill: luma(25))[Validation accuracy by epoch]) + place(top + left, dx: W - 91pt, dy: 5pt, box(width: 83pt, height: 13pt, fill: white, stroke: luma(220) + 0.35pt, { + line-legend-item(5pt, 6.5pt, blue.darken(50%), [A], text-width: 8pt) + line-legend-item(28pt, 6.5pt, red.darken(20%), [B], text-width: 8pt) + line-legend-item(52pt, 6.5pt, luma(150), [base], text-width: 17pt) + })) + place(top + left, dx: pl + 3pt, dy: pt + 3pt, text(size: 5.2pt, fill: luma(75))[Accuracy]) + for yi in (0.5, 0.6, 0.7, 0.8, 0.9) { + place(top + left, line(start: (pl, py(yi)), end: (W - pr, py(yi)), stroke: luma(222) + 0.45pt)) + tick-label(pl / 2, py(yi), pl - 8pt, str(yi)) + } + place(top + left, line(start: (pl, pt), end: (pl, H - pb), stroke: luma(95) + 0.65pt)) + place(top + left, line(start: (pl, H - pb), end: (W - pr, H - pb), stroke: luma(95) + 0.65pt)) + for xi in (0, 1, 2, 3, 4, 5) { + place(top + left, line( + start: (px(float(xi)), H - pb), + end: (px(float(xi)), H - pb + 2pt), + stroke: luma(95) + 0.45pt, + )) + tick-label(px(float(xi)), H - pb + 8pt, 12pt, str(xi)) + } + place(top + left, dx: W / 2 - 12pt, dy: H - 9pt, text(size: 5.5pt, fill: luma(65))[Epoch]) + place(top + left, mkpath(ya, blue.darken(50%))) + place(top + left, mkpath(yb, red.darken(20%))) + place(top + left, mkpath(yc, luma(160))) + markers(ya, blue.darken(50%)) + markers(yb, red.darken(20%)) + }) +}) + +#let grouped-bar-chart = align(center, { + let W = 248pt + let H = 126pt + let pl = 32pt + let pr = 12pt + let pt = 21pt + let pb = 26pt + let iw = W - pl - pr + let ih = H - pt - pb + let baseline = H - pb + let years = (2020, 2021, 2022, 2023, 2024) + let va = (62, 67, 71, 74, 78) + let vb = (55, 58, 61, 65, 69) + let maxv = 85.0 + let bw = 10.5pt + let gap = 4pt + let group-w = 2 * bw + gap + let group-step = iw / years.len() + let ca = blue.darken(50%) + let cb = red.darken(20%) + let py(score) = baseline - (float(score) / maxv) * ih + let group-center(i) = pl + (float(i) + 0.5) * group-step + let group-left(i) = group-center(i) - group-w / 2 + let bar-center(i, which) = group-left(i) + if which == "a" { bw / 2 } else { bw + gap + bw / 2 } + let tick-label(x, y, width, body, size: 5.2pt, fill: luma(70)) = { + place(top + left, dx: x - width / 2, dy: y - 4.5pt, box( + width: width, + height: 9pt, + inset: 0pt, + align(center + horizon, text(size: size, fill: fill, body)), + )) + } + let value-label(x, y, body, fill) = { + place(top + left, dx: x - 7pt, dy: y - 9pt, box( + width: 14pt, + height: 8pt, + inset: 0pt, + align(center + horizon, text(size: 4.8pt, fill: fill, body)), + )) + } + let swatch-legend-item(x, y, col, body, text-width: 8pt) = { + place(top + left, dx: x, dy: y - 2.5pt, rect(width: 6pt, height: 5pt, fill: col)) + place(top + left, dx: x + 9pt, dy: y - 6.5pt, box( + width: text-width, + height: 13pt, + inset: 0pt, + align(left + horizon, text(size: 5.3pt, fill: luma(45), body)), + )) + } + box(width: W, height: H, stroke: luma(180) + 0.45pt, fill: luma(252), { + place(top + left, dx: 8pt, dy: 5pt, text(size: 6.8pt, weight: "bold", fill: luma(25))[Mean score by cohort]) + place(top + left, dx: W - 58pt, dy: 5pt, box(width: 50pt, height: 13pt, fill: white, stroke: luma(220) + 0.35pt, { + swatch-legend-item(5pt, 6.5pt, ca, [A]) + swatch-legend-item(27pt, 6.5pt, cb, [B]) + })) + place(top + left, dx: pl + 3pt, dy: pt + 3pt, text(size: 5.2pt, fill: luma(75))[Score]) + for score in (20, 40, 60, 80) { + let yp = py(score) + place(top + left, line(start: (pl, yp), end: (W - pr, yp), stroke: luma(222) + 0.45pt)) + tick-label(pl / 2, yp, pl - 8pt, str(score)) + } + place(top + left, line(start: (pl, pt), end: (pl, baseline), stroke: luma(95) + 0.65pt)) + place(top + left, line(start: (pl, baseline), end: (W - pr, baseline), stroke: luma(95) + 0.65pt)) + for (i, yr) in years.enumerate() { + let a = va.at(i) + let b = vb.at(i) + let x0 = group-left(i) + let ya = py(a) + let yb = py(b) + place(top + left, dx: x0, dy: ya, rect(width: bw, height: baseline - ya, fill: ca)) + place(top + left, dx: x0 + bw + gap, dy: yb, rect(width: bw, height: baseline - yb, fill: cb)) + tick-label(group-center(i), baseline + 8pt, group-step - 2pt, str(yr), size: 5.1pt, fill: luma(55)) + value-label(bar-center(i, "a"), ya, str(a), ca) + value-label(bar-center(i, "b"), yb, str(b), cb) + } + place(top + left, dx: W / 2 - 9pt, dy: H - 9pt, text(size: 5.5pt, fill: luma(65))[Year]) + }) +}) + +#slide(title: "Model Accuracy and Cohort Scores (No Captions)")[ + #two-col( + left-width: 49%, + [ + The accuracy curves compare Model A, Model B, and a 50% baseline across epochs $x in [0,5]$. Model A stays ahead throughout, while both learned models rise well above the baseline. + + #fs-visual[#accuracy-chart] + At epoch 5, *Model A* reaches 92% and *Model B* reaches 83%. Their gap grows up to epoch 3 and then narrows from 12 to 9 percentage points by the final epoch. + ], + [ + The grouped bars compare mean test scores for Group A and Group B from 2020 to 2024. Both cohorts improve each year, with *Group A* leading every annual pair. + + #fs-visual[#grouped-bar-chart] + Scores rise from 62 to 78 for *Group A* and from 55 to 69 for *Group B*. The gap widens from 7 points in 2020 to 9 points in 2024. + ], + ) +] + +#slide(title: "Model Accuracy and Cohort Scores (With Captions)")[ + #two-col( + left-width: 49%, + [ + The accuracy curves compare Model A, Model B, and a 50% baseline across epochs $x in [0,5]$. Model A stays ahead throughout, while both learned models rise well above the baseline. + + #fs-figure(caption: [Accuracy vs. epoch for Model A, Model B, and random baseline.])[ + #accuracy-chart + ] + At epoch 5, *Model A* reaches 92% and *Model B* reaches 83%. Their gap grows up to epoch 3 and then narrows from 12 to 9 percentage points by the final epoch. + ], + [ + The grouped bars compare mean test scores for Group A and Group B from 2020 to 2024. Both cohorts improve each year, with *Group A* leading every annual pair. + + #fs-figure(caption: [Mean test score by group and year (2020-2024).])[ + #grouped-bar-chart + ] + Scores rise from 62 to 78 for *Group A* and from 55 to 69 for *Group B*. The gap widens from 7 points in 2020 to 9 points in 2024. + ], + ) +] + +// ============================================================ +// SECTION 8 -- THEOREM-STYLE BOXES +// ============================================================ +#new-section("Theorem-style Boxes") + +#slide(title: "Definitions, Theorems, and Lemmas")[ + #two-col( + left-width: 49%, + [ + #definition(name: "Metric Space")[ + A *metric space* $(M, d)$ is a set $M$ with $d: M times M -> RR_(>=0)$ satisfying non-negativity, identity ($d(x,y)=0 <=> x=y$), symmetry, and the triangle inequality. + ] + #theorem(name: "Banach Fixed-Point Theorem")[ + Let $(M, d)$ be complete and $f: M -> M$ a contraction with constant $k < 1$. Then $f$ has a *unique* fixed point $x^* in M$. + ] + #lemma()[ + Any contraction $f$ on $(M,d)$ is uniformly continuous and extends uniquely to the completion $overline(M)$. + ] + ], + [ + #corollary(name: "Picard-Lindelof")[ + Under Lipschitz continuity in $y$, the IVP $dot(y)=f(t,y)$, $y(t_0)=y_0$ has a unique local solution. + ] + #proof[ + Apply Banach's theorem to the Picard operator $T phi = y_0 + integral_(t_0)^t f(s,phi(s)) d s$, which is contractive on $C([t_0-delta, t_0+delta])$ for small $delta > 0$. + ] + #remark(name: "Completeness is necessary")[ + On $(0,1)$ with $f(x)=x/2$, the fixed point $0$ lies outside the space -- Banach's theorem fails. + ] + ], + ) +] + +#slide(title: "Examples, Exercises, Propositions, and Custom Boxes")[ + #two-col( + left-width: 49%, + [ + #example(name: "Euclidean Space")[ + $RR^n$ with $d(x,y)=norm(x-y)_2$ is complete. The iteration $x_(k+1)=A x_k+b$ converges iff $rho(A)<1$, to the unique solution of $x=A x+b$. + ] + #exercise()[ + Show that $ZZ_p$ is complete under the $p$-adic metric and use Banach's theorem to prove Hensel's lemma. + ] + #proposition(name: "Closed Subsets are Complete")[ + A closed subset of a complete metric space is itself a complete metric space. + ] + ], + [ + #fs-box("note", name: "Implementation tip", color: green.darken(30%))[ + Monitor $norm(x_(k+1)-x_k)$ as a stopping criterion. A-priori error: $norm(x_k - x^*) <= k^m/(1-k) norm(x_1-x_0)$. + ] + #fs-box("warning", name: "Common pitfall", color: red.darken(20%))[ + Contraction ($k<1$) is strictly stronger than nonexpansive ($k=1$). Rotations on $S^1$ are nonexpansive but have no fixed points. + ] + #fs-box("custom", name: "Any label works", color: purple.darken(15%))[ + Use `#fs-box("kind", name: "...", color: ...)` for any label and color -- observations, facts, algorithms, warnings. + ] + ], + ) +] + +#slide(title: "Custom Box Widths")[ + #fs-box("custom", name: "Default width", color: purple.darken(15%))[ + This box uses the default width, so it fills the available slide content area. It is the recommended form when the material belongs to the main flow of the slide. + ] + + #align(center)[ + #fs-box("custom", name: "Narrow custom width", color: purple.darken(15%), width: 62%)[ + This box has a smaller explicit width. It is useful for short claims, reminders, or side notes that should not dominate the slide. + ] + ] + + #fs-box("custom", name: "Short content, default width", color: purple.darken(15%))[ + A short phrase. + ] +] + +#slide(title: "Full-Width Mathematical Proof")[ + #theorem(name: "Cauchy-Schwarz Inequality")[ + For all vectors $u, v in RR^n$, + #fs-equation[$ + abs(u dot v) <= norm(u) norm(v). + $] + ] + + #proof[ + If $v = 0$, the claim is immediate, so assume $v != 0$. For every real $t$, the squared norm of $u - t v$ is non-negative: + + #fs-equation[$ + 0 <= norm(u - t v)^2 = norm(u)^2 - 2 t (u dot v) + t^2 norm(v)^2. + $] + + Choose the minimising value $t = (u dot v) / norm(v)^2$. Substitution gives + + #fs-equation[$ + 0 <= norm(u)^2 - (u dot v)^2 / norm(v)^2. + $] + + Multiplying by the positive number $norm(v)^2$, we obtain + + #fs-equation[$ + (u dot v)^2 <= norm(u)^2 norm(v)^2. + $] + + Taking square roots on both sides yields $abs(u dot v) <= norm(u) norm(v)$. Equality occurs precisely when the non-negative quadratic has a zero at its minimum, which means $u - t v = 0$ for some scalar $t$; equivalently, the two vectors are linearly dependent. + ] +] + +#slide(title: "Narrow Mathematical Proof")[ + Proof environments fill the available content width by default, matching the rhythm of ordinary slide material. When a shorter line length reads better, pass an explicit `width` and center the proof as below. + + #align(center)[ + #proof(width: 68%)[ + We prove Young's inequality in its weighted quadratic form. Let $a, b >= 0$ and fix $epsilon > 0$. Since every square is non-negative, + + #fs-equation[$ + 0 <= (sqrt(epsilon) a - b / sqrt(epsilon))^2. + $] + + Expanding the square gives + + #fs-equation[$ + 0 <= epsilon a^2 - 2 a b + b^2 / epsilon. + $] + + Rearranging terms and dividing by $2$ yields + + #fs-equation[$ + a b <= epsilon a^2 / 2 + b^2 / (2 epsilon). + $] + + This form is especially useful when a product term must be absorbed into a coercive estimate: one chooses $epsilon$ small enough for the first term and pays for it in the second term. + ] + ] +] + +#slide(title: "Theorem Box with Internal Proof")[ + #theorem(name: "Cauchy-Schwarz Inequality")[ + For all vectors $u, v in RR^n$, + #fs-equation[$ + abs(u dot v) <= norm(u) norm(v). + $] + + #proof[ + If $v = 0$, the claim is immediate. Otherwise, the quadratic expression $norm(u - t v)^2$ is non-negative for every $t in RR$. Expanding and choosing $t = (u dot v) / norm(v)^2$ gives + #fs-equation[$ + 0 <= norm(u)^2 - (u dot v)^2 / norm(v)^2. + $] + Multiplying by $norm(v)^2$ and taking square roots yields the desired inequality. + ] + ] +] + +#slide(title: "Exercise Box with Solution")[ + #exercise(name: "Spectral radius criterion")[ + Let $A in RR^(n times n)$ and suppose $norm(A) < 1$ for a matrix norm compatible with the vector norm. Prove that $I - A$ is invertible and derive a convergent series for its inverse. + + #box-separator("Solution", color: orange.darken(20%)) + + Since $norm(A^k) <= norm(A)^k$, the Neumann series $sum_(k=0)^infinity A^k$ converges absolutely. Multiplying partial sums by $I-A$ gives $I - A^(m+1)$, which tends to $I$. Thus, + #fs-equation[$ + (I - A)^(-1) = sum_(k=0)^infinity A^k. + $] + ] +] + +#slide(title: "Exercise Box with Two Solutions")[ + #exercise(name: "Arithmetic-geometric mean")[ + Prove that for $x, y >= 0$ one has $sqrt(x y) <= (x + y) / 2$. + + #box-separator("Solution 1", color: orange.darken(20%)) + + The square $(sqrt(x) - sqrt(y))^2$ is non-negative, so $x + y - 2 sqrt(x y) >= 0$. + + #box-separator("Solution 2", color: orange.darken(20%)) + + The function $log$ is concave on $(0, infinity)$. Applying Jensen's inequality to $x$ and $y$ gives + #fs-equation[$ + log((x + y) / 2) >= (log x + log y) / 2 = log(sqrt(x y)). + $] + ] +] + +#slide(title: "Theorem, Text, and Lemma")[ + The next result is stated in the same theorem-style box used throughout the template. The surrounding prose is deliberately included to show how normal paragraphs sit before, between, and after formal statements. + + #theorem(name: "Compactness criterion")[ + Every sequence in a compact metric space has a convergent subsequence whose limit belongs to the same space. + ] + + This theorem is often the bridge between qualitative assumptions and quantitative estimates. The intermediate text lets the slide show how ordinary prose separates theorem-style boxes without requiring a two-column layout. + + #lemma(name: "Closed image of a convergent sequence")[ + If $x_n -> x$ and $F$ is closed, then every sequence contained in $F$ can only converge to a point of $F$. + ] + + Together, the theorem and lemma give a compact workflow: first extract convergence, then use closedness to keep the limiting object inside the admissible set. This final paragraph checks the lower spacing after the last formal box. +] + +// ============================================================ +// Thank you +// ============================================================ +#final-slide diff --git a/packages/preview/black-angular-frame/0.1.0/thumbnail.png b/packages/preview/black-angular-frame/0.1.0/thumbnail.png new file mode 100644 index 0000000000..da27cc776d Binary files /dev/null and b/packages/preview/black-angular-frame/0.1.0/thumbnail.png differ diff --git a/packages/preview/black-angular-frame/0.1.0/typst.toml b/packages/preview/black-angular-frame/0.1.0/typst.toml new file mode 100644 index 0000000000..559e91efd2 --- /dev/null +++ b/packages/preview/black-angular-frame/0.1.0/typst.toml @@ -0,0 +1,23 @@ +[package] +name = "black-angular-frame" +version = "0.1.0" +entrypoint = "black-angular-frame.typ" +authors = ["Miguel Montes <@miguelm7654>"] +license = "MIT" +description = "Formal academic slide decks." +keywords = ["slides", "presentation", "academic", "beamer"] +categories = ["presentation"] +exclude = [ + "example.typ", + "example.pdf", + "assets/fonts/**", + "assets/curves.csv", + "tinymist.lock", + ".vscode/**", + "tmp/**", +] + +[template] +path = "template" +entrypoint = "main.typ" +thumbnail = "thumbnail.png"