mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2024-11-18 17:17:31 +00:00
David Vogel
3a73e13fb7
- Replace MedianBlendedImage with StitchedImage, a general implementation of a stitcher - Don't use hilbert curve when regenerating cache image - Cut workload rectangles to be always inside the cache image boundaries - Rename stitch.go to main.go - Add interface for overlays - Change how overlays are handled and drawn - Reduce error returns to simplify a lot of code - Add several blend functions - Remove offset field from image tile
272 lines
8.6 KiB
Go
272 lines
8.6 KiB
Go
// Copyright (c) 2022 David Vogel
|
|
//
|
|
// This software is released under the MIT License.
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"image"
|
|
"image/color"
|
|
"os"
|
|
|
|
"github.com/tdewolff/canvas"
|
|
"github.com/tdewolff/canvas/renderers/rasterizer"
|
|
)
|
|
|
|
//var entityDisplayFontFamily = canvas.NewFontFamily("times")
|
|
//var entityDisplayFontFace *canvas.FontFace
|
|
|
|
var entityDisplayAreaDamageStyle = canvas.Style{
|
|
FillColor: color.RGBA{100, 0, 0, 100},
|
|
StrokeColor: canvas.Transparent,
|
|
StrokeWidth: 1.0,
|
|
StrokeCapper: canvas.ButtCap,
|
|
StrokeJoiner: canvas.MiterJoin,
|
|
DashOffset: 0.0,
|
|
Dashes: []float64{},
|
|
FillRule: canvas.NonZero,
|
|
}
|
|
|
|
var entityDisplayMaterialAreaCheckerStyle = canvas.Style{
|
|
FillColor: color.RGBA{0, 0, 127, 127},
|
|
StrokeColor: canvas.Transparent,
|
|
StrokeWidth: 1.0,
|
|
StrokeCapper: canvas.ButtCap,
|
|
StrokeJoiner: canvas.MiterJoin,
|
|
DashOffset: 0.0,
|
|
Dashes: []float64{},
|
|
FillRule: canvas.NonZero,
|
|
}
|
|
|
|
var entityDisplayTeleportStyle = canvas.Style{
|
|
FillColor: color.RGBA{0, 127, 0, 127},
|
|
StrokeColor: canvas.Transparent,
|
|
StrokeWidth: 1.0,
|
|
StrokeCapper: canvas.ButtCap,
|
|
StrokeJoiner: canvas.MiterJoin,
|
|
DashOffset: 0.0,
|
|
Dashes: []float64{},
|
|
FillRule: canvas.NonZero,
|
|
}
|
|
|
|
var entityDisplayHitBoxStyle = canvas.Style{
|
|
FillColor: color.RGBA{64, 64, 0, 64},
|
|
StrokeColor: color.RGBA{0, 0, 0, 64},
|
|
StrokeWidth: 1.0,
|
|
StrokeCapper: canvas.ButtCap,
|
|
StrokeJoiner: canvas.MiterJoin,
|
|
DashOffset: 0.0,
|
|
Dashes: []float64{},
|
|
FillRule: canvas.NonZero,
|
|
}
|
|
|
|
var entityDisplayCollisionTriggerStyle = canvas.Style{
|
|
FillColor: color.RGBA{0, 64, 64, 64},
|
|
StrokeColor: color.RGBA{0, 0, 0, 64},
|
|
StrokeWidth: 1.0,
|
|
StrokeCapper: canvas.ButtCap,
|
|
StrokeJoiner: canvas.MiterJoin,
|
|
DashOffset: 0.0,
|
|
Dashes: []float64{},
|
|
FillRule: canvas.NonZero,
|
|
}
|
|
|
|
func init() {
|
|
//fontName := "NimbusRoman-Regular"
|
|
|
|
//if err := entityDisplayFontFamily.LoadLocalFont(fontName, canvas.FontRegular); err != nil {
|
|
// log.Printf("Couldn't load font %q: %v", fontName, err)
|
|
//}
|
|
|
|
//entityDisplayFontFace = entityDisplayFontFamily.Face(48.0, canvas.White, canvas.FontRegular, canvas.FontNormal)
|
|
}
|
|
|
|
type Entity struct {
|
|
Filename string `json:"filename"`
|
|
Transform EntityTransform `json:"transform"`
|
|
Children []Entity `json:"children"`
|
|
Components []Component `json:"components"`
|
|
Name string `json:"name"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
type EntityTransform struct {
|
|
X float32 `json:"x"`
|
|
Y float32 `json:"y"`
|
|
ScaleX float32 `json:"scaleX"`
|
|
ScaleY float32 `json:"scaleY"`
|
|
Rotation float32 `json:"rotation"`
|
|
}
|
|
|
|
type Component struct {
|
|
TypeName string `json:"typeName"`
|
|
Members map[string]any `json:"members"`
|
|
}
|
|
|
|
type Entities []Entity
|
|
|
|
func LoadEntities(path string) (Entities, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result Entities
|
|
|
|
jsonDec := json.NewDecoder(file)
|
|
if err := jsonDec.Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (e Entity) Draw(c *canvas.Context) {
|
|
x, y := float64(e.Transform.X), float64(e.Transform.Y)
|
|
|
|
for _, component := range e.Components {
|
|
switch component.TypeName {
|
|
case "AreaDamageComponent": // Area damage like in cursed rock.
|
|
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
|
if member, ok := component.Members["aabb_min"]; ok {
|
|
if aabbMin, ok := member.([]any); ok && len(aabbMin) == 2 {
|
|
aabbMinX, _ = aabbMin[0].(float64)
|
|
aabbMinY, _ = aabbMin[1].(float64)
|
|
}
|
|
}
|
|
if member, ok := component.Members["aabb_max"]; ok {
|
|
if aabbMax, ok := member.([]any); ok && len(aabbMax) == 2 {
|
|
aabbMaxX, _ = aabbMax[0].(float64)
|
|
aabbMaxY, _ = aabbMax[1].(float64)
|
|
}
|
|
}
|
|
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
|
c.Style = entityDisplayAreaDamageStyle
|
|
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
|
}
|
|
if member, ok := component.Members["circle_radius"]; ok {
|
|
if radius, ok := member.(float64); ok && radius > 0 {
|
|
// Theoretically we need to clip the damage area to the intersection of the AABB and the circle, but meh.
|
|
// TODO: Clip the area to the intersection of the box and the circle
|
|
cx, cy := (aabbMinX+aabbMaxX)/2, (aabbMinY+aabbMaxY)/2
|
|
c.Style = entityDisplayAreaDamageStyle
|
|
c.DrawPath(x+cx, y+cy, canvas.Circle(radius))
|
|
}
|
|
}
|
|
|
|
case "MaterialAreaCheckerComponent": // Checks for materials in the given AABB.
|
|
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
|
if member, ok := component.Members["area_aabb"]; ok {
|
|
if aabb, ok := member.([]any); ok && len(aabb) == 4 {
|
|
aabbMinX, _ = aabb[0].(float64)
|
|
aabbMinY, _ = aabb[1].(float64)
|
|
aabbMaxX, _ = aabb[2].(float64)
|
|
aabbMaxY, _ = aabb[3].(float64)
|
|
}
|
|
}
|
|
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
|
c.Style = entityDisplayMaterialAreaCheckerStyle
|
|
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
|
}
|
|
|
|
case "TeleportComponent":
|
|
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
|
if member, ok := component.Members["source_location_camera_aabb"]; ok {
|
|
if aabb, ok := member.([]any); ok && len(aabb) == 4 {
|
|
aabbMinX, _ = aabb[0].(float64)
|
|
aabbMinY, _ = aabb[1].(float64)
|
|
aabbMaxX, _ = aabb[2].(float64)
|
|
aabbMaxY, _ = aabb[3].(float64)
|
|
}
|
|
}
|
|
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
|
c.Style = entityDisplayTeleportStyle
|
|
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
|
}
|
|
|
|
case "HitboxComponent": // General hit box component.
|
|
var aabbMinX, aabbMinY, aabbMaxX, aabbMaxY float64
|
|
if member, ok := component.Members["aabb_min_x"]; ok {
|
|
aabbMinX, _ = member.(float64)
|
|
}
|
|
if member, ok := component.Members["aabb_min_y"]; ok {
|
|
aabbMinY, _ = member.(float64)
|
|
}
|
|
if member, ok := component.Members["aabb_max_x"]; ok {
|
|
aabbMaxX, _ = member.(float64)
|
|
}
|
|
if member, ok := component.Members["aabb_max_y"]; ok {
|
|
aabbMaxY, _ = member.(float64)
|
|
}
|
|
if aabbMinX < aabbMaxX && aabbMinY < aabbMaxY {
|
|
c.Style = entityDisplayHitBoxStyle
|
|
c.DrawPath(x+aabbMinX, y+aabbMinY, canvas.Rectangle(aabbMaxX-aabbMinX, aabbMaxY-aabbMinY))
|
|
}
|
|
|
|
case "CollisionTriggerComponent": // Checks if another entity is inside the given radius and box with the given width and height.
|
|
var width, height float64
|
|
path := &canvas.Path{}
|
|
if member, ok := component.Members["width"]; ok {
|
|
width, _ = member.(float64)
|
|
}
|
|
if member, ok := component.Members["height"]; ok {
|
|
height, _ = member.(float64)
|
|
}
|
|
if width > 0 && height > 0 {
|
|
path = canvas.Rectangle(width, height).Translate(-width/2, -height/2)
|
|
}
|
|
// Theoretically we need to clip the area to the intersection of the box and the circle, but meh.
|
|
// TODO: Clip the area to the intersection of the box and the circle
|
|
//if member, ok := component.Members["radius"]; ok {
|
|
// if radius, ok := member.(float64); ok && radius > 0 {
|
|
// path = path.Append(canvas.Circle(radius))
|
|
// path.And()
|
|
// }
|
|
//}
|
|
if !path.Empty() {
|
|
c.Style = entityDisplayCollisionTriggerStyle
|
|
c.DrawPath(x, y, path)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
c.SetFillColor(color.RGBA{255, 255, 255, 128})
|
|
c.SetStrokeColor(color.RGBA{255, 0, 0, 255})
|
|
c.DrawPath(x, y, canvas.Circle(3))
|
|
|
|
//text := canvas.NewTextLine(entityDisplayFontFace, fmt.Sprintf("%s\n%s", e.Name, e.Filename), canvas.Left)
|
|
//c.DrawText(x, y, text)
|
|
}
|
|
|
|
func (e Entities) Draw(destImage *image.RGBA) {
|
|
destRect := destImage.Bounds()
|
|
|
|
// Same as destImage, but top left is translated to (0, 0).
|
|
originImage := destImage.SubImage(destRect).(*image.RGBA)
|
|
originImage.Rect = originImage.Rect.Sub(destRect.Min)
|
|
|
|
c := canvas.New(float64(destRect.Dx()), float64(destRect.Dy()))
|
|
ctx := canvas.NewContext(c)
|
|
ctx.SetCoordSystem(canvas.CartesianIV)
|
|
ctx.SetCoordRect(canvas.Rect{X: -float64(destRect.Min.X), Y: -float64(destRect.Min.Y), W: float64(destRect.Dx()), H: float64(destRect.Dy())}, float64(destRect.Dx()), float64(destRect.Dy()))
|
|
|
|
// Set drawing style.
|
|
ctx.Style = playerPathDisplayStyle
|
|
|
|
for _, entity := range e {
|
|
// Check if entity origin is near or around the current image rectangle.
|
|
entityOrigin := image.Point{int(entity.Transform.X), int(entity.Transform.Y)}
|
|
if entityOrigin.In(destRect.Inset(-512)) {
|
|
entity.Draw(ctx)
|
|
}
|
|
}
|
|
|
|
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
|
r := rasterizer.FromImage(originImage, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
|
c.Render(r)
|
|
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
|
}
|