Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: render smear as quad #36

Merged
merged 20 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,25 @@ As an example of further configuration, you can tune the smear dynamics to be sn
```lua
opts = { -- Default Range
stiffness = 0.8, -- 0.6 [0, 1]
trailing_stiffness = 0.6, -- 0.3 [0, 1]
trailing_exponent = 0, -- 0.1 >= 0
trailing_stiffness = 0.5, -- 0.25 [0, 1]
distance_stop_animating = 0.5, -- 0.1 > 0
hide_target_hack = false, -- true boolean
},
```


> [!WARNING] Fire Hazard
> Feel free to experiment with all the configuration options, but be aware that some combinations may cause your cursor to flicker or even **catch fire**. That can happen with the following settings:
> ```lua
> opts = {
> cursor_color = "#ff8800",
> stiffness = 0.6,
> trailing_stiffness = 0.1,
> trailing_exponent = 5,
> gamma = 1,
> }
> ```

### Transparent background

Drawing the smear over a transparent background works better when using a font that supports legacy computing symbols, therefore setting the following option:
Expand Down
164 changes: 121 additions & 43 deletions lua/smear_cursor/animation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,133 @@ local round = require("smear_cursor.math").round
local screen = require("smear_cursor.screen")
local M = {}

local target_position = { 0, 0 }
local current_position = { 0, 0 }
local trailing_position = { 0, 0 }
local animating = false
local target_position = { 0, 0 }
local current_corners = {}
local target_corners = {}
local stiffnesses = { 0, 0, 0, 0 }

local function set_corners(corners, row, col)
corners[1] = { row, col }
corners[2] = { row, col + 1 }
corners[3] = { row + 1, col + 1 }
corners[4] = { row + 1, col }
end

vim.defer_fn(function()
local cursor_row, cursor_col = screen.get_screen_cursor_position()
target_position = { cursor_row, cursor_col }
current_position = { cursor_row, cursor_col }
trailing_position = { cursor_row, cursor_col }
set_corners(current_corners, cursor_row, cursor_col)
set_corners(target_corners, cursor_row, cursor_col)
end, 0)

local function update()
current_position[1] = current_position[1] + (target_position[1] - current_position[1]) * config.stiffness
current_position[2] = current_position[2] + (target_position[2] - current_position[2]) * config.stiffness

local trailing_distance_squared = (current_position[1] - trailing_position[1]) ^ 2
+ (current_position[2] - trailing_position[2]) ^ 2
local trailing_stiffness =
math.min(1, config.trailing_stiffness * trailing_distance_squared ^ config.trailing_exponent)
trailing_position[1] = trailing_position[1] + (current_position[1] - trailing_position[1]) * trailing_stiffness
trailing_position[2] = trailing_position[2] + (current_position[2] - trailing_position[2]) * trailing_stiffness
for i = 1, 4 do
local distance_squared = (current_corners[i][1] - target_corners[i][1]) ^ 2
+ (current_corners[i][2] - target_corners[i][2]) ^ 2
local stiffness = math.min(1, stiffnesses[i] * distance_squared ^ config.slowdown_exponent)
for j = 1, 2 do
current_corners[i][j] = current_corners[i][j] + (target_corners[i][j] - current_corners[i][j]) * stiffness
end
end
end

local function animate()
if not animating then
return
local function shrink_volume(corners)
local edges = {}
for i = 1, 3 do
edges[i] = {
corners[i + 1][1] - corners[1][1],
corners[i + 1][2] - corners[1][2],
}
end

local double_volumes = {}
for i = 1, 2 do
double_volumes[i] = edges[1][2] * edges[2][1] - edges[1][1] * edges[2][2]
end
local volume = (double_volumes[1] + double_volumes[2]) / 2

local center = {
(corners[1][1] + corners[2][1] + corners[3][1] + corners[4][1]) / 4,
(corners[1][2] + corners[2][2] + corners[3][2] + corners[4][2]) / 4,
}

local factor = (1 / volume) ^ (config.volume_reduction_exponent / 2)
factor = math.max(config.minimum_volume_factor, factor)
local shrunk_corners = {}
for i = 1, 4 do
shrunk_corners[i] = {
center[1] + (corners[i][1] - center[1]) * factor,
center[2] + (corners[i][2] - center[2]) * factor,
}
end

return shrunk_corners
end

local function animate()
animating = true
update()

if not config.dont_erase then
draw.clear()
local max_distance = 0
for i = 1, 4 do
local distance = math.sqrt(
(current_corners[i][1] - target_corners[i][1]) ^ 2 + (current_corners[i][2] - target_corners[i][2]) ^ 2
)
max_distance = math.max(max_distance, distance)
end
local end_reached = round(current_position[1]) == target_position[1]
and round(current_position[2]) == target_position[2]
draw.draw_line(trailing_position[1], trailing_position[2], current_position[1], current_position[2], end_reached)
if not end_reached and config.hide_target_hack then

draw.clear()

if max_distance <= config.distance_stop_animating then
animating = false
set_corners(current_corners, target_position[1], target_position[2])
return
end

