Anyone want text gradients?

SylphasSylphas Member Posts: 64 Capable
function gradient(rgb1, rgb2, text)
	local clamp = function (num) return math.max(math.min(num, 255), 0) end
	local rgb1 = {clamp(rgb1[1]), clamp(rgb1[2]), clamp(rgb1[3])}
	local rgb2 = {clamp(rgb2[1]), clamp(rgb2[2]), clamp(rgb2[3])}
	local gradient = {rgb1}
	if #text == 1 then
		local rgb = {}
		for color = 1, 3 do rgb[color] = (rgb1[color] + rgb2[color]) / 2 end
		return "<" .. rgb[1] .. "," .. rgb[2] .. "," .. rgb[3] .. ">" .. text
	end
	if #text == 2 then
		gradient[2] = rgb2
	else
		local length = #text - 1
		local r_step = math.floor(((rgb2[1] - rgb1[1]) / length) + 0.5)
		local g_step = math.floor(((rgb2[2] - rgb1[2]) / length) + 0.5)
		local b_step = math.floor(((rgb2[3] - rgb1[3]) / length) + 0.5)
		for step = 1, length do
			local red = math.min((r_step * step) + rgb1[1], 255)
			local green = math.min((g_step * step) + rgb1[2], 255)
			local blue = math.min((b_step * step) + rgb1[3], 255)
			gradient[step + 1] = {red, green, blue}
		end
	end
	local colored_text = ""
	for pos, color in ipairs(gradient) do
		colored_text = colored_text .. "<" .. color[1] .. "," .. color[2] .. "," .. color[3] .. ">" .. string.sub(text, pos, pos)
	end
	return colored_text
end

This expects to get two tables of numbers between 0-255 in {red, green, blue} format and the text you want formatted. It will return a string formatted for use with decho() in Mudlet. If you want it to just spit out the echo itself, change "return colored_text" to "decho(colored_text)". I hadn't seen this around and needed it for my system, so I threw it together at lunch, figured someone might find it neat.

Tagged:

