Add support for new texture modifiers

https://github.com/minetest/minetest/pull/10100
has introduced a few inconsistencies with the formats of previous texture modifiers:

`[fill` is the first texture modifier to have a modifying and a generating variant.
Thus `texmod` "constructors" and `texmod` "modifiers" / methods had to be separated;
`texmod.fill` is not the same as `tm.fill` where `tm` is `texmod` instance.
It is rather dirty that the generating variant would ignore
extraneous parameters of the modifying variant, so this is not replicated by the parser.

`[hsl`, `[colorizehsl`, `[contrast` and `[screen` are pretty standard texture modifiers
as far as the DSL is concerned. `[hsl` and `[colorizehsl` are very similar.

`[hardlight` is the first texture modifier to exist just for swapping the base and parameter.
`a^[overlay:b` and `b^[hardlight:a` are both normalized to `b:[hardlight:a` by the DSL.
`[overlay` (called "overlay blend" in the docs) creates a naming conflict with
literal overlaying (`^`). This is resolved by renaming `:overlay` to `:blit`.
This commit is contained in:
Lars Mueller 2023-05-31 20:01:34 +02:00
parent a9b5d80ca8
commit 2b1c9cff7c
5 changed files with 241 additions and 38 deletions

@ -6,9 +6,10 @@ local function component(component_name, ...)
end end
local texmod, metatable = component"dsl" local texmod, metatable = component"dsl"
texmod.write = component"write" local methods = metatable.__index
methods.write = component"write"
texmod.read = component("read", texmod) texmod.read = component("read", texmod)
texmod.calc_dims = component("calc_dims", texmod) methods.calc_dims = component("calc_dims", texmod)
function metatable:__tostring() function metatable:__tostring()
local rope = {} local rope = {}

@ -19,6 +19,10 @@ do
cd.mask = base_dim cd.mask = base_dim
cd.multiply = base_dim cd.multiply = base_dim
cd.colorize = base_dim cd.colorize = base_dim
cd.colorizehsl = base_dim
cd.hsl = base_dim
cd.screen = base_dim
cd.contrast = base_dim
end end
do do
@ -27,10 +31,23 @@ do
cd.combine = wh cd.combine = wh
end end
function cd:overlay(get_dims) function cd:fill(get_dims)
if self.base then return calc_dims(self.base, get_dims) end
return self.w, self.h
end
do
local function upscale_to_higher_res(self, get_dims)
local base_w, base_h = calc_dims(self.base, get_dims) local base_w, base_h = calc_dims(self.base, get_dims)
local over_w, over_h = calc_dims(self.over, get_dims) local over_w, over_h = calc_dims(self.over, get_dims)
return math.max(base_w, over_w), math.max(base_h, over_h) if base_w * base_h > over_w * over_h then
return base_w, base_h
end
return over_w, over_h
end
cd.blit = upscale_to_higher_res
cd.overlay = upscale_to_higher_res
cd.hardlight = upscale_to_higher_res
end end
function cd:transform(get_dims) function cd:transform(get_dims)

