Körperbewegung
This commit is contained in:
parent
b16b24e4f7
commit
95945c0306
78 changed files with 12503 additions and 0 deletions
97
mods/modlib/minetest/texmod/calc_dims.lua
Normal file
97
mods/modlib/minetest/texmod/calc_dims.lua
Normal file
|
@ -0,0 +1,97 @@
|
|||
local cd = {}
|
||||
|
||||
local function calc_dims(self, get_file_dims)
|
||||
return assert(cd[self.type])(self, get_file_dims)
|
||||
end
|
||||
|
||||
function cd:file(d)
|
||||
return d(self.filename)
|
||||
end
|
||||
|
||||
do
|
||||
local function base_dim(self, get_dims) return calc_dims(self.base, get_dims) end
|
||||
cd.opacity = base_dim
|
||||
cd.invert = base_dim
|
||||
cd.brighten = base_dim
|
||||
cd.noalpha = base_dim
|
||||
cd.makealpha = base_dim
|
||||
cd.lowpart = base_dim
|
||||
cd.mask = base_dim
|
||||
cd.multiply = base_dim
|
||||
cd.colorize = base_dim
|
||||
cd.colorizehsl = base_dim
|
||||
cd.hsl = base_dim
|
||||
cd.screen = base_dim
|
||||
cd.contrast = base_dim
|
||||
end
|
||||
|
||||
do
|
||||
local function wh(self) return self.w, self.h end
|
||||
cd.resize = wh
|
||||
cd.combine = wh
|
||||
end
|
||||
|
||||
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 over_w, over_h = calc_dims(self.over, get_dims)
|
||||
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.hardlight = upscale_to_higher_res
|
||||
end
|
||||
|
||||
function cd:transform(get_dims)
|
||||
if self.rotation_deg % 180 ~= 0 then
|
||||
local base_w, base_h = calc_dims(self.base, get_dims)
|
||||
return base_h, base_w
|
||||
end
|
||||
return calc_dims(self.base, get_dims)
|
||||
end
|
||||
|
||||
do
|
||||
local math_clamp = modlib.math.clamp
|
||||
local function next_pow_of_2(x)
|
||||
-- I don't want to use a naive 2^ceil(log(x)/log(2)) due to possible float precision issues.
|
||||
local m, e = math.frexp(x) -- x = _*2^e, _ in [0.5, 1)
|
||||
if m == 0.5 then e = e - 1 end -- x = 2^(e-1)
|
||||
return math.ldexp(1, e) -- 2^e, premature optimization here we go
|
||||
end
|
||||
function cd:inventorycube(get_dims)
|
||||
local top_w, top_h = calc_dims(self.top, get_dims)
|
||||
local left_w, left_h = calc_dims(self.left, get_dims)
|
||||
local right_w, right_h = calc_dims(self.right, get_dims)
|
||||
local d = math_clamp(next_pow_of_2(math.max(top_w, top_h, left_w, left_h, right_w, right_h)), 2, 64)
|
||||
return d, d
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local function frame_dims(self, get_dims)
|
||||
local base_w, base_h = calc_dims(self.base, get_dims)
|
||||
return base_w, math.floor(base_h / self.framecount)
|
||||
end
|
||||
cd.verticalframe = frame_dims
|
||||
cd.crack = frame_dims
|
||||
cd.cracko = frame_dims
|
||||
end
|
||||
|
||||
function cd:sheet(get_dims)
|
||||
local base_w, base_h = calc_dims(self.base, get_dims)
|
||||
return math.floor(base_w / self.w), math.floor(base_h / self.h)
|
||||
end
|
||||
|
||||
function cd:png()
|
||||
local png = modlib.minetest.decode_png(modlib.text.inputstream(self.data))
|
||||
return png.width, png.height
|
||||
end
|
||||
|
||||
return calc_dims
|
422
mods/modlib/minetest/texmod/dsl.lua
Normal file
422
mods/modlib/minetest/texmod/dsl.lua
Normal file
|
@ -0,0 +1,422 @@
|
|||
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
|
190
mods/modlib/minetest/texmod/gen_tex.lua
Normal file
190
mods/modlib/minetest/texmod/gen_tex.lua
Normal file
|
@ -0,0 +1,190 @@
|
|||
local tex = modlib.tex
|
||||
|
||||
local paths = modlib.minetest.media.paths
|
||||
local function read_png(fname)
|
||||
if fname == "blank.png" then return tex.new{w=1,h=1,0} end
|
||||
return tex.read_png(assert(paths[fname]))
|
||||
end
|
||||
|
||||
local gt = {}
|
||||
|
||||
-- TODO colorizehsl, hsl, contrast
|
||||
-- TODO (...) inventorycube; this is nontrivial.
|
||||
|
||||
function gt:file()
|
||||
return read_png(self.filename)
|
||||
end
|
||||
|
||||
function gt:opacity()
|
||||
local t = self.base:gen_tex()
|
||||
t:opacity(self.ratio / 255)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:invert()
|
||||
local t = self.base:gen_tex()
|
||||
t:invert(self.r, self.g, self.b, self.a)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:brighten()
|
||||
local t = self.base:gen_tex()
|
||||
t:brighten()
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:noalpha()
|
||||
local t = self.base:gen_tex()
|
||||
t:noalpha()
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:makealpha()
|
||||
local t = self.base:gen_tex()
|
||||
t:makealpha(self.r, self.g, self.b)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:multiply()
|
||||
local c = self.color
|
||||
local t = self.base:gen_tex()
|
||||
t:multiply_rgb(c.r, c.g, c.b)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:screen()
|
||||
local c = self.color
|
||||
local t = self.base:gen_tex()
|
||||
t:screen_blend_rgb(c.r, c.g, c.b)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:colorize()
|
||||
local c = self.color
|
||||
local t = self.base:gen_tex()
|
||||
t:colorize(c.r, c.g, c.b, self.ratio)
|
||||
return t
|
||||
end
|
||||
|
||||
local function resized_to_larger(a, b)
|
||||
if a.w * a.h > b.w * b.h then
|
||||
b = b:resized(a.w, a.h)
|
||||
else
|
||||
a = a:resized(b.w, b.h)
|
||||
end
|
||||
return a, b
|
||||
end
|
||||
|
||||
function gt:mask()
|
||||
local a, b = resized_to_larger(self.base:gen_tex(), self._mask:gen_tex())
|
||||
a:band(b)
|
||||
return a
|
||||
end
|
||||
|
||||
function gt:lowpart()
|
||||
local t = self.base:gen_tex()
|
||||
local over = self.over:gen_tex()
|
||||
local lowpart_h = math.ceil(self.percent/100 * over.h) -- TODO (?) ceil or floor
|
||||
if lowpart_h > 0 then
|
||||
t, over = resized_to_larger(t, over)
|
||||
local y = over.h - lowpart_h + 1
|
||||
over:crop(1, y, over.w, over.h)
|
||||
t:blit(1, y, over)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:resize()
|
||||
return self.base:gen_tex():resized(self.w, self.h)
|
||||
end
|
||||
|
||||
function gt:combine()
|
||||
local t = tex.filled(self.w, self.h, 0)
|
||||
for _, blt in ipairs(self.blits) do
|
||||
t:blit(blt.x + 1, blt.y + 1, blt.texture:gen_tex())
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:fill()
|
||||
if self.base then
|
||||
return self.base:gen_tex():fill(self.w, self.h, self.x, self.y, self.color:to_number())
|
||||
end
|
||||
return tex.filled(self.w, self.h, self.color:to_number())
|
||||
end
|
||||
|
||||
function gt:blit()
|
||||
local t, o = resized_to_larger(self.base:gen_tex(), self.over:gen_tex())
|
||||
t:blit(1, 1, o)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:hardlight()
|
||||
local t, o = resized_to_larger(self.base:gen_tex(), self.over:gen_tex())
|
||||
t:hardlight_blend(o)
|
||||
return t
|
||||
end
|
||||
|
||||
-- TODO (...?) optimize this
|
||||
function gt:transform()
|
||||
local t = self.base:gen_tex()
|
||||
if self.flip_axis == "x" then
|
||||
t:flip_x()
|
||||
elseif self.flip_axis == "y" then
|
||||
t:flip_y()
|
||||
end
|
||||
-- TODO implement counterclockwise rotations to get rid of this hack
|
||||
for _ = 1, 360 - self.rotation_deg / 90 do
|
||||
t = t:rotated_90()
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local frame = function(t, frame, framecount)
|
||||
local fh = math.floor(t.h / framecount)
|
||||
t:crop(1, frame * fh + 1, t.w, (frame + 1) * fh)
|
||||
end
|
||||
|
||||
local crack = function(self, o)
|
||||
local crack = read_png"crack_anylength.png"
|
||||
frame(crack, self.frame, math.floor(crack.h / crack.w))
|
||||
local t = self.base:gen_tex()
|
||||
local tile_w, tile_h = math.floor(t.w / self.tilecount), math.floor(t.h / self.framecount)
|
||||
crack = crack:resized(tile_w, tile_h)
|
||||
for ty = 1, t.h, tile_h do
|
||||
for tx = 1, t.w, tile_w do
|
||||
t[o and "blito" or "blit"](t, tx, ty, crack)
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:crack()
|
||||
return crack(self, false)
|
||||
end
|
||||
|
||||
function gt:cracko()
|
||||
return crack(self, true)
|
||||
end
|
||||
|
||||
function gt:verticalframe()
|
||||
local t = self.base:gen_tex()
|
||||
frame(t, self.frame, self.framecount)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:sheet()
|
||||
local t = self.base:gen_tex()
|
||||
local tw, th = math.floor(t.w / self.w), math.floor(t.h / self.h)
|
||||
local x, y = self.x, self.y
|
||||
t:crop(x * tw + 1, y * th + 1, (x + 1) * tw, (y + 1) * th)
|
||||
return t
|
||||
end
|
||||
|
||||
function gt:png()
|
||||
return tex.read_png_string(self.data)
|
||||
end
|
||||
|
||||
return function(self)
|
||||
return assert(gt[self.type])(self)
|
||||
end
|
429
mods/modlib/minetest/texmod/read.lua
Normal file
429
mods/modlib/minetest/texmod/read.lua
Normal file
|
@ -0,0 +1,429 @@
|
|||
local texmod = ...
|
||||
local colorspec = modlib.minetest.colorspec
|
||||
|
||||
-- Generator readers
|
||||
|
||||
local gr = {}
|
||||
|
||||
function gr.png(r)
|
||||
r:expect":"
|
||||
local base64 = r:match_str"[a-zA-Z0-9+/=]"
|
||||
return assert(minetest.decode_base64(base64), "invalid base64")
|
||||
end
|
||||
|
||||
function gr.inventorycube(r)
|
||||
local top = r:invcubeside()
|
||||
local left = r:invcubeside()
|
||||
local right = r:invcubeside()
|
||||
return top, left, right
|
||||
end
|
||||
|
||||
function gr.combine(r)
|
||||
r:expect":"
|
||||
local w = r:int()
|
||||
r:expect"x"
|
||||
local h = r:int()
|
||||
local blits = {}
|
||||
while r:match":" do
|
||||
if r.eof then break end -- we can just end with `:`, right?
|
||||
local x = r:int()
|
||||
r:expect","
|
||||
local y = r:int()
|
||||
r:expect"="
|
||||
table.insert(blits, {x = x, y = y, texture = r:subtexp()})
|
||||
end
|
||||
return w, h, blits
|
||||
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
|
||||
|
||||
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.noalpha() end
|
||||
|
||||
function pr.resize(r)
|
||||
r:expect":"
|
||||
local w = r:int()
|
||||
r:expect"x"
|
||||
local h = r:int()
|
||||
return w, h
|
||||
end
|
||||
|
||||
function pr.makealpha(r)
|
||||
r:expect":"
|
||||
local red = r:int()
|
||||
r:expect","
|
||||
local green = r:int()
|
||||
r:expect","
|
||||
local blue = r:int()
|
||||
return red, green, blue
|
||||
end
|
||||
|
||||
function pr.opacity(r)
|
||||
r:expect":"
|
||||
local ratio = r:int()
|
||||
return ratio
|
||||
end
|
||||
|
||||
function pr.invert(r)
|
||||
r:expect":"
|
||||
local channels = {}
|
||||
while true do
|
||||
local c = r:match_charset"[rgba]"
|
||||
if not c then break end
|
||||
channels[c] = true
|
||||
end
|
||||
return channels
|
||||
end
|
||||
|
||||
do
|
||||
function pr.transform(r)
|
||||
if r:match_charset"[iI]" then
|
||||
return pr.transform(r)
|
||||
end
|
||||
local idx = r:match_charset"[0-7]"
|
||||
if idx then
|
||||
return tonumber(idx), pr.transform(r)
|
||||
end
|
||||
if r:match_charset"[fF]" then
|
||||
local flip_axis = assert(r:match_charset"[xXyY]", "axis expected")
|
||||
return "f" .. flip_axis, pr.transform(r)
|
||||
end
|
||||
if r:match_charset"[rR]" then
|
||||
local deg = r:match_str"%d"
|
||||
-- Be strict here: Minetest won't recognize other ways to write these numbers (or other numbers)
|
||||
assert(deg == "90" or deg == "180" or deg == "270")
|
||||
return ("r%d"):format(deg), pr.transform(r)
|
||||
end
|
||||
-- return nothing, we're done
|
||||
end
|
||||
end
|
||||
|
||||
function pr.verticalframe(r)
|
||||
r:expect":"
|
||||
local framecount = r:int()
|
||||
r:expect":"
|
||||
local frame = r:int()
|
||||
return framecount, frame
|
||||
end
|
||||
|
||||
function pr.crack(r)
|
||||
r:expect":"
|
||||
local framecount = r:int()
|
||||
r:expect":"
|
||||
local frame = r:int()
|
||||
if r:match":" then
|
||||
return framecount, frame, r:int()
|
||||
end
|
||||
return framecount, frame
|
||||
end
|
||||
pr.cracko = pr.crack
|
||||
|
||||
function pr.sheet(r)
|
||||
r:expect":"
|
||||
local w = r:int()
|
||||
r:expect"x"
|
||||
local h = r:int()
|
||||
r:expect":"
|
||||
local x = r:int()
|
||||
r:expect","
|
||||
local y = r:int()
|
||||
return w, h, x, y
|
||||
end
|
||||
|
||||
function pr.multiply(r)
|
||||
r:expect":"
|
||||
return r:colorspec()
|
||||
end
|
||||
pr.screen = pr.multiply
|
||||
|
||||
function pr.colorize(r)
|
||||
r:expect":"
|
||||
local color = r:colorspec()
|
||||
if not r:match":" then
|
||||
return color
|
||||
end
|
||||
if not r:match"a" then
|
||||
return color, r:int()
|
||||
end
|
||||
for c in ("lpha"):gmatch"." do
|
||||
r:expect(c)
|
||||
end
|
||||
return color, "alpha"
|
||||
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()
|
||||
end
|
||||
|
||||
function pr.mask(r)
|
||||
r:expect":"
|
||||
return r:subtexp()
|
||||
end
|
||||
|
||||
function pr.lowpart(r)
|
||||
r:expect":"
|
||||
local percent = r:int()
|
||||
assert(percent)
|
||||
r:expect":"
|
||||
return percent, r:subtexp()
|
||||
end
|
||||
|
||||
-- Build a prefix tree of parameter readers to greedily match the longest texture modifier prefix;
|
||||
-- just matching `%a+` and looking it up in a table
|
||||
-- doesn't work since `[transform` may be followed by a lowercase transform name
|
||||
-- TODO (?...) consolidate with `modlib.trie`
|
||||
local texmod_reader_trie = {}
|
||||
for _, readers in pairs{pr, gr} do
|
||||
for type in pairs(readers) do
|
||||
local subtrie = texmod_reader_trie
|
||||
for char in type:gmatch"." do
|
||||
subtrie[char] = subtrie[char] or {}
|
||||
subtrie = subtrie[char]
|
||||
end
|
||||
subtrie.type = type
|
||||
end
|
||||
end
|
||||
|
||||
-- Reader methods. We use `r` instead of the `self` "sugar" for consistency (and to save us some typing).
|
||||
local rm = {}
|
||||
|
||||
function rm.peek(r, parenthesized)
|
||||
if r.eof then return end
|
||||
local expected_escapes = 0
|
||||
if r.level > 0 then
|
||||
-- Premature optimization my beloved (this is `2^(level-1)`)
|
||||
expected_escapes = math.ldexp(0.5, r.level)
|
||||
end
|
||||
if r.character:match"[&^:]" then -- "special" characters - these need to be escaped
|
||||
if r.escapes == expected_escapes then
|
||||
return r.character
|
||||
elseif parenthesized and r.character == "^" and r.escapes < expected_escapes then
|
||||
-- Special handling for `^` inside `(...)`: This is undocumented behavior but works in Minetest
|
||||
r.warn"parenthesized caret (`^`) with too few escapes"
|
||||
return r.character
|
||||
end
|
||||
elseif r.escapes <= expected_escapes then
|
||||
return r.character
|
||||
end if r.escapes >= 2*expected_escapes then
|
||||
return "\\"
|
||||
end
|
||||
end
|
||||
function rm.popchar(r)
|
||||
assert(not r.eof, "unexpected eof")
|
||||
r.escapes = 0
|
||||
while true do
|
||||
r.character = r:read_char()
|
||||
if r.character ~= "\\" then break end
|
||||
r.escapes = r.escapes + 1
|
||||
end
|
||||
if r.character == nil then
|
||||
assert(r.escapes == 0, "end of texmod expected")
|
||||
r.eof = true
|
||||
end
|
||||
end
|
||||
function rm.pop(r)
|
||||
local expected_escapes = 0
|
||||
if r.level > 0 then
|
||||
-- Premature optimization my beloved (this is `2^(level-1)`)
|
||||
expected_escapes = math.ldexp(0.5, r.level)
|
||||
end
|
||||
if r.escapes > 0 and r.escapes >= 2*expected_escapes then
|
||||
r.escapes = r.escapes - 2*expected_escapes
|
||||
return
|
||||
end
|
||||
return r:popchar()
|
||||
end
|
||||
function rm.match(r, char)
|
||||
if r:peek() == char then
|
||||
r:pop()
|
||||
return true
|
||||
end
|
||||
end
|
||||
function rm.expect(r, char)
|
||||
if not r:match(char) then
|
||||
error(("%q expected"):format(char))
|
||||
end
|
||||
end
|
||||
function rm.hat(r, parenthesized)
|
||||
if r:peek(parenthesized) == (r.invcube and "&" or "^") then
|
||||
r:pop()
|
||||
return true
|
||||
end
|
||||
end
|
||||
function rm.match_charset(r, set)
|
||||
local char = r:peek()
|
||||
if char and char:match(set) then
|
||||
r:pop()
|
||||
return char
|
||||
end
|
||||
end
|
||||
function rm.match_str(r, set)
|
||||
local c = r:match_charset(set)
|
||||
if not c then
|
||||
error(("character in %s expected"):format(set))
|
||||
end
|
||||
local t = {c}
|
||||
while true do
|
||||
c = r:match_charset(set)
|
||||
if not c then break end
|
||||
table.insert(t, c)
|
||||
end
|
||||
return table.concat(t)
|
||||
end
|
||||
function rm.int(r)
|
||||
local sign = 1
|
||||
if r:match"-" then sign = -1 end
|
||||
return sign * tonumber(r:match_str"%d")
|
||||
end
|
||||
function rm.fname(r)
|
||||
-- This is overly permissive, as is Minetest;
|
||||
-- we just allow arbitrary characters up until a character which may terminate the name.
|
||||
-- Inside an inventorycube, `&` also terminates names.
|
||||
-- Note that the constructor will however - unlike Minetest - perform validation.
|
||||
-- We could leverage the knowledge of the allowed charset here already,
|
||||
-- but that might lead to more confusing error messages.
|
||||
return r:match_str(r.invcube and "[^:^&){]" or "[^:^){]")
|
||||
end
|
||||
function rm.subtexp(r)
|
||||
r.level = r.level + 1
|
||||
local res = r:texp()
|
||||
r.level = r.level - 1
|
||||
return res
|
||||
end
|
||||
function rm.invcubeside(r)
|
||||
assert(not r.invcube, "can't nest inventorycube")
|
||||
r.invcube = true
|
||||
assert(r:match"{", "'{' expected")
|
||||
local res = r:texp()
|
||||
r.invcube = false
|
||||
return res
|
||||
end
|
||||
function rm.basexp(r)
|
||||
if r:match"(" then
|
||||
local res = r:texp(true)
|
||||
r:expect")"
|
||||
return res
|
||||
end
|
||||
if r:match"[" then
|
||||
local type = r:match_str"%a"
|
||||
local gen_reader = gr[type]
|
||||
if not gen_reader then
|
||||
error("invalid texture modifier: " .. type)
|
||||
end
|
||||
return texmod[type](gen_reader(r))
|
||||
end
|
||||
return texmod.file(r:fname())
|
||||
end
|
||||
function rm.colorspec(r)
|
||||
-- Leave exact validation up to colorspec, only do a rough greedy charset matching
|
||||
return assert(colorspec.from_string(r:match_str"[#%x%a]"))
|
||||
end
|
||||
function rm.texp(r, parenthesized)
|
||||
local base = r:basexp() -- TODO (?) make optional - warn about omitting the base
|
||||
while r:hat(parenthesized) do
|
||||
if r:match"[" then
|
||||
local reader_subtrie = texmod_reader_trie
|
||||
while true do
|
||||
local next_subtrie = reader_subtrie[r:peek()]
|
||||
if next_subtrie then
|
||||
reader_subtrie = next_subtrie
|
||||
r:pop()
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
local type = assert(reader_subtrie.type, "invalid texture modifier")
|
||||
local param_reader, gen_reader = pr[type], gr[type]
|
||||
assert(param_reader or gen_reader)
|
||||
if param_reader then
|
||||
-- Note: It is important that this takes precedence to properly handle `[fill`
|
||||
base = base[type](base, param_reader(r))
|
||||
elseif gen_reader then
|
||||
base = base:blit(texmod[type](gen_reader(r)))
|
||||
end
|
||||
-- TODO (?...) we could consume leftover parameters here to be as lax as Minetest
|
||||
else
|
||||
base = base:blit(r:basexp())
|
||||
end
|
||||
end
|
||||
return base
|
||||
end
|
||||
|
||||
local mt = {__index = rm}
|
||||
return function(read_char, warn --[[function(str)]])
|
||||
local r = setmetatable({
|
||||
level = 0,
|
||||
invcube = false,
|
||||
parenthesized = false,
|
||||
eof = false,
|
||||
read_char = read_char,
|
||||
warn = warn or error,
|
||||
}, mt)
|
||||
r:popchar()
|
||||
local res = r:texp(false)
|
||||
assert(r.eof, "eof expected")
|
||||
return res
|
||||
end
|
181
mods/modlib/minetest/texmod/write.lua
Normal file
181
mods/modlib/minetest/texmod/write.lua
Normal file
|
@ -0,0 +1,181 @@
|
|||
local pw = {} -- parameter writers: `[type] = func(self, write)`
|
||||
|
||||
function pw:png(w)
|
||||
w.colon(); w.str(minetest.encode_base64(self.data))
|
||||
end
|
||||
|
||||
function pw:combine(w)
|
||||
w.colon(); w.int(self.w); w.str"x"; w.str(self.h)
|
||||
for _, blit in ipairs(self.blits) do
|
||||
w.colon()
|
||||
w.int(blit.x); w.str","; w.int(blit.y); w.str"="
|
||||
w.esctex(blit.texture)
|
||||
end
|
||||
end
|
||||
|
||||
function pw:inventorycube(w)
|
||||
assert(not w.inventorycube, "[inventorycube may not be nested")
|
||||
local function write_side(side)
|
||||
w.str"{"
|
||||
w.inventorycube = true
|
||||
w.tex(self[side])
|
||||
w.inventorycube = false
|
||||
end
|
||||
write_side"top"
|
||||
write_side"left"
|
||||
write_side"right"
|
||||
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
|
||||
pw.brighten = modlib.func.no_op
|
||||
pw.noalpha = modlib.func.no_op
|
||||
|
||||
function pw:resize(w)
|
||||
w.colon(); w.int(self.w); w.str"x"; w.int(self.h)
|
||||
end
|
||||
|
||||
function pw:makealpha(w)
|
||||
w.colon(); w.int(self.r); w.str","; w.int(self.g); w.str","; w.int(self.b)
|
||||
end
|
||||
|
||||
function pw:opacity(w)
|
||||
w.colon(); w.int(self.ratio)
|
||||
end
|
||||
|
||||
function pw:invert(w)
|
||||
w.colon()
|
||||
if self.r then w.str"r" end
|
||||
if self.g then w.str"g" end
|
||||
if self.b then w.str"b" end
|
||||
if self.a then w.str"a" end
|
||||
end
|
||||
|
||||
function pw:transform(w)
|
||||
w.int(self.idx)
|
||||
end
|
||||
|
||||
function pw:verticalframe(w)
|
||||
w.colon(); w.int(self.framecount); w.colon(); w.int(self.frame)
|
||||
end
|
||||
|
||||
function pw:crack(w)
|
||||
w.colon(); w.int(self.tilecount); w.colon(); w.int(self.framecount); w.colon(); w.int(self.frame)
|
||||
end
|
||||
|
||||
pw.cracko = pw.crack
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
function pw:screen(w)
|
||||
w.colon()
|
||||
w.str(self.color:to_string())
|
||||
end
|
||||
|
||||
function pw:multiply(w)
|
||||
w.colon()
|
||||
w.str(self.color:to_string())
|
||||
end
|
||||
|
||||
function pw:colorize(w)
|
||||
w.colon()
|
||||
w.str(self.color:to_string())
|
||||
if self.ratio then
|
||||
w.colon()
|
||||
if self.ratio == "alpha" then
|
||||
w.str"alpha"
|
||||
else
|
||||
w.int(self.ratio)
|
||||
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)
|
||||
w.colon(); w.esctex(self._mask)
|
||||
end
|
||||
|
||||
function pw:lowpart(w)
|
||||
w.colon(); w.int(self.percent); w.colon(); w.esctex(self.over)
|
||||
end
|
||||
|
||||
-- Set of "non-modifiers" which do not modify a base image
|
||||
local non_modifiers = {file = true, png = true, combine = true, inventorycube = true}
|
||||
|
||||
return function(self, write_str)
|
||||
-- We could use a metatable here, but it wouldn't really be worth it;
|
||||
-- it would save us instantiating a handful of closures at the cost of constant `__index` events
|
||||
-- and having to constantly pass `self`, increasing code complexity
|
||||
local w = {}
|
||||
w.inventorycube = false
|
||||
w.level = 0
|
||||
w.str = write_str
|
||||
function w.esc()
|
||||
if w.level == 0 then return end
|
||||
w.str(("\\"):rep(math.ldexp(0.5, w.level)))
|
||||
end
|
||||
function w.hat()
|
||||
-- Note: We technically do not need to escape `&` inside an [inventorycube which is nested inside [combine,
|
||||
-- but we do it anyways for good practice and since we have to escape `&` inside [combine inside [inventorycube.
|
||||
w.esc()
|
||||
w.str(w.inventorycube and "&" or "^")
|
||||
end
|
||||
function w.colon()
|
||||
w.esc(); w.str":"
|
||||
end
|
||||
function w.int(int)
|
||||
w.str(("%d"):format(int))
|
||||
end
|
||||
function w.tex(tex)
|
||||
if tex.type == "file" then
|
||||
w.str(tex.filename)
|
||||
return
|
||||
end
|
||||
if tex.base then
|
||||
w.tex(tex.base)
|
||||
w.hat()
|
||||
end
|
||||
if tex.type == "blit" then -- simply `^`
|
||||
if non_modifiers[tex.over.type] then
|
||||
w.tex(tex.over)
|
||||
else
|
||||
-- Make sure the modifier is first applied to its base image
|
||||
-- and only after this overlaid on top of `tex.base`
|
||||
w.str"("; w.tex(tex.over); w.str")"
|
||||
end
|
||||
else
|
||||
w.str"["
|
||||
w.str(tex.type)
|
||||
pw[tex.type](tex, w)
|
||||
end
|
||||
end
|
||||
function w.esctex(tex)
|
||||
w.level = w.level + 1
|
||||
w.tex(tex)
|
||||
w.level = w.level - 1
|
||||
end
|
||||
w.tex(self)
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue