Tooling
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package planet
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"salty/terrain/internal/field"
|
||||
"salty/terrain/internal/plates"
|
||||
)
|
||||
|
||||
// The tectonic map: which plate every place belongs to, and what is happening where two of them meet.
|
||||
//
|
||||
// It earns its place the same way map_uplift does. Every belt, every fault and every basin this model
|
||||
// produces is a consequence of one line and what is happening across it, so when a range comes out in the
|
||||
// wrong place the question is always "what did the boundary there do", and this is the only picture that
|
||||
// answers it. The lines are drawn by *kind* rather than by rate, for the reason drawFaults gives about
|
||||
// stroking traces: a collision belt is tens of kilometres wide, a map of a whole planet is a few thousand
|
||||
// pixels, and the thing an author needs from it is where the margins are and which ones are closing.
|
||||
|
||||
// plateInk is the colour of each kind of margin. They are picked to be distinguishable from each other and
|
||||
// from the plate fills underneath, which are pastel by construction so that these read on top of them.
|
||||
var plateInk = map[plates.Kind][3]uint8{
|
||||
plates.Collision: {255, 64, 48}, // red: two continents, the thing that makes mountains
|
||||
plates.Subduction: {255, 156, 32}, // orange: an ocean going under
|
||||
plates.Rift: {96, 240, 120}, // green: a continent pulling apart
|
||||
plates.Ridge: {72, 196, 255}, // blue: new ocean floor
|
||||
plates.Transform: {236, 232, 128}, // yellow: sliding, neither up nor down
|
||||
}
|
||||
|
||||
// WritePlateMap draws the tectonic model over the painted land.
|
||||
func WritePlateMap(dir string, in *Inputs, width int) error {
|
||||
m := in.Plates
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A hue per plate, walked round the wheel by a step coprime-ish with the count so that neighbouring ids
|
||||
// are not neighbouring hues - plates 3 and 4 are usually adjacent on the map, and two greens meeting
|
||||
// would hide the very boundary this map exists to show.
|
||||
n := len(m.Plates)
|
||||
hue := make([]float64, n)
|
||||
for i := range hue {
|
||||
hue[i] = math.Mod(float64(i)*0.61803398875, 1)
|
||||
}
|
||||
|
||||
px, w, h := renderRGB(in, width, func(i, img int) [3]uint8 {
|
||||
id := m.PlateAt(in.P.XM(i%in.P.W), in.P.YM(i/in.P.W))
|
||||
if id < 0 || id >= n {
|
||||
return water
|
||||
}
|
||||
// Land is the plate's hue at full strength and sea is the same hue dimmed, so the painting stays
|
||||
// legible underneath: a margin is only interesting relative to where the coasts are.
|
||||
sat, val := 0.55, 0.78
|
||||
if in.Map.Sea[i] {
|
||||
sat, val = 0.38, 0.34
|
||||
}
|
||||
// A continental plate is warmer than an oceanic one at the same hue, because which of the two a
|
||||
// plate is decides what every convergent margin around it does.
|
||||
if !m.Plates[id].Continental {
|
||||
sat *= 0.5
|
||||
}
|
||||
c := field.HSV(hue[id]*360, sat, val)
|
||||
return [3]uint8{clamp8(c[0]), clamp8(c[1]), clamp8(c[2])}
|
||||
})
|
||||
|
||||
drawBoundaries(m, in, px, w, h)
|
||||
return write(filepath.Join(dir, "map_plates.png"), px, w, h)
|
||||
}
|
||||
|
||||
// WritePlateProposal writes a tectonic layer and its legend for an author to open and edit.
|
||||
//
|
||||
// A blank canvas is the wrong place to start this. Seven plates with plausible motions is a second's work for
|
||||
// the generator and an afternoon's by hand, and what an author actually wants to do is move two of them and
|
||||
// change a heading - which is editing. The pair it writes is exactly what `planet.plates.layer` and
|
||||
// `planet.plates.legend` take, so adopting it is two lines in the manifest.
|
||||
func WritePlateProposal(dir string, in *Inputs, width int) error {
|
||||
if in.Plates == nil {
|
||||
return nil
|
||||
}
|
||||
px, w, h, lg := in.Plates.Propose(width)
|
||||
const name = "plates_proposal"
|
||||
if err := write(filepath.Join(dir, name+".png"), px, w, h); err != nil {
|
||||
return err
|
||||
}
|
||||
lg.Image = name + ".png"
|
||||
data, err := plates.MarshalLegend(lg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(dir, name+".json"), data, 0o644)
|
||||
}
|
||||
|
||||
// boundaryWidthMYr is the closing rate, in metres a year, at which a margin is drawn at its full width. A
|
||||
// margin at a tenth of it is still one pixel, so a slow boundary is visible without a fast one being a blot.
|
||||
const boundaryWidthMYr = 0.08
|
||||
|
||||
// drawBoundaries strokes every margin, coloured by what it is doing and thickened by how fast.
|
||||
func drawBoundaries(m *plates.Model, in *Inputs, px []uint8, w, h int) {
|
||||
p := in.P
|
||||
sx := float64(w) / p.CircumferenceM()
|
||||
sy := float64(h) / p.HeightM()
|
||||
|
||||
set := func(x, y int, c [3]uint8) {
|
||||
if y < 0 || y >= h {
|
||||
return
|
||||
}
|
||||
x = ((x % w) + w) % w // X wraps, because the boundaries do
|
||||
o := (y*w + x) * 3
|
||||
px[o], px[o+1], px[o+2] = c[0], c[1], c[2]
|
||||
}
|
||||
disc := func(x, y, r int, c [3]uint8) {
|
||||
for dy := -r; dy <= r; dy++ {
|
||||
for dx := -r; dx <= r; dx++ {
|
||||
if dx*dx+dy*dy <= r*r {
|
||||
set(x+dx, y+dy, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range m.Boundaries {
|
||||
for j := 0; j+1 < len(b.V); j++ {
|
||||
v := b.V[j]
|
||||
ink := plateInk[v.Kind]
|
||||
r := int(math.Abs(v.ClosingMYr)/boundaryWidthMYr*2 + 0.5)
|
||||
if r > 2 {
|
||||
r = 2
|
||||
}
|
||||
ax, ay := v.XM*sx, v.YM*sy
|
||||
bx, by := b.V[j+1].XM*sx, b.V[j+1].YM*sy
|
||||
steps := int(math.Hypot(bx-ax, by-ay)) + 1
|
||||
for k := 0; k <= steps; k++ {
|
||||
t := float64(k) / float64(steps)
|
||||
disc(int(ax+(bx-ax)*t), int(ay+(by-ay)*t), r, ink)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user