Handwritten notebook style template for Polylux
1#import "@preview/polylux:0.4.0": *
2#import "@preview/suiji:0.4.0"
3#import "@preview/umbra:0.1.1"
4
5#let highlight-color-state = state("jotter-highlight-color", red)
6
7#let setup(
8 header: none,
9 highlight-color: red,
10 binding: true,
11 dots: true,
12 body,
13) = {
14 highlight-color-state.update(highlight-color)
15 set page(
16 paper: "presentation-16-9",
17 margin: (left: 2.3cm, top: 1cm, rest: 1cm),
18 fill: if dots {
19 tiling(
20 spacing: (5mm, 5mm),
21 {
22 place(square(width: 6mm, stroke: none, fill: white))
23 circle(radius: 1pt, fill: white.darken(10%))
24 },
25 )
26 },
27 background: if binding {
28 set align(top + left)
29 let gap = 1.5cm
30 let color = rgb("8aa")
31 place(rect(height: 100%, width: 5mm, stroke: none, fill: black))
32 place(dx: 5mm, rect(height: 100%, width: 5mm, stroke: none, fill: white))
33 for offset in range(20) {
34 let spiral(t, p) = curve(
35 stroke: (thickness: t, paint: p),
36 curve.move((.5cm, 1cm)),
37 curve.quad((-.5cm, 1.1cm), (1cm, 1.2cm)),
38 )
39 place(dy: offset * gap, spiral(2pt, color))
40 place(
41 dy: offset * gap - .2mm,
42 dx: -.2mm,
43 spiral(.5pt, color.darken(50%)),
44 )
45 place(
46 dx: 1cm - .5mm,
47 dy: offset * gap + 1.2cm - 1.5mm,
48 circle(radius: 1.5mm, stroke: color + .3mm, fill: color.darken(70%)),
49 )
50 }
51 },
52 footer: context {
53 set align(right)
54 set text(size: .6em, fill: text.fill.transparentize(30%))
55 context box(
56 curve(
57 stroke: (thickness: .05em, cap: "round", paint: text.fill),
58 curve.quad((.8em, -.9em), (1em, -2em)),
59 ),
60 )
61 toolbox.slide-number
62 },
63 header: context if header != none {
64 set align(right)
65 set text(size: .6em, fill: text.fill.transparentize(30%))
66 set par(spacing: .5em)
67 header
68 context {
69 let w = measure(header).width
70 curve(
71 stroke: (thickness: .05em, cap: "round", paint: text.fill),
72 curve.quad((.25 * w, -.3em), (1.2 * w, 0em)),
73 )
74 }
75 },
76 )
77 show emph: it => underline(
78 stroke: stroke(
79 thickness: .3em,
80 paint: highlight-color.transparentize(50%),
81 cap: "round",
82 ),
83 offset: -.1em,
84 extent: .1em,
85 evade: false,
86 background: true,
87 it.body,
88 )
89 show heading.where(level: 1): it => layout(sz => {
90 let w = measure(..sz, it).width
91 it
92 move(
93 dy: -.8em,
94 curve(
95 stroke: (thickness: .04em, cap: "round", paint: text.fill),
96 fill: text.fill,
97 curve.quad((.75 * w, -.5em), (w, 0em)),
98 curve.quad((w + .05em, .03em), (w, .06em)),
99 curve.quad((.75 * w, -.495em), (0em, 0em)),
100 ),
101 )
102 })
103 set list(marker: text(fill: highlight-color.lighten(10%), sym.bullet))
104 set enum(
105 numbering: (..n) => text(
106 fill: highlight-color.lighten(10%),
107 numbering("1.", ..n),
108 ),
109 )
110 body
111}
112
113#let title-slide(title, extra) = slide({
114 set align(center + horizon)
115 set page(footer: none, header: none)
116
117 if title != none {
118 set text(1.5em)
119 title
120 context {
121 let w = measure(title).width
122 let c(p) = curve(
123 stroke: (paint: p, thickness: .1em, cap: "round"),
124 curve.cubic((.2 * w, -.4em), (.95 * w, -.5em), (w + 1em, 0em)),
125 curve.cubic(auto, (w + .5em, .1em), (w, .3em)),
126 )
127 v(-.5em)
128 let hc = highlight-color-state.get()
129 place(center, c(hc.transparentize(20%)))
130 place(center, dx: .1em, dy: .1em, c(hc.transparentize(50%)))
131 }
132 }
133 par(hide[42])
134
135 extra
136})
137
138#let len2int(l) = int.from-bytes(l.pt().to-bytes())
139#let hasher(acc, val) = acc.bit-xor(val)
140#let hashed-position() = {
141 let (x, y) = here().position()
142 let n = counter("logical-slide").get()
143 (len2int(x), len2int(y), ..n).reduce(hasher)
144}
145
146#let framed-block(
147 body,
148 sloppiness: .05,
149 inset: .5em,
150 width: auto,
151 height: auto,
152) = layout(sz => {
153 let blocked-body = block(
154 inset: inset,
155 width: width,
156 height: height,
157 fill: none,
158 stroke: none,
159 body,
160 )
161 let (width: w, height: h) = measure(..sz, blocked-body)
162 let deflect = sloppiness * (w + h) / 2
163
164 let hash = (hashed-position(), len2int(w), len2int(h)).reduce(hasher)
165 let rng = suiji.gen-rng-f(hash)
166 let (rng, c1) = suiji.random-f(rng)
167 let (rng, c2) = suiji.random-f(rng)
168 let (rng, c3) = suiji.random-f(rng)
169 let (rng, c4) = suiji.random-f(rng)
170 let (rng, s1) = suiji.choice-f(rng, (-1, 1))
171 let (rng, s2) = suiji.choice-f(rng, (-1, 1))
172 let (rng, s3) = suiji.choice-f(rng, (-1, 1))
173 let (rng, s4) = suiji.choice-f(rng, (-1, 1))
174
175 let c(p) = curve(
176 stroke: stroke(thickness: .1em, cap: "round", paint: p),
177 curve.quad((c1 * w, s1 * deflect), (w + deflect / 3, 0pt)),
178 curve.move((w - deflect / 3, -deflect / 3)),
179 curve.quad((w + s2 * deflect, c2 * h), (w - deflect / 3, h + deflect / 3)),
180 curve.move((w + deflect / 3, h - deflect / 3)),
181 curve.quad((c3 * w, h + s3 * deflect), (0pt, h)),
182 curve.quad((s4 * deflect, c4 * h), (0pt, 0pt)),
183 )
184 place(dx: .2em, dy: .15em, c(text.fill.transparentize(50%)))
185 place(c(text.fill.transparentize(20%)))
186 blocked-body
187})
188
189#let post-it(
190 body,
191 fill: rgb("#FDE85F"),
192 angle: -10deg,
193 sloppiness: .2,
194) = context {
195 let hash = hashed-position()
196 let rng = suiji.gen-rng-f(hash)
197 let (rng, angle-deviation) = suiji.uniform-f(rng, low: -1, high: 1)
198 let angle = (1 + sloppiness * angle-deviation) * angle
199 let (rng, color-deviation) = suiji.uniform-f(rng, low: -1, high: 1)
200 let fill = fill.darken(sloppiness * color-deviation * 40%)
201
202 rotate(
203 angle,
204 reflow: false,
205 {
206 place(
207 dx: .0em,
208 dy: .0em,
209 umbra.shadow-path(
210 (1cm, 1cm),
211 (1cm, 5cm),
212 (5cm, 5cm),
213 (5cm, 4.9cm),
214 (4cm, 1cm),
215 correction: 10deg,
216 closed: false,
217 ),
218 )
219 place(
220 curve(
221 fill: fill,
222 curve.line((5cm, 0cm)),
223 curve.quad((5cm, 2.5cm), (5.2cm, 5cm)),
224 curve.quad((2.5cm, 5.2cm), (.1cm, 5cm)),
225 curve.quad((0cm, 2.5cm), (0cm, 0cm)),
226 ),
227 )
228 box(inset: .5em, width: 5cm, height: 5cm, clip: true, body)
229 },
230 )
231}