Написавши вступ до Typst, я захотів спробувати щось більше, ніж звичний звіт із заголовками, рівняннями та списком літератури. Що ще можна створити цим самим інструментом?
Почнемо з невеликої книжки, якій потрібна обкладинка. Потім зробимо візитки для команди й настільні таблички для конференції. Далі перетворимо PDF на посібник, у якому читач переходить між сторінками за посиланнями, підготуємо дві версії навчального робочого аркуша й намалюємо математичну модель просто в конспекті.
Кожен приклад починається зі зрозумілої ситуації: кому потрібен документ, що він має робити та з чим може допомогти Typst. У деяких прикладах працюємо зі списком людей. Для інших достатньо тексту, геометрії або кількох рівнянь.
Перш ніж почати
Усі шість прикладів скомпільовано й відтворено в Typst 0.15.0. Варто знати змінні, функції, імпорти та різницю між кодом і розміткою.
Щоб випробовувати приклади разом зі мною, завантаж вихідні файли:
Кожен файл .typ — це повний, самостійний документ. Наведені нижче фрагменти показують його основні частини й містять посилання на весь файл.
typst compile generative-art.typ art.pdf --ignore-system-fontsЦю саму команду можна виконати з будь-якою іншою назвою файлу. Приклади використовують шрифт Libertinus Serif, що постачається з Typst; прапорець --ignore-system-fonts забезпечує однаковий вибір шрифту в різних середовищах. Зміни розширення вихідного файлу на .svg, щоб отримати векторне зображення, або на .png для растрового перегляду. Для багатосторінкового документа візьми шаблон назви в лапки, наприклад "guide-{p}.svg", або експортуй одну сторінку за допомогою --pages 1.
Обкладинка книжки, намальована за рівнянням
Уяви, що пишеш невелику книжку про математику. Текст майже готовий, але обкладинка ще порожня. Хочеться простої графіки, яка пасує до теми, і можливості змінювати її форму та кольори, не малюючи все заново.
Typst може намалювати цю графіку в тому самому файлі, що й обкладинку. Якщо згодом напишеш другу книжку, зможеш зберегти дизайн і змінити кілька чисел, щоб надати їй власного вигляду.
Намалюємо полярну розетку з кількох шарів. Її радіус змінюється залежно від кута:
R визначає зовнішній радіус, k — кількість пелюсток, а φ — фазу зміни радіуса. Обчислимо точки на кривій, переведемо полярні координати в координати сторінки й намалюємо шість зменшених копій тієї самої кривої.
Побудуй малюнок
#let rosette(petals: 7, phase: 0deg, radius: 43mm) = {
let center = 48mm
let samples = 720
let points = range(samples + 1).map(i => {
let angle = i * 360deg / samples
let r = radius * (0.7 + 0.3 * calc.cos(petals * angle + phase))
(center + r * calc.cos(angle), center + r * calc.sin(angle))
})
block(width: 96mm, height: 96mm, {
for layer in range(6) {
let scale = 1 - layer * 0.09
let path = points.map(((x, y)) =>
(center + (x - center) * scale, center + (y - center) * scale))
place(top + left, curve(
stroke: (paint: color.mix((rgb("#68C6C4"), 5 - layer), (rgb("#F0AD79"), layer)), thickness: 0.55pt),
curve.move(path.first()),
..path.slice(1).map(point => curve.line(point)),
curve.close(),
))
}
})
} Елемент curve утворює фігуру, з’єднуючи 720 точок і замикаючи криву. За такого розміру короткі прямі відрізки виглядають як плавна крива. Більше точок дає точніше наближення, але й збільшує розмір файлу.
Блок, у якому малюємо, відводить ділянку 96 × 96 мм. Центр розташований у точці (48mm, 48mm), а радіус не перевищує 43 мм, тож навколо кривих залишається вільне місце. Масштабування кожної точки відносно центра зберігає вирівнювання шарів. Масштабування абсолютних координат зміщувало б малюнок до початку координат сторінки.
Вбудована функція color.mix створює палітру з поступовою зміною кольорів. Уся геометрія обчислюється у вихідному коді; немає вставленого зображення, яке довелося б оновлювати окремо.
Спробуй: зміни #rosette(petals: 7) на #rosette(petals: 11, phase: 40deg). З того самого дизайну отримаєш інший малюнок для обкладинки. Якщо потрібен інший розмір, змінюй радіус і ділянку для малювання разом. Щоб використати графіку в іншій книжці, перенеси функцію в модуль, а налаштування сторінки залиш у файлі самої книжки.
Цей підхід підійде й для оздоблення розділів або серії плакатів до подій. Його перевага в тому, що малюнок можна змінювати кількома зрозумілими значеннями.
Візитки для кожного в компанії
Уяви, що керуєш невеликою компанією й хочеш зробити візитки для всієї команди. У тебе вже є JSON-файл з іменами. Посад та особистих контактних даних у ньому немає: кожна візитка має містити ім’я людини, назву компанії й ту саму корпоративну електронну адресу.
Хочеться створити дизайн один раз, а потім сформувати окрему сторінку для кожної людини. Складність в іменах: «Ada Nováková» й «Alexandra Konstantinopoulos» мають поміститися в однаковому просторі. Розмір шрифту має підбиратися без ручного налаштування для кожного імені.
Наша проста візитка має розміри 85 × 55 мм. Для імені відведено прямокутник 75 × 12 мм і визначено допустимий діапазон розмірів шрифту. Перебираємо їх від найбільшого до найменшого, дозволяємо перенесення на новий рядок у межах прямокутника й повідомляємо про помилку, якщо жоден розмір не підходить.
Виміряй те, що справді буде надруковано
#let fit(value, width, height) = {
for size in (24pt, 22pt, 20pt, 18pt, 16pt, 14pt) {
let body = text(size: size, weight: "bold", value)
let extent = measure(body, width: width)
let tokens-fit = value.split(" ").all(word =>
measure(text(size: size, weight: "bold", word)).width <= width)
if tokens-fit and extent.width <= width and extent.height <= height {
return (body: body, size: size)
}
}
panic("Name exceeds the layout budget: " + value)
} Функції measure потрібен контекст верстання. Наведена нижче функція відтворення працює всередині context, після того як у документі задано шрифт, міжрядковий інтервал і переноси слів. Вимірюємо той самий вміст, який потім відтворюємо.
Також перевіряємо кожне слово окремо. Довге прізвище може виступати за межі прямокутника, навіть якщо абзац повертає задану йому ширину. Підрахунок літер не виявить цього, адже різні літери займають різну ширину.
Відведи компоненту фіксований простір
#let card(name, role, email, accent: rgb("#087F8C")) = context {
let fitted = fit(name, 75mm, 12mm)
block(width: 85mm, height: 55mm, fill: rgb("#F6F3EB"), {
place(top + left, dx: 5mm, dy: 6mm,
text(size: 7pt, tracking: 1pt)[FIELD / STUDIO])
place(top + left, dx: 5mm, dy: 17mm,
block(width: 75mm, fitted.body))
place(top + left, dx: 5mm, dy: 34mm, text(size: 9pt, role))
place(top + left, dx: 5mm, dy: 43mm, text(size: 8pt, email))
place(top + left, dy: 52mm,
rect(width: 85mm, height: 3mm, fill: accent, stroke: none))
[#metadata((name: name, size_pt: fitted.size / 1pt)) <name-fit>]
})
} Блок відводить прямокутник із фізичними розмірами візитки. Функція place розташовує поля відносно цього блока. У нашому прикладі для довгого імені обирається 20 pt; коротше ім’я може залишитися на 24 pt.
Завантажений файл створює одну зразкову візитку. Другий рядок тексту й електронна адреса в ньому задані незмінними значеннями; для нашої компанії використаємо їх для назви й спільної адреси. Якщо згодом додаси окремі посади чи адреси, передбач для цих полів власні перевірки розміру. Елементи, розташовані через place, не звільняють місце один для одного.
Створи візитку для кожного імені
Збережи список як people.json поруч із business-card.typ:
[
{ "name": "Ada Nováková" },
{ "name": "Alexandra Konstantinopoulos" }
]Залиш налаштування сторінки й обидві функції із завантаженого файлу. Заміни його останній виклик #card(...) на такий:
#let people = json("people.json")
#for (i, person) in people.enumerate() {
if i > 0 { pagebreak() }
card(person.name, "FIELD STUDIO", "[email protected]")
}Тепер скомпілюй business-card.typ, як раніше. Отримаєш PDF із двома сторінками — по одній візитці на кожній. Додай до JSON-файлу ще одне ім’я й скомпілюй знову, щоб створити візитку і для цієї людини. Дизайн залишається в одному місці, а функція підбору розміру імені враховує різну довжину.
Невидимі метадані зберігають вибраний розмір. У Typst 0.15.x їх можна переглянути за допомогою typst eval:
typst eval 'query(<name-fit>).map(it => it.value)' --in business-card.typ --ignore-system-fonts --prettyКоли будеш готовий друкувати цілу партію, розширений проєкт візиток запропонує більше можливостей: перевірку вхідних даних, QR-коди, припуски під обрізання та розміщення візиток на аркушах A4 для двобічного друку. Процес описано в його README.md. Невеликого прикладу вище достатньо, щоб зрозуміти, як один дизайн перетворюється на візитку для кожної людини.
Щоб надрукований QR-код залишався корисним після зміни контактних даних, закодуй у ньому сталу адресу профілю, якою керуєш. За потреби оновлюй цільову сторінку. Надрукований код залишається тим самим; PDF сам себе не оновлює.
Ту саму функцію підбору розміру тексту можна використати для бейджів, етикеток або сертифікатів. Задай найменший читабельний розмір і повідомляй про ім’я, яке не вміщується, щоб вирішити, як із ним бути.
Складані настільні таблички для конференції
Наступного тижня в тебе конференція, і кожному доповідачеві потрібна табличка з іменем на стіл. Хочеться, щоб люди по обидва боки могли прочитати ім’я. Таблички мають стояти самостійно, а надрукувати їх потрібно на звичайному принтері.
Можемо створити пласку розгортку, яка складається в трикутну підставку. На двох панелях буде ім’я, третя стане основою, а невеликий клапан утримуватиме форму. Почни з імені одного доповідача й перевір складання, перш ніж готувати решту.
Використаємо такі розміри:
| Частина | Розміри |
|---|---|
| Кожна з трьох панелей | 180 × 45 мм |
| Клапан для склеювання | 180 × 10 мм |
| Повний прямокутник для вирізання | 180 × 145 мм |
| Сторінка документа | 200 × 175 мм, включно з полями та інструкціями |
Дві надруковані сторони з’єднуються біля верхнього згину готової підставки. Тому на пласкому аркуші одна з них має бути перевернута догори дриґом, щоб після складання обидві читалися правильно.
Розмісти елементи таблички до складання
#let panel(name, subtitle) = block(width: width, height: face,
fill: rgb("#EFF4F3"), align(center + horizon)[
#text(size: 28pt, weight: "bold", name)
#v(2mm)
#text(size: 11pt, subtitle)
])
#block(width: width, height: 3 * face + flap, {
// The two panels share the apex fold; the upper face is upside down flat.
place(top + left, rotate(180deg, reflow: true,
panel("Ada Nováková", "Research / FIELD STUDIO")))
place(top + left, dy: face,
panel("Ada Nováková", "Research / FIELD STUDIO"))
place(top + left, dy: 2 * face,
block(width: width, height: face, align(center + horizon)[
#text(size: 9pt)[BASE / fold into a triangular stand]
]))
place(top + left, dy: 3 * face,
block(width: width, height: flap, fill: rgb("#D5E5E1"),
align(center + horizon)[#text(size: 7pt)[GLUE TAB]]))
for y in (face, 2 * face, 3 * face) {
place(top + left, dy: y,
line(end: (width, 0mm), stroke: (paint: rgb("#087F8C"), thickness: 0.6pt, dash: "dashed")))
}
place(top + left, rect(width: width, height: 3 * face + flap,
fill: none, stroke: 0.7pt + rgb("#183343")))
}) Функція rotate(180deg, reflow: true, ...) перевертає першу панель з іменем на аркуші догори дриґом. Після складання її можна буде читати правильно з іншого боку підставки. Обертаємо всю панель, тому її розташування не залежить від довжини імені. Лінії згину використовують те саме значення face, що й панелі, тож залишаються узгодженими з ними.
Суцільний контур позначає межу вирізання. Пунктирні лінії — місця згину. Надрукуй PDF у фактичному розмірі / 100 %, виріж по контуру, зігни вздовж трьох пунктирних ліній і приклей клапан зсередини до протилежного вільного краю. Спочатку спробуй звичайний папір, а вже потім друкуй партію на цупкішому. Геометрію згенерованої розгортки перевірено; фізичне складання ще варто випробувати на справжньому прототипі.
Коли зразок стоятиме як слід, заміни ім’я й підзаголовок для кожного доповідача. На обох надрукованих сторонах мають бути однакові значення. Для більшої конференції помісти всю підставку у функцію й викликай її для кожного імені, додаючи розрив сторінки між підставками. Отримаєш той самий процес пакетного створення, що й для візиток.
Так можна зробити й номери столів, таблички для робочих станцій на майстер-класі або невеликі підписи до експонатів. Якщо змінюєш розміри панелей, перевір також розмір сторінки й ділянку для тексту. Більші панелі автоматично не збільшують текст.
Посібник з усунення несправностей із переходами між сторінками
Ти допомагаєш організовувати роботу невеликого офісу, і колеги постійно запитують, що робити, коли спільний принтер перестає працювати. Довгий посібник легко відкласти. Потрібна коротка інструкція, яка ставить запитання й одразу веде читача до потрібних дій.
PDF може це робити за допомогою посилань між власними сторінками. На першій сторінці запитуємо, чи ввімкнений дисплей принтера. Натискання на відповідь відкриває відповідну сторінку; кожна сторінка з результатом містить посилання на початок. Файлом можна користуватися без інтернету в PDF-переглядачі, який підтримує внутрішні посилання.
Задай ціль переходу для кожного кроку
#let choice(destination, body) = block(width: 100%, spacing: 3mm,
link(destination, block(width: 100%, inset: 4mm,
radius: 2mm, fill: rgb("#EAF3EF"), body))) Функція choice перетворює текстову панель на посилання. Його ціль — мітка на зразок <power> або <queue>, додана до заголовка в іншій частині документа. Typst сам визначає потрібну сторінку. Можна додати обкладинку або перенести розділ, не переписуючи номери сторінок у посиланнях.
Ось початкове запитання:
= The printer is not printing <start>
Is the printer's display on?
#choice(<queue>)[Yes — check the print queue]
#choice(<power>)[No — check the power]Повний вихідний код містить цільові сторінки й додатковий вибір для випадку, коли проблему не вдалося усунути. Скомпілюй його в PDF і натисни на кожну відповідь, щоб перевірити, куди вона веде. Зображення вище показує вигляд; робочі посилання містяться в експортованому PDF.
Так читач обирає власний шлях посібником. Натискання не зберігає відповіді й не запускає програму. Для цього потрібен інший інструмент. Внутрішніх посилань достатньо для довідки без інтернету, музейного завдання або історії, у якій читач вирішує, що станеться далі.
Робочий аркуш для учнів і примірник для вчителя
Уяви, що викладаєш математику й готуєш вправи до завтрашнього уроку. Учням потрібен робочий аркуш із місцем для записів. Тобі — ті самі запитання з відповідями під ними, щоб перевіряти роботу під час заняття.
Можна підтримувати два документи, але тоді кожне виправлення доведеться вносити двічі. Натомість запишемо кожне запитання разом із відповіддю. Typst створить учнівський примірник із порожніми ділянками для розв’язання та вчительський — із розв’язками. Додавання запитання має оновити нумерацію в обох.
Запиши запитання й відповідь разом
#let exercise(prompt, answer) = {
number.step()
block(breakable: false, spacing: 5mm, [
#context text(weight: "bold")[Problem #number.display()]
#v(2mm)
#prompt
#v(3mm)
#if edition == "answers" {
block(inset: 4mm, width: 100%, fill: rgb("#EAF3EF"), answer)
} else {
rect(width: 100%, height: 22mm, fill: none,
stroke: (paint: rgb("#A2B5B8"), dash: "dotted"))
}
#context [#metadata((edition: edition, number: number.get().first())) <exercise-record>]
])
} У повному файлі значення edition задається через sys.inputs і перевіряється на відповідність student або answers. Функція exercise приймає два блоки вмісту, тож в обох аргументах можуть бути як звичайний текст, так і математичні вирази.
Лічильник counter збільшується, коли додаємо вправу. Typst читає його значення в тому місці, де друкується номер, тому запитання залишаються в правильному порядку. Налаштування breakable: false утримує кожне запитання разом із ділянкою для розв’язання або відповіддю на одній сторінці.
Ось як записано одну з вправ у повному файлі:
#exercise[Differentiate $f(x) = x^3$.][
By the power rule, $f'(x) = 3x^2$.
]Створи обидва документи:
typst compile workbook.typ student.pdf --input edition=student --ignore-system-fonts
typst compile workbook.typ answers.pdf --input edition=answers --ignore-system-fontsПорівняй учнівську версію та версію з відповідями
Спробуй: встав нову вправу між першими двома й створи обидва примірники. Номери запитань мають оновитися в обох, а формулювання — збігатися. Розриви сторінок можуть відрізнятися, адже відповідь і порожня ділянка для розв’язання потребують різної кількості місця.
Ділянка заввишки 22 мм підходить для цих коротких вправ. Для довших завдань зроби її висоту параметром. Якщо вправа може займати кілька сторінок, зміни правило breakable: false, замість того щоб силоміць уміщувати великий блок на маленькій сторінці.
Так само можна створити робочий зошит для майстер-класу й примірник для ведучого або книжку головоломок та їхніх розв’язків. Тримай запитання разом із відповідями, а вибір версії для показу доручи функції.
Діаграма, яка допомагає зрозуміти рівняння
На курсі фізики ти вивчаєш коливання. Розумієш, що тертя поступово сповільнює систему, яка рухається, але самі рівняння не дають наочно уявити її поведінку. Хочеться додати до конспекту діаграму, яку можна змінювати, порівнюючи різні рівні демпфування.
Використаємо просту модель осцилятора з демпфуванням. Тут x — положення, y — швидкість, а γ визначає демпфування:
Намалюємо стрілки, що показують напрям руху системи за різних поєднань положення та швидкості. Також побудуємо траєкторію з однієї початкової точки. Зміна γ оновлює і малюнок, і показане значення параметра, тож діаграма в конспекті залишається пов’язаною з моделлю.
Перетвори модель на малюнок
#let damping = float(sys.inputs.at("damping", default: "0.35"))
#assert(damping >= 0 and damping <= 2, message: "damping must be 0..2")
#let field(x, y) = (y, -x - damping * y)
#let point(x, y) = (50mm + x * 13mm, 50mm - y * 13mm)
#let arrow(x, y, dx, dy) = {
let norm = calc.sqrt(dx * dx + dy * dy)
if norm > 0 {
let (px, py) = point(x, y)
let (u, v) = (dx / norm, -dy / norm)
let shaft = 3.5mm
let (ex, ey) = (px + u * shaft, py + v * shaft)
place(top + left, dx: px, dy: py,
line(end: (u * shaft, v * shaft), stroke: 0.6pt + rgb("#087F8C")))
place(top + left, polygon(
(ex, ey),
(ex - u * 1.2mm + v * 0.7mm, ey - v * 1.2mm - u * 0.7mm),
(ex - u * 1.2mm - v * 0.7mm, ey - v * 1.2mm + u * 0.7mm),
fill: rgb("#087F8C"), stroke: none,
))
}
} Функція field повертає дві похідні. Функція point переводить абстрактну координату в міліметри. У другій складовій вона віднімає y: у математичній системі координат y зростає вгору, а на сторінці — вниз.
Усі стрілки мають однакову довжину. Вони показують, у якому напрямку рухається система, а не як швидко. У точці (0, 0) система перебуває в спокої, тому стрілку пропускаємо. Стрижень кожної стрілки малюємо за допомогою line, а трикутний наконечник — через polygon, використовуючи вбудовані інструменти малювання Typst.
Повний файл обчислює значення у вузлах сітки, а потім додає траєкторію:
// Explicit Euler samples: an illustrative path, not a high-accuracy solver.
let (x, y) = (2.7, 0.0)
let trajectory = (point(x, y),)
for _ in range(450) {
let (dx, dy) = field(x, y)
(x, y) = (x + 0.025 * dx, y + 0.025 * dy)
trajectory.push(point(x, y))
}
place(top + left, curve(stroke: 1pt + rgb("#B65C32"),
curve.move(trajectory.first()),
..trajectory.slice(1).map(p => curve.line(p)))) Щоб намалювати помаранчеву криву, починаємо з однієї точки й багато разів робимо невеликий крок у напрямку, заданому рівняннями. Це називається інтегруванням явним методом Ейлера. Його легко простежити у вихідному коді, але точність залежить від розміру кроку. Без демпфування він навіть може спричиняти штучне віддалення траєкторії від центра. Тут використовуй його, щоб дослідити зображення; якщо результати залежать від точної траєкторії, застосуй відповідний чисельний метод розв’язання й перевір збіжність.
Порівняй значення параметра:
typst compile vector-field.typ weak.pdf --input damping=0.15 --ignore-system-fonts
typst compile vector-field.typ strong.pdf --input damping=0.8 --ignore-system-fontsРозмісти обидва результати поруч у конспекті. Як швидко кожна траєкторія наближається до центра? Що зміниться, якщо встановити демпфування на нуль? Ці питання легше досліджувати, коли можна змінити модель і відразу перемалювати її. Для більшої симуляції обчислюй результати поза Typst та імпортуй їх у документ.
Пристосуй приклади до своїх потреб
Шість проєктів використовують різні можливості того самого інструмента:
| Коли документ має… | Використовуй… |
|---|---|
| Створювати набір споріднених графічних форм | Функції, цикли та вбудовані криві |
| Уміщувати текст різної довжини у фіксований простір | Контекст, вимірювання та явно задані просторові обмеження |
| Стати фізичним предметом | Міліметри, панелі з фіксованими розмірами та перетворення |
| Дозволяти читачеві обирати наступну сторінку | Мітки та внутрішні посилання |
| Створювати пов’язані версії | Блоки вмісту, лічильники та перевірені вхідні дані |
| Пояснювати математичну модель, що змінюється | Обчислення та перетворення координат |
Не обов’язково використовувати всі шість ідей в одному проєкті. Обери ту, що найближча до твоєї потреби, завантаж вихідний код і зміни щось одне: ім’я, колір, вправу або параметр моделі. Скомпілюй знову й подивися, що змінилося.
Потім перевір результат там, де його використовуватимуть. Переглянь обкладинку в остаточному розмірі. Надрукуй і склади табличку. Пройди всіма шляхами в PDF-посібнику. Після зміни запитання створи обидва примірники робочого аркуша. Для математичного малюнка перевір не лише вигляд, а й обчислення.
Коли приклад працюватиме так, як потрібно, перенеси корисну функцію в окремий файл. Її можна буде використати в наступному документі без старих налаштувань сторінки чи зразкового вмісту. Так невеликий експеримент стає тим, до чого повертаєшся знову.
Читайте далі
Ще кілька статей, які можуть вас зацікавити.