add automatic github-style avatar for dev auth #106
This commit is contained in:
Generated
+7
-1
@@ -126,6 +126,12 @@
|
||||
packages = ["."]
|
||||
revision = "542fd4642604d0d0c26112396ce5b1a9d01eee0b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/nullrocks/identicon"
|
||||
packages = ["."]
|
||||
revision = "7875f45b0022edded6377e40639d8aa620193a62"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/patrickmn/go-cache"
|
||||
packages = ["."]
|
||||
@@ -232,6 +238,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "af8b7f1817ce6e82746722a745184bd50c341733bcc024311aa956ea32475796"
|
||||
inputs-digest = "38280a8f373ce1799e64ac1709e9748ae8cf6cca30dfeabca807d2db9fe34ab8"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nullrocks/identicon"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
@@ -19,22 +22,28 @@ const devAuthPort = 8084
|
||||
// DevAuthServer is a fake oauth server for development
|
||||
// it provides stand-alone server running on its own port and pretending to be the real oauth2. It also provides
|
||||
// Dev Provider the same way as normal providers di, i.e. github, google and others.
|
||||
// can run in interractive and non-interactive mode. In interactive mode login attempts will show login form to select
|
||||
// can run in interactive and non-interactive mode. In interactive mode login attempts will show login form to select
|
||||
// desired user name.
|
||||
type DevAuthServer struct {
|
||||
Provider Provider
|
||||
|
||||
username string // unsafe, but fine for dev
|
||||
nonInteractive bool
|
||||
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
iconGen *identicon.Generator
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Run oauth2 dev server on port devAuthPort
|
||||
func (d *DevAuthServer) Run() {
|
||||
log.Printf("[INFO] run local oauth2 dev server on %d", devAuthPort)
|
||||
d.lock.Lock()
|
||||
var err error
|
||||
d.iconGen, err = identicon.New("github", 5, 3)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't create identicon, %s", err)
|
||||
}
|
||||
|
||||
d.httpServer = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", devAuthPort),
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -42,9 +51,9 @@ func (d *DevAuthServer) Run() {
|
||||
switch {
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"):
|
||||
// first time it will be called without usernam and will ask for onw
|
||||
// first time it will be called without username and will ask for onw
|
||||
if !d.nonInteractive && (r.ParseForm() != nil || r.Form.Get("username") == "") {
|
||||
if _, err := w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil {
|
||||
if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil {
|
||||
log.Printf("[WARN] can't write, %s", err)
|
||||
}
|
||||
return
|
||||
@@ -70,19 +79,33 @@ func (d *DevAuthServer) Run() {
|
||||
"state":"12345678"
|
||||
}`
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := w.Write([]byte(res)); err != nil {
|
||||
if _, err = w.Write([]byte(res)); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/user"):
|
||||
ava := fmt.Sprintf("http://127.0.0.1:%d/avatar?user=%s", devAuthPort, d.username)
|
||||
res := fmt.Sprintf(`{
|
||||
"id": "%s",
|
||||
"name":"%s"
|
||||
}`, d.username, d.username)
|
||||
"name":"%s",
|
||||
"picture":"%s"
|
||||
}`, d.username, d.username, ava)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := w.Write([]byte(res)); err != nil {
|
||||
if _, err = w.Write([]byte(res)); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/avatar"):
|
||||
user := r.URL.Query().Get("user")
|
||||
b, e := d.genAvatar(user)
|
||||
if e != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if _, err = w.Write(b); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -94,7 +117,7 @@ func (d *DevAuthServer) Run() {
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
err := d.httpServer.ListenAndServe()
|
||||
err = d.httpServer.ListenAndServe()
|
||||
log.Printf("[WARN] dev oauth2 server terminated, %s", err)
|
||||
}
|
||||
|
||||
@@ -128,13 +151,28 @@ func NewDev(p Params) Provider {
|
||||
userInfo := store.User{
|
||||
ID: data.value("id"),
|
||||
Name: data.value("name"),
|
||||
Picture: "",
|
||||
Picture: data.value("picture"),
|
||||
}
|
||||
return userInfo
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DevAuthServer) genAvatar(user string) ([]byte, error) {
|
||||
if d.iconGen == nil {
|
||||
return nil, errors.Errorf("no iconGen, skip avatar generation for %s", user)
|
||||
}
|
||||
|
||||
ii, err := d.iconGen.Draw(user) // Generate an IdentIcon
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to draqw avatar for %s", user)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
err = ii.Png(300, buf)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
var devUserForm = `
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -62,7 +62,15 @@ func TestDevProvider(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
|
||||
u := *claims.User
|
||||
assert.Equal(t, store.User{Name: "dev_user", ID: "dev_user", Picture: "", IP: "",
|
||||
assert.Equal(t, store.User{Name: "dev_user", ID: "dev_user", Picture: "http://127.0.0.1:8084/avatar?user=dev_user", IP: "",
|
||||
Admin: true, Blocked: false, Verified: false}, u)
|
||||
|
||||
// check avatar
|
||||
resp, err = client.Get("http://127.0.0.1:8084/avatar?user=dev_user")
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 985, len(body))
|
||||
t.Logf("headers: %+v", resp.Header)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Testing and Developing
|
||||
main/
|
||||
experimental/
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.vscode/*
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Ruben Rivera
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# IdentIcon
|
||||
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/nullrocks/identicon) [](http://godoc.org/github.com/nullrocks/identicon)
|
||||
|
||||
**IdentIcon** is an open source avatar generator inspired by GitHub avatars.
|
||||
|
||||
|
||||
IdentIcon uses a deterministic algorithm that generates an image (using Golang's stdlib image encoders) based on a text (Generally Usernames, Emails or just random strings), by hashing it and iterating over the bytes of the digest to pick whether to draw a point, pick a color or choose where to go next.
|
||||
|
||||
|
||||
IdentIcon's Generator enables the creation of customized figures: (NxN size, points density, custom color palette) as well as multiple exporting formats in case the developers want to generate their own images.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
$ go get github.com/nullrocks/identicon
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/nullrocks/identicon"
|
||||
)
|
||||
|
||||
// New Generator: Rehuse
|
||||
ig, err := identicon.New(
|
||||
"github", // Namespace
|
||||
5, // Number of blocks (Size)
|
||||
3, // Density
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err) // Invalid Size or Density
|
||||
}
|
||||
|
||||
username := "nullrocks" // Text - decides the resulting figure
|
||||
ii, err := ig.Draw(username) // Generate an IdentIcon
|
||||
|
||||
if err != nil {
|
||||
panic(err) // Text is empty
|
||||
}
|
||||
|
||||
// File writer
|
||||
img, _ := os.Create("icon.png")
|
||||
defer img.Close()
|
||||
// Takes the size in pixels and any io.Writer
|
||||
ii.Png(300, img) // 300px * 300px
|
||||
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 5x5
|
||||
|nullrocks | johndoe | abc123 | modulo |
|
||||
:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:--------------------------------------------|
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
|
||||
### 7x7
|
||||
|nullrocks | johndoe | abc123 | modulo |
|
||||
:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:---------------------------------------------|
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
|
||||
### 10x10
|
||||
|nullrocks | johndoe | abc123 | modulo |
|
||||
:--------------------------------------------------:|:-----------------------------------------------:|:---------------------------------------------:|:----------------------------------------------|
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
|
||||
[View examples](./examples)
|
||||
|
||||
## Documentation
|
||||
|
||||
## Changelog
|
||||
|
||||
## Contribution
|
||||
|
||||
## License
|
||||
MIT
|
||||
|
||||
Copyright (c) 2018-present, Ruben Rivera
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"image"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Canvas contains what is needed to generate an image. It contains properties
|
||||
// that could be useful when rendering the image.
|
||||
// - Having MinY and MaxY allows you to vertically center the figure.
|
||||
// - VisitedYPoints could be useful to determine whether there is a big empty
|
||||
// vertical space in the figure.
|
||||
type Canvas struct {
|
||||
// Size same value specified in identicon.New(...).
|
||||
Size int
|
||||
// PointsMap contains all coordinates and it's values that form the figure.
|
||||
PointsMap map[int]map[int]int
|
||||
// MinY is the upper Y-axis that has at least one point drawn.
|
||||
MinY int
|
||||
// MaxY is the lower Y-axis that has at least one point drawn.
|
||||
MaxY int
|
||||
// VisitedYPoints contains all Y-axis that had been visited. Helpful to
|
||||
// determine big blank spaces in the resulting figure.
|
||||
VisitedYPoints map[int]bool
|
||||
// FilledPoints is the number of points filled at least once.
|
||||
FilledPoints int
|
||||
}
|
||||
|
||||
// Array generates a two-dimensional array version of the IdentIcon figure.
|
||||
func (c *Canvas) Array() [][]int {
|
||||
canvasArray := make([][]int, c.Size)
|
||||
for i := range canvasArray {
|
||||
canvasArray[i] = make([]int, c.Size)
|
||||
}
|
||||
|
||||
for y := range c.PointsMap {
|
||||
for x := range c.PointsMap[y] {
|
||||
canvasArray[y][x] = c.PointsMap[y][x]
|
||||
}
|
||||
}
|
||||
|
||||
return canvasArray
|
||||
}
|
||||
|
||||
// ToString generates a string version of the IdentIcon figure.
|
||||
func (c *Canvas) String(separator string, fillEmptyWith string) string {
|
||||
tp := c.Size * c.Size
|
||||
// Total number of characters considering:
|
||||
strLen := c.Size - 1 // Line Breaks
|
||||
strLen += (tp - c.Size) * len(separator) // Separators
|
||||
strLen += c.FilledPoints // Points
|
||||
strLen += (tp - c.FilledPoints) * len(fillEmptyWith) // Fill Empty
|
||||
|
||||
// Concatenating strings with the `+` is slow and uses a lot of memory,
|
||||
// using `copy` in a slice of bytes has been proved to be a better approach.
|
||||
bs := make([]byte, strLen)
|
||||
// Keep track of the length of the bytes array, concatenations will occur
|
||||
// using `bl` as the right-most index.
|
||||
bl := 0
|
||||
|
||||
for y := 0; y < c.Size; y++ {
|
||||
if mapY, exists := c.PointsMap[y]; exists {
|
||||
for x := 0; x < c.Size; x++ {
|
||||
if value, exists := mapY[x]; exists {
|
||||
if value > 9 {
|
||||
value = 9
|
||||
}
|
||||
bl += copy(bs[bl:], []byte(strconv.Itoa(value)))
|
||||
} else {
|
||||
bl += copy(bs[bl:], []byte(fillEmptyWith))
|
||||
}
|
||||
if x < c.Size-1 {
|
||||
bl += copy(bs[bl:], []byte(separator))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There aren't any values in this row, fill it the row anyway.
|
||||
for x := 0; x < c.Size; x++ {
|
||||
bl += copy(bs[bl:], []byte(fillEmptyWith))
|
||||
if x < c.Size-1 {
|
||||
bl += copy(bs[bl:], []byte(separator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if y < c.Size-1 {
|
||||
// Append a line break except when it's the last line.
|
||||
bl += copy(bs[bl:], "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// Points generates an array of points of a two-dimensional plane as [x, y]
|
||||
// that correspond to all filled points in the IdentIcon figure.
|
||||
func (c *Canvas) Points() []image.Point {
|
||||
points := []image.Point{}
|
||||
|
||||
for y, value := range c.PointsMap {
|
||||
for x := range value {
|
||||
points = append(points, image.Point{X: x, Y: y})
|
||||
}
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
// IntCoordinates generates an array of points of a two-dimensional plane as:
|
||||
// - [x, y] that correspond to all filled points in the IdentIcon figure.
|
||||
func (c *Canvas) IntCoordinates() [][]int {
|
||||
points := [][]int{}
|
||||
|
||||
for y, value := range c.PointsMap {
|
||||
for x := range value {
|
||||
points = append(points, []int{x, y})
|
||||
}
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Golang CircleCI 2.0 configuration file
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/nullrocks/identicon
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
- run: go test -v ./...
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"image/color"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Generator represents a predefined set of configurations that can be reused to
|
||||
// create multiple icons by passing a Text string only.
|
||||
type Generator struct {
|
||||
// Namespace that will be concatenated previous to the icon generation.
|
||||
Namespace string
|
||||
// Size is the number of blocks of the figure.
|
||||
Size int
|
||||
// Density * Size = times to iterate over the hash of Text:Namespace:Seed.
|
||||
Density int
|
||||
// hashFunction used to generate a fixed length array of bytes.
|
||||
hashFunction func([]byte) []byte
|
||||
// fillColorFunction used to pick a color to fill the squares of the figure.
|
||||
fillColorFunction func([]byte) color.Color
|
||||
// backgroundColorFunction used to pick a background color for the figure.
|
||||
backgroundColorFunction func([]byte, color.Color) color.Color
|
||||
// isRandom flag to decide whether the generated image will be randomized.
|
||||
isRandom bool
|
||||
// rand is the source of randomness.
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
// option Configuration functional approach
|
||||
type option func(*Generator)
|
||||
|
||||
// New returns a pointer to a Generator with the desired configuration.
|
||||
func New(
|
||||
namespace string,
|
||||
size int,
|
||||
density int,
|
||||
opts ...option,
|
||||
) (*Generator, error) {
|
||||
|
||||
if size < MinSize {
|
||||
// Smaller values will generate a meaningless Generator.
|
||||
return nil, errors.New(
|
||||
"Size cannot be less than " + strconv.Itoa(MinSize),
|
||||
)
|
||||
}
|
||||
|
||||
if density < 1 {
|
||||
return nil, errors.New(
|
||||
"Density cannot be less than 1",
|
||||
)
|
||||
}
|
||||
|
||||
g := Generator{
|
||||
Size: size,
|
||||
Namespace: namespace,
|
||||
Density: density,
|
||||
isRandom: false,
|
||||
hashFunction: _sha256,
|
||||
fillColorFunction: _fillColor,
|
||||
backgroundColorFunction: _backgroundColor,
|
||||
}
|
||||
|
||||
g.Option(opts...)
|
||||
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
// Draw returns a pointer to an IdentIcon with a generated figure and a color.
|
||||
func (g Generator) Draw(text string) (*IdentIcon, error) {
|
||||
|
||||
var randomGenerator *rand.Rand
|
||||
|
||||
if g.isRandom {
|
||||
// In order to generate a randomized Canvas, use UnixNano as the source
|
||||
// of randomess. By doing this, random numbers won't be consistent.
|
||||
randomGenerator = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
} else {
|
||||
// rand will generate consistent values since the source is Size.
|
||||
randomGenerator = rand.New(rand.NewSource(int64(g.Size)))
|
||||
}
|
||||
|
||||
ii, err := newIdentIcon(
|
||||
text,
|
||||
g.Namespace,
|
||||
g.Size,
|
||||
g.Density,
|
||||
g.isRandom,
|
||||
randomGenerator,
|
||||
g.hashFunction,
|
||||
g.fillColorFunction,
|
||||
g.backgroundColorFunction,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate Canvas
|
||||
ii.Draw()
|
||||
|
||||
return ii, nil
|
||||
}
|
||||
|
||||
// Option sets the options specified.
|
||||
func (g *Generator) Option(opts ...option) {
|
||||
for _, opt := range opts {
|
||||
opt(g)
|
||||
}
|
||||
}
|
||||
|
||||
// SetHashFunction replaces the default hash function (Sha256).
|
||||
func SetHashFunction(hf func([]byte) []byte) option {
|
||||
return func(g *Generator) {
|
||||
g.hashFunction = hf
|
||||
}
|
||||
}
|
||||
|
||||
// SetFillColorFunction replaces the default color generation function (HSL).
|
||||
func SetFillColorFunction(fcf func([]byte) color.Color) option {
|
||||
return func(g *Generator) {
|
||||
g.fillColorFunction = fcf
|
||||
}
|
||||
}
|
||||
|
||||
// SetBackgroundColorFunction replaces the default background's color generation
|
||||
// function (HSL).
|
||||
func SetBackgroundColorFunction(bcf func([]byte, color.Color) color.Color) option {
|
||||
return func(g *Generator) {
|
||||
g.backgroundColorFunction = bcf
|
||||
}
|
||||
}
|
||||
|
||||
// SetRandom to append a random string to the generator text everytime Draw is
|
||||
// called.
|
||||
func SetRandom(r bool) option {
|
||||
return func(g *Generator) {
|
||||
g.isRandom = r
|
||||
}
|
||||
}
|
||||
|
||||
func _sha256(b []byte) []byte {
|
||||
digest := sha256.Sum256(b)
|
||||
return digest[:]
|
||||
}
|
||||
|
||||
func _fillColor(hashBytes []byte) color.Color {
|
||||
cb1, cb2 := uint32(hashBytes[0]), uint32(hashBytes[1])
|
||||
h := (cb1 + cb2) % 360
|
||||
s := (cb1 % 30) + 60
|
||||
l := (cb2 % 20) + 40
|
||||
|
||||
// Some colors in the HSL color model are too bright and don't play well
|
||||
// with the default background color. This is a naïve normalization method.
|
||||
if (h >= 50 && h <= 85) || (h >= 170 && h <= 190) {
|
||||
s = 80
|
||||
l -= 20
|
||||
} else if h > 85 && h < 170 {
|
||||
l -= 10
|
||||
}
|
||||
|
||||
return HSL{h, s, l}
|
||||
}
|
||||
|
||||
func _backgroundColor(hashBytes []byte, fill color.Color) color.Color {
|
||||
return color.NRGBA{R: 240, G: 240, B: 240, A: 255}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package identicon
|
||||
|
||||
// Identicon WebColor maxium values.
|
||||
const (
|
||||
// hueMax is the maximum allowed value for Hue in the HSL color model.
|
||||
hueMax = 360
|
||||
// saturationMax is the maximum allowed value for Saturation in the HSL
|
||||
// color model.
|
||||
saturationMax = 100
|
||||
// lightnessMax is the maximum allowed value for lightnessMax in the HSL
|
||||
// color model.
|
||||
lightnessMax = 100
|
||||
// rgbaMax is the maximum allowed value for any R, G, B, A property value.
|
||||
rgbaMax = 255
|
||||
)
|
||||
|
||||
// HSL is a color model representation based on RGB. HSL facilitates the
|
||||
// generation of colors that look similar between themselves by changing the
|
||||
// value of Hue H while keeping Saturation S and Lightness L the same.
|
||||
type HSL struct {
|
||||
// Hue [0, 360]
|
||||
H uint32
|
||||
// Saturation [0, 100]
|
||||
S uint32
|
||||
// Lightness [0, 100]
|
||||
L uint32
|
||||
}
|
||||
|
||||
// RGBA conversion
|
||||
func (hsl HSL) RGBA() (r, g, b, a uint32) {
|
||||
h := 1.0 / float64(hueMax) * float64(hsl.H)
|
||||
s := float64(hsl.S) / float64(saturationMax)
|
||||
l := float64(hsl.L) / float64(lightnessMax)
|
||||
r, g, b = hslToRgb(h, s, l)
|
||||
a = rgbaMax
|
||||
r |= r << 8
|
||||
g |= g << 8
|
||||
b |= b << 8
|
||||
a |= a << 8
|
||||
return
|
||||
}
|
||||
|
||||
// Golang port of Mohen's code in Stack Overflow.
|
||||
// https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
|
||||
func hslToRgb(h, s, l float64) (uint32, uint32, uint32) {
|
||||
var q, p float64
|
||||
var r, g, b float64
|
||||
|
||||
if s == 0 {
|
||||
r = l
|
||||
g = l
|
||||
b = l
|
||||
} else {
|
||||
if l < 0.5 {
|
||||
q = l * (1 + s)
|
||||
} else {
|
||||
q = (l + s) - (l * s)
|
||||
}
|
||||
p = (2 * l) - q
|
||||
r = hueToRgb(p, q, h+(1.0/3.0))
|
||||
g = hueToRgb(p, q, h)
|
||||
b = hueToRgb(p, q, h-(1.0/3.0))
|
||||
}
|
||||
|
||||
return uint32(r * rgbaMax), uint32(g * rgbaMax), uint32(b * rgbaMax)
|
||||
}
|
||||
|
||||
func hueToRgb(p, q, t float64) float64 {
|
||||
if t < 0 {
|
||||
t++
|
||||
} else if t > 1 {
|
||||
t--
|
||||
}
|
||||
switch {
|
||||
case 6*t < 1:
|
||||
return (p + (q-p)*6*t)
|
||||
case 2*t < 1:
|
||||
return q
|
||||
case 3*t < 2:
|
||||
return p + (q-p)*((2.0/3.0)-t)*6
|
||||
}
|
||||
return p
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
// Package identicon is an open source avatar generator inspired by GitHub avatars.
|
||||
//
|
||||
// IdentIcon uses a deterministic algorithm that generates an image (using Golang's
|
||||
// stdlib image encoders) based on a text (Generally Usernames, Emails or just
|
||||
// random strings), by hashing it and iterating over the bytes of the digest to pick
|
||||
// whether to draw a point, pick a color or choose where to go next.
|
||||
//
|
||||
// IdentIcon's Generator enables the creation of customized figures: (NxN size,
|
||||
// points density, custom color palette) as well as multiple exporting formats in
|
||||
// case the developers want to generate their own images.
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
// Bits used to give continuity
|
||||
moveUp = 0x80
|
||||
moveDown = 0x40
|
||||
moveLeft = 0x20
|
||||
moveRight = 0x10
|
||||
|
||||
// Either 0x8 or 0x2 are active
|
||||
fillPoint = 0xA
|
||||
)
|
||||
|
||||
// Constrains for the size of the IdentIcon.
|
||||
const (
|
||||
// MinSize is the minimal number of blocks allowed, anything lower that this
|
||||
// wouldn't make sense.
|
||||
MinSize = 4
|
||||
)
|
||||
|
||||
// IdentIcon represents a mirror-symmetry image generated from a string and a
|
||||
// set of configurations.
|
||||
type IdentIcon struct {
|
||||
// Text is the base string that will generate the canvas after being hashed.
|
||||
Text string
|
||||
// Namespace
|
||||
Namespace string
|
||||
|
||||
// Size is the number of blocks of the figure.
|
||||
Size int
|
||||
// Density * Size = times to iterate over the hash of Text.
|
||||
Density int
|
||||
// Canvas is a map of maps that contains the points and values that has been
|
||||
// visited and filled.
|
||||
Canvas Canvas
|
||||
|
||||
// FillColor is the color used to fill squares in the figure when encoding
|
||||
// to PNG or JPEG.
|
||||
FillColor color.Color
|
||||
// BackgroundColor is the background color of the figure when encoding it to
|
||||
// PNG or JPEG.
|
||||
BackgroundColor color.Color
|
||||
// fillColorFunction used to pick a color to fill the squares of the figure.
|
||||
fillColorFunction func([]byte) color.Color
|
||||
// backgroundColorFunction used to pick a background color for the figure.
|
||||
backgroundColorFunction func([]byte, color.Color) color.Color
|
||||
|
||||
// drawableWidth represents the length of the left half of the canvas.
|
||||
drawableWidth int
|
||||
// hasBeenDrawn indicates whether the Draw() has been called before.
|
||||
hasBeenDrawn bool
|
||||
|
||||
// hashFunction used to generate a fixed length array of bytes.
|
||||
hashFunction func([]byte) []byte
|
||||
|
||||
// isRandom flag to decide whether the generated image will be randomized.
|
||||
isRandom bool
|
||||
// randomSeed
|
||||
randomSeed string
|
||||
// rand is the source of randomness.
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
// Draw a figure in Canvas.
|
||||
// - If isRandom == true, the figure will redrawn everytime Draw() is called,
|
||||
// - If isRandom == false and Draw() was called before, it won't redraw.
|
||||
func (ii *IdentIcon) Draw() {
|
||||
|
||||
if ii.hasBeenDrawn && !ii.isRandom {
|
||||
// Don't redraw once twice unless isRandom is enabled.
|
||||
return
|
||||
} else if ii.isRandom {
|
||||
// Set a new randomSeed everytime Draw is executed to produce different
|
||||
// results on each execution.
|
||||
ii.randomSeed = strconv.Itoa(ii.rand.Int())
|
||||
}
|
||||
|
||||
ii.hasBeenDrawn = true
|
||||
|
||||
// Make sure that the canvas has been initialized.
|
||||
ii.initCanvas()
|
||||
|
||||
// current index of the digested bytes array.
|
||||
var i int
|
||||
// Number of bytes readed.
|
||||
var readedBytes int
|
||||
// Flag to know whether it as completed a full cycle.
|
||||
var hasCompletedCycle bool
|
||||
// Position that represents a point in the canvas.
|
||||
var current image.Point
|
||||
|
||||
// Text:Namespace:randomSeed
|
||||
generatingBytes := []byte(ii.GeneratorText())
|
||||
|
||||
// Produce fixed-length array of bytes that will be used to control the
|
||||
// drawing process.
|
||||
hashBytes := ii.hashFunction(generatingBytes)
|
||||
hashBytesLen := len(hashBytes)
|
||||
|
||||
ii.FillColor = ii.fillColorFunction(hashBytes)
|
||||
ii.BackgroundColor = ii.backgroundColorFunction(hashBytes, ii.FillColor)
|
||||
|
||||
// Total number of iterations over the digested hash.
|
||||
bytesToRead := ii.Density * ii.Size
|
||||
|
||||
for {
|
||||
if hasCompletedCycle {
|
||||
// If the number of bytes to read exceeds the length of the hash,
|
||||
// it will cycle through it. After it has completed a whole cycle,
|
||||
// altering the value will produce more varied figures.
|
||||
//
|
||||
// XOR pseudo-random produces interesting results.
|
||||
hashBytes[i] ^= byte(ii.rand.Intn(255))
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// Everytime a new cycle is starting, change the current point to
|
||||
// cover multiple areas of the canvas.
|
||||
current = initialPoint(
|
||||
hashBytes[0],
|
||||
ii.rand.Intn(ii.drawableWidth),
|
||||
ii.rand.Intn(ii.Size),
|
||||
)
|
||||
}
|
||||
|
||||
// value to add in the current point, zeroes will be ignored.
|
||||
value := getFillValue(hashBytes[i])
|
||||
|
||||
if value != 0 {
|
||||
// Initialize the map for Y-axis, making sure that the map that
|
||||
// contains X-axis values won't be nil.
|
||||
createMapIfDoesntExist(&ii.Canvas, current.Y)
|
||||
|
||||
firstTimeFilled := false
|
||||
if ii.Canvas.PointsMap[current.Y][current.X] == 0 {
|
||||
// Increment FilledPoints the first time this point is visited.
|
||||
ii.Canvas.FilledPoints++
|
||||
firstTimeFilled = true
|
||||
}
|
||||
|
||||
// Add the value to current position
|
||||
ii.Canvas.PointsMap[current.Y][current.X] += value
|
||||
|
||||
// Mark Y value as visited. This will be helpful to determine big
|
||||
// blank spaces in the resulting figure.
|
||||
ii.Canvas.VisitedYPoints[current.Y] = true
|
||||
|
||||
// Update the maximum and minimum Y-axis values, useful to
|
||||
// vertically center the figure at image creation.
|
||||
if current.Y < ii.Canvas.MinY {
|
||||
ii.Canvas.MinY = current.Y
|
||||
}
|
||||
if current.Y > ii.Canvas.MaxY {
|
||||
ii.Canvas.MaxY = current.Y
|
||||
}
|
||||
|
||||
// When Size is an odd number, prevent points in the middle to be
|
||||
// added twice. By substrating oddDiff to drawableWidth we make sure
|
||||
// that it doesn't happens.
|
||||
oddDiff := ii.Size % 2
|
||||
if current.X < (ii.drawableWidth - oddDiff) {
|
||||
// Calculate the mirror position for X-axis
|
||||
mirror := mirrorSymmetric(current, ii.Size)
|
||||
// Add value to the mirrowed position
|
||||
ii.Canvas.PointsMap[mirror.Y][mirror.X] += value
|
||||
if firstTimeFilled {
|
||||
ii.Canvas.FilledPoints++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decide the next position relative to the current position.
|
||||
current = nextPoint(hashBytes[i], current, ii.drawableWidth, ii.Size)
|
||||
|
||||
i++
|
||||
readedBytes++
|
||||
|
||||
if readedBytes >= bytesToRead {
|
||||
// The total number of bytes to read has been reached, stop.
|
||||
break
|
||||
}
|
||||
|
||||
if i == hashBytesLen-1 {
|
||||
// A full cycle has been completed, reset the index to prevent
|
||||
// getting out of bounds.
|
||||
i = 0
|
||||
// Further iterations will add a pesudo-random number to hashBytes.
|
||||
hasCompletedCycle = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// GeneratorText returns the string later to be hashed using the format:
|
||||
// - Text[:Namespace][:randomSeed]
|
||||
func (ii *IdentIcon) GeneratorText() string {
|
||||
gt := ii.Text
|
||||
|
||||
if ii.Namespace != "" {
|
||||
gt += ":" + ii.Namespace
|
||||
}
|
||||
|
||||
if ii.isRandom && ii.randomSeed != "" {
|
||||
gt += ":" + ii.randomSeed
|
||||
}
|
||||
|
||||
return gt
|
||||
}
|
||||
|
||||
// Array generates a two-dimensional array version of the IdentIcon figure.
|
||||
func (ii *IdentIcon) Array() [][]int {
|
||||
return ii.Canvas.Array()
|
||||
}
|
||||
|
||||
// ToString generates a string version of the IdentIcon figure.
|
||||
func (ii *IdentIcon) String(separator string, fillEmptyWith string) string {
|
||||
return ii.Canvas.String(separator, fillEmptyWith)
|
||||
}
|
||||
|
||||
// Points generates an array of points of a two-dimensional plane as [x, y]
|
||||
// that correspond to all filled points in the IdentIcon figure.
|
||||
func (ii *IdentIcon) Points() []image.Point {
|
||||
return ii.Canvas.Points()
|
||||
}
|
||||
|
||||
// IntCoordinates generates an array of points of a two-dimensional plane as:
|
||||
// - [x, y] that correspond to all filled points in the IdentIcon figure.
|
||||
func (ii *IdentIcon) IntCoordinates() [][]int {
|
||||
return ii.Canvas.IntCoordinates()
|
||||
}
|
||||
|
||||
// New returns a pointer to IdentIcon.
|
||||
func newIdentIcon(
|
||||
text string,
|
||||
namespace string,
|
||||
size int,
|
||||
density int,
|
||||
isRandom bool,
|
||||
rand *rand.Rand,
|
||||
hashFunction func([]byte) []byte,
|
||||
fillColorFunction func([]byte) color.Color,
|
||||
backgroundColorFunction func([]byte, color.Color) color.Color,
|
||||
) (*IdentIcon, error) {
|
||||
|
||||
if text == "" {
|
||||
// Text is the minimum requirement to generate an IdentIcon.
|
||||
return nil, errors.New("Text can't be empty")
|
||||
}
|
||||
|
||||
if size < MinSize {
|
||||
// Smaller values will generate a meaningless Generator.
|
||||
return nil, errors.New(
|
||||
"Size cannot be less than " + strconv.Itoa(MinSize),
|
||||
)
|
||||
}
|
||||
|
||||
if density < 1 {
|
||||
return nil, errors.New(
|
||||
"Density cannot be less than 1",
|
||||
)
|
||||
}
|
||||
|
||||
identicon := IdentIcon{
|
||||
Text: text,
|
||||
Namespace: namespace,
|
||||
Size: size,
|
||||
Density: density,
|
||||
isRandom: isRandom,
|
||||
rand: rand,
|
||||
hashFunction: hashFunction,
|
||||
fillColorFunction: fillColorFunction,
|
||||
backgroundColorFunction: backgroundColorFunction,
|
||||
}
|
||||
|
||||
// Reflection Line
|
||||
identicon.drawableWidth = identicon.Size / 2
|
||||
|
||||
// Since the canvas is a symmetrical reflection make sure to:
|
||||
// - Handle even and odd Canvas sizes
|
||||
if identicon.Size%2 == 1 {
|
||||
// Is odd, the vertical middle point exist.
|
||||
identicon.drawableWidth++
|
||||
}
|
||||
|
||||
return &identicon, nil
|
||||
}
|
||||
|
||||
// initCanvas initializes and erases everything that was in the Canvas map.
|
||||
func (ii *IdentIcon) initCanvas() {
|
||||
ii.Canvas = Canvas{
|
||||
Size: ii.Size,
|
||||
PointsMap: make(map[int]map[int]int),
|
||||
MinY: ii.Size,
|
||||
MaxY: 0,
|
||||
VisitedYPoints: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func nextPoint(control byte, p image.Point, width, heigth int) image.Point {
|
||||
// Active bits will decide the destination of the next point.
|
||||
// - If two opposite bits are active, it will keep its current position.
|
||||
if control&moveUp == moveUp {
|
||||
p.Y--
|
||||
}
|
||||
if control&moveDown == moveDown {
|
||||
p.Y++
|
||||
}
|
||||
if control&moveLeft == moveLeft {
|
||||
p.X--
|
||||
}
|
||||
if control&moveRight == moveRight {
|
||||
p.X++
|
||||
}
|
||||
|
||||
// Transform to 0-based indices.
|
||||
width--
|
||||
heigth--
|
||||
|
||||
// Teleport to opposite bounds when the limit has been reached.
|
||||
if p.X > width {
|
||||
p.X = 0
|
||||
} else if p.X < 0 {
|
||||
p.X = width
|
||||
}
|
||||
if p.Y > heigth {
|
||||
p.Y = 0
|
||||
} else if p.Y < 0 {
|
||||
p.Y = heigth
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func initialPoint(control byte, width, heigth int) image.Point {
|
||||
return image.Point{
|
||||
Y: heigth,
|
||||
X: width,
|
||||
}
|
||||
}
|
||||
|
||||
func mirrorSymmetric(p image.Point, size int) image.Point {
|
||||
return image.Point{
|
||||
Y: p.Y,
|
||||
X: size - p.X - 1,
|
||||
}
|
||||
}
|
||||
|
||||
func getFillValue(control byte) int {
|
||||
if control&fillPoint > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func createMapIfDoesntExist(canvas *Canvas, y int) {
|
||||
_, exist := canvas.PointsMap[y]
|
||||
if !exist {
|
||||
canvas.PointsMap[y] = make(map[int]int)
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Image genetares an image.Image of size
|
||||
func (ii *IdentIcon) Image(pixels int) image.Image {
|
||||
|
||||
// Padding is relative to the number of blocks.
|
||||
padding := pixels / (ii.Size * MinSize)
|
||||
drawableArea := pixels - (padding * 2)
|
||||
blockSize := drawableArea / ii.Size
|
||||
|
||||
// Add the residue (pixels that won't be filled) to the padding.
|
||||
// Try to center the figure regardless when the drawable area is not
|
||||
// divisible by the block pixels.
|
||||
padding += (drawableArea % ii.Size) / 2
|
||||
|
||||
img := image.NewNRGBA(image.Rect(0, 0, pixels, pixels))
|
||||
|
||||
// Background
|
||||
draw.Draw(
|
||||
img,
|
||||
img.Bounds(),
|
||||
&image.Uniform{ii.BackgroundColor},
|
||||
image.ZP,
|
||||
draw.Src,
|
||||
)
|
||||
|
||||
for y, mapX := range ii.Canvas.PointsMap {
|
||||
for x := range mapX {
|
||||
|
||||
ix := blockSize*x + padding
|
||||
iy := blockSize*y + padding
|
||||
|
||||
draw.Draw(img,
|
||||
image.Rect(
|
||||
ix,
|
||||
iy,
|
||||
ix+blockSize,
|
||||
iy+blockSize,
|
||||
),
|
||||
&image.Uniform{ii.FillColor},
|
||||
image.ZP,
|
||||
draw.Src,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
// Png writes an image of pixels
|
||||
func (ii *IdentIcon) Png(pixels int, w io.Writer) error {
|
||||
img := ii.Image(pixels)
|
||||
return png.Encode(w, img)
|
||||
}
|
||||
|
||||
// Jpeg writes an image of pixels and quality
|
||||
func (ii *IdentIcon) Jpeg(pixels int, quality int, w io.Writer) error {
|
||||
img := ii.Image(pixels)
|
||||
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
|
||||
}
|
||||
|
||||
// Svg writes an image of pixels
|
||||
func (ii *IdentIcon) Svg(pixels int, w io.Writer) error {
|
||||
return svgEncode(w, ii, pixels)
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"io"
|
||||
"strconv"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type svgRect struct {
|
||||
X int
|
||||
Y int
|
||||
Width int
|
||||
Height int
|
||||
FillColor string
|
||||
}
|
||||
|
||||
type svgTmpl struct {
|
||||
Pixels int
|
||||
BackgroundColor string
|
||||
FillColor string
|
||||
Rects []svgRect
|
||||
}
|
||||
|
||||
const svgTemplate = `
|
||||
<!-- <?xml version="1.0"?> -->
|
||||
<svg version="1.1" baseprofile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" width="{{.Pixels}}" height="{{.Pixels}}" viewBox="0 0 {{.Pixels}} {{.Pixels}}" >
|
||||
<rect width="100%" height="100%" fill="{{.BackgroundColor}}"/>
|
||||
{{range $index, $r := .Rects}}
|
||||
<rect x="{{$r.X}}" y="{{$r.Y}}" width="{{$r.Width}}" height="{{$r.Height}}" fill="{{$r.FillColor}}"/>{{end}}
|
||||
</svg>`
|
||||
|
||||
func colorToRGBAString(c color.Color) string {
|
||||
r, g, b, a := c.RGBA()
|
||||
r >>= 8
|
||||
g >>= 8
|
||||
b >>= 8
|
||||
a >>= 8
|
||||
|
||||
rs := strconv.Itoa(int(r))
|
||||
gs := strconv.Itoa(int(g))
|
||||
bs := strconv.Itoa(int(b))
|
||||
as := strconv.Itoa(int(a))
|
||||
|
||||
return "rgba(" + rs + "," + gs + "," + bs + "," + as + ")"
|
||||
}
|
||||
|
||||
// Encode an IdentIcon to SVG
|
||||
func svgEncode(w io.Writer, ii *IdentIcon, pixels int) error {
|
||||
|
||||
// Padding is relative to the number of blocks.
|
||||
padding := pixels / (ii.Size * MinSize)
|
||||
drawableArea := pixels - (padding * 2)
|
||||
blockSize := drawableArea / ii.Size
|
||||
|
||||
// Add the residue (pixels that won't be filled) to the padding.
|
||||
// Try to center the figure regardless when the drawable area is not
|
||||
// divisible by the block pixels.
|
||||
padding += (drawableArea % ii.Size) / 2
|
||||
|
||||
fillColor := colorToRGBAString(ii.FillColor)
|
||||
backgroundColor := colorToRGBAString(ii.BackgroundColor)
|
||||
|
||||
b, err := template.New("svg").Parse(svgTemplate)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := svgTmpl{
|
||||
Pixels: pixels,
|
||||
BackgroundColor: backgroundColor,
|
||||
FillColor: fillColor,
|
||||
Rects: make([]svgRect, ii.Canvas.FilledPoints),
|
||||
}
|
||||
|
||||
i := 0
|
||||
for y, mapX := range ii.Canvas.PointsMap {
|
||||
for x := range mapX {
|
||||
t.Rects[i] = svgRect{
|
||||
blockSize*x + padding,
|
||||
blockSize*y + padding,
|
||||
blockSize,
|
||||
blockSize,
|
||||
fillColor,
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return b.Execute(w, t)
|
||||
}
|
||||
Reference in New Issue
Block a user