|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +package anim |
| 3 | + |
| 4 | +import ( |
| 5 | + "math" |
| 6 | + "slices" |
| 7 | + "strconv" |
| 8 | + |
| 9 | + "github.com/go-text/typesetting/language" |
| 10 | +) |
| 11 | + |
| 12 | +// Day is one of the last days on the strip under the numbers. |
| 13 | +type Day struct { |
| 14 | + No int // the day's number, #17 |
| 15 | + Played bool // answered and scored |
| 16 | + Best bool // the person's best day |
| 17 | +} |
| 18 | + |
| 19 | +// Tile is one number of a person's statistics with what it counts. |
| 20 | +type Tile struct { |
| 21 | + Value, Label string |
| 22 | + Flame bool // a streak: the flame goes before the number |
| 23 | + Gold bool // a first place or a record |
| 24 | +} |
| 25 | + |
| 26 | +// Profile is the picture of a person's own statistics, shown above the text of |
| 27 | +// the statistics panel: their pins of every day on the globe, their points, |
| 28 | +// six numbers and the strip of their last days. The numbers stand still and |
| 29 | +// the Earth turns under the pins, once a loop, so the loop has no seam. |
| 30 | +// Every text shrinks to its room, since a label in German is twice as long |
| 31 | +// as in Russian. |
| 32 | +type Profile struct { |
| 33 | + Title string // "statistics" |
| 34 | + Name string |
| 35 | + Total Tile // the big number under the name |
| 36 | + Tiles [6]Tile // two rows of three |
| 37 | + Days []Day // the last days, oldest first |
| 38 | + DaysLabel string |
| 39 | + Pins []Guess // every day's answer; Medal 1 marks the best day |
| 40 | + Lang language.Language |
| 41 | +} |
| 42 | + |
| 43 | +func NewProfile(lang string) *Profile { return &Profile{Lang: language.NewLanguage(lang)} } |
| 44 | + |
| 45 | +func (s *Profile) Size() (int, int) { return 1200, 630 } |
| 46 | +func (s *Profile) Duration() float64 { return profileDur } |
| 47 | + |
| 48 | +// One turn of the Earth, slow enough to read the numbers beside it. |
| 49 | +const profileDur = 24 |
| 50 | + |
| 51 | +func (s *Profile) Draw(c *Canvas, t float64) { |
| 52 | + c.Lang = s.Lang |
| 53 | + c.Fill(field) |
| 54 | + |
| 55 | + // The globe of the person's pins, turned to where most of them are. |
| 56 | + const mx, my, mw, mh = 24, 24, 540, 582 |
| 57 | + c.RoundRect(mx, my, mw, mh, 28, 28, deep, 1, nil) |
| 58 | + clip := c.clip |
| 59 | + clip.Clear() |
| 60 | + clip.RoundRect(mx, my, mw, mh, 28, 28) |
| 61 | + lat0, lon0 := 20.0, 15.0 |
| 62 | + if len(s.Pins) > 0 { |
| 63 | + var x, y, z float64 |
| 64 | + for _, p := range s.Pins { |
| 65 | + sl, cl := math.Sincos(p.Lat * rad) |
| 66 | + so, co := math.Sincos(p.Lon * rad) |
| 67 | + x, y, z = x+cl*co, y+cl*so, z+sl |
| 68 | + } |
| 69 | + lat0, lon0 = math.Atan2(z, math.Hypot(x, y))/rad, math.Atan2(y, x)/rad |
| 70 | + lat0 = max(-35, min(45, lat0)) |
| 71 | + } |
| 72 | + g := newGlobeView(mx+mw/2, my+mh/2, 250, lat0*rad, (lon0-360*t/profileDur)*rad) |
| 73 | + g.x0, g.y0, g.x1, g.y1 = mx, my, mx+mw, my+mh |
| 74 | + c.Disc(g.cx, g.cy, g.R+2, field, 0.55, clip) |
| 75 | + c.mapDots(g, ripple{}, 0.5, clip) |
| 76 | + pins := slices.Clone(s.Pins) |
| 77 | + slices.SortStableFunc(pins, func(a, b Guess) int { return a.Medal - b.Medal }) |
| 78 | + for _, p := range pins { |
| 79 | + x, y, z := g.at(p.Lat, p.Lon) |
| 80 | + if z < 0.05 { |
| 81 | + continue |
| 82 | + } |
| 83 | + a := smooth(0.05, 0.2, z) |
| 84 | + if p.Medal == 1 { |
| 85 | + c.Pin(x, y, 40, PinStyle{Fill: gold, Edge: deep, Hole: deep}, a, clip) |
| 86 | + continue |
| 87 | + } |
| 88 | + c.Disc(x, y, 7.5, deep, a, clip) |
| 89 | + c.Disc(x, y, 5.5, paper.Mix(gold, 1-smooth(200, 3000, p.Km)), a, clip) |
| 90 | + } |
| 91 | + |
| 92 | + // The texts on the right. |
| 93 | + const x0, x1 = 600, 1176 |
| 94 | + w := c.Text(Display, 52, "maply", x0, 70, Left, white, 1, nil) |
| 95 | + nw := c.Text(Body, c.fit(Body, 26, 18, s.Name, 220), s.Name, x1, 68, Right, white, 1, nil) |
| 96 | + c.Text(Display, c.fit(Display, 30, 20, s.Title, x1-nw-24-(x0+w+12)), s.Title, x0+w+12, 70, Left, ink, 1, nil) |
| 97 | + |
| 98 | + vw := c.Text(Display, 104, s.Total.Value, x0, 182, Left, white, 1, nil) |
| 99 | + c.Text(BodyMedium, c.fit(BodyMedium, 22, 14, s.Total.Label, x1-x0-vw-16), s.Total.Label, x0+vw+16, 180, Left, ink, 1, nil) |
| 100 | + |
| 101 | + const tw, th, gap = 184, 104, 12 |
| 102 | + for i, tile := range s.Tiles { |
| 103 | + x, y := x0+float64(i%3)*(tw+gap), 210+float64(i/3)*(th+gap) |
| 104 | + c.RoundRect(x, y, tw, th, 18, 18, deep, 1, nil) |
| 105 | + col := white |
| 106 | + if tile.Gold { |
| 107 | + col = gold |
| 108 | + } |
| 109 | + tx := x + 18 |
| 110 | + if tile.Flame { |
| 111 | + c.flame(tx+11, y+58, 34, 1) |
| 112 | + tx += 32 |
| 113 | + } |
| 114 | + c.Text(Display, c.fit(Display, 50, 30, tile.Value, x+tw-14-tx), tile.Value, tx, y+58, Left, col, 1, nil) |
| 115 | + c.Text(BodyMedium, c.fit(BodyMedium, 18, 12, tile.Label, tw-30), tile.Label, x+18, y+88, Left, ink, 1, nil) |
| 116 | + } |
| 117 | + |
| 118 | + // The last days: a full disc for a day played, a ring for a day missed, |
| 119 | + // gold for the best one, the day's number under each. |
| 120 | + if n := len(s.Days); n > 0 { |
| 121 | + const cy, r = 548, 14 |
| 122 | + c.Text(BodyMedium, c.fit(BodyMedium, 18, 12, s.DaysLabel, x1-x0), s.DaysLabel, x0, 508, Left, ink, 1, nil) |
| 123 | + step := (x1 - x0 - 2*r) / float64(max(1, n-1)) |
| 124 | + for i, d := range s.Days { |
| 125 | + x := x0 + r + float64(i)*step |
| 126 | + col := ink |
| 127 | + switch { |
| 128 | + case d.Best: |
| 129 | + c.Disc(x, cy, r, gold, 1, nil) |
| 130 | + col = gold |
| 131 | + case d.Played: |
| 132 | + c.Disc(x, cy, r, paper, 1, nil) |
| 133 | + default: |
| 134 | + c.Ring(x, cy, r-1.5, 3, tint, 1, nil) |
| 135 | + } |
| 136 | + c.Text(BodyMedium, 14, "#"+strconv.Itoa(d.No), x, cy+r+22, Center, col, 1, nil) |
| 137 | + } |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +// fit is the size, from size down to least, at which s fits in width. |
| 142 | +func (c *Canvas) fit(f *Font, size, least float64, s string, width float64) float64 { |
| 143 | + for size > least && c.Measure(f, size, s) > width { |
| 144 | + size-- |
| 145 | + } |
| 146 | + return size |
| 147 | +} |
0 commit comments