Comments

  • Vadi2Vadi2 Member Posts: 26 Apprentice
    How's it look like with a screenshot?
  • SylphasSylphas Member Posts: 64 Capable
    Found a bug with negative steps, fixed version:
    function gradient(rgb1, rgb2, text)
    local clamp = function (num) return math.max(math.min(num, 255), 0) end
    local rgb1 = {clamp(rgb1[1]), clamp(rgb1[2]), clamp(rgb1[3])}
    	local rgb2 = {clamp(rgb2[1]), clamp(rgb2[2]), clamp(rgb2[3])}
    	local gradient = {rgb1}
    	if #text == 1 then
    		local rgb = {}
    		for color = 1, 3 do rgb[color] = (rgb1[color] + rgb2[color]) / 2 end
    		return "<" .. rgb[1] .. "," .. rgb[2] .. "," .. rgb[3] .. ">" .. text
    	end
    	if #text == 2 then
    		gradient[2] = rgb2
    	else
    		local length = #text - 1
    		local r_step = ((rgb2[1] - rgb1[1]) / length) + 0.5
    		local g_step = ((rgb2[2] - rgb1[2]) / length) + 0.5
    		local b_step = ((rgb2[3] - rgb1[3]) / length) + 0.5
    		if r_step < 0 then r_step = math.ceil(r_step) else r_step = math.floor(r_step) end
    		if g_step < 0 then g_step = math.ceil(g_step) else g_step = math.floor(g_step) end
    		if b_step < 0 then b_step = math.ceil(b_step) else b_step = math.floor(b_step) end
    		for step = 1, length do
    			local red = clamp((r_step * step) + rgb1[1], 255)
    			local green = clamp((g_step * step) + rgb1[2], 255)
    			local blue = clamp((b_step * step) + rgb1[3], 255)
    			gradient[step + 1] = {red, green, blue}
    		end
    	end
    	local colored_text = ""
    	for pos, color in ipairs(gradient) do
    		colored_text = colored_text .. "<" .. color[1] .. "," .. color[2] .. "," .. color[3] .. ">" .. string.sub(text, pos, pos)
    	end
    	return colored_text
    end

  • UniUni Member Posts: 2 Inept
    Hey @Sylphas

    Do you mind if I port and share your function for Nexus?

    Added a loop function so the gradient turns around.


  • UniUni Member Posts: 2 Inept
    edited December 2017
    Sorry meant to include the code

    grd = (function() {  var gen = function(rgba1, rgba2, text, loop) {   var clamp = function(num) { return Math.max(Math.min(num, 255), 0) }   var rgba1 = [ clamp(rgba1[0]), clamp(rgba1[1]), clamp(rgba1[2]) ]   var rgba2 = [ clamp(rgba2[0]), clamp(rgba2[1]), clamp(rgba2[2]) ]   var gradn = [ rgba1 ]   if (text.length == 1) {     var rgb = []     for (var color=0; color<3; color++) { rgb[color] = ( rgba1[color] + rgba2[color] ) / 2 }     return 'style="color:rgba('+rgb[0]+','+rgb[1]+','+rgb[2]+');"' }   if (text.length == 2) {     gradn[1] = rgba2   } else {     var len = text.length - 1         if (loop) { len = Math.floor((len + 1) / 2) }     var rs  = Math.floor(((rgba2[0] - rgba1[0]) / len) + 0.5)     var gs  = Math.floor(((rgba2[1] - rgba1[1]) / len) + 0.5)     var bs  = Math.floor(((rgba2[2] - rgba1[2]) / len) + 0.5)     for (var step=0; step<len; step++) {      var red = Math.min((rs * step) + rgba1[0], 255)      var gre = Math.min((gs * step) + rgba1[1], 255)      var blu = Math.min((bs * step) + rgba1[2], 255)      gradn[step] = [red,gre,blu]     }     gradn.push(rgba2)         if (loop) {           for (var i=(gradn.length-1); i>0; i--) {            gradn.push( gradn[i] )              }           if (gradn.length > text.length) {             gradn.slice(0, text.length)              }         }   }   return gradn  }    return {   gen : gen,    } })()

    A bit verbose but works.
  • SylphasSylphas Member Posts: 64 Capable
    Uni said:
    Hey @Sylphas

    Do you mind if I port and share your function for Nexus?

    Added a loop function so the gradient turns around.


    Feel free! I'm always happy to see someone getting use out my stuff. :smile: One note, after looking over it a bit more: you're better off not rounding until you apply the steps, it really smooths things out for certain cases, because the math all works perfectly fine without it, so you only need integers when you're ready to actually feed in the RGB values you've come up with.
  • SylphasSylphas Member Posts: 64 Capable
    Update:
    • Split the code into cgradient() and dgradient(), to work with cecho() or decho().
    • Added the ability to generate the gradients as tables with cgradient_table() and dgradient_table(). This will output a table with integer keys corresponding to character number in the given string and values each being a table with the first index a three value table containing the RGB values and the second index the character itself, e.g. {{255, 255, 255}, "a"}.
    • cgradient() and dgradient() will each accept a string argument followed by any number of color nodes formatted as tables of the form {r, g, b}.
    Future plans:
    • cgradient() works surprisingly well in my tests so far, but if anyone spots a way to smooth it out more, please let me know. Currently it's simply running a test against Mudlet's built-in color_table to find the closest named color, but there are plenty of gaps in that set of colors. I'm considering simply inserting the calculated values into color_table on the fly and just using their hex value as the key or something, but that seems messy without a good way to clean up later, since we don't know when the gradient will actually be displayed.
    • I should probably implement memoization in the underlying _gradient() function, so that given the same length and color nodes it won't have to recalculate the gradient each time and can simply fetch the values it already has. I haven't noticed any processing issues with this so far, but given my eventual plans to integrate gradient color bars into my custom prompt, I figure any performance boost can't hurt.
    • I may fiddle with the point at which I sanity check the inputs. Right now feeding it something like {-300, 0, 0} gets you the same {0,0,0} black you'd expect from being clamped to the RGB color boundaries, but the gradient between {-300,0,0} and {255,255,255} is not the same as it would be if it started at {0,0,0}, because the numbers aren't clamped until after all the steps have been calculated. That is, using numbers beyond the 0-255 boundary shouldn't break anything, but it will "shape" the gradient differently. I'm honestly not sure if I consider that a bug or a feature at this point.

    This is a comparison between dgradient() and cgradient(), respectively, over three different gradients. The middle is simple {255,0,0}-{0,0,255} red to blue, but the first shows off the ability to feed in multiple nodes, being an input of seven nodes: {255,0,0}, {255,128,0}, {255,255,0}, {0,255,0}, {0,255,255}, {0,128,255}, {128,0,255}. The final pair is simply {50,50,50}, {0,255,0}, {50,50,50}.

    function _clamp(num1, num2, num3)
    	local smaller = math.min(num2, num3)
    	local larger = math.max(num2, num3)
    	local minimum = math.max(0, smaller)
    	local maximum = math.min(255, larger)
    	return math.min(maximum, math.max(minimum, num1))
    end
    
    function _gradient(length, rgb1, rgb2)
    	assert(length > 0)
    	if length == 1 then
    		return {rgb1}
    	elseif length == 2 then
    		return {rgb1, rgb2}
    	else
    		local color_range = {}
    		local step = {}
    		for color = 1, 3 do step[color] = (rgb2[color] - rgb1[color]) / (length - 2) end
    		local gradient = {rgb1}
    		for iter = 1, length - 2 do
    			gradient[iter + 1] = {}
    			for color = 1, 3 do
    				gradient[iter + 1][color] = math.ceil(rgb1[color] + (iter * step[color]))
    			end
    		end
    		gradient[length] = rgb2
    		for index, color in ipairs(gradient) do
    			for iter = 1, 3 do gradient[index][iter] = _clamp(color[iter], rgb1[iter], rgb2[iter]) end
    		end
    		return gradient
    	end
    end
    
    function _gradients(length, ...)
    	local arg = {...}
    	if #arg == 0 then
    		return {}
    	elseif #arg == 1 then
    		return arg[1]
    	elseif #arg == 2 then
    		return _gradient(length, arg[1], arg[2])
    	else
    		local quotient = math.floor(length / (#arg - 1))
    		local remainder = length % (#arg - 1)
    		local gradients = {}
    		for section = 1, #arg - 1 do
    			local length = quotient
    			if section <= remainder then length = length + 1 end
    			local gradient = _gradient(length, arg[section], arg[section + 1])
    			for _, rgb in ipairs(gradient) do
    				table.insert(gradients, rgb)
    			end
    		end
    		return gradients
    	end
    end
    
    function color_name(rgb)
    	local least_distance = math.huge
    	local color_name = ""
    	for name, color in pairs(color_table) do
    		color_distance = math.sqrt((color[1] - rgb[1])^2 + (color[2] - rgb[2])^2 + (color[3] - rgb[3])^2)
    		if color_distance < least_distance then
    			least_distance = color_distance
    			color_name = name
    		end
    	end
    	return color_name
    end
    
    function dgradient_table(text, ...)
    	local gradients = _gradients(#text, ...)
    	local dgradient = {}
    	for character = 1, #text do
    		table.insert(dgradient, {gradients[character], text:sub(character, character)})
    	end
    	return dgradient
    end
    
    function dgradient(text, ...)
    	local gradients = _gradients(#text, ...)
    	local dgradient = ""
    	for character = 1, #text do
    		dgradient = dgradient .. "<" .. table.concat(gradients[character], ",") .. ">" .. text:sub(character, character)
    	end
    	return dgradient
    end
    
    function cgradient_table(text, ...)
    	local gradients = _gradients(#text, ...)
    	local cgradient = {}
    	for character = 1, #text do
    		table.insert(cgradient, {color_name(gradients[character]), text:sub(character, character)})
    	end
    	return cgradient
    end
    
    function cgradient(text, ...)
    	local gradients = _gradients(#text, ...)
    	local cgradient = ""
    	for character = 1, #text do
    		cgradient = cgradient .. "<" .. color_name(gradients[character]) .. ">" .. text:sub(character, character)
    	end
    	return cgradient
    end
    The same code is attached as a text file if that's more convenient.

  • SylphasSylphas Member Posts: 64 Capable
    Looking at that last gradient in my screenshot, the grey to green, I'm thinking of making a function that takes a line of text and highlights a given substring with a "glow" effect, implemented by finding the string you want and centering a subtle gradient on it, something like the greyish white default text to bright white and back.
  • HiriakoHiriako Member Posts: 374 Gifted
    Sylphas: Thank you. 
Sign In or Register to comment.