diff --git a/README.md b/README.md index b7e36f5..2894376 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/lua/smear_cursor/animation.lua b/lua/smear_cursor/animation.lua index a5aef30..975b8db 100644 --- a/lua/smear_cursor/animation.lua +++ b/lua/smear_cursor/animation.lua @@ -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 @@ -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 diff --git a/lua/smear_cursor/config.lua b/lua/smear_cursor/config.lua index b3e7c29..2ce786a 100644 --- a/lua/smear_cursor/config.lua +++ b/lua/smear_cursor/config.lua @@ -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 @@ -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 diff --git a/lua/smear_cursor/draw.lua b/lua/smear_cursor/draw.lua index 3a5a655..fb887e2 100644 --- a/lua/smear_cursor/draw.lua +++ b/lua/smear_cursor/draw.lua @@ -97,10 +97,7 @@ local function get_window(tab, row, col) return window_id, buffer_id end -M.draw_character = function(row, col, character, hl_group, L) - if L ~= nil and L.end_reached and row == L.row_end_rounded and col == L.col_end_rounded then - return - end +M.draw_character = function(row, col, character, hl_group) -- logging.debug("Drawing character " .. character .. " at (" .. row .. ", " .. col .. ")") local current_tab = vim.api.nvim_get_current_tabpage() local _, buffer_id = get_window(current_tab, row, col) @@ -119,10 +116,10 @@ M.clear = function() local wb = tab_windows.windows[i] if wb and vim.api.nvim_win_is_valid(wb.window_id) then + vim.api.nvim_buf_del_extmark(wb.buffer_id, cursor_namespace, extmark_id) if can_hide then vim.api.nvim_win_set_config(wb.window_id, { hide = true }) else - vim.api.nvim_buf_del_extmark(wb.buffer_id, cursor_namespace, extmark_id) vim.api.nvim_win_set_config(wb.window_id, { relative = "editor", row = 0, col = 0 }) end end @@ -132,14 +129,17 @@ M.clear = function() end end -local function draw_partial_block(row, col, character_list, character_index, hl_group, L) +local function draw_partial_block(row, col, character_list, character_index, hl_group) local character = character_list[character_index + 1] - M.draw_character(row, col, character, hl_group, L) + M.draw_character(row, col, character, hl_group) end -local function draw_matrix_character(row, col, matrix, L) - local threshold = config.diagonal_pixel_value_threshold - * math.max(matrix[1][1], matrix[1][2], matrix[2][1], matrix[2][2]) +local function draw_matrix_character(row, col, matrix) + local max = math.max(matrix[1][1], matrix[1][2], matrix[2][1], matrix[2][2]) + if max < config.matrix_pixel_threshold then + return + end + local threshold = max * config.matrix_pixel_min_factor local bit_1 = (matrix[1][1] > threshold) and 1 or 0 local bit_2 = (matrix[1][2] > threshold) and 1 or 0 local bit_3 = (matrix[2][1] > threshold) and 1 or 0 @@ -158,10 +158,10 @@ local function draw_matrix_character(row, col, matrix, L) return end - M.draw_character(row, col, character, color.get_hl_group({ level = hl_group_index }), L) + M.draw_character(row, col, character, color.get_hl_group({ level = hl_group_index })) end -local function draw_vertically_shifted_sub_block(row_top, row_bottom, col, L) +local function draw_vertically_shifted_sub_block(row_top, row_bottom, col, shade) if row_top >= row_bottom then return end @@ -180,7 +180,7 @@ local function draw_vertically_shifted_sub_block(row_top, row_bottom, col, L) end local character_thickness = character_index / 8 - local shade = thickness / character_thickness + shade = shade * thickness / character_thickness local hl_group_index = round(shade * config.color_levels) if hl_group_index == 0 then return @@ -201,7 +201,7 @@ local function draw_vertically_shifted_sub_block(row_top, row_bottom, col, L) end local character_thickness = 1 - character_index / 8 - local shade = thickness / character_thickness + shade = shade * thickness / character_thickness local hl_group_index = round(shade * config.color_levels) if hl_group_index == 0 then return @@ -211,19 +211,10 @@ local function draw_vertically_shifted_sub_block(row_top, row_bottom, col, L) hl_group = color.get_hl_group({ level = hl_group_index }) end - draw_partial_block(row, col, character_list, character_index, hl_group, L) + draw_partial_block(row, col, character_list, character_index, hl_group) end -local function draw_vertically_shifted_block(row_float, col, L) - local top = row_float + 0.5 - L.thickness / 2 - local bottom = top + L.thickness - local row = math.floor(top) - - draw_vertically_shifted_sub_block(top, math.min(bottom, row + 1), col, L) - draw_vertically_shifted_sub_block(row + 1, bottom, col, L) -end - -local function draw_horizontally_shifted_sub_block(row, col_left, col_right, L) +local function draw_horizontally_shifted_sub_block(row, col_left, col_right, shade) if col_left >= col_right then return end @@ -242,7 +233,7 @@ local function draw_horizontally_shifted_sub_block(row, col_left, col_right, L) end local character_thickness = character_index / 8 - local shade = thickness / character_thickness + shade = shade * thickness / character_thickness local hl_group_index = round(shade * config.color_levels) if hl_group_index == 0 then return @@ -258,7 +249,7 @@ local function draw_horizontally_shifted_sub_block(row, col_left, col_right, L) end local character_thickness = 1 - character_index / 8 - local shade = thickness / character_thickness + shade = shade * thickness / character_thickness local hl_group_index = round(shade * config.color_levels) if hl_group_index == 0 then return @@ -273,229 +264,230 @@ local function draw_horizontally_shifted_sub_block(row, col_left, col_right, L) end end - draw_partial_block(row, col, character_list, character_index, hl_group, L) -end - -local function draw_horizontally_shifted_block(row, col_float, L) - local left = col_float + 0.5 - L.thickness / 2 - local right = left + L.thickness - local col = math.floor(left) - - draw_horizontally_shifted_sub_block(row, left, math.min(right, col + 1), L) - draw_horizontally_shifted_sub_block(row, col + 1, right, L) + draw_partial_block(row, col, character_list, character_index, hl_group) end -local function fill_matrix_vertical_sub_block(matrix, row_top, row_bottom, col) - if row_top >= row_bottom then - return - end - local row = math.floor(row_top) - if row < 1 or row > #matrix then - return - end - local shade = row_bottom - row_top - matrix[row][col] = math.max(matrix[row][col], shade) -end +local function precompute_quad_geometry(corners) + local G = {} -local function fill_matrix_vertically(matrix, row_float, col, thickness) - local top = row_float + 1 - thickness * config.diagonal_thickness_factor - local bottom = top + 2 * thickness * config.diagonal_thickness_factor - local row = math.floor(top) - -- logging.debug("top: " .. top .. ", bottom: " .. bottom) + -- Bounding box + G.top = math.floor(math.min(corners[1][1], corners[2][1], corners[3][1], corners[4][1])) + G.bottom = math.ceil(math.max(corners[1][1], corners[2][1], corners[3][1], corners[4][1])) - 1 + G.left = math.floor(math.min(corners[1][2], corners[2][2], corners[3][2], corners[4][2])) + G.right = math.ceil(math.max(corners[1][2], corners[2][2], corners[3][2], corners[4][2])) - 1 - fill_matrix_vertical_sub_block(matrix, top, math.min(bottom, row + 1), col) - fill_matrix_vertical_sub_block(matrix, row + 1, math.min(bottom, row + 2), col) - fill_matrix_vertical_sub_block(matrix, row + 2, bottom, col) -end + -- Slopes + G.slopes = {} -local function draw_diagonal_horizontal_block(row_float, col, L) - local row = math.floor(row_float) - local shift = row_float - row - -- Matrix of lit quarters - local m = { - { 0, 0 }, -- Top of row above - { 0, 0 }, -- Bottom of row above - { 0, 0 }, -- Top of current row - { 0, 0 }, -- Bottom of current row - { 0, 0 }, -- Top of row below - { 0, 0 }, -- Bottom of row below - } - - -- Lit from the left - if col > L.left then - local shift_left = shift - 0.5 * L.slope - fill_matrix_vertically(m, 3 + 2 * shift_left, 1, L.thickness) + for i = 1, 4 do + local edge = { + corners[i % 4 + 1][1] - corners[i][1], + corners[i % 4 + 1][2] - corners[i][2], + } + G.slopes[i] = edge[1] / edge[2] end - -- Lit from center - fill_matrix_vertically(m, 3 + 2 * shift, 1, L.thickness) - fill_matrix_vertically(m, 3 + 2 * shift, 2, L.thickness) - - -- Lit from the right - if col < L.right then - local shift_right = shift + 0.5 * L.slope - fill_matrix_vertically(m, 3 + 2 * shift_right, 2, L.thickness) - end - - for i = -1, 1 do - local row_i = row + i - draw_matrix_character(row_i, col, { m[2 * i + 3], m[2 * i + 4] }, L) + G.top_horizontal = math.abs(G.slopes[1]) <= config.max_slope_horizontal + G.bottom_horizontal = math.abs(G.slopes[3]) <= config.max_slope_horizontal + G.left_vertical = math.abs(G.slopes[4]) >= config.min_slope_vertical + G.right_vertical = math.abs(G.slopes[2]) >= config.min_slope_vertical + + -- Intersections + -- Intersection of quad edge with centerline of cells + G.top_centerlines = {} + -- Lowest intersection of quad edge with lateral edges of cells + G.top_edges = {} + -- Intersection of quad edge with lines at 0.25 and 0.75 + G.top_fractions = {} + G.bottom_centerlines = {} + G.bottom_edges = {} + G.bottom_fractions = {} + G.left_centerlines = {} + G.left_edges = {} + G.left_fractions = {} + G.right_centerlines = {} + G.right_edges = {} + G.right_fractions = {} + + for col = G.left, G.right do + G.top_centerlines[col] = corners[1][1] + (col + 0.5 - corners[1][2]) * G.slopes[1] + G.top_edges[col] = G.top_centerlines[col] + 0.5 * math.abs(G.slopes[1]) + G.top_fractions[col] = {} + G.bottom_centerlines[col] = corners[3][1] + (col + 0.5 - corners[3][2]) * G.slopes[3] + G.bottom_edges[col] = G.bottom_centerlines[col] - 0.5 * math.abs(G.slopes[3]) + G.bottom_fractions[col] = {} + + for i = 1, 2 do + local shift = (i == 1) and -0.25 or 0.25 + G.top_fractions[col][i] = G.top_centerlines[col] + shift * G.slopes[1] + G.bottom_fractions[col][i] = G.bottom_centerlines[col] + shift * G.slopes[3] + end end -end -local function fill_matrix_horizontal_sub_block(matrix, row, col_left, col_right) - if col_left >= col_right then - return - end - local col = math.floor(col_left) - if col < 1 or col > #matrix[1] then - return + for row = G.top, G.bottom do + G.right_centerlines[row] = corners[2][2] + (row + 0.5 - corners[2][1]) / G.slopes[2] + G.right_edges[row] = G.right_centerlines[row] - 0.5 / math.abs(G.slopes[2]) + G.right_fractions[row] = {} + G.left_centerlines[row] = corners[4][2] + (row + 0.5 - corners[4][1]) / G.slopes[4] + G.left_edges[row] = G.left_centerlines[row] + 0.5 / math.abs(G.slopes[4]) + G.left_fractions[row] = {} + + for i = 1, 2 do + local shift = (i == 1) and -0.25 or 0.25 + G.right_fractions[row][i] = G.right_centerlines[row] + shift / G.slopes[2] + G.left_fractions[row][i] = G.left_centerlines[row] + shift / G.slopes[4] + end end - local shade = col_right - col_left - matrix[row][col] = math.max(matrix[row][col], shade) -end - -local function fill_matrix_horizontally(matrix, row, col_float, thickness) - local left = col_float + 1 - thickness * config.diagonal_thickness_factor - local right = left + 2 * thickness * config.diagonal_thickness_factor - local col = math.floor(left) - -- logging.debug("left: " .. left .. ", right: " .. right) - fill_matrix_horizontal_sub_block(matrix, row, left, math.min(right, col + 1)) - fill_matrix_horizontal_sub_block(matrix, row, col + 1, math.min(right, col + 2)) - fill_matrix_horizontal_sub_block(matrix, row, col + 2, right) + return G end -local function draw_diagonal_vertical_block(row, col_float, L) - local col = math.floor(col_float) - local shift = col_float - col - -- Matrix of lit quarters - local m = { - { 0, 0, 0, 0, 0, 0 }, -- Top - { 0, 0, 0, 0, 0, 0 }, -- Bottom - } -- c-1 c c+1 - - -- Lit from the top - if row > L.top then - local shift_top = shift - 0.5 / L.slope - fill_matrix_horizontally(m, 1, 3 + 2 * shift_top, L.thickness) +M.draw_quad = function(corners, target_position) + if target_position == nil then + target_position = { 0, 0 } end - -- Lit from center - local half_row = round(shift * 2) - fill_matrix_horizontally(m, 1, 3 + half_row, L.thickness) - fill_matrix_horizontally(m, 2, 3 + half_row, L.thickness) + local G = precompute_quad_geometry(corners) - -- Lit from the bottom - if row < L.bottom then - local shift_bottom = shift + 0.5 / L.slope - fill_matrix_horizontally(m, 2, 3 + 2 * shift_bottom, L.thickness) - end + for row = G.top, G.bottom do + local left = corners[4][2] + (row + 0.5 - corners[4][1]) / G.slopes[4] + left = left - 0.5 / math.abs(G.slopes[4]) + local right = corners[2][2] + (row + 0.5 - corners[2][1]) / G.slopes[2] + right = right + 0.5 / math.abs(G.slopes[2]) - for i = -1, 1 do - local col_i = col + i - draw_matrix_character(row, col_i, { - { m[1][2 * i + 3], m[1][2 * i + 4] }, - { m[2][2 * i + 3], m[2][2 * i + 4] }, - }, L) - end -end + for col = math.max(G.left, math.floor(left)), math.min(G.right, math.ceil(right)) do + -- Check if on target + if row == target_position[1] and col == target_position[2] then + goto continue + end -local function draw_horizontal_ish_line(L, draw_block_function) - for col = L.col_start_rounded, L.col_end_rounded, L.col_direction do - local row_float = L.row_start + L.row_shift * (col - L.col_start) / L.col_shift - draw_block_function(row_float, col, L) - end -end + local is_vertically_shifted = false + local vertical_shade = 1 + local is_horizontally_shifted = false + local horizontal_shade = 1 + + -- Check if vertically shifted block + local left_in = G.left_edges[row] > col + local right_in = G.right_edges[row] < col + 1 + if not (left_in and not G.left_vertical) and not (right_in and not G.right_vertical) then + local top_near = G.top_centerlines[col] > row + local bottom_near = G.bottom_centerlines[col] < row + 1 + if + (top_near and G.top_horizontal and (not bottom_near or G.bottom_horizontal)) + or (bottom_near and G.bottom_horizontal and (not top_near or G.top_horizontal)) + then + is_vertically_shifted = true + vertical_shade = math.min(row + 1, G.bottom_centerlines[col]) + - math.max(row, G.top_centerlines[col]) + end + end -local function draw_vertical_ish_line(L, draw_block_function) - for row = L.row_start_rounded, L.row_end_rounded, L.row_direction do - local col_float = L.col_start + L.col_shift * (row - L.row_start) / L.row_shift - draw_block_function(row, col_float, L) - end -end + -- Check if horizontally shifted block + local top_in = G.top_edges[col] > row + local bottom_in = G.bottom_edges[col] < row + 1 + if not (top_in and not G.top_horizontal) and not (bottom_in and not G.bottom_horizontal) then + local left_near = G.left_centerlines[row] > col + local right_near = G.right_centerlines[row] < col + 1 + if + (left_near and G.left_vertical and (not right_near or G.right_vertical)) + or (right_near and G.right_vertical and (not left_near or G.left_vertical)) + then + is_horizontally_shifted = true + horizontal_shade = math.min(col + 1, G.right_centerlines[row]) + - math.max(col, G.left_centerlines[row]) + end + end -local function draw_ending(L) - -- Apply correction to avoid jump before stop animating - local correction = config.distance_stop_animating + (1 - config.distance_stop_animating) * L.shift - local row_shift = L.row_shift * correction - local col_shift = L.col_shift * correction + -- Draw shifted block + if is_vertically_shifted and is_horizontally_shifted then + if vertical_shade < config.max_shade_no_matrix and horizontal_shade < config.max_shade_no_matrix then + is_horizontally_shifted = false + is_vertically_shifted = false + elseif vertical_shade < horizontal_shade then + is_horizontally_shifted = false + else + is_vertically_shifted = false + end + end - -- Apply factors to reduce size of diagonal partial blocks - row_shift = row_shift * (1 - math.abs(col_shift)) - col_shift = col_shift * (1 - math.abs(row_shift)) + if is_vertically_shifted and horizontal_shade > 0 then + draw_vertically_shifted_sub_block( + math.max(row, G.top_centerlines[col]), + math.min(row + 1, G.bottom_centerlines[col]), + col, + horizontal_shade + ) + goto continue + end - draw_vertically_shifted_block(L.row_end_rounded - row_shift, L.col_end_rounded, L) - draw_horizontally_shifted_block(L.row_end_rounded, L.col_end_rounded - col_shift, L) -end + if is_horizontally_shifted and vertical_shade > 0 then + draw_horizontally_shifted_sub_block( + row, + math.max(col, G.left_centerlines[row]), + math.min(col + 1, G.right_centerlines[row]), + vertical_shade + ) + goto continue + end -M.draw_line = function(row_start, col_start, row_end, col_end, end_reached) - -- logging.debug("Drawing line from (" .. row_start .. ", " .. col_start .. ") to (" .. row_end .. ", " .. col_end .. ")") - - local L = { - row_start = row_start, - col_start = col_start, - row_end = row_end, - col_end = col_end, - row_start_rounded = round(row_start), - col_start_rounded = round(col_start), - row_end_rounded = round(row_end), - col_end_rounded = round(col_end), - row_shift = row_end - row_start, - col_shift = col_end - col_start, - end_reached = end_reached, - } - - L.top = math.min(L.row_start_rounded, L.row_end_rounded) - L.bottom = math.max(L.row_start_rounded, L.row_end_rounded) - L.left = math.min(L.col_start_rounded, L.col_end_rounded) - L.right = math.max(L.col_start_rounded, L.col_end_rounded) - L.row_direction = L.row_shift >= 0 and 1 or -1 - L.col_direction = L.col_shift >= 0 and 1 or -1 - L.slope = L.row_shift / L.col_shift - L.slope_abs = math.abs(L.slope) - L.shift = math.sqrt(L.row_shift ^ 2 + L.col_shift ^ 2) - L.thickness = math.min(1 / L.shift, 1) ^ config.thickness_reduction_exponent - L.thickness = math.max(L.thickness, config.minimum_thickness) - - if L.slope ~= L.slope then - M.draw_character(L.row_end_rounded, L.col_end_rounded, "█", color.get_hl_group(), L) - return - end + -- Draw matrix + local row_float, col_float, matrix_index, shade + local matrix = { + { 1, 1 }, + { 1, 1 }, + } + + for i = 1, 2 do + -- Intersection with top quad edge + row_float = 2 * (G.top_fractions[col][i] - row) + matrix_index = math.floor(row_float) + 1 + for index = 1, math.min(2, matrix_index - 1) do + matrix[index][i] = 0 + end + if matrix_index == 1 or matrix_index == 2 then + shade = 1 - (row_float % 1) + matrix[matrix_index][i] = matrix[matrix_index][i] * shade + end - if L.end_reached and L.shift < 1 then - draw_ending(L) - return - end + -- Intersection with right quad edge + col_float = 2 * (G.right_fractions[row][i] - col) + matrix_index = math.floor(col_float) + 1 + for index = math.max(1, matrix_index + 1), 2 do + matrix[i][index] = 0 + end + if matrix_index == 1 or matrix_index == 2 then + shade = col_float % 1 + matrix[i][matrix_index] = matrix[i][matrix_index] * shade + end - if L.slope_abs <= config.max_slope_horizontal then - -- logging.debug("Drawing horizontal-ish line") - -- if math.abs(L.row_shift) > 1 then - -- -- Avoid bulging on thin lines - -- L.thickness = math.max(L.thickness, 1) - -- end - draw_horizontal_ish_line(L, draw_vertically_shifted_block) - return - end + -- Intersection with bottom quad edge + row_float = 2 * (G.bottom_fractions[col][i] - row) + matrix_index = math.floor(row_float) + 1 + for index = math.max(1, matrix_index + 1), 2 do + matrix[index][i] = 0 + end + if matrix_index == 1 or matrix_index == 2 then + shade = row_float % 1 + matrix[matrix_index][i] = matrix[matrix_index][i] * shade + end - if L.slope_abs >= config.min_slope_vertical then - -- logging.debug("Drawing vertical-ish line") - -- if math.abs(L.col_shift) > 1 then - -- -- Avoid bulging on thin lines - -- L.thickness = math.max(L.thickness, 1) - -- end - draw_vertical_ish_line(L, draw_horizontally_shifted_block) - return - end + -- Intersection with left quad edge + col_float = 2 * (G.left_fractions[row][i] - col) + matrix_index = math.floor(col_float) + 1 + for index = 1, math.min(2, matrix_index - 1) do + matrix[i][index] = 0 + end + if matrix_index == 1 or matrix_index == 2 then + shade = 1 - (col_float % 1) + matrix[i][matrix_index] = matrix[i][matrix_index] * shade + end + end - if L.slope_abs <= 1 then - -- logging.debug("Drawing diagonal-horizontal line") - draw_horizontal_ish_line(L, draw_diagonal_horizontal_block) - return - end + draw_matrix_character(row, col, matrix) - -- logging.debug("Drawing diagonal-vertical line") - draw_vertical_ish_line(L, draw_diagonal_vertical_block) + ::continue:: + end + end end return M diff --git a/lua/smear_cursor/events.lua b/lua/smear_cursor/events.lua index b06dde0..b92ef2a 100644 --- a/lua/smear_cursor/events.lua +++ b/lua/smear_cursor/events.lua @@ -11,7 +11,7 @@ local function move_cursor() or ( not config.smear_between_neighbor_lines and not switching_buffer - and math.abs(row - animation.current_position[1]) <= 1 + and math.abs(row - animation.target_position[1]) <= 1 ) animation.change_target_position(row, col, jump) diff --git a/tests/test_draw_quad.lua b/tests/test_draw_quad.lua new file mode 100644 index 0000000..df8edbd --- /dev/null +++ b/tests/test_draw_quad.lua @@ -0,0 +1,188 @@ +-- Instructions: open this file in Neovim and run `source %` +-- Warning: this will open a lot of floating windows + +local draw = require("smear_cursor.draw") + +draw.clear() + +local row = 2 +local col = 2 + +draw.draw_quad({ + { row, col }, + { row, col + 2 }, + { row + 2, col + 2 }, + { row + 2, col }, +}) + +-- Quads slope 1/8 + +col = 6 + +draw.draw_quad({ + { row, col }, + { row + 1, col + 9 }, + { row + 10, col + 8 }, + { row + 9, col - 1 }, +}) + +row = 3 +col = 16 + +draw.draw_quad({ + { row, col }, + { row - 1, col + 9 }, + { row + 8, col + 10 }, + { row + 9, col + 1 }, +}) + +-- Quads slope 1/4 + +row = 2 +col = 29 + +draw.draw_quad({ + { row, col }, + { row + 2, col + 9 }, + { row + 11, col + 7 }, + { row + 9, col - 2 }, +}) + +row = 4 +col = 39 + +draw.draw_quad({ + { row, col }, + { row - 2, col + 9 }, + { row + 7, col + 11 }, + { row + 9, col + 2 }, +}) + +-- Quads slope 1/2 + +row = 2 +col = 55 + +draw.draw_quad({ + { row, col }, + { row + 4, col + 9 }, + { row + 13, col + 5 }, + { row + 9, col - 4 }, +}) + +row = 6 +col = 65 + +draw.draw_quad({ + { row, col }, + { row - 4, col + 9 }, + { row + 5, col + 13 }, + { row + 9, col + 4 }, +}) + +-- Quads slope 1 + +row = 13 +col = 6 + +draw.draw_quad({ + { row, col }, + { row + 4, col + 5 }, + { row + 9, col + 1 }, + { row + 5, col - 4 }, +}) + +row = 17 +col = 12 + +draw.draw_quad({ + { row, col }, + { row - 4, col + 5 }, + { row + 1, col + 9 }, + { row + 5, col + 4 }, +}) + +-- Degenerate quads (aligned points) + +row = 14 +col = 23 + +draw.draw_quad({ + { row, col }, + { row - 1, col + 2 }, + { row + 3, col + 2 }, + { row + 3, col - 1 }, +}) + +-- Lines + +row = 23 +col = 2 + +for i = 0, 8 do + draw.draw_quad({ + { row, col }, + { row + i, col + 9 }, + { row + i + 1, col + 9 }, + { row + 1, col }, + }) + + col = col + 10 +end + +-- Rhombuses + +row = 26 +col = 2 + +for i = 0, 4 do + draw.draw_quad({ + { row, col }, + { row + i / 2, col + 5 }, + { row + i + 1, col + 9 }, + { row + i / 2 + 1, col + 4 }, + }) + + col = col + 10 +end + +-- Thin horizontal lines + +row = 29 +col = 2 + +for i = 0, 2 do + draw.draw_quad({ + { row, col }, + { row + i, col + 9 }, + { row + i + 1 / 8, col + 9 }, + { row + 1 / 8, col }, + }) + + col = col + 10 +end + +-- Vertical lines + +row = 32 +col = 2 + +for i = 8, 0, -1 do + draw.draw_quad({ + { row, col }, + { row, col + 1 }, + { row + 9, col + i + 1 }, + { row + 9, col + i }, + }) + + col = col + 4 + + draw.draw_quad({ + { row, col }, + { row, col + 1 / 8 }, + { row + 9, col + i + 1 / 8 }, + { row + 9, col + i }, + }) + + col = col + 5 +end