diff --git a/TODO.md b/TODO.md index 37632c2..d50a2f2 100644 --- a/TODO.md +++ b/TODO.md @@ -18,12 +18,12 @@ - [ ] Get HTML format to output the _right_ image extension (perhaps provide argument for what to add into templates?) - [ ] Store number of sides in the Metadata - [ ] Validate the number of sides in the metadata with the number of images provided -- [x] Show images when chosen in PostOffice -- [ ] Allow selection of secrets in PostOffice - [ ] Allow conversion of postcard files on PostOffice - [ ] Put buttons for downloading CSS and HTML on web-js output (PostOffice) - [ ] Allow uploading/choice of SVGs for the back side, to make a "blank" postcard - [ ] Responsive layout for the back — in physical units +- [ ] Allow secrets to be removed #PostOffice +- [ ] Figure out why secrets dragging is janky #PostOffice ## Done @@ -46,4 +46,6 @@ - [x] Creating a USD(Z) from an image that doesn't have resolution data (eg front/back portrait fixtures) seems to nil pointer fail. #bug - [x] Get this CLI tool building automatically - [x] Web (webp) with transparency not convertible (eg. to USD) #bug -- [x] Create USDZ directly (no need for OpenUSD tooling) \ No newline at end of file +- [x] Create USDZ directly (no need for OpenUSD tooling) +- [x] Show images when chosen in PostOffice +- [x] Allow selection of secrets in PostOffice diff --git a/internal/www/postoffice/index.html b/internal/www/postoffice/index.html index 8a64874..6d036b7 100644 --- a/internal/www/postoffice/index.html +++ b/internal/www/postoffice/index.html @@ -13,7 +13,7 @@

Welcome to Postoffice, a tool for creating digital files that represent Postcards, and all the information you care to share about them.

It can create 3D models in the USDZ format, and JPEG/PNG images with the HTML & CSS needed to allow them to 'flip' on your site. See an example at shutup.jp.

