I get told to shut up a lot by my friend. This is the microsite that documents this in detail. shutup.jp
postcards microsite
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

An early prototype

Too many on a page makes it painfully slow even on a very fast machine.

Doesn't work well on mobile; zooming and narrow width mess with the rotation amount.

+258 -97
+1
.gitignore
··· 1 + /dist/
+14
Taskfile.yml
··· 1 + # https://taskfile.dev 2 + 3 + version: '3' 4 + 5 + tasks: 6 + build: 7 + cmds: 8 + - go run . 9 + serve: 10 + cmds: 11 + - serve -n dist/ 12 + dev: 13 + cmds: 14 + - task serve & find {*.tmpl,static,*.go} -type f | entr -cc task build
+3
go.mod
··· 1 + module github.com/jphastings/shutup.jp/build 2 + 3 + go 1.21.1
-93
index.html
··· 1 - <!doctype html> 2 - <html class="no-js" lang=""> 3 - <head> 4 - <meta charset="utf-8"> 5 - <meta http-equiv="x-ua-compatible" content="ie=edge"> 6 - <title>Postcard demo</title> 7 - <meta name="description" content="Demo of new postcard shape"> 8 - <meta name="viewport" content="width=device-width, initial-scale=1"> 9 - <link rel="stylesheet" href="postcard.css"> 10 - <link rel="stylesheet" href="shutup.css"> 11 - </head> 12 - <body> 13 - <section class="explain"> 14 - <hgroup> 15 - <h1>"Shut up JP"</h1> 16 - </hgroup> 17 - 18 - <p>One day, after months of silence, my friend <a href="">Claire</a> sent me a text message with only 3 words in it &ldquo;<strong>Shut up, JP.</strong>&rdquo; Not long afterwards I started receiving this advice by mail as well, from every country she visited and in every language she could convince friendly, and presumably confused, strangers to help her write in.</p> 19 - </section> 20 - 21 - <figure> 22 - <div class="postcard right-hand portrait" style="--postcard: url('seattle.webp'); --aspect-ratio: 1376 / 1931"></div> 23 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 24 - </figure> 25 - <figure> 26 - <div class="postcard book" style="--postcard: url('sri-lanka.webp'); --aspect-ratio: 2286 / 1647;"></div> 27 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 28 - </figure> 29 - <figure> 30 - <div class="postcard left-hand portrait" style="--postcard: url('morocco.webp'); --aspect-ratio: 1575 / 2224"></div> 31 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 32 - </figure> 33 - <figure> 34 - <div class="postcard right-hand portrait" style="--postcard: url('seattle.webp'); --aspect-ratio: 1376 / 1931"></div> 35 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 36 - </figure> 37 - <figure> 38 - <div class="postcard book" style="--postcard: url('sri-lanka.webp'); --aspect-ratio: 2286 / 1647;"></div> 39 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 40 - </figure> 41 - <figure> 42 - <div class="postcard left-hand portrait" style="--postcard: url('morocco.webp'); --aspect-ratio: 1575 / 2224"></div> 43 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 44 - </figure> 45 - <figure> 46 - <div class="postcard right-hand portrait" style="--postcard: url('seattle.webp'); --aspect-ratio: 1376 / 1931"></div> 47 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 48 - </figure> 49 - <figure> 50 - <div class="postcard book" style="--postcard: url('sri-lanka.webp'); --aspect-ratio: 2286 / 1647;"></div> 51 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 52 - </figure> 53 - <figure> 54 - <div class="postcard left-hand portrait" style="--postcard: url('morocco.webp'); --aspect-ratio: 1575 / 2224"></div> 55 - <figcaption>This is some text about this postcard. It is not a lot of ytext, but it is also not nothing.</figcaption> 56 - </figure> 57 - 58 - <section class="explain">How fun is this??</section> 59 - 60 - 61 - <script> 62 - const debounce = (fn) => { 63 - let frame; 64 - return (...params) => { 65 - if (frame) { 66 - cancelAnimationFrame(frame); 67 - } 68 - frame = requestAnimationFrame(() => { 69 - fn(...params); 70 - }); 71 - } 72 - }; 73 - 74 - const storeScroll = () => { 75 - document.documentElement.style.setProperty('--scroll', window.scrollY); 76 - } 77 - document.addEventListener('scroll', debounce(storeScroll), { passive: true }); 78 - storeScroll(); 79 - 80 - const setYPos = () => { 81 - document.documentElement.style.setProperty('--viewportY', Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)); 82 - Array.from(document.getElementsByClassName('postcard')).forEach((pc) => { 83 - pc.style.setProperty('--yPos', 84 - window.scrollY + pc.getBoundingClientRect().top + pc.getBoundingClientRect().height / 2 85 - ); 86 - }) 87 - } 88 - window.addEventListener('resize', debounce(setYPos)); 89 - setYPos(); 90 - 91 - </script> 92 - </body> 93 - </html>
+67
index.html.tmpl
··· 1 + <!doctype html> 2 + <html class="no-js" lang=""> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta http-equiv="x-ua-compatible" content="ie=edge"> 6 + <title>Postcard demo</title> 7 + <meta name="description" content="Demo of new postcard shape"> 8 + <meta name="viewport" content="width=device-width, initial-scale=1"> 9 + <link rel="stylesheet" href="postcard.css"> 10 + <link rel="stylesheet" href="shutup.css"> 11 + </head> 12 + <body> 13 + <section class="explain"> 14 + <hgroup> 15 + <h1>"Shut up JP"</h1> 16 + </hgroup> 17 + 18 + <p>One day, after months of silence, my friend <a href="">Claire</a> sent me a text message with only 3 words in it &ldquo;<strong>Shut up, JP.</strong>&rdquo; Not long afterwards I started receiving this advice by mail as well, from every country she visited and in every language she could convince friendly, and presumably confused, strangers to help her write in.</p> 19 + </section> 20 + 21 + {{ range . }} 22 + <figure> 23 + <div class="postcard {{ .Flip }} {{ if gt .FrontDimensions.PxHeight .FrontDimensions.PxWidth }}portrait{{ else }}landscape{{ end }}" style="--postcard: url('{{ .Name }}.webp'); --aspect-ratio: {{ .FrontDimensions.PxWidth }} / {{ .FrontDimensions.PxHeight }}"> 24 + <img src="{{ .Name }}.webp" alt="{{ .Front.Description }}"> 25 + </div> 26 + <!--<figcaption style="--map: url(seattle.svg)">--> 27 + <figcaption> 28 + {{- .Back.Transcription | safeHTML -}} 29 + {{ with .SentOn }}<time datetime="{{ . }}">{{ .Format "2 Jan, 2006" }}</time>{{ end }} 30 + </figcaption> 31 + </figure> 32 + {{ end }} 33 + 34 + <section class="explain">How fun is this??</section> 35 + 36 + <script> 37 + const debounce = (fn) => { 38 + let frame; 39 + return (...params) => { 40 + if (frame) { 41 + cancelAnimationFrame(frame); 42 + } 43 + frame = requestAnimationFrame(() => { 44 + fn(...params); 45 + }); 46 + } 47 + }; 48 + 49 + const storeScroll = () => { 50 + document.documentElement.style.setProperty('--scroll', window.scrollY); 51 + } 52 + document.addEventListener('scroll', storeScroll, { passive: true }); 53 + storeScroll(); 54 + 55 + const setYPos = () => { 56 + document.documentElement.style.setProperty('--viewportY', Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)); 57 + Array.from(document.getElementsByClassName('postcard')).forEach((pc) => { 58 + pc.style.setProperty('--yPos', 59 + window.scrollY + pc.getBoundingClientRect().top + pc.getBoundingClientRect().height / 2 60 + ); 61 + }) 62 + } 63 + window.addEventListener('resize', setYPos); 64 + setYPos(); 65 + </script> 66 + </body> 67 + </html>
+61
main.go
··· 1 + package main 2 + 3 + import ( 4 + _ "embed" 5 + "encoding/json" 6 + "html/template" 7 + "io" 8 + "os" 9 + "path/filepath" 10 + "sort" 11 + ) 12 + 13 + //go:embed index.html.tmpl 14 + var indexStr string 15 + var indexTmpl = template.Must(template.New("index").Funcs(template.FuncMap{ 16 + "safeHTML": func(str string) template.HTML { 17 + return template.HTML(str) 18 + }, 19 + }).Parse(indexStr)) 20 + 21 + func check(e error) { 22 + if e != nil { 23 + panic(e) 24 + } 25 + } 26 + 27 + func main() { 28 + files, err := filepath.Glob("postcards/*.json") 29 + check(err) 30 + 31 + var pcs []Postcard 32 + toCopy := []string{"static/postcard.css", "static/shutup.css"} 33 + 34 + for _, file := range files[:3] { 35 + var pc Postcard 36 + f, err := os.Open(file) 37 + check(err) 38 + check(json.NewDecoder(f).Decode(&pc)) 39 + 40 + pc.Name = filepath.Base(file)[:len(filepath.Base(file))-5] 41 + pcs = append(pcs, pc) 42 + toCopy = append(toCopy, "postcards/"+pc.Name+".webp") 43 + } 44 + 45 + indexF, err := os.OpenFile("dist/index.html", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 46 + check(err) 47 + 48 + sort.Sort(BySentOn(pcs)) 49 + check(indexTmpl.Execute(indexF, pcs)) 50 + 51 + for _, tc := range toCopy { 52 + src, err := os.Open(tc) 53 + check(err) 54 + 55 + dst, err := os.OpenFile("dist/"+filepath.Base(tc), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 56 + check(err) 57 + 58 + _, err = io.Copy(dst, src) 59 + check(err) 60 + } 61 + }
morocco.webp