local shrunk_corners = shrink_volume(current_corners)
-- stylua: ignore start
local target_reached = (
math.floor(shrunk_corners[1][1]) == target_position[1] and
math.floor(shrunk_corners[1][2]) == target_position[2]
) or (
math.floor(shrunk_corners[2][1]) == target_position[1] and
math.ceil(shrunk_corners[2][2]) - 1 == target_position[2]
) or (
math.ceil(shrunk_corners[3][1]) - 1 == target_position[1] and
math.ceil(shrunk_corners[3][2]) - 1 == target_position[2]
) or (
math.ceil(shrunk_corners[4][1]) - 1 == target_position[1] and
math.floor(shrunk_corners[4][2]) == target_position[2]
)
-- stylua: ignore end

if not target_reached and config.hide_target_hack then
draw.draw_character(target_position[1], target_position[2], " ", color.get_hl_group({ inverted = true }))
end

local trailing_distance =
math.sqrt((target_position[1] - trailing_position[1]) ^ 2 + (target_position[2] - trailing_position[2]) ^ 2)
if trailing_distance > config.distance_stop_animating then
animating = true
vim.defer_fn(animate, config.time_interval)
else
animating = false
if not config.dont_erase then
draw.clear()
end
current_position = { target_position[1], target_position[2] }
trailing_position = { target_position[1], target_position[2] }
draw.draw_quad(shrunk_corners, target_position)
vim.defer_fn(animate, config.time_interval)
end

local function set_stiffnesses(head_stiffness, trailing_stiffness)
local target_center = { target_position[1] + 0.5, target_position[2] + 0.5 }
local distances = {}
local min_distance = math.huge
local max_distance = 0

for i = 1, 4 do
local distance =
math.sqrt((current_corners[i][1] - target_center[1]) ^ 2 + (current_corners[i][2] - target_center[2]) ^ 2)
min_distance = math.min(min_distance, distance)
max_distance = math.max(max_distance, distance)
distances[i] = distance
end

for i = 1, 4 do
local x = (distances[i] - min_distance) / (max_distance - min_distance)
local stiffness = head_stiffness + (trailing_stiffness - head_stiffness) * x ^ config.trailing_exponent
stiffnesses[i] = math.min(1, stiffness)
end
end

Expand All @@ -66,29 +141,32 @@ M.change_target_position = function(row, col, jump)
end
draw.clear()

-- Draw end of previous smear
if animating then
draw.draw_line(trailing_position[1], trailing_position[2], target_position[1], target_position[2])
current_position = { target_position[1], target_position[2] }
trailing_position = { target_position[1], target_position[2] }
set_stiffnesses(1, 0)
update()
draw.draw_quad(shrink_volume(current_corners), target_position)
set_corners(current_corners, target_position[1], target_position[2])
end

target_position = { row, col }
set_corners(target_corners, row, col)
set_stiffnesses(config.stiffness, config.trailing_stiffness)

if jump then
current_position = { row, col }
trailing_position = { row, col }
set_corners(current_corners, row, col)
return
end

if not animating then
animating = true
animate()
end
end

setmetatable(M, {
__index = function(_, key)
if key == "current_position" then
return current_position
if key == "target_position" then
return target_position
else
return nil
end
Expand Down
23 changes: 14 additions & 9 deletions lua/smear_cursor/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@ M.time_interval = 17 -- milliseconds
-- 0: no movement, 1: instantaneous
M.stiffness = 0.6

-- How fast the smear's tail moves towards the head.
-- How fast the smear's tail moves towards the target.
-- 0: no movement, 1: instantaneous
M.trailing_stiffness = 0.3
M.trailing_stiffness = 0.25

-- How much the tail slows down when getting close to the head.
-- Controls if middle points are closer to the head or the tail.
-- < 1: closer to the tail, > 1: closer to the head
M.trailing_exponent = 2

-- How much the smear slows down when getting close to the target.
-- 0: no slowdown, more: more slowdown
M.trailing_exponent = 0.1
M.slowdown_exponent = 0

-- Stop animating when the smear's tail is within this distance (in characters) from the target.
M.distance_stop_animating = 0.1
Expand All @@ -40,14 +44,15 @@ M.min_slope_vertical = 2

M.color_levels = 16 -- Minimum 1
M.gamma = 2.2 -- For color blending
M.diagonal_pixel_value_threshold = 0.5 -- 0.1: more pixels, 0.9: less pixels
M.diagonal_thickness_factor = 0.7 -- Put less than 1 to reduce diagonal smear fatness
M.thickness_reduction_exponent = 0.2 -- 0: no reduction, 1: full reduction
M.minimum_thickness = 0.7 -- 0: no limit, 1: no reduction
M.max_shade_no_matrix = 0.75 -- 0: more overhangs, 1: more matrices
M.matrix_pixel_threshold = 0.5 -- 0: all pixels, 1: no pixel
M.matrix_pixel_min_factor = 0.5 -- 0: all pixels, 1: no pixel
M.volume_reduction_exponent = 0.3 -- 0: no reduction, 1: full reduction
M.minimum_volume_factor = 0.5 -- 0: no limit, 1: no reduction

-- For debugging ---------------------------------------------------------------

M.logging_level = vim.log.levels.INFO
M.dont_erase = false -- Set to true for debugging, or use trailing_stiffness = 0
-- Set trailing_stiffness to 0 for debugging

return M
Loading
Loading