package www
import (
"bytes"
"fmt"
"html/template"
"io/fs"
"math"
"strings"
"amadan.net/rastrillo/fiesta"
)
// landingTemplate is the landing page's one template, under the site
// source. Every language renders from it with its own catalog.
const landingTemplate = "landing/page.html.tmpl"
// page is what the template sees: the language and its direction for
// the html element, the site's Origin for a URL that has to be fully
// qualified, the version the footer shows, where the design system is
// in the page's language, the still bunting, every language the page
// is built in for the hreflang links and the language menu, and the
// catalog behind T.
type page struct {
Lang, Dir, Origin, Version string
DesignSystem string
Bunting template.HTML
Languages []menuLanguage
strings map[string]string
}
// menuLanguage is one entry of the language menu and one hreflang
// link: a language's code, its name in its own language, where its
// page lives, and whether it is the page being rendered.
type menuLanguage struct {
Code, Name, URL string
Current bool
}
// endonyms names each language in its own language, as Fiesta's
// locale-menu example does (examples/locale-menu/twelve-languages-
// current-highlighted.html), so the site's menu and Fiesta's agree.
// TestEveryPageLinksEveryLanguage holds the menu to the example.
var endonyms = map[string]string{
"en": "English",
"ar": "العربية",
"bn": "বাংলা",
"es": "Español",
"ga": "Gaeilge",
"hi": "हिन्दी",
"ja": "日本語",
"pt-PT": "Português",
"ru": "Русский",
"vi": "Tiếng Việt",
"zh-Hans": "简体中文",
}
// pagePath is where the landing page in lang lives in the tree:
// English at the root, every other language under its code, so each
// is a real page that a reader can bookmark and that needs no script.
func pagePath(lang string) string {
if lang == "en" {
return "index.html"
}
return lang + "/index.html"
}
// pageURL is the same page's URL, a directory the edge serves its
// index from.
func pageURL(lang string) string {
if lang == "en" {
return "/"
}
return "/" + lang + "/"
}
// designSystemURL is where the landing page in lang links the design
// system: its root for English, which is the default theme's English
// Overview, and the default theme's Overview in lang for every other
// language, so a reader arrives in the language they were reading.
func designSystemURL(lang string) string {
if lang == "en" {
return Mount + "/"
}
return Mount + "/" + fiesta.ThemeNames()[0] + "/" + lang + "/"
}
// languages is the menu and hreflang list for the page in current, in
// Languages' order, with the current one marked.
func languages(current string) []menuLanguage {
out := make([]menuLanguage, 0, len(Languages))
for _, l := range Languages {
out = append(out, menuLanguage{Code: l, Name: endonyms[l], URL: pageURL(l), Current: l == current})
}
return out
}
// buntingGeometry is the bunting's shape in the SVG's own units, one
// unit a pixel at scale 1: pennants FW wide at the top and FH to the
// tip, a lattice NY rows deep, hung Tie below a rope that starts TopY
// from the top, Pitch apart, Per to a swag with End clear at each
// tack, Swags swags across a row H tall. The rest are the cloth's
// physics: gravity G, velocity damping Damp, the cloth's Mass, Iter
// constraint passes a step of DT, a velocity cap VMax, the rope's
// inverse mass RopeInv, the bending stiffness Bend and the sewn tape's
// Hem, the rope's Slack per link, RopePasses extra passes over the
// rope, and the Hang in steps before the first frame.
//
// These are bunting.js's own numbers, so the still picture a reader
// gets with no JS or under reduced motion is the hang the canvas opens
// on: TestTheStillBuntingIsTheAnimations holds the geometry to the
// script's constants, and TestTheStillBuntingMatchesTheMovingOne holds
// the two pictures to each other.
type buntingGeometry struct {
FW, FH, NY, TopY, Tie, Pitch, Per, End, Swags, H int
G, Damp, Mass, DT, VMax, RopeInv, Bend, Hem, Slack float64
Iter, RopePasses, Hang int
}
var buntingShape = buntingGeometry{
FW: 36, FH: 44, NY: 7, TopY: 4, Tie: 7, Pitch: 48, Per: 6, End: 34, Swags: 8, H: 96,
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,
Iter: 5, RopePasses: 6, Hang: 120,
}
// pennantFills are the six cloths, in the order they hang. They live
// on the SVG as attributes, and bunting.js reads them from there, so
// site.css carries Fiesta's tokens only.
var pennantFills = []string{"#2257df", "#f2b632", "#e8553f", "#16a37f", "#7aa2ff", "#e84f8a"}
// buntingRest is the bunting hung at rest: the rope's nodes in order,
// and for each pennant the rope nodes its two ties hang from and its
// two top corners.
type buntingRest struct {
ropeX, ropeY []float64
flags []pennantRest
}
type pennantRest struct {
tieL, tieR [2]float64 // the rope nodes the ties hang from
cornerL, cornerR [2]float64 // the cloth's top corners
}
// rest hangs the bunting as bunting.js does before its first frame,
// with the same rope, ties and cloth lattices, the same Verlet step
// and the same constraint passes in the same order, Hang steps under
// gravity and nothing else. The script's hang is calm (no wind), so
// the two land on the same pose; it keeps x and y only, since with no
// wind nothing ever leaves the plane.
func (g buntingGeometry) rest() buntingRest {
span := g.Pitch*g.Per + 2*g.End
row := span * g.Swags
fw, tie, topY := float64(g.FW), float64(g.Tie), float64(g.TopY)
dx, dy := fw/float64(g.NY-1), float64(g.FH)/float64(g.NY-1)
dd := math.Hypot(dx/2, dy)
// The rope, span by span: a tack, then for each pennant a node over
// each top corner and one between them, a node in each gap, and
// the closing tack.
var rx []float64
var ties [][2]int
for s := 0; s < g.Swags; s++ {
x0 := float64(s * span)
rx = append(rx, x0, x0+float64(g.End)/2)
for p := 0; p < g.Per; p++ {
cx := x0 + float64(g.End+g.Pitch*p) + float64(g.Pitch)/2
ties = append(ties, [2]int{len(rx), len(rx) + 2})
rx = append(rx, cx-fw/2, cx, cx+fw/2)
if p < g.Per-1 {
rx = append(rx, cx+float64(g.Pitch)/2)
} else {
rx = append(rx, x0+float64(span)-float64(g.End)/2)
}
}
}
rx = append(rx, float64(row))
ropeN := len(rx)
n := ropeN + len(ties)*g.NY*(g.NY+1)/2
X, Y, PX, PY, INV := make([]float64, n), make([]float64, n), make([]float64, n), make([]float64, n), make([]float64, n)
// cons is a, b, length, stiffness, four per constraint, in the
// order the script solves them.
var cons []float64
for i, x := range rx {
X[i], PX[i], Y[i], PY[i] = x, x, topY, topY
if math.Mod(x, float64(span)) != 0 {
INV[i] = g.RopeInv
}
if i > 0 {
cons = append(cons, float64(i-1), float64(i), (x-rx[i-1])*g.Slack, 1)
}
}
idx := ropeN
var corners [][2]int
for k, t := range ties {
cx := float64(g.End+(k%g.Per)*g.Pitch) + float64(g.Pitch)/2 + float64((k/g.Per)*span)
rows := make([][]int, g.NY)
for j := 0; j < g.NY; j++ {
nj, w := g.NY-j, fw-dx*float64(j)
for i := 0; i < nj; i++ {
X[idx], PX[idx] = cx-w/2+dx*float64(i), cx-w/2+dx*float64(i)
Y[idx], PY[idx] = topY+tie+dy*float64(j), topY+tie+dy*float64(j)
INV[idx] = 1
rows[j] = append(rows[j], idx)
idx++
}
}
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)
for j := 0; j < g.NY; j++ {
rj := rows[j]
nj := len(rj)
for i := 0; i < nj; i++ {
a := float64(rj[i])
if i < nj-1 {
cons = append(cons, a, float64(rj[i+1]), dx, 1)
}
if j < g.NY-1 {
rb := rows[j+1]
if i > 0 {
cons = append(cons, a, float64(rb[i-1]), dd, 1)
}
if i < len(rb) {
cons = append(cons, a, float64(rb[i]), dd, 1)
}
}
if j < g.NY-2 && i > 0 && i-1 < len(rows[j+2]) {
cons = append(cons, a, float64(rows[j+2][i-1]), 2*dy, g.Bend)
}
if i < nj-2 {
st := g.Bend
if j == 0 {
st = g.Hem
}
cons = append(cons, a, float64(rj[i+2]), 2*dx, st)
}
}
}
corners = append(corners, [2]int{rows[0][0], rows[0][g.NY-1]})
}
solve := func(a, b int, length, stiff float64) {
wa, wb := INV[a], INV[b]
ws := wa + wb
if ws == 0 {
return
}
ex, ey := X[b]-X[a], Y[b]-Y[a]
d := math.Sqrt(ex*ex + ey*ey)
if d == 0 {
d = 1e-6
}
s := (d - length) / d * stiff / ws
X[a] += ex * s * wa
Y[a] += ey * s * wa
X[b] -= ex * s * wb
Y[b] -= ey * s * wb
}
dt2 := g.DT * g.DT
for step := 0; step < g.Hang; step++ {
for id := 0; id < n; id++ {
if INV[id] == 0 {
continue
}
vx, vy := (X[id]-PX[id])*g.Damp, (Y[id]-PY[id])*g.Damp
if v2 := vx*vx + vy*vy; v2 > g.VMax*g.VMax {
k := g.VMax / math.Sqrt(v2)
vx, vy = vx*k, vy*k
}
PX[id], PY[id] = X[id], Y[id]
X[id] += vx
Y[id] += vy + g.G*dt2
}
for it := 0; it < g.Iter; it++ {
for k := 0; k < len(cons); k += 4 {
solve(int(cons[k]), int(cons[k+1]), cons[k+2], cons[k+3])
}
for r := 0; r < g.RopePasses; r++ {
for k := 0; k < (ropeN-1)*4; k += 4 {
solve(int(cons[k]), int(cons[k+1]), cons[k+2], 1)
}
}
}
}
out := buntingRest{ropeX: X[:ropeN], ropeY: Y[:ropeN]}
for k, c := range corners {
t := ties[k]
out.flags = append(out.flags, pennantRest{
tieL: [2]float64{X[t[0]], Y[t[0]]}, tieR: [2]float64{X[t[1]], Y[t[1]]},
cornerL: [2]float64{X[c[0]], Y[c[0]]}, cornerR: [2]float64{X[c[1]], Y[c[1]]},
})
}
return out
}
// stillBunting draws the bunting at rest as an SVG: one pennant
// definition per cloth, origin at the middle of its top edge, with a
// sewn tape and a stitch line along the top; the rope through every
// node of the hang; and each pennant's two ties and the pennant
// itself, translated to its top edge's middle and turned to its tilt.
// The classes are what site.css colours (the rope and ties are a
// Fiesta token) and what bunting.js reads.
func stillBunting(g buntingGeometry) template.HTML {
span := g.Pitch*g.Per + 2*g.End
half := float64(g.FW) / 2
hang := g.rest()
var b strings.Builder
fmt.Fprintf(&b, `")
return template.HTML(b.String())
}
// T is one catalog string by key. A key the catalog lacks is an error,
// so a typo in the template fails the render rather than rendering an
// empty element.
func (p page) T(key string) (string, error) {
s, ok := p.strings[key]
if !ok {
return "", fmt.Errorf("the landing page asks for %q, which the catalog does not have", key)
}
return s, nil
}
// landing renders the landing page in lang from the site source: the
// template at landing/page.html.tmpl filled from Strings' catalog, with
// the version from fiesta.Version and every language's name and URL
// for the menu. html/template escapes every string for where it lands,
// so the install line's < reads as itself. A lang outside Languages
// has no page, and is refused before its catalog is looked for.
func landing(siteDir fs.FS, lang string) ([]byte, error) {
if _, ok := endonyms[lang]; !ok {
return nil, fmt.Errorf("www: %s is not a language the landing page is built in", lang)
}
strings, err := Strings(siteDir, lang)
if err != nil {
return nil, err
}
src, err := fs.ReadFile(siteDir, landingTemplate)
if err != nil {
return nil, fmt.Errorf("www: reading the landing page's template: %w", err)
}
tmpl, err := template.New(landingTemplate).Parse(string(src))
if err != nil {
return nil, fmt.Errorf("www: %w", err)
}
var buf bytes.Buffer
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 {
return nil, fmt.Errorf("www: rendering the landing page in %s: %w", lang, err)
}
return buf.Bytes(), nil
}