@ -1,5 +1,8 @@
local colorspec = modlib.minetest.colorspec
local texmod = {} local texmod = {}
local metatable = {__index = texmod} local mod = {}
local metatable = {__index = mod}
local function new(self) local function new(self)
return setmetatable(self, metatable) return setmetatable(self, metatable)
@ -52,31 +55,65 @@ function texmod.inventorycube(top, left, right)
} }
end end
-- As a base generator, `fill` ignores `x` and `y`. Leave them as `nil`.
function texmod.fill(w, h, color)
assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0)
return new{
type = "fill",
w = w,
h = h,
color = colorspec.from_any(color)
}
end
-- Methods / "modifiers" -- Methods / "modifiers"
function texmod:overlay(overlay) local function assert_int_range(num, min, max)
assert(num % 1 == 0 and num >= min and num <= max)
end
-- As a modifier, `fill` takes `x` and `y`
function mod:fill(w, h, x, y, color)
assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0)
assert(x % 1 == 0 and x >= 0)
assert(y % 1 == 0 and y >= 0)
return new{ return new{
type = "overlay", type = "fill",
base = self,
w = w,
h = h,
x = x,
y = y,
color = colorspec.from_any(color)
}
end
-- This is the real "overlay", associated with `^`.
function mod:blit(overlay)
return new{
type = "blit",
base = self, base = self,
over = overlay over = overlay
} }
end end
function texmod:brighten() function mod:brighten()
return new{ return new{
type = "brighten", type = "brighten",
base = self, base = self,
} }
end end
function texmod:noalpha() function mod:noalpha()
return new{ return new{
type = "noalpha", type = "noalpha",
base = self base = self
} }
end end
function texmod:resize(w, h) function mod:resize(w, h)
assert(w % 1 == 0 and w > 0) assert(w % 1 == 0 and w > 0)
assert(h % 1 == 0 and h > 0) assert(h % 1 == 0 and h > 0)
return new{ return new{
@ -88,10 +125,10 @@ function texmod:resize(w, h)
end end
local function assert_uint8(num) local function assert_uint8(num)
assert(num % 1 == 0 and num >= 0 and num <= 0xFF) assert_int_range(num, 0, 0xFF)
end end
function texmod:makealpha(r, g, b) function mod:makealpha(r, g, b)
assert_uint8(r); assert_uint8(g); assert_uint8(b) assert_uint8(r); assert_uint8(g); assert_uint8(b)
return new{ return new{
type = "makealpha", type = "makealpha",
@ -100,7 +137,7 @@ function texmod:makealpha(r, g, b)
} }
end end
function texmod:opacity(ratio) function mod:opacity(ratio)
assert_uint8(ratio) assert_uint8(ratio)
return new{ return new{
type = "opacity", type = "opacity",
@ -113,7 +150,7 @@ local function tobool(val)
return not not val return not not val
end end
function texmod:invert(channels --[[set with keys "r", "g", "b", "a"]]) function mod:invert(channels --[[set with keys "r", "g", "b", "a"]])
return new{ return new{
type = "invert", type = "invert",
base = self, base = self,
@ -124,14 +161,14 @@ function texmod:invert(channels --[[set with keys "r", "g", "b", "a"]])
} }
end end
function texmod:flip(flip_axis --[["x" or "y"]]) function mod:flip(flip_axis --[["x" or "y"]])
return self:transform(assert( return self:transform(assert(
(flip_axis == "x" and "fx") (flip_axis == "x" and "fx")
or (flip_axis == "y" and "fy") or (flip_axis == "y" and "fy")
or (not flip_axis and "i"))) or (not flip_axis and "i")))
end end
function texmod:rotate(deg) function mod:rotate(deg)
assert(deg % 90 == 0) assert(deg % 90 == 0)
deg = deg % 360 deg = deg % 360
return self:transform(("r%d"):format(deg)) return self:transform(("r%d"):format(deg))
@ -201,7 +238,7 @@ do
transform_idx(mat_2x2_compose(transform_mats[i], transform_mats[j]))]) transform_idx(mat_2x2_compose(transform_mats[i], transform_mats[j]))])
end end
end end
function texmod:transform(...) function mod:transform(...)
if select("#", ...) == 0 then return self end if select("#", ...) == 0 then return self end
local idx = ... local idx = ...
if type(idx) == "string" then if type(idx) == "string" then
@ -227,7 +264,7 @@ do
end end
end end
function texmod:verticalframe(framecount, frame) function mod:verticalframe(framecount, frame)
assert(framecount >= 1) assert(framecount >= 1)
assert(frame >= 0) assert(frame >= 0)
return new{ return new{
@ -258,16 +295,16 @@ local function crack(self, name, ...)
} }
end end
function texmod:crack(...) function mod:crack(...)
return crack(self, "crack", ...) return crack(self, "crack", ...)
end end
function texmod:cracko(...) function mod:cracko(...)
return crack(self, "cracko", ...) return crack(self, "cracko", ...)
end end
texmod.crack_with_opacity = texmod.cracko mod.crack_with_opacity = mod.cracko
function texmod:sheet(w, h, x, y) function mod:sheet(w, h, x, y)
assert(w % 1 == 0 and w >= 1) assert(w % 1 == 0 and w >= 1)
assert(h % 1 == 0 and h >= 1) assert(h % 1 == 0 and h >= 1)
assert(x % 1 == 0 and x >= 0) assert(x % 1 == 0 and x >= 0)
@ -282,18 +319,24 @@ function texmod:sheet(w, h, x, y)
} }
end end
local colorspec = modlib.minetest.colorspec function mod:screen(color)
function texmod:multiply(color)
return new{ return new{
type = "multiply", type = "screen",
base = self, base = self,
color = colorspec.from_any(color) -- copies a given colorspec color = colorspec.from_any(color),
} }
end end
function texmod:colorize(color, ratio) function mod:multiply(color)
color = colorspec.from_any(color) -- copies a given colorspec return new{
type = "multiply",
base = self,
color = colorspec.from_any(color)
}
end
function mod:colorize(color, ratio)
color = colorspec.from_any(color)
if ratio == "alpha" then if ratio == "alpha" then
assert(color.alpha or 0xFF == 0xFF) assert(color.alpha or 0xFF == 0xFF)
else else
@ -311,15 +354,62 @@ function texmod:colorize(color, ratio)
} }
end end
function texmod:mask(_mask) local function hsl(type, s_def, s_max, l_def)
return function(self, h, s, l)
s, l = s or s_def, l or l_def
assert_int_range(h, -180, 180)
assert_int_range(s, 0, s_max)
assert_int_range(l, -100, 100)
return new{ return new{
type = "mask", type = type,
base = self, base = self,
_mask = _mask hue = h,
saturation = s,
lightness = l,
}
end
end
mod.colorizehsl = hsl("colorizehsl", 50, 100, 0)
mod.hsl = hsl("hsl", 0, math.huge, 0)
function mod:contrast(contrast, brightness)
brightness = brightness or 0
assert_int_range(contrast, -127, 127)
assert_int_range(brightness, -127, 127)
return new{
type = "contrast",
base = self,
contrast = contrast,
brightness = brightness,
} }
end end
function texmod:lowpart(percent, overlay) function mod:mask(mask_texmod)
return new{
type = "mask",
base = self,
_mask = mask_texmod
}
end
function mod:hardlight(overlay)
return new{
type = "hardlight",
base = self,
over = overlay
}
end
-- Overlay *blend*.
-- This was unfortunately named `[overlay` in Minetest,
-- and so is named `:overlay` for consistency.
--! Do not confuse this with the simple `^` used for blitting
function mod:overlay(overlay)
return overlay:hardlight(self)
end
function mod:lowpart(percent, overlay)
assert(percent % 1 == 0 and percent >= 0 and percent <= 100) assert(percent % 1 == 0 and percent >= 0 and percent <= 100)
return new{ return new{
type = "lowpart", type = "lowpart",

@ -35,10 +35,39 @@ function gr.combine(r)
return w, h, blits return w, h, blits
end end
function gr.fill(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
r:expect":"
-- Be strict(er than Minetest): Do not accept x, y for a base
local color = r:colorspec()
return w, h, color
end
-- Parameter readers -- Parameter readers
local pr = {} local pr = {}
function pr.fill(r)
r:expect":"
local w = r:int()
r:expect"x"
local h = r:int()
r:expect":"
if assert(r:peek(), "unexpected eof"):find"%d" then
local x = r:int()
r:expect","
local y = r:int()
r:expect":"
local color = r:colorspec()
return w, h, x, y, color
end
local color = r:colorspec()
return w, h, color
end
function pr.brighten() end function pr.brighten() end
function pr.noalpha() end function pr.noalpha() end
@ -137,6 +166,7 @@ function pr.multiply(r)
r:expect":" r:expect":"
return r:colorspec() return r:colorspec()
end end
pr.screen = pr.multiply
function pr.colorize(r) function pr.colorize(r)
r:expect":" r:expect":"
@ -153,6 +183,41 @@ function pr.colorize(r)
return color, "alpha" return color, "alpha"
end end
function pr.colorizehsl(r)
r:expect":"
local hue = r:int()
if not r:match":" then
return hue
end
local saturation = r:int()
if not r:match":" then
return hue, saturation
end
local lightness = r:int()
return hue, saturation, lightness
end
pr.hsl = pr.colorizehsl
function pr.contrast(r)
r:expect":"
local contrast = r:int()
if not r:match":" then
return contrast
end
local brightness = r:int()
return contrast, brightness
end
function pr.overlay(r)
r:expect":"
return r:subtexp()
end
function pr.hardlight(r)
r:expect":"
return r:subtexp(r)
end
function pr.mask(r) function pr.mask(r)
r:expect":" r:expect":"
return r:subtexp(r) return r:subtexp(r)
@ -325,12 +390,14 @@ function rm.texp(r)
local param_reader, gen_reader = pr[type], gr[type] local param_reader, gen_reader = pr[type], gr[type]
assert(param_reader or gen_reader) assert(param_reader or gen_reader)
if param_reader then if param_reader then
-- Note: It is important that this takes precedence to properly handle `[fill`
base = base[type](base, param_reader(r)) base = base[type](base, param_reader(r))
elseif gen_reader then elseif gen_reader then
base = base:overlay(texmod[type](gen_reader(r))) base = base:blit(texmod[type](gen_reader(r)))
end end
-- TODO (?...) we could consume leftover parameters here to be as lax as Minetest
else else
base = base:overlay(r:basexp()) base = base:blit(r:basexp())
end end
end end
return base return base

@ -26,6 +26,15 @@ function pw:inventorycube(w)
write_side"right" write_side"right"
end end
-- Handles both the generator & the modifier
function pw:fill(w)
w.colon(); w.int(self.w); w.str"x"; w.int(self.h)
if self.base then
w.colon(); w.int(self.x); w.str","; w.int(self.y)
end
w.colon(); w.str(self.color:to_string())
end
-- No parameters to write -- No parameters to write
pw.brighten = modlib.func.no_op pw.brighten = modlib.func.no_op
pw.noalpha = modlib.func.no_op pw.noalpha = modlib.func.no_op
@ -68,6 +77,11 @@ function pw:sheet(w)
w.colon(); w.int(self.w); w.str"x"; w.int(self.h); w.colon(); w.int(self.x); w.str","; w.int(self.y) w.colon(); w.int(self.w); w.str"x"; w.int(self.h); w.colon(); w.int(self.x); w.str","; w.int(self.y)
end end
function pw:screen(w)
w.colon()
w.str(self.color:to_string())
end
function pw:multiply(w) function pw:multiply(w)
w.colon() w.colon()
w.str(self.color:to_string()) w.str(self.color:to_string())
@ -86,6 +100,20 @@ function pw:colorize(w)
end end
end end
function pw:colorizehsl(w)
w.colon(); w.int(self.hue); w.colon(); w.int(self.saturation); w.colon(); w.int(self.lightness)
end
pw.hsl = pw.colorizehsl
function pw:contrast(w)
w.colon(); w.int(self.contrast); w.colon(); w.int(self.brightness)
end
-- We don't have to handle `[overlay`; the DSL normalizes everything to `[hardlight`
function pw:hardlight(w)
w.colon(); w.esctex(self.over)
end
function pw:mask(w) function pw:mask(w)
w.colon(); w.esctex(self._mask) w.colon(); w.esctex(self._mask)
end end
@ -130,7 +158,7 @@ return function(self, write_str)
w.tex(tex.base) w.tex(tex.base)
w.hat() w.hat()
end end
if tex.type == "overlay" then if tex.type == "blit" then -- simply `^`
if non_modifiers[tex.over.type] then if non_modifiers[tex.over.type] then
w.tex(tex.over) w.tex(tex.over)
else else