mirror of
https://github.com/appgurueu/modlib.git
synced 2024-08-20 13:24:11 +02:00
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`.
423 lines
8.5 KiB
Lua
423 lines
8.5 KiB
Lua
local colorspec = modlib.minetest.colorspec
|
|
|
|
local texmod = {}
|
|
local mod = {}
|
|
local metatable = {__index = mod}
|
|
|
|
local function new(self)
|
|
return setmetatable(self, metatable)
|
|
end
|
|
|
|
-- `texmod{...}` may be used to create texture modifiers, bypassing the checks
|
|
setmetatable(texmod, {__call = new})
|
|
|
|
-- Constructors / "generators"
|
|
|
|
function texmod.file(filename)
|
|
-- See `TEXTURENAME_ALLOWED_CHARS` in Minetest (`src/network/networkprotocol.h`)
|
|
assert(not filename:find"[^%w_.-]", "invalid characters in file name")
|
|
return new{
|
|
type = "file",
|
|
filename = filename
|
|
}
|
|
end
|
|
|
|
function texmod.png(data)
|
|
assert(type(data) == "string")
|
|
return new{
|
|
type = "png",
|
|
data = data
|
|
}
|
|
end
|
|
|
|
function texmod.combine(w, h, blits)
|
|
assert(w % 1 == 0 and w > 0)
|
|
assert(h % 1 == 0 and h > 0)
|
|
for _, blit in ipairs(blits) do
|
|
assert(blit.x % 1 == 0)
|
|
assert(blit.y % 1 == 0)
|
|
assert(blit.texture)
|
|
end
|
|
return new{
|
|
type = "combine",
|
|
w = w,
|
|
h = h,
|
|
blits = blits
|
|
}
|
|
end
|
|
|
|
function texmod.inventorycube(top, left, right)
|
|
return new{
|
|
type = "inventorycube",
|
|
top = top,
|
|
left = left,
|
|
right = right
|
|
}
|
|
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"
|
|
|
|
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{
|
|
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,
|
|
over = overlay
|
|
}
|
|
end
|
|
|
|
function mod:brighten()
|
|
return new{
|
|
type = "brighten",
|
|
base = self,
|
|
}
|
|
end
|
|
|
|
function mod:noalpha()
|
|
return new{
|
|
type = "noalpha",
|
|
base = self
|
|
}
|
|
end
|
|
|
|
function mod:resize(w, h)
|
|
assert(w % 1 == 0 and w > 0)
|
|
assert(h % 1 == 0 and h > 0)
|
|
return new{
|
|
type = "resize",
|
|
base = self,
|
|
w = w,
|
|
h = h,
|
|
}
|
|
end
|
|
|
|
local function assert_uint8(num)
|
|
assert_int_range(num, 0, 0xFF)
|
|
end
|
|
|
|
function mod:makealpha(r, g, b)
|
|
assert_uint8(r); assert_uint8(g); assert_uint8(b)
|
|
return new{
|
|
type = "makealpha",
|
|
base = self,
|
|
r = r, g = g, b = b
|
|
}
|
|
end
|
|
|
|
function mod:opacity(ratio)
|
|
assert_uint8(ratio)
|
|
return new{
|
|
type = "opacity",
|
|
base = self,
|
|
ratio = ratio
|
|
}
|
|
end
|
|
|
|
local function tobool(val)
|
|
return not not val
|
|
end
|
|
|
|
function mod:invert(channels --[[set with keys "r", "g", "b", "a"]])
|
|
return new{
|
|
type = "invert",
|
|
base = self,
|
|
r = tobool(channels.r),
|
|
g = tobool(channels.g),
|
|
b = tobool(channels.b),
|
|
a = tobool(channels.a)
|
|
}
|
|
end
|
|
|
|
function mod:flip(flip_axis --[["x" or "y"]])
|
|
return self:transform(assert(
|
|
(flip_axis == "x" and "fx")
|
|
or (flip_axis == "y" and "fy")
|
|
or (not flip_axis and "i")))
|
|
end
|
|
|
|
function mod:rotate(deg)
|
|
assert(deg % 90 == 0)
|
|
deg = deg % 360
|
|
return self:transform(("r%d"):format(deg))
|
|
end
|
|
|
|
-- D4 group transformations (see https://proofwiki.org/wiki/Definition:Dihedral_Group_D4),
|
|
-- represented using indices into a table of matrices
|
|
-- TODO (...) try to come up with a more elegant solution
|
|
do
|
|
-- Matrix multiplication for composition: First applies a, then b <=> b * a
|
|
local function mat_2x2_compose(a, b)
|
|
local a_1_1, a_1_2, a_2_1, a_2_2 = unpack(a)
|
|
local b_1_1, b_1_2, b_2_1, b_2_2 = unpack(b)
|
|
return {
|
|
a_1_1 * b_1_1 + a_2_1 * b_1_2, a_1_2 * b_1_1 + a_2_2 * b_1_2;
|
|
a_1_1 * b_2_1 + a_2_1 * b_2_2, a_1_2 * b_2_1 + a_2_2 * b_2_2
|
|
}
|
|
end
|
|
local r90 ={
|
|
0, -1;
|
|
1, 0
|
|
}
|
|
local fx = {
|
|
-1, 0;
|
|
0, 1
|
|
}
|
|
local fy = {
|
|
1, 0;
|
|
0, -1
|
|
}
|
|
local r180 = mat_2x2_compose(r90, r90)
|
|
local r270 = mat_2x2_compose(r180, r90)
|
|
local fxr90 = mat_2x2_compose(fx, r90)
|
|
local fyr90 = mat_2x2_compose(fy, r90)
|
|
local transform_mats = {[0] = {1, 0; 0, 1}, r90, r180, r270, fx, fxr90, fy, fyr90}
|
|
local transform_idx_by_name = {i = 0, r90 = 1, r180 = 2, r270 = 3, fx = 4, fxr90 = 5, fy = 6, fyr90 = 7}
|
|
-- Lookup tables for getting the flipped axis / rotation angle
|
|
local flip_by_idx = {
|
|
[4] = "x",
|
|
[5] = "x",
|
|
[6] = "y",
|
|
[7] = "y",
|
|
}
|
|
local rot_by_idx = {
|
|
[1] = 90,
|
|
[2] = 180,
|
|
[3] = 270,
|
|
[5] = 90,
|
|
[7] = 90,
|
|
}
|
|
local idx_by_mat_2x2 = {}
|
|
local function transform_idx(mat)
|
|
-- note: assumes mat[i] in {-1, 0, 1}
|
|
return mat[1] + 3*(mat[2] + 3*(mat[3] + 3*mat[4]))
|
|
end
|
|
for i = 0, 7 do
|
|
idx_by_mat_2x2[transform_idx(transform_mats[i])] = i
|
|
end
|
|
-- Compute a multiplication table
|
|
local composition_idx = {}
|
|
local function ij_idx(i, j)
|
|
return i*8 + j
|
|
end
|
|
for i = 0, 7 do
|
|
for j = 0, 7 do
|
|
composition_idx[ij_idx(i, j)] = assert(idx_by_mat_2x2[
|
|
transform_idx(mat_2x2_compose(transform_mats[i], transform_mats[j]))])
|
|
end
|
|
end
|
|
function mod:transform(...)
|
|
if select("#", ...) == 0 then return self end
|
|
local idx = ...
|
|
if type(idx) == "string" then
|
|
idx = assert(transform_idx_by_name[idx:lower()])
|
|
end
|
|
local base = self
|
|
if self.type == "transform" then
|
|
-- Merge with a `^[transform` base image
|
|
assert(transform_mats[idx])
|
|
base = self.base
|
|
idx = composition_idx[ij_idx(self.idx, idx)]
|
|
end
|
|
assert(transform_mats[idx])
|
|
if idx == 0 then return base end -- identity
|
|
return new{
|
|
type = "transform",
|
|
base = base,
|
|
idx = idx,
|
|
-- Redundantly store this information for convenience. Do not modify!
|
|
flip_axis = flip_by_idx[idx],
|
|
rotation_deg = rot_by_idx[idx] or 0,
|
|
}:transform(select(2, ...))
|
|
end
|
|
end
|
|
|
|
function mod:verticalframe(framecount, frame)
|
|
assert(framecount >= 1)
|
|
assert(frame >= 0)
|
|
return new{
|
|
type = "verticalframe",
|
|
base = self,
|
|
framecount = framecount,
|
|
frame = frame
|
|
}
|
|
end
|
|
|
|
local function crack(self, name, ...)
|
|
local tilecount, framecount, frame
|
|
if select("#", ...) == 2 then
|
|
tilecount, framecount, frame = 1, ...
|
|
else
|
|
assert(select("#", ...) == 3, "invalid number of arguments")
|
|
tilecount, framecount, frame = ...
|
|
end
|
|
assert(tilecount >= 1)
|
|
assert(framecount >= 1)
|
|
assert(frame >= 0)
|
|
return new{
|
|
type = name,
|
|
base = self,
|
|
tilecount = tilecount,
|
|
framecount = framecount,
|
|
frame = frame
|
|
}
|
|
end
|
|
|
|
function mod:crack(...)
|
|
return crack(self, "crack", ...)
|
|
end
|
|
|
|
function mod:cracko(...)
|
|
return crack(self, "cracko", ...)
|
|
end
|
|
mod.crack_with_opacity = mod.cracko
|
|
|
|
function mod:sheet(w, h, x, y)
|
|
assert(w % 1 == 0 and w >= 1)
|
|
assert(h % 1 == 0 and h >= 1)
|
|
assert(x % 1 == 0 and x >= 0)
|
|
assert(y % 1 == 0 and y >= 0)
|
|
return new{
|
|
type = "sheet",
|
|
base = self,
|
|
w = w,
|
|
h = h,
|
|
x = x,
|
|
y = y
|
|
}
|
|
end
|
|
|
|
function mod:screen(color)
|
|
return new{
|
|
type = "screen",
|
|
base = self,
|
|
color = colorspec.from_any(color),
|
|
}
|
|
end
|
|
|
|
function mod:multiply(color)
|
|
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
|
|
assert(color.alpha or 0xFF == 0xFF)
|
|
else
|
|
ratio = ratio or color.alpha or 0xFF
|
|
assert_uint8(ratio)
|
|
if color.alpha == ratio then
|
|
ratio = nil
|
|
end
|
|
end
|
|
return new{
|
|
type = "colorize",
|
|
base = self,
|
|
color = color,
|
|
ratio = ratio
|
|
}
|
|
end
|
|
|
|
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{
|
|
type = type,
|
|
base = self,
|
|
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
|
|
|
|
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)
|
|
return new{
|
|
type = "lowpart",
|
|
base = self,
|
|
percent = percent,
|
|
over = overlay
|
|
}
|
|
end
|
|
|
|
return texmod, metatable
|