mirror of
https://github.com/Insality/druid
synced 2025-06-27 10:27:48 +02:00
422 lines
10 KiB
Lua
422 lines
10 KiB
Lua
--- Component to handle scroll content
|
|
-- @module base.scroll
|
|
|
|
local helper = require("druid.helper")
|
|
local const = require("druid.const")
|
|
local settings = require("druid.settings").scroll
|
|
|
|
local M = {}
|
|
|
|
M.interest = {
|
|
const.ON_UPDATE,
|
|
const.ON_SWIPE,
|
|
}
|
|
|
|
-- Global on all scrolls
|
|
-- TODO: remove it
|
|
M.current_scroll = nil
|
|
|
|
|
|
function M.init(self, scroll_parent, input_zone, border)
|
|
self.node = helper.node(scroll_parent)
|
|
self.input_zone = helper.node(input_zone)
|
|
self.zone_size = gui.get_size(self.input_zone)
|
|
self.soft_size = settings.SOFT_ZONE_SIZE
|
|
|
|
-- Distance from node to node's center
|
|
local offset = helper.get_pivot_offset(gui.get_pivot(self.input_zone))
|
|
self.center_offset = vmath.vector3(self.zone_size)
|
|
self.center_offset.x = self.center_offset.x * offset.x
|
|
self.center_offset.y = self.center_offset.y * offset.y
|
|
|
|
self.is_inert = true
|
|
self.inert = vmath.vector3(0)
|
|
self.start_pos = gui.get_position(self.node)
|
|
self.pos = self.start_pos
|
|
self.target = vmath.vector3(self.pos)
|
|
|
|
self.input = {
|
|
touch = false,
|
|
start_x = 0,
|
|
start_y = 0,
|
|
side = false,
|
|
}
|
|
|
|
self:set_border(border)
|
|
end
|
|
|
|
|
|
local function set_pos(self, pos)
|
|
self.pos.x = pos.x
|
|
self.pos.y = pos.y
|
|
|
|
gui.set_position(self.node, self.pos)
|
|
end
|
|
|
|
|
|
--- Return scroll, if it outside of scroll area
|
|
-- Using the lerp with BACK_SPEED koef
|
|
local function check_soft_target(self)
|
|
local t = self.target
|
|
local b = self.border
|
|
|
|
if t.y < b.y then
|
|
t.y = helper.step(t.y, b.y, math.abs(t.y - b.y) * settings.BACK_SPEED)
|
|
end
|
|
if t.x < b.x then
|
|
t.x = helper.step(t.x, b.x, math.abs(t.x - b.x) * settings.BACK_SPEED)
|
|
end
|
|
if t.y > b.w then
|
|
t.y = helper.step(t.y, b.w, math.abs(t.y - b.w) * settings.BACK_SPEED)
|
|
end
|
|
if t.x > b.z then
|
|
t.x = helper.step(t.x, b.z, math.abs(t.x - b.z) * settings.BACK_SPEED)
|
|
end
|
|
end
|
|
|
|
|
|
--- Free inert update function
|
|
local function update_hand_scroll(self, dt)
|
|
local inert = self.inert
|
|
local delta_x = self.target.x - self.pos.x
|
|
local delta_y = self.target.y - self.pos.y
|
|
|
|
if helper.sign(delta_x) ~= helper.sign(inert.x) then
|
|
inert.x = 0
|
|
end
|
|
if helper.sign(delta_y) ~= helper.sign(inert.y) then
|
|
inert.y = 0
|
|
end
|
|
|
|
inert.x = inert.x + delta_x
|
|
inert.y = inert.y + delta_y
|
|
|
|
inert.x = math.abs(inert.x) * helper.sign(delta_x)
|
|
inert.y = math.abs(inert.y) * helper.sign(delta_y)
|
|
|
|
inert.x = inert.x * settings.FRICT_HOLD
|
|
inert.y = inert.y * settings.FRICT_HOLD
|
|
|
|
set_pos(self, self.target)
|
|
end
|
|
|
|
|
|
local function get_zone_center(self)
|
|
return self.pos + self.center_offset
|
|
end
|
|
|
|
|
|
--- Find closer point of interest
|
|
-- if no inert, scroll to next point by scroll direction
|
|
-- if inert, find next point by scroll director
|
|
local function check_points(self)
|
|
if not self.points then
|
|
return
|
|
end
|
|
|
|
local inert = self.inert
|
|
if not self.is_inert then
|
|
if math.abs(inert.x) > settings.DEADZONE then
|
|
self:scroll_to_index(self.selected - helper.sign(inert.x))
|
|
return
|
|
end
|
|
if math.abs(inert.y) > settings.DEADZONE then
|
|
self:scroll_to_index(self.selected + helper.sign(inert.y))
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Find closest point and point by scroll direction
|
|
-- Scroll to one of them (by scroll direction in priority)
|
|
local temp_dist = math.huge
|
|
local temp_dist_on_inert = math.huge
|
|
local index = false
|
|
local index_on_inert = false
|
|
local pos = get_zone_center(self)
|
|
for i = 1, #self.points do
|
|
local p = self.points[i]
|
|
local dist = helper.distance(pos.x, pos.y, p.x, p.y)
|
|
local on_inert = true
|
|
-- If inert ~= 0, scroll only by move direction
|
|
if inert.x ~= 0 and helper.sign(inert.x) ~= helper.sign(p.x - pos.x) then
|
|
on_inert = false
|
|
end
|
|
if inert.y ~= 0 and helper.sign(inert.y) ~= helper.sign(p.y - pos.y) then
|
|
on_inert = false
|
|
end
|
|
|
|
if dist < temp_dist then
|
|
index = i
|
|
temp_dist = dist
|
|
end
|
|
if on_inert and dist < temp_dist_on_inert then
|
|
index_on_inert = i
|
|
temp_dist_on_inert = dist
|
|
end
|
|
end
|
|
self:scroll_to_index(index_on_inert or index)
|
|
end
|
|
|
|
|
|
local function check_threshold(self)
|
|
local inert = self.inert
|
|
if not self.is_inert or vmath.length(inert) < settings.INERT_THRESHOLD then
|
|
check_points(self)
|
|
inert.x = 0
|
|
inert.y = 0
|
|
end
|
|
end
|
|
|
|
|
|
local function update_free_inert(self, dt)
|
|
local inert = self.inert
|
|
if inert.x ~= 0 or inert.y ~= 0 then
|
|
self.target.x = self.pos.x + (inert.x * dt * settings.INERT_SPEED)
|
|
self.target.y = self.pos.y + (inert.y * dt * settings.INERT_SPEED)
|
|
|
|
inert.x = inert.x * settings.FRICT
|
|
inert.y = inert.y * settings.FRICT
|
|
|
|
-- Stop, when low inert speed and go to points
|
|
check_threshold(self)
|
|
end
|
|
|
|
check_soft_target(self)
|
|
set_pos(self, self.target)
|
|
end
|
|
|
|
|
|
--- Cancel animation on other animation or input touch
|
|
local function cancel_animate(self)
|
|
if self.animate then
|
|
self.target = gui.get_position(self.node)
|
|
self.pos.x = self.target.x
|
|
self.pos.y = self.target.y
|
|
gui.cancel_animation(self.node, gui.PROP_POSITION)
|
|
self.animate = false
|
|
end
|
|
end
|
|
|
|
|
|
function M.update(self, dt)
|
|
if self.input.touch then
|
|
if M.current_scroll == self then
|
|
update_hand_scroll(self, dt)
|
|
end
|
|
else
|
|
update_free_inert(self, dt)
|
|
end
|
|
end
|
|
|
|
|
|
local function add_delta(self, dx, dy)
|
|
local t = self.target
|
|
local b = self.border
|
|
local soft = self.soft_size
|
|
-- TODO: Can we calc it more easier?
|
|
-- A lot of calculations for every side of border
|
|
|
|
-- Handle soft zones
|
|
-- Percent - multiplier for delta. Less if outside of scroll zone
|
|
local x_perc = 1
|
|
local y_perc = 1
|
|
|
|
if t.x < b.x and dx < 0 then
|
|
x_perc = (soft - (b.x - t.x)) / soft
|
|
end
|
|
if t.x > b.z and dx > 0 then
|
|
x_perc = (soft - (t.x - b.z)) / soft
|
|
end
|
|
-- If disabled scroll by x
|
|
if not self.can_x then
|
|
x_perc = 0
|
|
end
|
|
|
|
if t.y < b.y and dy < 0 then
|
|
y_perc = (soft - (b.y - t.y)) / soft
|
|
end
|
|
if t.y > b.w and dy > 0 then
|
|
y_perc = (soft - (t.y - b.w)) / soft
|
|
end
|
|
-- If disabled scroll by y
|
|
if not self.can_y then
|
|
y_perc = 0
|
|
end
|
|
|
|
-- Reset inert if outside of scroll zone
|
|
if x_perc ~= 1 then
|
|
self.inert.x = 0
|
|
end
|
|
if y_perc ~= 1 then
|
|
self.inert.y = 0
|
|
end
|
|
|
|
t.x = t.x + dx * x_perc
|
|
t.y = t.y + dy * y_perc
|
|
end
|
|
|
|
|
|
function M.on_input(self, action_id, action)
|
|
if action_id ~= const.ACTION_TOUCH then
|
|
return false
|
|
end
|
|
local inp = self.input
|
|
local inert = self.inert
|
|
local result = false
|
|
|
|
if gui.pick_node(self.input_zone, action.x, action.y) then
|
|
if action.pressed then
|
|
inp.touch = true
|
|
inp.start_x = action.x
|
|
inp.start_y = action.y
|
|
inert.x = 0
|
|
inert.y = 0
|
|
self.target.x = self.pos.x
|
|
self.target.y = self.pos.y
|
|
else
|
|
local dist = helper.distance(action.x, action.y, inp.start_x, inp.start_y)
|
|
if not M.current_scroll and dist >= settings.DEADZONE then
|
|
local dx = math.abs(inp.start_x - action.x)
|
|
local dy = math.abs(inp.start_y - action.y)
|
|
inp.side = (dx > dy) and const.SIDE.X or const.SIDE.Y
|
|
|
|
-- Check scroll side if we can scroll
|
|
if (self.can_x and inp.side == const.SIDE.X or
|
|
self.can_y and inp.side == const.SIDE.Y) then
|
|
M.current_scroll = self
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if inp.touch and not action.pressed then
|
|
if M.current_scroll == self then
|
|
add_delta(self, action.dx, action.dy)
|
|
result = true
|
|
end
|
|
end
|
|
|
|
if action.released then
|
|
inp.touch = false
|
|
inp.side = false
|
|
if M.current_scroll == self then
|
|
M.current_scroll = nil
|
|
result = true
|
|
end
|
|
|
|
check_threshold(self)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
|
|
--- Start scroll to target point
|
|
-- @function scroll:scroll_to
|
|
-- @tparam point vector3 target point
|
|
-- @tparam[opt] bool is_instant instant scroll flag
|
|
-- @usage scroll:scroll_to(vmath.vector3(0, 50, 0))
|
|
-- @usage scroll:scroll_to(vmath.vector3(0), true)
|
|
function M.scroll_to(self, point, is_instant)
|
|
local b = self.border
|
|
local target = vmath.vector3(point)
|
|
target.x = helper.clamp(point.x - self.center_offset.x, b.x, b.z)
|
|
target.y = helper.clamp(point.y - self.center_offset.y, b.y, b.w)
|
|
|
|
cancel_animate(self)
|
|
|
|
self.animate = not is_instant
|
|
|
|
if is_instant then
|
|
self.target = target
|
|
set_pos(self, target)
|
|
else
|
|
gui.animate(self.node, gui.PROP_POSITION, target, gui.EASING_OUTSINE, settings.ANIM_SPEED, 0, function()
|
|
self.animate = false
|
|
self.target = target
|
|
set_pos(self, target)
|
|
end)
|
|
end
|
|
end
|
|
|
|
|
|
--- Scroll to item in scroll by point index
|
|
-- @function scroll:init
|
|
-- @tparam table self Component instance
|
|
-- @tparam number index Point index
|
|
-- @tparam[opt] boolean skip_cb If true, skip the point callback
|
|
function M.scroll_to_index(self, index, skip_cb)
|
|
index = helper.clamp(index, 1, #self.points)
|
|
|
|
if self.selected ~= index then
|
|
self.selected = index
|
|
|
|
if not skip_cb and self.on_point_callback then
|
|
self.on_point_callback(self.context, index, self.points[index])
|
|
end
|
|
end
|
|
|
|
self:scroll_to(self.points[index])
|
|
end
|
|
|
|
|
|
--- Set points of interest.
|
|
-- Scroll will always centered on closer points
|
|
-- @function scroll:set_points
|
|
-- @tparam table self Component instance
|
|
-- @tparam table points Array of vector3 points
|
|
function M.set_points(self, points)
|
|
self.points = points
|
|
-- cause of parent move in other side by y
|
|
for i = 1, #self.points do
|
|
self.points[i].y = -self.points[i].y
|
|
end
|
|
|
|
table.sort(self.points, function(a, b)
|
|
return a.x > b.x or a.y < b.y
|
|
end)
|
|
check_threshold(self)
|
|
end
|
|
|
|
|
|
--- Enable or disable scroll inert.
|
|
-- If disabled, scroll through points (if exist)
|
|
-- If no points, just simple drag without inertion
|
|
-- @function scroll:set_inert
|
|
-- @tparam table self Component instance
|
|
-- @tparam boolean state Inert scroll state
|
|
function M.set_inert(self, state)
|
|
self.is_inert = state
|
|
end
|
|
|
|
|
|
--- Set the callback on scrolling to point (if exist)
|
|
-- @function scroll:on_point_move
|
|
-- @tparam table self Component instance
|
|
-- @tparam function callback Callback on scroll to point of interest
|
|
function M.on_point_move(self, callback)
|
|
self.on_point_callback = callback
|
|
end
|
|
|
|
|
|
--- Set the scroll possibly area
|
|
-- @function scroll:set_border
|
|
-- @tparam table self Component instance
|
|
-- @tparam vmath.vector3 border Size of scrolling area
|
|
function M.set_border(self, border)
|
|
self.border = border
|
|
|
|
self.border.x = self.border.x + self.start_pos.x
|
|
self.border.z = self.border.z + self.start_pos.x
|
|
self.border.y = self.border.y + self.start_pos.y
|
|
self.border.w = self.border.w + self.start_pos.y
|
|
|
|
border.z = math.max(border.x, border.z)
|
|
border.w = math.max(border.y, border.w)
|
|
self.can_x = (border.x ~= border.z)
|
|
self.can_y = (border.y ~= border.w)
|
|
end
|
|
|
|
|
|
return M
|