Noah Petherbridge
ecaa8c6cef
* Add new pixel attributes: SemiSolid and Slippery (the latter is WIP) * SemiSolid pixels are only solid below the player character. You can walk on them and up and down SemiSolid slopes, but can freely pass through from the sides or jump through from below. * Update the Palette Editor UI to replace the Attributes buttons: instead of text labels they now have smaller icons (w/ tooltips) for the Solid, SemiSolid, Fire, Water and Slippery attributes. * Bugfix in Palette Editor: use cropped (24x24) images for the Tex buttons so that the large Bubbles texture stays within its designated space! * uix.Actor.SetGrounded() to also set the Y velocity to zero when an actor becomes grounded. This fixes a minor bug where the player's Y velocity (due to gravity) was not updated while they were grounded, which may eventually become useful to allow them to jump down thru a SemiSolid floor. Warp Doors needed a fix to work around the bug, to set the player's Grounded(false) or else they would hover a few pixels above the ground at their destination, since Grounded status paused gravity calculations.
286 lines
6.9 KiB
Go
286 lines
6.9 KiB
Go
// 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))
|
|
}
|