191 lines
8.1 KiB
GDScript
191 lines
8.1 KiB
GDScript
## 卷轴视口承载件(level-map.md 布局决策 5——关卡画面选路屏):画布宿主与裁切、平移状态
|
||
## 与 clamp、锚点跟随与平滑滚动、指引件(屏缘渐隐带+平移提示 chip)。
|
||
## 纯呈现态:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);跟随只随停点切换/
|
||
## 兑付回图发生(零自动推进)。拖拽/滚轮判定留画布 _gui_input(事件流同源——悬停/选路同源),
|
||
## 本件只管裁切/offset/clamp/跟随/指引;画布包络=画布实际尺寸(open_choices 拟合后同帧收敛)。
|
||
class_name LevelMapViewport
|
||
extends Control
|
||
|
||
const SCROLL_DUR := 0.3 # 平滑滚动时长(布局决策 5 provisional 0.3s 单段缓动)
|
||
const FADE_BAND_W := 70.0 # 屏缘渐隐带宽(布局决策 5/像素级判据②)
|
||
const CHIP_SIZE := Vector2(240, 30) # 平移提示 chip(像素级判据③——图区底部居中)
|
||
const CHIP_CORNER := 6 # chip 圆角(布局决策 5)
|
||
const CHIP_BOTTOM_GAP := 24.0 # chip 距视口底缘(效果图档同族取值)
|
||
const CHIP_TEXT := "◄ 拖拽画布 / 滚轮平移 ►" # 14px(FS_HINT 档——布局决策 5)
|
||
const EDGE_EPS := 0.5 # 包络边界判定容差(浮点平移累积)
|
||
|
||
var _canvas: Control
|
||
var _anchor_provider: Callable # () -> Vector2 锚点画布坐标(screen 提供——当前节点/入口组)
|
||
var _viewport_size := Vector2.ZERO # 窗口尺寸兜底(setup 注入设计基座——实况窗口以控件 size 为准,布局晚于构造)
|
||
var _offset := Vector2.ZERO: # 画布左上角在视口件坐标系的位置(纯呈现态——直驱画布 position)
|
||
set(value):
|
||
_offset = value
|
||
if _canvas != null:
|
||
_canvas.position = _offset
|
||
var _view_tween: Tween
|
||
var _guided_visible := false # 指引件可见态(画布超窗才暗示——浅图无平移余量即隐藏)
|
||
|
||
|
||
## 一次交齐注入(code-conventions §1.6 拆分交接:画布宿主、锚点来源、窗口兜底尺寸)
|
||
func setup(canvas: Control, anchor_provider: Callable, viewport_size: Vector2) -> void:
|
||
_canvas = canvas
|
||
_anchor_provider = anchor_provider
|
||
_viewport_size = viewport_size
|
||
clip_contents = true
|
||
add_child(canvas)
|
||
_build_guidance()
|
||
|
||
|
||
func _notification(what: int) -> void:
|
||
# 窗口实况尺寸落定/变化即重锚(首开布局晚于 open_choices 的锚定——隐藏子树容器排序
|
||
# 首个可见帧才传播,4.7 行为;实况窗口以本件 size 为准,落定即收敛锚点)
|
||
if what == NOTIFICATION_RESIZED and _canvas != null and _anchor_provider.is_valid():
|
||
reanchor(false)
|
||
|
||
|
||
## 锚点跟随(布局决策 5:锚点居中;smooth=停点切换/兑付回图平滑滚动,减动效开=跳终态;
|
||
## 重入先杀在飞 tween 再建新——快速连续停点切换防叠加)
|
||
func reanchor(smooth: bool) -> void:
|
||
if _canvas == null or not _anchor_provider.is_valid():
|
||
return
|
||
var target := _clamped_center_offset(_anchor_provider.call())
|
||
_kill_scroll_tween()
|
||
if not smooth or ArmedTimer.motion_still():
|
||
_offset = target
|
||
_sync_guidance()
|
||
return
|
||
_view_tween = create_tween()
|
||
_view_tween.tween_property(self, "_offset", target, UiKit.motion(SCROLL_DUR)) \
|
||
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
|
||
_sync_guidance()
|
||
|
||
|
||
## 手动平移(画布 _gui_input 判定后调用——拖拽沿逐帧/滚轮步进;纯呈现零核心写)
|
||
func pan_by(delta: Vector2) -> void:
|
||
if _canvas == null:
|
||
return
|
||
_kill_scroll_tween()
|
||
var win := _window_size()
|
||
_offset = Vector2(
|
||
_axis_pan_offset(_offset.x, delta.x, _canvas.size.x, win.x),
|
||
_axis_pan_offset(_offset.y, delta.y, _canvas.size.y, win.y))
|
||
_sync_guidance()
|
||
|
||
|
||
## 视口中线的画布坐标(锚点居中判据投影面——浅图=画布中心,深图=夹紧域内当前注视点)
|
||
func viewport_center_canvas_pos() -> Vector2:
|
||
return _window_size() / 2.0 - _offset
|
||
|
||
|
||
## 视口是否钉在画布包络边界(任一超窗轴 offset 顶边——clamp 判据面)
|
||
func is_clamped_at_edge() -> bool:
|
||
var win := _window_size()
|
||
if _canvas == null:
|
||
return false
|
||
return _axis_at_edge(_offset.x, _canvas.size.x, win.x) \
|
||
or _axis_at_edge(_offset.y, _canvas.size.y, win.y)
|
||
|
||
|
||
func _kill_scroll_tween() -> void:
|
||
if _view_tween != null and _view_tween.is_valid():
|
||
_view_tween.kill()
|
||
_view_tween = null
|
||
|
||
|
||
## 单轴锚定 offset:画布不超窗=居中分支(夹紧域退化——布局决策 5);
|
||
## 超窗=锚点夹紧在画布包络内,锚点落视口中线(offset = 半窗 − 锚点)
|
||
func _axis_center_offset(anchor: float, canvas_len: float, half_win: float) -> float:
|
||
if canvas_len <= half_win * 2.0:
|
||
return half_win - canvas_len / 2.0
|
||
return half_win - clampf(anchor, half_win, canvas_len - half_win)
|
||
|
||
|
||
func _clamped_center_offset(anchor: Vector2) -> Vector2:
|
||
var half := _window_size() / 2.0
|
||
return Vector2(
|
||
_axis_center_offset(anchor.x, _canvas.size.x, half.x),
|
||
_axis_center_offset(anchor.y, _canvas.size.y, half.y))
|
||
|
||
|
||
## 单轴平移收敛:居中轴零平移余量不动;超窗轴 offset 收敛在 [半窗−画布, 0](不出露屏底空档)
|
||
func _axis_pan_offset(cur: float, delta: float, canvas_len: float, win_len: float) -> float:
|
||
if canvas_len <= win_len:
|
||
return cur
|
||
return clampf(cur + delta, win_len - canvas_len, 0.0)
|
||
|
||
|
||
func _axis_at_edge(offset: float, canvas_len: float, win_len: float) -> bool:
|
||
if canvas_len <= win_len:
|
||
return false
|
||
return offset >= -EDGE_EPS or offset <= win_len - canvas_len + EDGE_EPS
|
||
|
||
|
||
## 实况窗口优先(容器布局晚于构造——注入值只兜构造帧数学)
|
||
func _window_size() -> Vector2:
|
||
if size.x > 0.0 and size.y > 0.0:
|
||
return size
|
||
return _viewport_size
|
||
|
||
|
||
## 指引件(布局决策 5/像素级判据②③):屏缘渐隐带盖在画布之上+图区底部居中提示 chip;
|
||
## 可见性=画布超窗(_sync_guidance 随锚定/平移刷新)
|
||
func _build_guidance() -> void:
|
||
var band_l := TextureRect.new()
|
||
band_l.name = "FadeL"
|
||
band_l.texture = _fade_gradient(false)
|
||
band_l.set_anchors_preset(Control.PRESET_LEFT_WIDE)
|
||
band_l.offset_right = FADE_BAND_W
|
||
band_l.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
band_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(band_l)
|
||
var band_r := TextureRect.new()
|
||
band_r.name = "FadeR"
|
||
band_r.texture = _fade_gradient(true)
|
||
band_r.set_anchors_preset(Control.PRESET_RIGHT_WIDE)
|
||
band_r.offset_left = -FADE_BAND_W
|
||
band_r.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
band_r.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
add_child(band_r)
|
||
var chip := PanelContainer.new()
|
||
chip.name = "PanHint"
|
||
chip.add_theme_stylebox_override("panel",
|
||
UiKit.panel_flat(UiKit.HP_BAR_BACK, UiKit.STONE, CHIP_CORNER, 1, Vector4(12, 5, 12, 5)))
|
||
chip.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
||
chip.offset_left = -CHIP_SIZE.x / 2.0
|
||
chip.offset_right = CHIP_SIZE.x / 2.0
|
||
chip.offset_top = -CHIP_SIZE.y - CHIP_BOTTOM_GAP
|
||
chip.offset_bottom = -CHIP_BOTTOM_GAP
|
||
chip.custom_minimum_size = CHIP_SIZE
|
||
chip.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
var chip_label := UiKit.label(CHIP_TEXT, UiKit.FS_HINT, false, UiKit.WARM)
|
||
chip_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
chip_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
chip.add_child(chip_label)
|
||
add_child(chip)
|
||
_sync_guidance()
|
||
|
||
|
||
## 屏缘渐隐渐变(色取屏底同族 BG_ROOT——代拍板:不透明端 0.9 单段衰减)
|
||
func _fade_gradient(transparent_at_left: bool) -> GradientTexture2D:
|
||
var grad := Gradient.new()
|
||
grad.offsets = PackedFloat32Array([0.0, 1.0])
|
||
grad.colors = PackedColorArray([Color(UiKit.BG_ROOT, 0.9), Color(UiKit.BG_ROOT, 0.0)])
|
||
var tex := GradientTexture2D.new()
|
||
tex.gradient = grad
|
||
tex.fill_from = Vector2(1, 0.5) if transparent_at_left else Vector2(0, 0.5)
|
||
tex.fill_to = Vector2(0, 0.5) if transparent_at_left else Vector2(1, 0.5)
|
||
tex.width = int(FADE_BAND_W)
|
||
return tex
|
||
|
||
|
||
func _sync_guidance() -> void:
|
||
var win := _window_size()
|
||
var over := _canvas != null and (_canvas.size.x > win.x or _canvas.size.y > win.y)
|
||
if over == _guided_visible:
|
||
return
|
||
_guided_visible = over
|
||
for nm in ["FadeL", "FadeR", "PanHint"]:
|
||
var node := get_node_or_null(NodePath(nm)) as Control
|
||
if node != null:
|
||
node.visible = over
|