Okay.
I noticed a couple of things:
1) It actually needs 3 rays, not just 2: one to progressively reveal characters on the same line, one to show all previous lines, and one to hide all lines below.
2) This trick works better with monospace (AKA fixed width) fonts, because there are some kerning pairs in non-monospace fonts where the letters overlap horizontally, so there's no way that a vertical ray can isolate only one of the characters.
This is a picture of the problem, look how the capital T is fully visible, but since it overhangs on that lowercase E, part of that E is also revealed: This problem doesn't happen with monospace/fixed width fonts, where each glyph is entirely contained within its own "box".
That said, here's the .love in the attachments. Included is a monospaced font, Bitstream Vera Sans Mono (Bold).
I'll also paste the main.lua below in case someone just wants to read it without having to extract it from the .love:
Code: Select all
-- =======================================
-- Shader-based text reveal effect.
-- Version 0.2
--
-- By Rafael Navega (2023)
-- License: Public Domain
-- =======================================
io.stdout:setvbuf('no')
local utf8 = require('utf8')
-- Bitstream Vera Sans Mono, Bold.
local font = love.graphics.newFont("Bitstream Vera Sans Mono/VeraMoBd.ttf", 16)
font:setLineHeight(1.2)
love.graphics.setFont(font)
local text = [[An old silent pond
A frog jumps into the pond—
Splash! Silence again.]]
local CHARS_PER_SEC = 16.0
local textObject
local charOffsets
-- Start with a negative index so it waits for a bit before showing anything.
local offsetIndex = -6
local textGeometry = {
TEXT_DRAW_X = 160,
TEXT_DRAW_Y = 20,
canStepLine = true,
lineRevealY = 0.0,
charRevealX = 0.0,
charHideY = 0.0
}
local pixelShaderSource = [[
extern float charRevealX;
extern float charHideY;
extern float lineRevealY;
// Minimum transparency possible. Set this to zero to completely hide characters.
const float MINIMUM_ALPHA = 0.1;
vec4 effect(vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords)
{
vec4 texturecolor = Texel(tex, texture_coords);
float charRevealAlpha = max(
step(screen_coords.y, lineRevealY),
step(screen_coords.x, charRevealX) * step(screen_coords.y, charHideY)
);
texturecolor.a *= max(MINIMUM_ALPHA, charRevealAlpha);
return texturecolor * color;
}
]]
local textShader
-- Create an offsets table with the steps in pixels between each character in the text.
-- When the Y is unchanged, store only the absolute X position.
-- When a linebreak is found, store both the X and Y in a child table.
-- So later when iterating this offsets table, any table elements will indicate
-- a change in both X and Y, and direct numbers will only indicate a change in X.
local function makeCharOffsets(text, font, textGeometry)
local charOffsets = {}
local lineHeight = font:getHeight() * font:getLineHeight()
local xPosition = 0.0
local yPosition = lineHeight
for c in text:gmatch(utf8.charpattern) do
if c == '\n' then
-- Reset the absolute position, X goes back to zero and Y advances
-- by 1 line.
xPosition = 0.0
yPosition = yPosition + lineHeight
table.insert(charOffsets, {xPosition, yPosition})
print(#charOffsets, xPosition, yPosition)
else
-- Advance the absolute X position.
local width = font:getWidth(c)
xPosition = xPosition + width
table.insert(charOffsets, xPosition)
print(#charOffsets, xPosition)
-- You can add some custom text wrapping in here, if the accumulated width
-- overflows a limit etc.
end
end
-- Make sure that the first element of this offsets table is a child table so that
-- the code that uses it later will reset both X and Y right at the start.
charOffsets[1] = {charOffsets[1], lineHeight}
-- Duplicate the last element (so no change in position) so the final character
-- can be displayed.
table.insert(charOffsets, charOffsets[#charOffsets])
return charOffsets
end
function love.load()
love.graphics.setBackgroundColor(0.05, 0.1, 0.3)
charOffsets = makeCharOffsets(text, font)
textGeometry.charRevealX = textGeometry.TEXT_DRAW_X
textGeometry.charHideY = textGeometry.TEXT_DRAW_Y
textGeometry.lineRevealY = textGeometry.TEXT_DRAW_Y
textShader = love.graphics.newShader(pixelShaderSource)
-- Create a static Text object, to prove that the animation is being
-- done in the shader and not at the string level.
textObject = love.graphics.newText(font, text)
end
function love.update(dt)
-- Just for debug, hold at the end for about this many characters long.
local RESET_DELAY = 7
offsetIndex = offsetIndex + CHARS_PER_SEC * dt
if offsetIndex > (#charOffsets + RESET_DELAY) then
offsetIndex = -6
-- Reset the text-revealing geometry when restarting the scanning.
textGeometry.lineRevealY = textGeometry.TEXT_DRAW_Y
textGeometry.charRevealX = textGeometry.TEXT_DRAW_X
textGeometry.charHideY = textGeometry.TEXT_DRAW_Y
textGeometry.canStepLine = true
else
local currentOffset = charOffsets[math.floor(offsetIndex)]
if currentOffset then
if type(currentOffset) == 'table' then
-- Make sure to only step a line once, because on consecutive frames
-- the SAME X,Y table might be sampled many times before 'offsetIndex'
-- accumulates up to the next whole number.
if textGeometry.canStepLine then
textGeometry.lineRevealY = textGeometry.charHideY
textGeometry.charRevealX = currentOffset[1] + textGeometry.TEXT_DRAW_X
textGeometry.charHideY = currentOffset[2] + textGeometry.TEXT_DRAW_Y
textGeometry.canStepLine = false
end
else
-- A simple number element indicates a change in the absolute X position
-- and the Y position stays the same.
textGeometry.charRevealX = currentOffset + textGeometry.TEXT_DRAW_X
textGeometry.canStepLine = true
end
end
end
end
function love.draw()
love.graphics.setColor(0.0, 0.0, 0.0)
love.graphics.circle('fill', 80, 20, 200)
-- Draw the shader lines.
-- Change this between true/false for debugging.
if true then
love.graphics.setColor(1.0, 0.0, 0.0, 0.7)
love.graphics.line(textGeometry.charRevealX, 0, textGeometry.charRevealX, 120)
love.graphics.setColor(0.0, 0.7, 1.0, 0.7)
love.graphics.line(0, textGeometry.lineRevealY, 600, textGeometry.lineRevealY)
love.graphics.setColor(1.0, 1.0, 0.0, 0.7)
love.graphics.line(0, textGeometry.charHideY, 600, textGeometry.charHideY)
end
love.graphics.setColor(1.0, 1.0, 1.0)
love.graphics.setShader(textShader)
textShader:send('charRevealX', textGeometry.charRevealX)
textShader:send('charHideY', textGeometry.charHideY)
textShader:send('lineRevealY', textGeometry.lineRevealY)
love.graphics.draw(textObject, textGeometry.TEXT_DRAW_X, textGeometry.TEXT_DRAW_Y)
love.graphics.setShader()
end
function love.keypressed(key)
if key == 'escape' then
love.event.quit()
end
end