This is a binary file and will not be displayed.

+8 -2
postcard.css static/postcard.css
··· 34 34 align-items: center; 35 35 justify-content: center; 36 36 backface-visibility: hidden; 37 + -webkit-backface-visibility: hidden; 37 38 transform-style: preserve-3d; 38 39 background-blend-mode: multiply; 39 40 } ··· 46 47 .postcard::after { 47 48 background: no-repeat top/100%; 48 49 mask: var(--postcard) top/100%; 49 - -webkit-mask: var(--postcard) top/100%; 50 50 } 51 51 52 52 .postcard::before { 53 53 background: no-repeat bottom/100%; 54 54 mask: var(--postcard) bottom/100%; 55 - -webkit-mask: var(--postcard) bottom/100%; 56 55 } 57 56 58 57 .postcard * { ··· 122 121 rgb(var(--v2), var(--v2), var(--v2)) 123 122 ), var(--postcard); 124 123 } 124 + 125 + /* Hacks */ 126 + 127 + /* Webkit can't handle masks without stuttering, so disable them; postcards will have a white border */ 128 + _::-webkit-full-page-media, _:future, :root .postcard::before, :root .postcard::after { 129 + mask: none; 130 + }
seattle.webp

This is a binary file and will not be displayed.

+20 -2
shutup.css static/shutup.css
··· 1 1 :root { 2 2 --front: #222; 3 - --muted: #000; 3 + --muted: #555; 4 4 --back: #f7f7f7; 5 5 6 6 @media (prefers-color-scheme: dark) { ··· 52 52 padding: 0 10vh; 53 53 } 54 54 figcaption { 55 - height: 45%; 55 + position: relative; 56 + height: 50%; 56 57 width: 40%; 57 58 margin: 5%; 59 + background-image: var(--map); 60 + background-size: contain; 61 + background-position: center; 62 + background-repeat: no-repeat; 63 + white-space: pre; 58 64 } 65 + 59 66 section.explain { 60 67 height: 50vh; 61 68 flex-direction: column; ··· 73 80 + 0.75 74 81 ) * 360deg); 75 82 } 83 + 84 + svg polygon { 85 + fill:#eee; 86 + stroke:black; 87 + stroke-width:2; 88 + stroke-linecap:round; 89 + stroke-linejoin:round; 90 + } 91 + svg * { 92 + transform-box: fill-box; 93 + }
sri-lanka.webp

This is a binary file and will not be displayed.

+84
types.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "math/big" 6 + "time" 7 + ) 8 + 9 + type Postcard struct { 10 + Name string 11 + Location Location `json:"location,omitempty"` 12 + Flip Flip `json:"flip" yaml:"flip"` 13 + SentOn Date `json:"sentOn,omitempty" yaml:"sent_on"` 14 + Front Side `json:"front,omitempty"` 15 + Back Side `json:"back,omitempty"` 16 + FrontDimensions Size `json:"frontSize" yaml:"front_size,omitempty"` 17 + Context Context `json:"context,omitempty"` 18 + } 19 + 20 + type Location struct { 21 + Name string `json:"name"` 22 + Latitude *float64 `json:"lat,omitempty" yaml:"latitude,omitempty"` 23 + Longitude *float64 `json:"long,omitempty" yaml:"longitude,omitempty"` 24 + } 25 + 26 + type Side struct { 27 + Description string `json:"description,omitempty"` 28 + Transcription string `json:"transcription,omitempty"` 29 + } 30 + 31 + type Context struct { 32 + Description string `json:"description"` 33 + } 34 + 35 + type Flip string 36 + 37 + const ( 38 + FlipBook Flip = "book" 39 + FlipLeftHand Flip = "left-hand" 40 + FlipCalendar Flip = "calendar" 41 + FlipRightHand Flip = "right-hand" 42 + ) 43 + 44 + type Date string 45 + 46 + func (d Date) Valid() bool { 47 + _, _, _, err := d.Components() 48 + return err == nil 49 + } 50 + 51 + func (d Date) Components() (year int, month int, day int, err error) { 52 + _, err = fmt.Sscanf(string(d), "%d-%d-%d", &year, &month, &day) 53 + return year, month, day, err 54 + } 55 + 56 + func (d Date) Time() (time.Time, error) { 57 + year, month, day, err := d.Components() 58 + if err != nil { 59 + return time.Time{}, err 60 + } 61 + return time.Date(year, time.Month(month), day, 23, 0, 0, 0, time.UTC), nil 62 + } 63 + 64 + func (d Date) Format(layout string) string { 65 + t, err := d.Time() 66 + if err != nil { 67 + return "" 68 + } 69 + return t.Format(layout) 70 + } 71 + 72 + type Size struct { 73 + CmWidth *big.Rat `json:"cmW,omitempty"` 74 + CmHeight *big.Rat `json:"cmH,omitempty"` 75 + PxWidth int `json:"pxW"` 76 + PxHeight int `json:"pxH"` 77 + } 78 + 79 + type BySentOn []Postcard 80 + 81 + // Implement the sort.Interface - Len, Less, and Swap methods 82 + func (a BySentOn) Len() int { return len(a) } 83 + func (a BySentOn) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 84 + func (a BySentOn) Less(i, j int) bool { return a[i].SentOn < a[j].SentOn }