rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package www
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "io/fs"
8 "math"
9 "strings"
10
11 "amadan.net/rastrillo/fiesta"
12)
13
14// landingTemplate is the landing page's one template, under the site
15// source. Every language renders from it with its own catalog.
16const landingTemplate = "landing/page.html.tmpl"
17
18// page is what the template sees: the language and its direction for
19// the html element, the site's Origin for a URL that has to be fully
20// qualified, the version the footer shows, where the design system is
21// in the page's language, the still bunting, every language the page
22// is built in for the hreflang links and the language menu, and the
23// catalog behind T.
24type page struct {
25 Lang, Dir, Origin, Version string
26 DesignSystem string
27 Bunting template.HTML
28 Languages []menuLanguage
29 strings map[string]string
30}
31
32// menuLanguage is one entry of the language menu and one hreflang
33// link: a language's code, its name in its own language, where its
34// page lives, and whether it is the page being rendered.
35type menuLanguage struct {
36 Code, Name, URL string
37 Current bool
38}
39
40// endonyms names each language in its own language, as Fiesta's
41// locale-menu example does (examples/locale-menu/twelve-languages-
42// current-highlighted.html), so the site's menu and Fiesta's agree.
43// TestEveryPageLinksEveryLanguage holds the menu to the example.
44var endonyms = map[string]string{
45 "en": "English",
46 "ar": "العربية",
47 "bn": "বাংলা",
48 "es": "Español",
49 "ga": "Gaeilge",
50 "hi": "हिन्दी",
51 "ja": "日本語",
52 "pt-PT": "Português",
53 "ru": "Русский",
54 "vi": "Tiếng Việt",
55 "zh-Hans": "简体中文",
56}
57
58// pagePath is where the landing page in lang lives in the tree:
59// English at the root, every other language under its code, so each
60// is a real page that a reader can bookmark and that needs no script.
61func pagePath(lang string) string {
62 if lang == "en" {
63 return "index.html"
64 }
65 return lang + "/index.html"
66}
67
68// pageURL is the same page's URL, a directory the edge serves its
69// index from.
70func pageURL(lang string) string {
71 if lang == "en" {
72 return "/"
73 }
74 return "/" + lang + "/"
75}
76
77// designSystemURL is where the landing page in lang links the design
78// system: its root for English, which is the default theme's English
79// Overview, and the default theme's Overview in lang for every other
80// language, so a reader arrives in the language they were reading.
81func designSystemURL(lang string) string {
82 if lang == "en" {
83 return Mount + "/"
84 }
85 return Mount + "/" + fiesta.ThemeNames()[0] + "/" + lang + "/"
86}
87
88// languages is the menu and hreflang list for the page in current, in
89// Languages' order, with the current one marked.
90func languages(current string) []menuLanguage {
91 out := make([]menuLanguage, 0, len(Languages))
92 for _, l := range Languages {
93 out = append(out, menuLanguage{Code: l, Name: endonyms[l], URL: pageURL(l), Current: l == current})
94 }
95 return out
96}
97
98// buntingGeometry is the bunting's shape in the SVG's own units, one
99// unit a pixel at scale 1: pennants FW wide at the top and FH to the
100// tip, a lattice NY rows deep, hung Tie below a rope that starts TopY
101// from the top, Pitch apart, Per to a swag with End clear at each
102// tack, Swags swags across a row H tall. The rest are the cloth's
103// physics: gravity G, velocity damping Damp, the cloth's Mass, Iter
104// constraint passes a step of DT, a velocity cap VMax, the rope's
105// inverse mass RopeInv, the bending stiffness Bend and the sewn tape's
106// Hem, the rope's Slack per link, RopePasses extra passes over the
107// rope, and the Hang in steps before the first frame.
108//
109// These are bunting.js's own numbers, so the still picture a reader
110// gets with no JS or under reduced motion is the hang the canvas opens
111// on: TestTheStillBuntingIsTheAnimations holds the geometry to the
112// script's constants, and TestTheStillBuntingMatchesTheMovingOne holds
113// the two pictures to each other.
114type buntingGeometry struct {
115 FW, FH, NY, TopY, Tie, Pitch, Per, End, Swags, H int
116 G, Damp, Mass, DT, VMax, RopeInv, Bend, Hem, Slack float64
117 Iter, RopePasses, Hang int
118}
119
120var buntingShape = buntingGeometry{
121 FW: 36, FH: 44, NY: 7, TopY: 4, Tie: 7, Pitch: 48, Per: 6, End: 34, Swags: 8, H: 96,
122 G: 650, Damp: 0.955, Mass: 1.15, DT: 1.0 / 60, VMax: 6, RopeInv: 0.25, Bend: 0.1, Hem: 0.7, Slack: 1.005,
123 Iter: 5, RopePasses: 6, Hang: 120,
124}
125
126// pennantFills are the six cloths, in the order they hang. They live
127// on the SVG as attributes, and bunting.js reads them from there, so
128// site.css carries Fiesta's tokens only.
129var pennantFills = []string{"#2257df", "#f2b632", "#e8553f", "#16a37f", "#7aa2ff", "#e84f8a"}
130
131// buntingRest is the bunting hung at rest: the rope's nodes in order,
132// and for each pennant the rope nodes its two ties hang from and its
133// two top corners.
134type buntingRest struct {
135 ropeX, ropeY []float64
136 flags []pennantRest
137}
138
139type pennantRest struct {
140 tieL, tieR [2]float64 // the rope nodes the ties hang from
141 cornerL, cornerR [2]float64 // the cloth's top corners
142}
143
144// rest hangs the bunting as bunting.js does before its first frame,
145// with the same rope, ties and cloth lattices, the same Verlet step
146// and the same constraint passes in the same order, Hang steps under
147// gravity and nothing else. The script's hang is calm (no wind), so
148// the two land on the same pose; it keeps x and y only, since with no
149// wind nothing ever leaves the plane.
150func (g buntingGeometry) rest() buntingRest {
151 span := g.Pitch*g.Per + 2*g.End
152 row := span * g.Swags
153 fw, tie, topY := float64(g.FW), float64(g.Tie), float64(g.TopY)
154 dx, dy := fw/float64(g.NY-1), float64(g.FH)/float64(g.NY-1)
155 dd := math.Hypot(dx/2, dy)
156
157 // The rope, span by span: a tack, then for each pennant a node over
158 // each top corner and one between them, a node in each gap, and
159 // the closing tack.
160 var rx []float64
161 var ties [][2]int
162 for s := 0; s < g.Swags; s++ {
163 x0 := float64(s * span)
164 rx = append(rx, x0, x0+float64(g.End)/2)
165 for p := 0; p < g.Per; p++ {
166 cx := x0 + float64(g.End+g.Pitch*p) + float64(g.Pitch)/2
167 ties = append(ties, [2]int{len(rx), len(rx) + 2})
168 rx = append(rx, cx-fw/2, cx, cx+fw/2)
169 if p < g.Per-1 {
170 rx = append(rx, cx+float64(g.Pitch)/2)
171 } else {
172 rx = append(rx, x0+float64(span)-float64(g.End)/2)
173 }
174 }
175 }
176 rx = append(rx, float64(row))
177 ropeN := len(rx)
178 n := ropeN + len(ties)*g.NY*(g.NY+1)/2
179 X, Y, PX, PY, INV := make([]float64, n), make([]float64, n), make([]float64, n), make([]float64, n), make([]float64, n)
180 // cons is a, b, length, stiffness, four per constraint, in the
181 // order the script solves them.
182 var cons []float64
183 for i, x := range rx {
184 X[i], PX[i], Y[i], PY[i] = x, x, topY, topY
185 if math.Mod(x, float64(span)) != 0 {
186 INV[i] = g.RopeInv
187 }
188 if i > 0 {
189 cons = append(cons, float64(i-1), float64(i), (x-rx[i-1])*g.Slack, 1)
190 }
191 }
192 idx := ropeN
193 var corners [][2]int
194 for k, t := range ties {
195 cx := float64(g.End+(k%g.Per)*g.Pitch) + float64(g.Pitch)/2 + float64((k/g.Per)*span)
196 rows := make([][]int, g.NY)
197 for j := 0; j < g.NY; j++ {
198 nj, w := g.NY-j, fw-dx*float64(j)
199 for i := 0; i < nj; i++ {
200 X[idx], PX[idx] = cx-w/2+dx*float64(i), cx-w/2+dx*float64(i)
201 Y[idx], PY[idx] = topY+tie+dy*float64(j), topY+tie+dy*float64(j)
202 INV[idx] = 1
203 rows[j] = append(rows[j], idx)
204 idx++
205 }
206 }
207 cons = append(cons, float64(t[0]), float64(rows[0][0]), tie, 1, float64(t[1]), float64(rows[0][g.NY-1]), tie, 1, float64(rows[0][0]), float64(rows[0][g.NY-1]), fw, g.Hem)
208 for j := 0; j < g.NY; j++ {
209 rj := rows[j]
210 nj := len(rj)
211 for i := 0; i < nj; i++ {
212 a := float64(rj[i])
213 if i < nj-1 {
214 cons = append(cons, a, float64(rj[i+1]), dx, 1)
215 }
216 if j < g.NY-1 {
217 rb := rows[j+1]
218 if i > 0 {
219 cons = append(cons, a, float64(rb[i-1]), dd, 1)
220 }
221 if i < len(rb) {
222 cons = append(cons, a, float64(rb[i]), dd, 1)
223 }
224 }
225 if j < g.NY-2 && i > 0 && i-1 < len(rows[j+2]) {
226 cons = append(cons, a, float64(rows[j+2][i-1]), 2*dy, g.Bend)
227 }
228 if i < nj-2 {
229 st := g.Bend
230 if j == 0 {
231 st = g.Hem
232 }
233 cons = append(cons, a, float64(rj[i+2]), 2*dx, st)
234 }
235 }
236 }
237 corners = append(corners, [2]int{rows[0][0], rows[0][g.NY-1]})
238 }
239
240 solve := func(a, b int, length, stiff float64) {
241 wa, wb := INV[a], INV[b]
242 ws := wa + wb
243 if ws == 0 {
244 return
245 }
246 ex, ey := X[b]-X[a], Y[b]-Y[a]
247 d := math.Sqrt(ex*ex + ey*ey)
248 if d == 0 {
249 d = 1e-6
250 }
251 s := (d - length) / d * stiff / ws
252 X[a] += ex * s * wa
253 Y[a] += ey * s * wa
254 X[b] -= ex * s * wb
255 Y[b] -= ey * s * wb
256 }
257 dt2 := g.DT * g.DT
258 for step := 0; step < g.Hang; step++ {
259 for id := 0; id < n; id++ {
260 if INV[id] == 0 {
261 continue
262 }
263 vx, vy := (X[id]-PX[id])*g.Damp, (Y[id]-PY[id])*g.Damp
264 if v2 := vx*vx + vy*vy; v2 > g.VMax*g.VMax {
265 k := g.VMax / math.Sqrt(v2)
266 vx, vy = vx*k, vy*k
267 }
268 PX[id], PY[id] = X[id], Y[id]
269 X[id] += vx
270 Y[id] += vy + g.G*dt2
271 }
272 for it := 0; it < g.Iter; it++ {
273 for k := 0; k < len(cons); k += 4 {
274 solve(int(cons[k]), int(cons[k+1]), cons[k+2], cons[k+3])
275 }
276 for r := 0; r < g.RopePasses; r++ {
277 for k := 0; k < (ropeN-1)*4; k += 4 {
278 solve(int(cons[k]), int(cons[k+1]), cons[k+2], 1)
279 }
280 }
281 }
282 }
283
284 out := buntingRest{ropeX: X[:ropeN], ropeY: Y[:ropeN]}
285 for k, c := range corners {
286 t := ties[k]
287 out.flags = append(out.flags, pennantRest{
288 tieL: [2]float64{X[t[0]], Y[t[0]]}, tieR: [2]float64{X[t[1]], Y[t[1]]},
289 cornerL: [2]float64{X[c[0]], Y[c[0]]}, cornerR: [2]float64{X[c[1]], Y[c[1]]},
290 })
291 }
292 return out
293}
294
295// stillBunting draws the bunting at rest as an SVG: one pennant
296// definition per cloth, origin at the middle of its top edge, with a
297// sewn tape and a stitch line along the top; the rope through every
298// node of the hang; and each pennant's two ties and the pennant
299// itself, translated to its top edge's middle and turned to its tilt.
300// The classes are what site.css colours (the rope and ties are a
301// Fiesta token) and what bunting.js reads.
302func stillBunting(g buntingGeometry) template.HTML {
303 span := g.Pitch*g.Per + 2*g.End
304 half := float64(g.FW) / 2
305 hang := g.rest()
306 var b strings.Builder
307 fmt.Fprintf(&b, `<svg class="site-flags" viewBox="0 0 %d %d" preserveAspectRatio="xMidYMin slice" focusable="false">`+"\n<defs>\n", span*g.Swags, g.H)
308 for k, fill := range pennantFills {
309 fmt.Fprintf(&b, `<g id="site-p%d"><polygon fill="%s" points="%g,0 %g,0 0,%d"/><polygon fill="#000" opacity=".16" points="%g,0 %g,0 %.1f,3.2 %.1f,3.2"/><path fill="none" stroke="#fff" stroke-width=".35" stroke-dasharray=".9 .7" opacity=".45" d="M%.1f 3.6H%.1f"/></g>`+"\n",
310 k, fill, -half, half, g.FH, -half, half, half-1.3, -(half - 1.3), -(half - 1.5), half-1.5)
311 }
312 b.WriteString("</defs>\n")
313 b.WriteString(`<path class="site-span" d="`)
314 for i := range hang.ropeX {
315 if i == 0 {
316 fmt.Fprintf(&b, "M%.2f %.2f", hang.ropeX[i], hang.ropeY[i])
317 } else {
318 fmt.Fprintf(&b, "L%.2f %.2f", hang.ropeX[i], hang.ropeY[i])
319 }
320 }
321 b.WriteString("\"/>\n")
322 for k, f := range hang.flags {
323 fmt.Fprintf(&b, `<path class="site-tie" d="M%.2f %.2fL%.2f %.2fM%.2f %.2fL%.2f %.2f"/>`+"\n",
324 f.tieL[0], f.tieL[1], f.cornerL[0], f.cornerL[1], f.tieR[0], f.tieR[1], f.cornerR[0], f.cornerR[1])
325 mx, my := (f.cornerL[0]+f.cornerR[0])/2, (f.cornerL[1]+f.cornerR[1])/2
326 tilt := math.Atan2(f.cornerR[1]-f.cornerL[1], f.cornerR[0]-f.cornerL[0]) * 180 / math.Pi
327 fmt.Fprintf(&b, `<g class="site-flag" data-c="%d" transform="translate(%.2f %.2f) rotate(%.2f)"><use href="#site-p%d"/></g>`+"\n", k%len(pennantFills), mx, my, tilt, k%len(pennantFills))
328 }
329 b.WriteString("</svg>")
330 return template.HTML(b.String())
331}
332
333// T is one catalog string by key. A key the catalog lacks is an error,
334// so a typo in the template fails the render rather than rendering an
335// empty element.
336func (p page) T(key string) (string, error) {
337 s, ok := p.strings[key]
338 if !ok {
339 return "", fmt.Errorf("the landing page asks for %q, which the catalog does not have", key)
340 }
341 return s, nil
342}
343
344// landing renders the landing page in lang from the site source: the
345// template at landing/page.html.tmpl filled from Strings' catalog, with
346// the version from fiesta.Version and every language's name and URL
347// for the menu. html/template escapes every string for where it lands,
348// so the install line's < reads as itself. A lang outside Languages
349// has no page, and is refused before its catalog is looked for.
350func landing(siteDir fs.FS, lang string) ([]byte, error) {
351 if _, ok := endonyms[lang]; !ok {
352 return nil, fmt.Errorf("www: %s is not a language the landing page is built in", lang)
353 }
354 strings, err := Strings(siteDir, lang)
355 if err != nil {
356 return nil, err
357 }
358 src, err := fs.ReadFile(siteDir, landingTemplate)
359 if err != nil {
360 return nil, fmt.Errorf("www: reading the landing page's template: %w", err)
361 }
362 tmpl, err := template.New(landingTemplate).Parse(string(src))
363 if err != nil {
364 return nil, fmt.Errorf("www: %w", err)
365 }
366 var buf bytes.Buffer
367 if err := tmpl.Execute(&buf, page{Lang: lang, Dir: Dir(lang), Origin: Origin, Version: fiesta.Version, DesignSystem: designSystemURL(lang), Bunting: stillBunting(buntingShape), Languages: languages(lang), strings: strings}); err != nil {
368 return nil, fmt.Errorf("www: rendering the landing page in %s: %w", lang, err)
369 }
370 return buf.Bytes(), nil
371}
372