diff --git a/shared/identicon/block.go b/shared/identicon/block.go new file mode 100644 index 0000000..46377bd --- /dev/null +++ b/shared/identicon/block.go @@ -0,0 +1,100 @@ +package identicon + +import ( + "git.mstar.dev/mstar/canvas" + mathutils "git.mstar.dev/mstar/goutils/math" +) + +type vec2 struct { + X, Y float64 +} + +type block struct { + canvas *canvas.Canvas + blockType int + primary string + secondary string + hash int + pos vec2 + cellSize int + margin int + scale float64 +} + +func (b *block) Offset() { + b.canvas.Save() + b.canvas.Translate(0.6*b.scale, -0.6*b.scale) +} + +func (b *block) ResetOffset() { + b.canvas.Restore() +} + +func (b *block) MakePath(hash int, offset int) { + mod := mathutils.Abs(hash+offset) % 4 + switch mod { + case 0: + // Top + b.canvas.BeginPath() + b.canvas.MoveTo(float64(b.pos.X), float64(b.pos.Y)) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), float64(b.pos.Y)) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), b.pos.Y+float64(int(b.cellSize))) + b.canvas.ClosePath() + case 1: + // Right + b.canvas.BeginPath() + b.canvas.MoveTo(b.pos.X+float64(int(b.cellSize)), float64(b.pos.Y)) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), b.pos.Y+float64(int(b.cellSize))) + b.canvas.LineTo(float64(b.pos.X), b.pos.Y+float64(int(b.cellSize))) + b.canvas.ClosePath() + case 2: + // Bottom + b.canvas.BeginPath() + b.canvas.MoveTo(float64(b.pos.X), float64(b.pos.Y)) + b.canvas.LineTo(float64(b.pos.X), b.pos.Y+float64(int(b.cellSize))) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), b.pos.Y+float64(int(b.cellSize))) + b.canvas.ClosePath() + case 3: + // Left + b.canvas.BeginPath() + b.canvas.MoveTo(float64(b.pos.X), float64(b.pos.Y)) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), float64(b.pos.Y)) + b.canvas.LineTo(float64(b.pos.X), b.pos.Y+float64(int(b.cellSize))) + b.canvas.ClosePath() + default: + // Top + b.canvas.BeginPath() + b.canvas.MoveTo(float64(b.pos.X), float64(b.pos.Y)) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), float64(b.pos.Y)) + b.canvas.LineTo(b.pos.X+float64(int(b.cellSize)), b.pos.Y+float64(int(b.cellSize))) + b.canvas.ClosePath() + } +} + +func (b *block) Draw() { + b.Offset() + switch b.blockType { + case 1: + b.MakePath(b.hash, b.hash%3) + b.canvas.SetFillStyle(b.primary) + b.canvas.Fill() + b.MakePath(b.hash, b.hash%5) + b.canvas.SetFillStyle(b.secondary) + b.canvas.Fill() + case 2: + b.MakePath(b.hash, b.hash%4) + b.canvas.SetFillStyle(b.secondary) + b.canvas.Fill() + b.MakePath(b.hash, b.hash%3) + b.canvas.SetFillStyle(b.primary) + b.canvas.Fill() + default: + b.MakePath(b.hash, b.hash%7) + b.canvas.SetFillStyle(b.secondary) + b.canvas.Fill() + b.MakePath(b.hash, b.hash%8) + b.canvas.SetFillStyle(b.primary) + b.canvas.Fill() + } + b.ResetOffset() +} diff --git a/shared/identicon/identicons.go b/shared/identicon/identicons.go new file mode 100644 index 0000000..a7cc217 --- /dev/null +++ b/shared/identicon/identicons.go @@ -0,0 +1,218 @@ +package identicon + +import ( + "errors" + "fmt" + "image" + "math" + "strings" + + "git.mstar.dev/mstar/canvas" + "git.mstar.dev/mstar/canvas/backend/softwarebackend" + mathutils "git.mstar.dev/mstar/goutils/math" + "git.mstar.dev/mstar/goutils/sliceutils" +) + +type identicon struct { + canvas *canvas.Canvas + primary string + secondary string + margin int + scale float64 + cellSize int + hash int + blocks []block + shape shape + hasStroke bool + strokeWeight int + strokeColor string + compositeOperation string + palette []string +} + +const canvasSize = 500 + +var ( + palette = []string{ + "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", + "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", + "#FFEB3B", "#FFC107", "#FF9800", "#FF5722", "#795548", "#607D8B", + } + validOperations = []string{ + "source-over", "source-in", "source-out", "source-atop", "destination-over", + "destination-in", "destination-out", "destination-atop", "lighter", "copy", + "xor", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", + "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", + "saturation", "color", "luminosity", + } +) + +// Deprecated: Doesn't work +func GenerateIdenticon(text string) *image.RGBA { + backend := softwarebackend.New(canvasSize, canvasSize) + canvas := canvas.New(backend) + i := identicon{ + canvas: canvas, + hash: hash(text), + } + i.Init() + i.Draw() + tmp := *backend.Image + return &tmp +} + +func hash(text string) int { + runes := []rune(text) + return sliceutils.Compact( + sliceutils.Map(runes, func(t rune) int { return int(t) }), + func(acc, next int) int { + acc = ((acc << 5) - acc) + next + return acc & acc + }, + ) +} + +func (i *identicon) SetUsername(username string) { + i.hash = hash(username) +} + +func (i *identicon) SetPalette(pal []string) error { + if len(pal) < 2 { + return errors.New("palette to small") + } + i.palette = pal + return nil +} + +func (i *identicon) SetCompositeOperation(newOp string) error { + newOp = strings.ToLower(newOp) + if !sliceutils.Contains(validOperations, newOp) { + return errors.New("invalid operation") + } + i.compositeOperation = newOp + return nil +} + +func (i *identicon) Draw() { + i.Init() + i.canvas.Save() + i.canvas.Translate(float64(canvasSize/2), float64(canvasSize/2)) + i.canvas.Rotate(-math.Pi / 4) + i.canvas.Translate(float64(canvasSize/2), float64(canvasSize/2)) + i.GenerateBlocks() + i.DrawBlocks() + if i.hasStroke { + i.DrawOutline() + } + i.shape = shape{ + canvas: i.canvas, + hash: i.hash, + primary: i.primary, + secondary: i.secondary, + pos: vec2{ + X: float64(i.margin*int(i.scale)) + 1.5*float64(i.cellSize), + Y: float64(i.margin)*i.scale + 0.5*float64(i.cellSize), + }, + scale: i.scale, + cellSize: i.cellSize, + strokeColor: i.strokeColor, + } + i.shape.Draw(i.hasStroke, i.strokeWeight) + i.canvas.Restore() +} + +func (i *identicon) Init() { + i.blocks = []block{} + i.shape = shape{} + i.primary = palette[mathutils.Abs(i.hash%len(palette))] + i.secondary = palette[mathutils.Abs(hash(fmt.Sprint(i.hash))%len(palette))] + // i.canvas.SetGlobalCompositeOperation("source-over") + i.canvas.ClearRect(0, 0, canvasSize, canvasSize) + // i.canvas.SetGlobalCompositeOperation(i.compositeOperation) +} + +func (i *identicon) Offset() { + i.canvas.Save() + i.canvas.Translate(0.6*i.scale, -0.6*i.scale) +} + +func (i *identicon) ResetOffset() { + i.canvas.Restore() +} + +func (i *identicon) GenerateBlocks() { + i.blocks = append(i.blocks, block{ + canvas: i.canvas, + blockType: 1, + primary: i.primary, + secondary: i.secondary, + hash: i.hash, + pos: vec2{float64(i.margin) * i.scale, float64(i.margin) * i.scale}, + cellSize: i.cellSize, + margin: i.margin, + scale: i.scale, + }, block{ + canvas: i.canvas, + blockType: 2, + primary: i.primary, + secondary: i.primary, + hash: i.hash, + pos: vec2{float64(i.margin) * i.scale, float64(i.canvas.Height()) / 2}, + cellSize: i.cellSize, + margin: i.margin, + scale: i.scale, + }, block{ + canvas: i.canvas, + blockType: 3, + primary: i.primary, + secondary: i.secondary, + hash: i.hash, + pos: vec2{float64(i.canvas.Width()) / 2, float64(i.canvas.Height()) / 2}, + cellSize: i.cellSize, + margin: i.margin, + scale: i.scale, + }, + ) +} + +func (i *identicon) DrawOutline() { + i.Offset() + // i.canvas.SetGlobalCompositeOperation("source-over") + i.canvas.BeginPath() + i.canvas.MoveTo(float64(i.margin)*i.scale, float64(i.margin)*i.scale) + i.canvas.LineTo(float64(i.margin)*i.scale, float64(canvasSize)-float64(i.margin)*i.scale) + i.canvas.LineTo( + float64(i.canvas.Width())-float64(i.margin)*i.scale, + float64(i.canvas.Height())-float64(i.margin)*i.scale, + ) + i.canvas.LineTo( + float64(i.canvas.Width())-float64(i.margin)*i.scale, + float64(i.canvas.Height())/2, + ) + i.canvas.LineTo(float64(i.canvas.Width())/2, float64(i.canvas.Height())/2) + i.canvas.LineTo(float64(i.canvas.Width())/2, float64(i.margin)*i.scale) + i.canvas.ClosePath() + + i.canvas.SetStrokeStyle(i.strokeColor) + i.canvas.SetLineWidth(i.scale * (float64(i.strokeWeight) / float64(i.canvas.Width()))) + i.canvas.SetLineJoin(canvas.Round) + i.canvas.SetLineCap(canvas.Round) + i.canvas.Stroke() + + i.canvas.BeginPath() + i.canvas.MoveTo(float64(i.canvas.Width())/2, float64(i.canvas.Height())/2) + i.canvas.LineTo(float64(i.margin)*i.scale, float64(i.canvas.Height())/2) + i.canvas.MoveTo(float64(i.canvas.Width())/2, float64(i.canvas.Height())/2) + i.canvas.LineTo( + float64(i.canvas.Width())/2, + float64(i.canvas.Height())-(float64(i.margin)*i.scale), + ) + i.canvas.Stroke() + i.ResetOffset() +} + +func (i *identicon) DrawBlocks() { + for _, b := range i.blocks { + b.Draw() + } +} diff --git a/shared/identicon/shape.go b/shared/identicon/shape.go new file mode 100644 index 0000000..4c80c95 --- /dev/null +++ b/shared/identicon/shape.go @@ -0,0 +1,112 @@ +package identicon + +import ( + "math" + + "git.mstar.dev/mstar/canvas" + mathutils "git.mstar.dev/mstar/goutils/math" +) + +type shape struct { + canvas *canvas.Canvas + hash int + primary string + secondary string + pos vec2 + scale float64 + cellSize int + strokeColor string +} + +func (s *shape) GetColor() string { + if mathutils.Abs(s.hash) == 0 { + return s.primary + } else { + return s.secondary + } +} + +func (s *shape) MakePath() { + mod := mathutils.Abs(s.hash+1) % 4 + switch mod { + case 0: + // square + s.canvas.BeginPath() + s.canvas.MoveTo(float64(s.pos.X), float64(s.pos.Y)) + s.canvas.LineTo(float64(s.pos.X+float64(s.cellSize/2)), float64(s.pos.Y)) + s.canvas.LineTo( + float64(s.pos.X+float64(s.cellSize/2)), + float64(s.pos.Y-float64(s.cellSize/2)), + ) + s.canvas.LineTo(float64(s.pos.X), float64(s.pos.Y-float64(s.cellSize/2))) + s.canvas.ClosePath() + case 1: + // circle + s.canvas.BeginPath() + s.canvas.Arc( + float64(s.pos.X)+float64(s.cellSize)/math.Pi-5, + float64(s.pos.Y)-float64(s.cellSize)/math.Pi+5, + float64(s.cellSize)/3, + 0, + math.Pi*2, + true, + ) + case 2: + // triangle + s.canvas.BeginPath() + s.canvas.MoveTo(float64(s.pos.X), float64(s.pos.Y)) + s.canvas.LineTo(float64(s.pos.X)+float64(s.cellSize)*0.65, float64(s.pos.Y)) + s.canvas.LineTo(float64(s.pos.X), float64(s.pos.Y)-float64(s.cellSize)*0.65) + s.canvas.ClosePath() + case 3: + // oval + s.canvas.BeginPath() + s.canvas.MoveTo( + float64(s.pos.X)-float64(s.cellSize)*0.2, + float64(s.pos.Y)+float64(s.cellSize)*0.2, + ) + s.canvas.QuadraticCurveTo( + float64(s.pos.X)+float64(s.cellSize)*0.4, + float64(s.pos.Y), + float64(s.pos.X)+float64(s.cellSize)*0.5, + float64(s.pos.Y)-float64(s.cellSize)*0.5, + ) + s.canvas.MoveTo( + float64(s.pos.X)+float64(s.cellSize)*0.5, + float64(s.pos.Y)-float64(s.cellSize)*0.5, + ) + s.canvas.QuadraticCurveTo( + float64(s.pos.X), + float64(s.pos.Y)-float64(s.cellSize)*0.4, + float64(s.pos.X)-float64(s.cellSize)*0.2, + float64(s.pos.Y)+float64(s.cellSize)*0.2, + ) + default: + // square + s.canvas.BeginPath() + s.canvas.MoveTo(float64(s.pos.X), float64(s.pos.Y)) + s.canvas.LineTo(float64(s.pos.X+float64(s.cellSize/2)), float64(s.pos.Y)) + s.canvas.LineTo( + float64(s.pos.X+float64(s.cellSize/2)), + float64(s.pos.Y-float64(s.cellSize/2)), + ) + s.canvas.LineTo(float64(s.pos.X), float64(s.pos.Y-float64(s.cellSize/2))) + s.canvas.ClosePath() + } +} + +func (s *shape) Draw(hasStroke bool, strokeWeight int) { + color := s.GetColor() + // THIS SHIT ISN'T IMPLEMENTED! I hope it will work regardless + // s.canvas.SetGlobalCompositeOperation("source-over") + s.MakePath() + s.canvas.SetFillStyle(color) + s.canvas.SetStrokeStyle(s.strokeColor) + s.canvas.SetLineWidth(s.scale * (4 / 5 * float64(strokeWeight)) / float64(canvasSize)) + s.canvas.SetLineJoin(canvas.Round) + s.canvas.SetLineCap(canvas.Round) + s.canvas.Fill() + if hasStroke { + s.canvas.Stroke() + } +}