Brush Pattern Textures
Palette swatches gain a new property: Pattern. Patterns are grayscale textures that the swatch color will sample against when drawing pixels to the level, by taking the world coordinate modulo a value inside the texture. A few algorithms were tried (Screen, Overlay), this branch lands on one that tries to cast the color from grayscale which comes out rather dark; to get a patterned color to look black while still seeing the pattern, the color needs to be as bright as #777 to get the effect.
BIN
assets/pattern/bars.png
Normal file
After Width: | Height: | Size: 989 B |
BIN
assets/pattern/circles.png
Normal file
After Width: | Height: | Size: 652 B |
BIN
assets/pattern/dashed.png
Normal file
After Width: | Height: | Size: 594 B |
BIN
assets/pattern/grid.png
Normal file
After Width: | Height: | Size: 594 B |
BIN
assets/pattern/ink.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/pattern/marker.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/pattern/noise.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
|
@ -4,7 +4,7 @@ package branding
|
||||||
const (
|
const (
|
||||||
AppName = "Sketchy Maze"
|
AppName = "Sketchy Maze"
|
||||||
Summary = "A drawing-based maze game"
|
Summary = "A drawing-based maze game"
|
||||||
Version = "0.6.0-alpha"
|
Version = "0.6.1-alpha"
|
||||||
Website = "https://www.sketchymaze.com"
|
Website = "https://www.sketchymaze.com"
|
||||||
Copyright = "2021 Noah Petherbridge"
|
Copyright = "2021 Noah Petherbridge"
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||||
"git.kirsle.net/apps/doodle/pkg/native"
|
"git.kirsle.net/apps/doodle/pkg/native"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/pattern"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
golog "git.kirsle.net/go/log"
|
golog "git.kirsle.net/go/log"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
@ -95,6 +96,9 @@ func (d *Doodle) SetupEngine() error {
|
||||||
// Initialize the UI modal manager.
|
// Initialize the UI modal manager.
|
||||||
modal.Initialize(d.Engine)
|
modal.Initialize(d.Engine)
|
||||||
|
|
||||||
|
// Preload the builtin brush patterns.
|
||||||
|
pattern.LoadBuiltins(d.Engine)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Stroke struct {
|
||||||
ID int // Unique ID per each stroke
|
ID int // Unique ID per each stroke
|
||||||
Shape Shape
|
Shape Shape
|
||||||
Color render.Color
|
Color render.Color
|
||||||
|
Pattern string
|
||||||
Thickness int // 0 = 1px; thickness creates a box N pixels away from each point
|
Thickness int // 0 = 1px; thickness creates a box N pixels away from each point
|
||||||
ExtraData interface{} // arbitrary storage for extra data to attach
|
ExtraData interface{} // arbitrary storage for extra data to attach
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/pattern"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -150,6 +151,7 @@ func (c *Chunk) ToBitmap(filename string, mask render.Color) (render.Texturer, e
|
||||||
img := image.NewRGBA(imgSize)
|
img := image.NewRGBA(imgSize)
|
||||||
|
|
||||||
// Blank out the pixels.
|
// Blank out the pixels.
|
||||||
|
// TODO PERF: may be slow?
|
||||||
for x := 0; x < img.Bounds().Max.X; x++ {
|
for x := 0; x < img.Bounds().Max.X; x++ {
|
||||||
for y := 0; y < img.Bounds().Max.Y; y++ {
|
for y := 0; y < img.Bounds().Max.Y; y++ {
|
||||||
img.Set(x, y, balance.DebugChunkBitmapBackground.ToColor())
|
img.Set(x, y, balance.DebugChunkBitmapBackground.ToColor())
|
||||||
|
@ -166,6 +168,12 @@ func (c *Chunk) ToBitmap(filename string, mask render.Color) (render.Texturer, e
|
||||||
// Blot all the pixels onto it.
|
// Blot all the pixels onto it.
|
||||||
for px := range c.Iter() {
|
for px := range c.Iter() {
|
||||||
var color = px.Swatch.Color
|
var color = px.Swatch.Color
|
||||||
|
|
||||||
|
// If the swatch has a pattern, mesh it in.
|
||||||
|
if px.Swatch.Pattern != "" {
|
||||||
|
color = pattern.SampleColor(px.Swatch.Pattern, color, px.Point())
|
||||||
|
}
|
||||||
|
|
||||||
if mask != render.Invisible {
|
if mask != render.Invisible {
|
||||||
// A semi-transparent mask will overlay on top of the actual color.
|
// A semi-transparent mask will overlay on top of the actual color.
|
||||||
if mask.Alpha < 255 {
|
if mask.Alpha < 255 {
|
||||||
|
|
|
@ -34,6 +34,7 @@ func DefaultPalette() *Palette {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBlueprintPalette returns the blueprint theme's color palette.
|
// NewBlueprintPalette returns the blueprint theme's color palette.
|
||||||
|
// DEPRECATED in favor of DefaultPalettes.
|
||||||
func NewBlueprintPalette() *Palette {
|
func NewBlueprintPalette() *Palette {
|
||||||
return &Palette{
|
return &Palette{
|
||||||
Swatches: []*Swatch{
|
Swatches: []*Swatch{
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
type Swatch struct {
|
type Swatch struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Color render.Color `json:"color"`
|
Color render.Color `json:"color"`
|
||||||
|
Pattern string `json:"pattern"` // like "noise.png"
|
||||||
|
|
||||||
// Optional attributes.
|
// Optional attributes.
|
||||||
Solid bool `json:"solid,omitempty"`
|
Solid bool `json:"solid,omitempty"`
|
||||||
|
|
230
pkg/pattern/pattern.go
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
// Package pattern applies a kind of brush texture to a palette swatch.
|
||||||
|
package pattern
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/sprites"
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pattern applies a texture to a color in level drawings.
|
||||||
|
type Pattern struct {
|
||||||
|
Name string
|
||||||
|
Filename string
|
||||||
|
Hidden bool // boolProp showHiddenDoodads true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builtins are the list of the game's built-in patterns.
|
||||||
|
var Builtins = []Pattern{
|
||||||
|
{
|
||||||
|
Name: "No pattern",
|
||||||
|
Filename: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Noise",
|
||||||
|
Filename: "noise.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Marker",
|
||||||
|
Filename: "marker.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Ink",
|
||||||
|
Filename: "ink.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Dashed Lines",
|
||||||
|
Filename: "circles.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Grid",
|
||||||
|
Filename: "grid.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Bars (debug)",
|
||||||
|
Filename: "bars.png",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Images is a map of file names to ui.Image widgets,
|
||||||
|
// after LoadBuiltins had been called.
|
||||||
|
var images map[string]*ui.Image
|
||||||
|
|
||||||
|
// LoadBuiltins loads all of the PNG textures of built-in patterns
|
||||||
|
// into ui.Image widgets.
|
||||||
|
func LoadBuiltins(e render.Engine) {
|
||||||
|
images = map[string]*ui.Image{}
|
||||||
|
|
||||||
|
for _, pat := range Builtins {
|
||||||
|
if pat.Filename == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := sprites.LoadImage(e, "assets/pattern/"+pat.Filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Load pattern %s: %s", pat.Filename, err)
|
||||||
|
}
|
||||||
|
images[pat.Filename] = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImage returns the ui.Image for a builtin pattern.
|
||||||
|
func GetImage(filename string) (*ui.Image, error) {
|
||||||
|
if images == nil {
|
||||||
|
return nil, errors.New("pattern.GetImage: LoadBuiltins() was not called")
|
||||||
|
}
|
||||||
|
|
||||||
|
if im, ok := images[filename]; ok {
|
||||||
|
return im, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pattern.GetImage: filename %s not found", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SampleColor samples a color with the pattern for a given coordinate in infinite space.
|
||||||
|
func SampleColor(filename string, color render.Color, point render.Point) render.Color {
|
||||||
|
if filename == "" {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not loaded in memory?
|
||||||
|
if _, ok := images[filename]; !ok {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate the world coord (point) into the bounds of the texture image.
|
||||||
|
var (
|
||||||
|
image = images[filename].Image // the Go image.Image
|
||||||
|
bounds = image.Bounds()
|
||||||
|
coord = render.Point{
|
||||||
|
// The world coordinate bounded to the pattern image size.
|
||||||
|
X: render.AbsInt(point.X % bounds.Max.X),
|
||||||
|
Y: render.AbsInt(point.Y % bounds.Max.Y),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample the color from the pattern texture.
|
||||||
|
colorAt = render.FromColor(image.At(coord.X, coord.Y))
|
||||||
|
|
||||||
|
// Average the RGBA color out to a grayscale brightness.
|
||||||
|
// sourceAvgGray = (int(color.Red) + int(color.Blue) + int(color.Green)/3) % 255
|
||||||
|
// patternAvgGray = (int(colorAt.Red) + int(colorAt.Blue) + int(colorAt.Green)/3) % 255
|
||||||
|
)
|
||||||
|
|
||||||
|
// See if the gray average is brighter or lower than the color.
|
||||||
|
// if sourceAvgGray < patternAvgGray {
|
||||||
|
// delta := patternAvgGray - sourceAvgGray
|
||||||
|
// color = color.Lighten(delta)
|
||||||
|
// } else if sourceAvgGray > patternAvgGray {
|
||||||
|
// color = color.Darken(sourceAvgGray - patternAvgGray)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return OverlayFilter(color, colorAt)
|
||||||
|
// return ScreenFilter(color, colorAt)
|
||||||
|
return GrayToColor(color, colorAt)
|
||||||
|
|
||||||
|
// log.Info("color: %s at point: %s image point: %s", color, point, coord)
|
||||||
|
|
||||||
|
// return color
|
||||||
|
}
|
||||||
|
|
||||||
|
// GrayToColor samples a colorful swatch with the grayscale pattern img.
|
||||||
|
func GrayToColor(color, grayscale render.Color) render.Color {
|
||||||
|
// The grayscale image ranges from 0 to 255.
|
||||||
|
// The color might be #FF0000 (red)
|
||||||
|
// 127 in grayscale should be FF0000 (perfectly red)
|
||||||
|
// 0 (black) in grayscale should be black in output
|
||||||
|
// 255 (white) in grayscale should be white in output
|
||||||
|
var (
|
||||||
|
AR = float64(color.Red)
|
||||||
|
AG = float64(color.Green)
|
||||||
|
AB = float64(color.Blue)
|
||||||
|
BR = float64(grayscale.Red)
|
||||||
|
BG = float64(grayscale.Green)
|
||||||
|
BB = float64(grayscale.Blue)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the pattern has a fully transparent pixel here, return transparent.
|
||||||
|
if grayscale.Alpha == 0 {
|
||||||
|
return render.RGBA(1, 0, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
convert := func(cc, gs float64) uint8 {
|
||||||
|
var delta float64
|
||||||
|
if gs < 127 {
|
||||||
|
// return uint8(cc + cc/gs)
|
||||||
|
delta = cc * (gs / 255)
|
||||||
|
} else {
|
||||||
|
delta = cc * (gs / 255)
|
||||||
|
}
|
||||||
|
return uint8(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
return render.RGBA(
|
||||||
|
convert(AR, BR),
|
||||||
|
convert(AG, BG),
|
||||||
|
convert(AB, BB),
|
||||||
|
255,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScreenFilter applies a "screen" blend mode between the two colors (a > b).
|
||||||
|
func ScreenFilter(a, b render.Color) render.Color {
|
||||||
|
// The algorithm we're going for is:
|
||||||
|
// 1 - (1 - a) * (1 - b)
|
||||||
|
var (
|
||||||
|
AR = a.Red
|
||||||
|
AG = a.Green
|
||||||
|
AB = a.Blue
|
||||||
|
BR = b.Red
|
||||||
|
BG = b.Green
|
||||||
|
BB = b.Blue
|
||||||
|
|
||||||
|
deltaR = 255 - (255-AR)*(255-BR)
|
||||||
|
deltaG = 255 - (255-AG)*(255-BG)
|
||||||
|
deltaB = 255 - (255-AB)*(255-BB)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the pattern has a fully transparent pixel here, return transparent.
|
||||||
|
// if b.Alpha == 0 {
|
||||||
|
// return render.RGBA(1, 0, 0, 1)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return render.RGBA(deltaR, deltaG, deltaB, a.Alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OverlayFilter applies an "overlay" blend mode between the two colors.
|
||||||
|
func OverlayFilter(a, b render.Color) render.Color {
|
||||||
|
// The algorithm we're going for is:
|
||||||
|
// If a < 0.5: 2ab
|
||||||
|
// Otherwise: 1 - 2(1 - a)(1 - b)
|
||||||
|
munch := func(a, b uint8) uint8 {
|
||||||
|
if a < 127 {
|
||||||
|
return 2 * a * b
|
||||||
|
}
|
||||||
|
return 255 - (2 * (255 - a) * (255 - b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the pattern has a fully transparent pixel here, return transparent.
|
||||||
|
if b.Alpha == 0 {
|
||||||
|
return render.RGBA(1, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
AR = a.Red
|
||||||
|
AG = a.Green
|
||||||
|
AB = a.Blue
|
||||||
|
BR = b.Red
|
||||||
|
BG = b.Green
|
||||||
|
BB = b.Blue
|
||||||
|
|
||||||
|
deltaR = munch(AR, BR)
|
||||||
|
deltaG = munch(AG, BG)
|
||||||
|
deltaB = munch(AB, BB)
|
||||||
|
)
|
||||||
|
|
||||||
|
return render.RGBA(deltaR, deltaG, deltaB, a.Alpha)
|
||||||
|
}
|
|
@ -146,6 +146,7 @@ func (w *Canvas) loopEditable(ev *event.State) error {
|
||||||
// Initialize a new Stroke for this atomic drawing operation?
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
||||||
w.currentStroke.Thickness = w.BrushSize
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.AddStroke(w.currentStroke)
|
w.AddStroke(w.currentStroke)
|
||||||
|
@ -198,6 +199,7 @@ func (w *Canvas) loopEditable(ev *event.State) error {
|
||||||
// Initialize a new Stroke for this atomic drawing operation?
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
||||||
w.currentStroke.Thickness = w.BrushSize
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
||||||
|
@ -219,6 +221,7 @@ func (w *Canvas) loopEditable(ev *event.State) error {
|
||||||
// Initialize a new Stroke for this atomic drawing operation?
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
||||||
w.currentStroke.Thickness = w.BrushSize
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
||||||
|
@ -237,6 +240,7 @@ func (w *Canvas) loopEditable(ev *event.State) error {
|
||||||
if ev.Button1 {
|
if ev.Button1 {
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Ellipse, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Ellipse, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
||||||
w.currentStroke.Thickness = w.BrushSize
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/pattern"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
@ -237,6 +238,12 @@ func (w *Canvas) DrawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does the swatch have a pattern to sample?
|
||||||
|
color := stroke.Color
|
||||||
|
if stroke.Pattern != "" {
|
||||||
|
color = pattern.SampleColor(stroke.Pattern, color, rect.Point())
|
||||||
|
}
|
||||||
|
|
||||||
// Destination rectangle to draw to screen, taking into account
|
// Destination rectangle to draw to screen, taking into account
|
||||||
// the position of the Canvas itself.
|
// the position of the Canvas itself.
|
||||||
dest := render.Rect{
|
dest := render.Rect{
|
||||||
|
@ -264,7 +271,7 @@ func (w *Canvas) DrawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
||||||
if balance.DebugCanvasStrokeColor != render.Invisible {
|
if balance.DebugCanvasStrokeColor != render.Invisible {
|
||||||
e.DrawBox(balance.DebugCanvasStrokeColor, dest)
|
e.DrawBox(balance.DebugCanvasStrokeColor, dest)
|
||||||
} else {
|
} else {
|
||||||
e.DrawBox(stroke.Color, dest)
|
e.DrawBox(color, dest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -273,6 +280,12 @@ func (w *Canvas) DrawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Does the swatch have a pattern to sample?
|
||||||
|
color := stroke.Color
|
||||||
|
if stroke.Pattern != "" {
|
||||||
|
color = pattern.SampleColor(stroke.Pattern, color, point)
|
||||||
|
}
|
||||||
|
|
||||||
dest := render.Point{
|
dest := render.Point{
|
||||||
X: P.X + w.Scroll.X + w.BoxThickness(1) + point.X,
|
X: P.X + w.Scroll.X + w.BoxThickness(1) + point.X,
|
||||||
Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + point.Y,
|
Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + point.Y,
|
||||||
|
@ -281,7 +294,7 @@ func (w *Canvas) DrawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
||||||
if balance.DebugCanvasStrokeColor != render.Invisible {
|
if balance.DebugCanvasStrokeColor != render.Invisible {
|
||||||
e.DrawPoint(balance.DebugCanvasStrokeColor, dest)
|
e.DrawPoint(balance.DebugCanvasStrokeColor, dest)
|
||||||
} else {
|
} else {
|
||||||
e.DrawPoint(stroke.Color, dest)
|
e.DrawPoint(color, dest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/pattern"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
@ -44,8 +45,9 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
height = (buttonSize * balance.DoodadDropperRows) + 64 // account for button borders :(
|
height = (buttonSize * balance.DoodadDropperRows) + 64 // account for button borders :(
|
||||||
|
|
||||||
// Column sizes of the palette table.
|
// Column sizes of the palette table.
|
||||||
col1 = 30 // ID no.
|
col1 = 15 // ID no.
|
||||||
col2 = 24 // Color
|
col2 = 24 // Color
|
||||||
|
col5 = 24 // Texture
|
||||||
col3 = 130 // Name
|
col3 = 130 // Name
|
||||||
col4 = 140 // Attributes
|
col4 = 140 // Attributes
|
||||||
// col5 = 150 // Delete
|
// col5 = 150 // Delete
|
||||||
|
@ -80,6 +82,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
}{
|
}{
|
||||||
{"ID", col1},
|
{"ID", col1},
|
||||||
{"Col", col2},
|
{"Col", col2},
|
||||||
|
{"Tex", col5},
|
||||||
{"Name", col3},
|
{"Name", col3},
|
||||||
{"Attributes", col4},
|
{"Attributes", col4},
|
||||||
// {"Delete", col5},
|
// {"Delete", col5},
|
||||||
|
@ -125,6 +128,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
row.Hide()
|
row.Hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////
|
||||||
// ID label.
|
// ID label.
|
||||||
idLabel := ui.NewLabel(ui.Label{
|
idLabel := ui.NewLabel(ui.Label{
|
||||||
Text: idStr + ".",
|
Text: idStr + ".",
|
||||||
|
@ -135,6 +139,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
Height: 24,
|
Height: 24,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//////////////
|
||||||
// Name button (click to rename the swatch)
|
// Name button (click to rename the swatch)
|
||||||
btnName := ui.NewButton("Name", ui.NewLabel(ui.Label{
|
btnName := ui.NewButton("Name", ui.NewLabel(ui.Label{
|
||||||
TextVariable: &swatch.Name,
|
TextVariable: &swatch.Name,
|
||||||
|
@ -157,6 +162,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
})
|
})
|
||||||
config.Supervisor.Add(btnName)
|
config.Supervisor.Add(btnName)
|
||||||
|
|
||||||
|
//////////////
|
||||||
// Color Choice button.
|
// Color Choice button.
|
||||||
btnColor := ui.NewButton("Color", ui.NewFrame("Color Frame"))
|
btnColor := ui.NewButton("Color", ui.NewFrame("Color Frame"))
|
||||||
btnColor.SetStyle(&style.Button{
|
btnColor.SetStyle(&style.Button{
|
||||||
|
@ -209,6 +215,43 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
})
|
})
|
||||||
config.Supervisor.Add(btnColor)
|
config.Supervisor.Add(btnColor)
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
// Texture (pattern) option.
|
||||||
|
selTexture := ui.NewSelectBox("Texture", ui.Label{
|
||||||
|
Font: balance.MenuFont,
|
||||||
|
})
|
||||||
|
selTexture.Configure(ui.Config{
|
||||||
|
Width: col5,
|
||||||
|
Height: 24,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, t := range pattern.Builtins {
|
||||||
|
if t.Hidden && !balance.ShowHiddenDoodads {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
selTexture.AddItem(t.Name, t.Filename, func() {})
|
||||||
|
}
|
||||||
|
selTexture.SetValue(swatch.Pattern)
|
||||||
|
setImageOnSelect(selTexture, swatch.Pattern)
|
||||||
|
|
||||||
|
selTexture.Handle(ui.Change, func(ed ui.EventData) error {
|
||||||
|
if val, ok := selTexture.GetValue(); ok {
|
||||||
|
filename, _ := val.Value.(string)
|
||||||
|
setImageOnSelect(selTexture, filename)
|
||||||
|
|
||||||
|
swatch.Pattern = filename
|
||||||
|
if config.OnChange != nil {
|
||||||
|
config.OnChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
selTexture.Supervise(config.Supervisor)
|
||||||
|
config.Supervisor.Add(selTexture)
|
||||||
|
|
||||||
|
//////////////
|
||||||
// Attribute flags.
|
// Attribute flags.
|
||||||
attrFrame := ui.NewFrame("Attributes")
|
attrFrame := ui.NewFrame("Attributes")
|
||||||
attrFrame.Configure(ui.Config{
|
attrFrame.Configure(ui.Config{
|
||||||
|
@ -254,6 +297,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////
|
||||||
// Pack all the widgets.
|
// Pack all the widgets.
|
||||||
row.Pack(idLabel, ui.Pack{
|
row.Pack(idLabel, ui.Pack{
|
||||||
Side: ui.W,
|
Side: ui.W,
|
||||||
|
@ -263,6 +307,10 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
Side: ui.W,
|
Side: ui.W,
|
||||||
PadX: 2,
|
PadX: 2,
|
||||||
})
|
})
|
||||||
|
row.Pack(selTexture, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
PadX: 2,
|
||||||
|
})
|
||||||
row.Pack(btnName, ui.Pack{
|
row.Pack(btnName, ui.Pack{
|
||||||
Side: ui.W,
|
Side: ui.W,
|
||||||
PadX: 2,
|
PadX: 2,
|
||||||
|
@ -377,3 +425,14 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
window.Hide()
|
window.Hide()
|
||||||
return window
|
return window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get the Tex (pattern) select box to
|
||||||
|
// show the image by its filename... for both onChange and
|
||||||
|
// initial render needs.
|
||||||
|
func setImageOnSelect(sel *ui.SelectBox, filename string) {
|
||||||
|
if image, err := pattern.GetImage(filename); err == nil {
|
||||||
|
sel.SetImage(image)
|
||||||
|
} else {
|
||||||
|
sel.SetImage(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|