- Begin! + Describe your postcard!
diff --git a/internal/www/postoffice/postoffice.css b/internal/www/postoffice/postoffice.css index 04c0a0c..d66f93d 100644 --- a/internal/www/postoffice/postoffice.css +++ b/internal/www/postoffice/postoffice.css @@ -78,6 +78,42 @@ label[for$="-image"] { align-items: center; cursor: pointer; margin-bottom: 1rem; + position: relative; + + .secret { + input { + display: none; + } + position: absolute; + + border: 1px solid var(--text); + --a: hsl(from var(--text) h s l / 50%); + --b: hsl(from var(--bg) h s l / 50%); + background: repeating-linear-gradient(45deg, var(--a), var(--a) 5px, var(--b) 5px, var(--b) 10px); + background-size: 14.1421356237px 14.1421356237px; + animation: sweep-gradient 2s linear infinite; + + &::before, &::after { + position: absolute; + width: 10px; + height: 10px; + content: ''; + border: inherit; + background-color: var(--bg); + } + + &::before { + top: -5px; + left: -5px; + cursor:nw-resize; + } + + &::after { + bottom: -5px; + right: -5px; + cursor:se-resize; + } + } } input[type="file"][id$="-image"] { @@ -157,3 +193,8 @@ fieldset:has(input[name="flip"]) { from { transform: perspective(1000px) rotate3d(1,1,0, 0deg) } to { transform: perspective(1000px) rotate3d(1,1,0, 360deg) } } + +@keyframes sweep-gradient { + 0% { background-position: 0 0; } + 100% { background-position: -14.1421356237px 14.1421356237px; } +} \ No newline at end of file diff --git a/internal/www/postoffice/postoffice.js b/internal/www/postoffice/postoffice.js index 3dcdb29..e161a98 100644 --- a/internal/www/postoffice/postoffice.js +++ b/internal/www/postoffice/postoffice.js @@ -129,6 +129,8 @@ function sideImageChanged(e) { const label = document.querySelector(`label[for="${input.id}"]`) label.innerHTML = '' label.appendChild(img) + + allowSecrets(img) }) reader.addEventListener('error', () => { @@ -138,6 +140,83 @@ function sideImageChanged(e) { reader.readAsDataURL(file) } +function allowSecrets(img) { + const label = img.parentElement + img.draggable = false + + const stopFileChooser = (e) => { e.preventDefault() } + + let dragged, startPoint; + img.addEventListener('mousedown', (e) => { + label.addEventListener('click', stopFileChooser) + + dragged = document.createElement('div') + dragged.classList.add('secret') + + const rect = label.getBoundingClientRect() + startPoint = { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + } + dragged.style.left = `${startPoint.x}px` + dragged.style.top = `${startPoint.y}px` + + img.parentElement.appendChild(dragged) + + const endDrag = (e) => { + if (!dragged) return; + + const rect = label.getBoundingClientRect() + const imgRect = img.getBoundingClientRect() + const b = makeSecretBox(startPoint, { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }) + // Translate coords relative & scaled to image, as needed + const imgBox = { + type: 'box', + left: (b.left - (imgRect.left - rect.left)) / imgRect.width, + top: (b.top - (imgRect.top - rect.top)) / imgRect.height, + width: b.width / imgRect.width, + height: b.height / imgRect.height, + } + const sideName = label.htmlFor.split('-')[0] + + dragged.innerHTML = `` + dragged.querySelector('input').value = JSON.stringify(imgBox) + + // Wait a moment, or the value isn't set properly (?!) + setTimeout(() => { dragged = null }, 1) + + window.removeEventListener('mouseup', endDrag) + } + + window.addEventListener('mouseup', endDrag) + }) + + img.addEventListener('mousemove', (e) => { + if (!dragged) return; + + const rect = label.getBoundingClientRect() + const b = makeSecretBox(startPoint, { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }) + + dragged.style.left = `${b.left}px` + dragged.style.top = `${b.top}px` + dragged.style.width = `${b.width}px` + dragged.style.height = `${b.height}px` + }) +} + +const makeSecretBox = (startPoint, thisPoint) => ({ + left: Math.min(startPoint.x, thisPoint.x), + top: Math.min(startPoint.y, thisPoint.y), + width: Math.abs(startPoint.x - thisPoint.x), + height: Math.abs(startPoint.y - thisPoint.y), +}) + function showAppropriateFlips() { const frontOrientation = document.getElementById('front-image')?.dataset?.orientation const backOrientation = document.getElementById('back-image')?.dataset?.orientation diff --git a/pkg/postoffice/http.go b/pkg/postoffice/http.go index ace57b7..592de2c 100644 --- a/pkg/postoffice/http.go +++ b/pkg/postoffice/http.go @@ -1,6 +1,7 @@ package postoffice import ( + "encoding/json" "fmt" "io" "mime/multipart" @@ -115,6 +116,20 @@ func requestToPostcard(codecChoices CodecChoices, r *http.Request) (types.Postca meta.Front.Transcription.Text = r.FormValue("front.transcription.text") meta.Back.Transcription.Text = r.FormValue("back.transcription.text") + for _, secret := range r.Form["front.secrets"] { + var poly types.Polygon + if err := json.Unmarshal([]byte(secret), &poly); err == nil { + meta.Front.Secrets = append(meta.Front.Secrets, poly) + } + } + + for _, secret := range r.Form["back.secrets"] { + var poly types.Polygon + if err := json.Unmarshal([]byte(secret), &poly); err == nil { + meta.Back.Secrets = append(meta.Back.Secrets, poly) + } + } + meta.Context.Description = r.FormValue("context.description") meta.Context.Author.Scan(r.FormValue("context.author")) diff --git a/types/json.go b/types/json.go index 61820b3..10ecf72 100644 --- a/types/json.go +++ b/types/json.go @@ -5,6 +5,12 @@ import ( "time" ) +func (poly *Polygon) UnmarshalJSON(b []byte) error { + return poly.multiPolygonUnmarshaller(func(into interface{}) error { + return json.Unmarshal(b, into) + }) +} + func (d *Date) UnmarshalJSON(b []byte) (err error) { str := string(b) if str == "null" { @@ -25,3 +31,4 @@ func (d Date) MarshalJSON() ([]byte, error) { var _ json.Marshaler = (*Date)(nil) var _ json.Unmarshaler = (*Date)(nil) +var _ json.Unmarshaler = (*Polygon)(nil) diff --git a/types/secrets.go b/types/secrets.go new file mode 100644 index 0000000..422baa0 --- /dev/null +++ b/types/secrets.go @@ -0,0 +1,93 @@ +package types + +import ( + "fmt" +) + +type SecretType struct { + Type string `json:"type" yaml:"type"` +} + +type SecretPolygon struct { + Type string `json:"type" yaml:"type"` + Prehidden bool `json:"prehidden" yaml:"prehidden"` + Points []Point `json:"points" yaml:"points"` +} + +type SecretBox struct { + Type string `json:"type" yaml:"type"` + Prehidden bool `json:"prehidden" yaml:"prehidden"` + Width float64 `json:"width" yaml:"width"` + Height float64 `json:"height" yaml:"height"` + Left float64 `json:"left" yaml:"left"` + Top float64 `json:"top" yaml:"top"` +} + +// Allows polygons with type: box *and* type: polygon to be unmarshalled +func (poly *Polygon) multiPolygonUnmarshaller(decode func(interface{}) error) error { + var typer SecretType + if err := decode(&typer); err != nil { + return fmt.Errorf("invalid secret definition") + } + + switch typer.Type { + case "box": + var box SecretBox + if err := decode(&box); err != nil { + return fmt.Errorf("invalid box secret definition") + } + + return box.intoPolygon(poly) + case "polygon": + var polygon SecretPolygon + if err := decode(&polygon); err != nil { + return fmt.Errorf("invalid polygon secret definition") + } + + poly.Prehidden = polygon.Prehidden + poly.Points = polygon.Points + + return nil + default: + return fmt.Errorf("unknown secret type: %s", typer.Type) + } +} + +func (box SecretBox) intoPolygon(poly *Polygon) error { + poly.Prehidden = box.Prehidden + + if outOfBounds(box.Width) { + return fmt.Errorf("width of box secret is larger than 100%% of the postcard") + } + if outOfBounds(box.Height) { + return fmt.Errorf("height of box secret is larger than 100%% of the postcard") + } + if outOfBounds(box.Left) { + return fmt.Errorf("left edge of box secret is outside the postcard") + } + if outOfBounds(box.Top) { + return fmt.Errorf("top edge of box secret is outside the postcard") + } + + bottom := box.Top + box.Height + if outOfBounds(bottom) { + return fmt.Errorf("bottom edge of box secret is outside the postcard") + } + right := box.Left + box.Width + if outOfBounds(right) { + return fmt.Errorf("right edge of box secret is outside the postcard") + } + + poly.Points = []Point{ + {X: box.Left, Y: box.Top}, + {X: right, Y: box.Top}, + {X: right, Y: bottom}, + {X: box.Left, Y: bottom}, + } + + return nil +} + +func outOfBounds(d float64) bool { + return d < 0.0 || d > 1.0 +} diff --git a/types/yaml.go b/types/yaml.go index 1da699a..9f462fc 100644 --- a/types/yaml.go +++ b/types/yaml.go @@ -51,25 +51,6 @@ func (at AnnotatedText) MarshalYAML() (interface{}, error) { }, nil } -type SecretType struct { - Type string `yaml:"type"` -} - -type SecretPolygon struct { - Type string `yaml:"type"` - Prehidden bool `yaml:"prehidden"` - Points []Point `yaml:"points"` -} - -type SecretBox struct { - Type string `yaml:"type"` - Prehidden bool `yaml:"prehidden"` - Width float64 `yaml:"width"` - Height float64 `yaml:"height"` - Left float64 `yaml:"left"` - Top float64 `yaml:"top"` -} - func (poly Polygon) MarshalYAML() (interface{}, error) { secret := SecretPolygon{ Type: "polygon", @@ -80,6 +61,12 @@ func (poly Polygon) MarshalYAML() (interface{}, error) { return secret, nil } +func (poly *Polygon) UnmarshalYAML(y *yaml.Node) error { + return poly.multiPolygonUnmarshaller(func(into interface{}) error { + return y.Decode(into) + }) +} + func (p Point) MarshalYAML() (interface{}, error) { node := yaml.Node{ Kind: yaml.SequenceNode, @@ -114,74 +101,6 @@ func (p *Point) UnmarshalYAML(y *yaml.Node) error { return nil } -func (poly *Polygon) UnmarshalYAML(y *yaml.Node) error { - var typer SecretType - if err := y.Decode(&typer); err != nil { - return fmt.Errorf("invalid secret definition") - } - - switch typer.Type { - case "box": - var box SecretBox - if err := y.Decode(&box); err != nil { - return fmt.Errorf("invalid box secret definition") - } - - return box.intoPolygon(poly) - case "polygon": - var polygon SecretPolygon - if err := y.Decode(&polygon); err != nil { - return fmt.Errorf("invalid polygon secret definition") - } - - poly.Prehidden = polygon.Prehidden - poly.Points = polygon.Points - - return nil - default: - return fmt.Errorf("unknown secret type: %s", typer.Type) - } -} - -func (box SecretBox) intoPolygon(poly *Polygon) error { - poly.Prehidden = box.Prehidden - - if outOfBounds(box.Width) { - return fmt.Errorf("width of box secret is larger than 100%% of the postcard") - } - if outOfBounds(box.Height) { - return fmt.Errorf("height of box secret is larger than 100%% of the postcard") - } - if outOfBounds(box.Left) { - return fmt.Errorf("left edge of box secret is outside the postcard") - } - if outOfBounds(box.Top) { - return fmt.Errorf("top edge of box secret is outside the postcard") - } - - bottom := box.Top + box.Height - if outOfBounds(bottom) { - return fmt.Errorf("bottom edge of box secret is outside the postcard") - } - right := box.Left + box.Width - if outOfBounds(right) { - return fmt.Errorf("right edge of box secret is outside the postcard") - } - - poly.Points = []Point{ - {X: box.Left, Y: box.Top}, - {X: right, Y: box.Top}, - {X: right, Y: bottom}, - {X: box.Left, Y: bottom}, - } - - return nil -} - -func outOfBounds(d float64) bool { - return d < 0.0 || d > 1.0 -} - func (s Size) MarshalYAML() (interface{}, error) { w, _ := s.CmWidth.Float64() h, _ := s.CmHeight.Float64()