diff --git a/README.md b/README.md index a957cc6..c2fd064 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # figura-skin -my figura avatar!! this currently targets version **0.15** +my figura avatar!! this currently targets version **0.1.5** `textures/main.png` contains the skins base, which can be used as your default minecraft skin if you so choose @@ -8,4 +8,4 @@ my figura avatar!! this currently targets version **0.15** to get proper typings, find the correct branch for your figura version [on this repository](github.com/GrandpaScout/FiguraRewriteVSDocs/). then, copy everything from the `src/` folder into your current working folder here -unfortunately, 0.15 typings aren't done yet, so just make sure it works :) +unfortunately, 0.1.5 typings aren't done yet, so just make sure it works :) diff --git a/avatar.json b/avatar.json index 4802630..2cb9aad 100644 --- a/avatar.json +++ b/avatar.json @@ -4,7 +4,8 @@ "authors": [ "reidlab", "mrsirsquishy: Squishy's API", - "purpledeni + Manuel_: Gradient Scroll Nickname", + "Agapurnis: Gradient and nameplate script", + "skyevg: Oklab script", "adristel: Wet Clothes/Fur Script" ], "color": "#d87b5a", diff --git a/scripts/libs/gradient.lua b/scripts/libs/gradient.lua new file mode 100644 index 0000000..6af0dc3 --- /dev/null +++ b/scripts/libs/gradient.lua @@ -0,0 +1,370 @@ +-- work in pewgross +-- do not share !! + +---@class UnitRGBA: Vector4 [r, g, b, a]; all values are 0-1. +local UnitRGBA = {}; + +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-1) +---@return UnitRGBA? output nil the vector with the new metatable if valid, otherwise nil +---@return string? error a string describing the error if invalid, otherwise nil +---@nodiscard +---@see UnitRGBA.fromUnitStrict to throw an error instead of returning nil +function UnitRGBA.fromUnit(vector) + for i, v in ipairs({vector:unpack()}) do + if type(v) ~= "number" then + return nil, "Vector component " .. i .. " is not a number"; + end + if v < 0 or v > 1 then + return nil, "Vector component " .. i .. " is not in the range 0-1; got " .. v; + end + end + return setmetatable({ vector = vector }, UnitRGBA), nil; +end +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-1) +---@return UnitRGBA vector the vector with the new metatable if valid. otherwise, an error is thrown +---@nodiscard +---@see UnitRGBA.fromUnit to return nil instead of throwing an error +function UnitRGBA.fromUnitStrict(vector) + local result, why = UnitRGBA.fromUnit(vector); + if result == nil then + error("Invalid vector: " .. why); + end + return result; +end +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-255) +---@return UnitRGBA? output nil the vector with the new metatable if valid, otherwise nil +---@return string? error a string describing the error if invalid, otherwise nil +---@nodiscard +---@see UnitRGBA.fromU8Strict to throw an error instead of returning nil +function UnitRGBA.fromU8(vector) + for i, v in ipairs(vector) do + if type(v) ~= "number" then + return nil, "Vector component " .. i .. " is not a number"; + end + if v < 0 or v > 255 then + return nil, "Vector component " .. i .. " is not in the range 0-255"; + end + end + return setmetatable({ vector = vector / 255 }, UnitRGBA), nil; +end +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-255) +---@return UnitRGBA vector the vector with the new metatable if valid. otherwise, an error is thrown +---@nodiscard +---@see UnitRGBA.fromU8 to return nil instead of throwing an error +function UnitRGBA.fromU8Strict(vector) + local result, why = UnitRGBA.fromU8(vector); + if result == nil then + error("Invalid vector: " .. why); + end + return result; +end + +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-1) +---@return UnitRGBA vector the vector with the new metatable +---@nodiscard +function UnitRGBA.fromUnitClamping(vector) + vector[1] = math.clamp(vector[1], 0, 1); + vector[2] = math.clamp(vector[2], 0, 1); + vector[3] = math.clamp(vector[3], 0, 1); + vector[4] = math.clamp(vector[4], 0, 1); + return setmetatable({ vector = vector }, UnitRGBA); +end + +---@param hex string a color in hexadecimal format (e.g. "#FF00FF") +---@return UnitRGBA? output the color with the new metatable if valid, otherwise nil +---@return string? error a string describing the error if invalid, otherwise nil +---@nodiscard +---@see UnitRGBA.fromHexStrict to throw an error instead of returning nil +function UnitRGBA.fromHex(hex) + if type(hex) ~= "string" then + return nil, "not a string"; + end + if hex:sub(1, 1) == "#" then + hex = hex:sub(2); + end + if hex:len() ~= 6 and hex:len() ~= 8 then + return nil, "not 6 or 8 characters long"; + end + local vector = vec(0, 0, 0, 1); + for i = 1, 3 do + local value = tonumber(hex:sub(i * 2 - 1, i * 2), 16); + if value == nil then + return nil, "contains non-hex characters"; + end + vector[i] = value / 255; + end + if hex:len() == 8 then + local value = tonumber(hex:sub(7, 8), 16); + if value == nil then + return nil, "contains non-hex characters"; + end + vector[4] = value / 255; + else + vector[4] = 1; + end + return setmetatable({ vector = vector }, UnitRGBA), nil; +end +---@param hex string a color in hexadecimal format (e.g. "#FF00FF") +---@return UnitRGBA color the color with the new metatable if valid. otherwise, an error is thrown +---@nodiscard +---@see UnitRGBA.fromHex to return nil instead of throwing an error +function UnitRGBA.fromHexStrict(hex) + local result, why = UnitRGBA.fromHex(hex); + if result == nil then + error("Invalid hex color: " .. why); + end + return result; +end + +---@return boolean opaque whether the color is *fully opaque* (as in, a == 1, not just a > 0). +---@see UnitRGBA:isTranslucent to see if the color is partially or fully transparent. +---@see UnitRGBA:isTransparent to see if the color is fully transparent. +---@nodiscard +function UnitRGBA:isOpaque() + return self.vector[4] == 1; +end +---@return boolean transparent whether the color is *fully transparent* (as in, a == 0, not just a < 1). +---@see UnitRGBA:isTranslucent to see if the color is partially transparent. +---@see UnitRGBA:isOpaque to see if the color is fully opaque. +---@nodiscard +function UnitRGBA:isTransparent() + return self.vector[4] == 0; +end +---@return boolean translucent whether the color is *translucent* (as in, a < 1). +---@see UnitRGBA:isTransparent to see if the color is fully transparent. +---@see UnitRGBA:isOpaque to see if the color is fully opaque. +---@nodiscard +function UnitRGBA:isTranslucent() + return self.vector[4] < 1; +end + +function UnitRGBA:toVec3() + return vec(self.vector[1], self.vector[2], self.vector[3]); +end + + +---@return Vector4 vector Vector4 with format ([r, g, b, a]; all values are 0-255) +---@nodiscard +function UnitRGBA:toU8vec4() + return vec(self.vector[1] * 255, self.vector[2] * 255, self.vector[3] * 255, self.vector[4] * 255); +end +---@return Vector3 vector Vector3 with format ([r, g, b]; all values are 0-255) +---@nodiscard +function UnitRGBA:toU8vec3() + local a = vec(self.vector[1] * 255, self.vector[2] * 255, self.vector[3] * 255) + return a +end + + +---@param hash? boolean? whether to include the hash symbol (#) in the output. defaults to true +---@param alpha? boolean? whether to include the alpha channel in the output. defaults to true +---@return string hex the color in RGBA hexadecimal format (e.g. "#FF00FF") +---@nodiscard +function UnitRGBA:toHex(hash, alpha) + local hex = (hash == nil or hash) and "#" or ""; + local iter = (alpha == nil or alpha) and 4 or 3; + for i = 1, iter do + local value = math.round(self.vector[i] * 255); + hex = hex .. string.format("%02X", value); + end + return hex; +end + +function UnitRGBA:__index(key) + return rawget(UnitRGBA, key) or self.vector[key]; +end +function UnitRGBA:__tostring() + return "UnitRGBA(" .. table.concat(self.vector, ", ") .. ")"; +end + +function UnitRGBA:__sub(other) + return UnitRGBA.fromUnitClamping(vec( + self.vector[1] - other.vector[1], + self.vector[2] - other.vector[2], + self.vector[3] - other.vector[3], + self.vector[4] - other.vector[4] + )); +end + +--- Transparent white. +UnitRGBA.TRANSPARENT_WHITE = UnitRGBA.fromUnitStrict(vec(1, 1, 1, 0)); +--- Transparent black. +UnitRGBA.TRANSPARENT_BLACK = UnitRGBA.fromUnitStrict(vec(0, 0, 0, 0)); +--- Opaque white. +UnitRGBA.WHITE = UnitRGBA.fromUnitStrict(vec(1, 1, 1, 1)); +--- Opaque black. +UnitRGBA.BLACK = UnitRGBA.fromUnitStrict(vec(0, 0, 0, 1)); + +---@class Keypoint - A keypoint (interpolation point definition) in a gradient. +---@field public color UnitRGBA color of the keypoint +---@field public time number a number between 0 and 1 (inclusive on both ends) +local Keypoint = {}; + +---Returns a new Keypoint object. +---@param color UnitRGBA color of the keypoint +---@param time number a number between 0 and 1 (inclusive on both ends) +---@return Keypoint +---@nodiscard +function Keypoint.new(color, time) + return setmetatable({ + color = color, + time = time + }, Keypoint); +end + +---@class Gradient A gradient definition. +---@field public keypoints Keypoint[] a list of keypoint objects +---@field public loop boolean whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. +local Gradient = {}; + +---Returns a new Gradient object. +---@param keypoints Keypoint[] a list of keypoint objects +---@param loop boolean? whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. defaults to true. +---@return Gradient +---@nodiscard +function Gradient.new(keypoints, loop) + return setmetatable({ + keypoints = keypoints, + loop = loop == nil or loop + }, Gradient); +end + +local function easeInOutQuad(t) + return t < 0.5 and 2 * t * t or 1 - (-2 * t + 2)^2 / 2 +end + +local oklab = require("scripts.libs.oklab") +---Returns the color of the gradient at a given time. +---@param time number a number between 0 and 1 (inclusive on both ends) (can extend beyond this range if Gradient.loop is true) +---@return UnitRGBA? color the color of the gradient at the given time, or nil if the time is out of bounds and Gradient.loop is false +function Gradient:at(time) + if time < 0 or time > 1 then + if not self.loop then + return nil; + end + time = time % 1; + end + + if time == 0 then return self.keypoints[1].color + elseif time == 1 then return self.keypoints[#self.keypoints].color + end + + for i = 1, #self.keypoints - 1 do + local this = self.keypoints[i] + local next = self.keypoints[i + 1] + if time >= this.time and time < next.time then + local t = easeInOutQuad((time - this.time) / (next.time - this.time)); + + local alpha = math.lerp(this.color.a, next.color.a, t) + local La, Aa, Ba = oklab.srgbToLinear(this.color.xyz):unpack() + local Lb, Ab, Bb = oklab.srgbToLinear(next.color.xyz):unpack() + local mixed = vec( + math.lerp(La, Lb, easeInOutQuad(t)), + math.lerp(Aa, Ab, t), + math.lerp(Ba, Bb, t) + ) + + mixed = oklab.linearToSrgb(mixed); + mixed = vec(mixed.x, mixed.y, mixed.z, alpha) + return UnitRGBA.fromUnitClamping(mixed); + end + end + + if #self.keypoints == 1 then + return self.keypoints[1].color; + end + + error("Gradient.at: time " .. time .. " is out of bounds"); +end + +---@param colors string[] a list of colors in hexadecimal format (e.g. "#FF00FF") +---@param loop boolean? whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. defaults to true. +---@return Gradient gradient the gradient with the new metatable if valid. otherwise, an error is thrown +function Gradient.distributedHex(colors, loop) + local keypoints = {}; + for i, hex in ipairs(colors) do + local time = (i - 1) / (#colors - 1); + local color = UnitRGBA.fromHexStrict(hex); + table.insert(keypoints, Keypoint.new(color, time)); + end + return Gradient.new(keypoints, loop); +end + +function Gradient:__index(key) + return rawget(Gradient, key) or self[key]; +end + + +---@class SimpleGradientBuilder +---@field public colors UnitRGBA[] a list of colors +local SimpleGradientBuilder = {}; +---@return SimpleGradientBuilder builder a simple gradient builder +function SimpleGradientBuilder.new() + return setmetatable({ colors = {} }, { __index = SimpleGradientBuilder }); +end +---Reflect the colors of the gradient; this makes it nicer when looping. +---@param reflect_last boolean? whether to also reflect the final color, such that it would appear twice in a row at the middle. if false, it'll only be present once +---@return SimpleGradientBuilder self builder for chaining +function SimpleGradientBuilder:reflect(reflect_last) + ---@type UnitRGBA[] + local reflected = {}; + for i = 1, #self.colors - 1 do + table.insert(reflected, self.colors[i]); + end + table.insert(reflected, self.colors[#self.colors]) + if reflect_last then + table.insert(reflected, self.colors[#self.colors]) + end + for i = #self.colors - 1, 1, -1 do + table.insert(reflected, self.colors[i]) + end + self.colors = reflected + return self; +end +---"Thicken" the gradient by duplicating every keypoint. This means that the actual colors themselves will have as much presence as the transitions. +---@param amount? integer how many times to duplicate each keypoint. defaults to 1. +---@return SimpleGradientBuilder self builder for chaining +function SimpleGradientBuilder:thicken(amount) + amount = amount or 1; + local thickened = {}; + for i = 1, #self.colors do + for _ = 0, amount do + table.insert(thickened, self.colors[i]) + end + end + self.colors = thickened; + return self; +end +---@param loop boolean? whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. defaults to true. +---@return Gradient gradient the constructed gradient +function SimpleGradientBuilder:build(loop) + loop = loop == nil or loop; + + ---@type Keypoint[] + local keypoints = {}; + for i, color in ipairs(self.colors) do + table.insert(keypoints, Keypoint.new(color, (i - 1) / (#self.colors - 1))); + end + + return setmetatable({ + keypoints = keypoints, + loop = loop + }, Gradient); +end +---@param colors string[] | string hexadecimal color(s) to add +function SimpleGradientBuilder:add(colors) + if type(colors) == "string" then + colors = {colors} + end + for _, color in ipairs(colors) do + table.insert(self.colors, UnitRGBA.fromHexStrict(color)) + end + return self; +end + +return { + UnitRGBA = UnitRGBA, + Keypoint = Keypoint, + Gradient = Gradient, + SimpleGradientBuilder = SimpleGradientBuilder, +} diff --git a/scripts/libs/oklab.lua b/scripts/libs/oklab.lua new file mode 100644 index 0000000..f12a95a --- /dev/null +++ b/scripts/libs/oklab.lua @@ -0,0 +1,113 @@ +local lib = {} + +---@param x number +---@return number +local function fromLinear(x) + if x >= 0.0031308 then + return 1.055 * math.pow(x, 1.0/2.4) - 0.055 + else + return 12.92 * x + end +end + +---@param x number +---@return number +local function toLinear(x) + if x >= 0.04045 then + return math.pow((x + 0.055)/(1 + 0.055), 2.4) + else + return x / 12.92 + end +end + +---Converts from sRGB to Linear sRGB +---@param color Vector3 +---@return Vector3 +function lib.srgbToLinear(color) + return vec(toLinear(color.x), toLinear(color.y), toLinear(color.z)) +end + +---Converts from Linear sRGB to sRGB +---@param color Vector3 +---@return Vector3 +function lib.linearToSrgb(color) + return vec(fromLinear(color.x), fromLinear(color.y), fromLinear(color.z)) +end + +---Converts from Linear sRGB to OKLAB +---@param color Vector3 +---@return Vector3 +function lib.linearToOklab(color) + local l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b + local m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b + local s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b + + local l_ = math.pow(l, 1/3) + local m_ = math.pow(m, 1/3) + local s_ = math.pow(s, 1/3) + + return vec( + 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_, + 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_, + 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_ + ) +end + +---Converts from OKLAB to Linear sRGB +---@param color Vector3 +---@return Vector3 +function lib.oklabToLinear(color) + local l_ = color.x + 0.3963377774 * color.y + 0.2158037573 * color.z; + local m_ = color.x - 0.1055613458 * color.y - 0.0638541728 * color.z; + local s_ = color.x - 0.0894841775 * color.y - 1.2914855480 * color.z; + + local l = l_*l_*l_; + local m = m_*m_*m_; + local s = s_*s_*s_; + + return vec( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s + ) +end + +---Converts from LAB to LCh +---@param color Vector3 +---@return Vector3 +function lib.labToLch(color) + return vec( + color.x, + math.sqrt(color.y * color.y + color.z * color.z), + math.deg(math.atan2(color.z, color.y)) + ) +end + +---Converts from LCh to LAB +---@param color Vector3 +---@return Vector3 +function lib.lchToLab(color) + return vec( + color.x, + color.y * math.cos(math.rad(color.z)), + color.y * math.sin(math.rad(color.z)) + ) +end + + + +---Converts from sRGB to OKLCh +---@param color Vector3 +---@return Vector3 +function lib.srgbToOklch(color) + return lib.labToLch(lib.linearToOklab(lib.srgbToLinear(color))) +end + +---Converts from OKLCh to sRGB +---@param color Vector3 +---@return Vector3 +function lib.oklchToSrgb(color) + return lib.linearToSrgb(lib.oklabToLinear(lib.lchToLab(color))) +end + +return lib \ No newline at end of file diff --git a/scripts/nameplate.lua b/scripts/nameplate.lua index 93c749b..95cb7b4 100644 --- a/scripts/nameplate.lua +++ b/scripts/nameplate.lua @@ -1,22 +1,110 @@ -local name = "reidlab!" -local colors = { "#d87b5a", "#e0ab91" } -local offset = 0.05 -local speed = 0.05 +local g = require("scripts.libs.gradient") -colors[#colors + 1] = colors[1] -offset = offset / speed +---@class ColoredText +---@field text string +---@field color string -events.TICK:register(function () - local newName = "[" - - for i = 1, #name, 1 do - local counter = (((world.getTime() + offset * i) * speed) % (#colors - 1)) + 1 - local counterFloored = math.floor(counter) - local color = math.lerp(vectors.hexToRGB(colors[counterFloored]), vectors.hexToRGB(colors[counterFloored + 1]), counter - counterFloored) - newName = newName .. '{"text":"' .. name:sub(i,i) .. '","color":"#' .. vectors.rgbToHex(color) .. '"},' - avatar:setColor(color) +---@param name string +---@param gradient Gradient +---@param nameplates Nameplate | Nameplate[] where to use the animated name +---@param count number number of gradients to be present in the span +---@param phase_shift_rate number how fast to shift the gradient +---@returns fun():void # function to unregister +local function animate_gradient_name(name, gradient, nameplates, count, phase_shift_rate) + if type(nameplates) ~= "table" then + nameplates = {nameplates} end - newName = newName:sub(1, #newName - 1) .. "]" - nameplate.ALL:setText(newName) -end) + local string_width = client.getTextWidth(name) / count; + local string_width_chars = {}; + for i = 1, #name do + string_width_chars[i] = client.getTextWidth(name:sub(i, i)) + end + + ---@type ColoredText[] + local result = {} + + local phase_shift = 0; + local function render() + if client.isPaused() then + return + end + + local acc = 0; + for i = 1, #name do + local offset = acc + string_width_chars[i]; + local color = gradient:at((offset / string_width) + phase_shift); + acc = offset + + result[i] = { + text = name:sub(i, i), + color = color:toHex(true, false) + } + end + + phase_shift = phase_shift + phase_shift_rate + + local json = toJson(result); + for _, nameplate in pairs(nameplates) do + nameplate:setText(json) + end + end + + events.render:register(render) + return function () + events.render:remove(render) + end +end + +---@param name string +---@param gradient Gradient +---@param nameplates Nameplate | Nameplate[] where to use the static name +---@param count number number of gradients to be present in the span +---@param phase_shift number shift of the gradient +local function static_gradient_name(name, gradient, nameplates, count, phase_shift) + if type(nameplates) ~= "table" then + nameplates = {nameplates} + end + + local string_width = client.getTextWidth(name) / count; + local string_width_chars = {}; + for i = 1, #name do + string_width_chars[i] = client.getTextWidth(name:sub(i, i)) + end + + ---@type ColoredText[] + local result = {} + + local acc = 0; + for i = 1, #name do + local offset = acc + string_width_chars[i]; + local color = gradient:at((offset / string_width) + phase_shift); + acc = offset + + result[i] = { + text = name:sub(i, i), + color = color:toHex(true, false) + } + end + + local json = toJson(result); + for _, nameplate in pairs(nameplates) do + nameplate:setText(json) + end +end + +local name = "reidlab!" +local gradient_count = 1 +local gradient = g.SimpleGradientBuilder.new() + :add({ "#d87b5a", "#e0ab91" }) + :reflect(false) + :build() + +animate_gradient_name(name, gradient, { + nameplate.ENTITY, + nameplate.LIST +}, gradient_count, 0.005) + +static_gradient_name(name, gradient, { + nameplate.CHAT +}, gradient_count, 0) diff --git a/scripts/soggy.lua b/scripts/soggy.lua index 3367573..2d8c2eb 100644 --- a/scripts/soggy.lua +++ b/scripts/soggy.lua @@ -24,7 +24,6 @@ events.TICK:register(function () if world.getTime() % driptime == 0 and tw ~= 1 and not (player:isUnderwater()) then for _ = 0, driptime*0.5 do particles:newParticle("falling_dripstone_water",player:getPos():add(offset+vec(0,0.7,0)),offset2) end end if world.getTime() % driptime == 0 and bw ~= 1 and not (player:isInWater()) then for _ = 0, driptime*0.5 do particles:newParticle("falling_dripstone_water",player:getPos():add(offset),offset2:mul(0.5)) end end - utils.forEachNonGroup(models.models.main.LeftArm, function (part) part:setColor(tw,tw,tw) end) utils.forEachNonGroup(models.models.main.RightArm, function (part) part:setColor(tw,tw,tw) end) utils.forEachNonGroup(models.models.main.Head, function (part) part:setColor(tw,tw,tw) end) diff --git a/scripts/vanilla_model.lua b/scripts/vanilla_model.lua index 1e0ffda..f6c7877 100644 --- a/scripts/vanilla_model.lua +++ b/scripts/vanilla_model.lua @@ -1,4 +1,4 @@ -for _, vanillaModel in ipairs({ +for _, vanillaModel in pairs({ vanilla_model.PLAYER, vanilla_model.ARMOR }) do