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, ``+"\n\n", span*g.Swags, g.H) for k, fill := range pennantFills { fmt.Fprintf(&b, ``+"\n", k, fill, -half, half, g.FH, -half, half, half-1.3, -(half - 1.3), -(half - 1.5), half-1.5) } b.WriteString("\n") b.WriteString(`\n") for k, f := range hang.flags { fmt.Fprintf(&b, ``+"\n", f.tieL[0], f.tieL[1], f.cornerL[0], f.cornerL[1], f.tieR[0], f.tieR[1], f.cornerR[0], f.cornerR[1]) mx, my := (f.cornerL[0]+f.cornerR[0])/2, (f.cornerL[1]+f.cornerR[1])/2 tilt := math.Atan2(f.cornerR[1]-f.cornerL[1], f.cornerR[0]-f.cornerL[0]) * 180 / math.Pi fmt.Fprintf(&b, ``+"\n", k%len(pennantFills), mx, my, tilt, k%len(pennantFills)) } b.WriteString("") 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 }