// Package pattern applies a kind of brush texture to a palette swatch. package pattern import ( "errors" "fmt" "image" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/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: "Perlin Noise", Filename: "perlin-noise.png", }, { Name: "Bubbles", Filename: "bubbles.png", }, { Name: "Circles", 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 // Cache of cropped images (e.g. 24x24 icons for palette editor) var croppedImages map[string]map[render.Rect]*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{} croppedImages = map[string]map[render.Rect]*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) } // GetImageCropped gets a cropped ui.Image for a builtin pattern. func GetImageCropped(filename string, crop render.Rect) (*ui.Image, error) { // Have it cached already? if sizes, ok := croppedImages[filename]; ok { if cached, ok := sizes[crop]; ok { return cached, nil } } uiImage, err := GetImage(filename) if err != nil { return nil, err } cropped, err := CropImage(uiImage.Image, image.Rect(0, 0, crop.W, crop.H)) if err != nil { return nil, err } // Cache it for the future so we don't leak textures every time the palette editor asks. if _, ok := croppedImages[filename]; !ok { croppedImages[filename] = map[render.Rect]*ui.Image{} } croppedImages[filename][crop] = cropped return cropped, nil } // 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) } // CropImage crops an image to a smaller size (such as the 24x24 selectbox button in the Palette Editor UI) func CropImage(img image.Image, crop image.Rectangle) (*ui.Image, error) { type subImager interface { SubImage(r image.Rectangle) image.Image } simg, ok := img.(subImager) if !ok { return nil, fmt.Errorf("image does not support cropping") } return ui.ImageFromImage(simg.SubImage(crop)) }