概述
这是2024年7月份的一项工作,在研究外XML和SVG解析与生成过后,想到可以将自己写的绘图函数库ShapePoints
拆分为图形类,于是就有了CanvasShape
类。它包含了从填充、轮廓、阴影、虚线、顶点和中心点绘制的全部要素。只需要给定points
属性和调用draw()
方法就可以在CanvasItem
节点上绘制大多数多边形和折线了。
CanvasShapeGroup
是一个可以包含多个CanvasShape
或CanvasShapeGroup
的元素,基于它,可以创建树形的图形元素结构。方便动态管理和输出保存。
是否创建子类型的考虑
原本的打算是基于类的继承形式,创建CanvasShape
的子类型,来表示各种具体的图形元素,如果是单纯的绘制图形,完全没有必要写这些子类型。但是如果要进行文档化处理,就需要设计各种子类。所以好的思路是不去破坏原来的ShapePoints
函数库,而是建立一套独立的CanvasShape
体系。
CanvasShape
作为所有CanvasItem
可绘制图形的基类。提供共同的属性和方法,尤其是图形参数、点集合和绘制选项,以及绘制方法。
# =============================================
# 名称:CanvasShape
# 类型:类
# 描述:CanvasItem绘图函数图形基类
# 作者:巽星石
# 创建时间:2024年7月25日15:25:24
# 最后修改时间:2024年7月25日20:23:58
# =============================================
class_name CanvasShape
# ============================== 属性 ==============================
# -------------------- 基础样式
var border_width:int = 1 # 轮廓线宽度
var border_color:Color = Color.BLACK # 轮廓线颜色
var fill_color:Color = Color.WHITE # 填充颜色
var dash = 0.0 # 虚线间隔
# -------------------- 路径点
var points:PackedVector2Array = [] # 点集合
var close_path:bool = false # 是否闭合轮廓线
# -------------------- 变换
var position:Vector2 = Vector2.ZERO # 绘制位置
# -------------------- 阴影设置
var has_shadow:bool = false # 是否显示阴影
var shadow_color:Color = Color(Color.BLACK.lightened(0.1),0.6) # 阴影颜色
var shadow_offset:Vector2 = Vector2(10,10) # 阴影偏移
# -------------------- 绘制设置
var draw_border:bool = true # 是否绘制轮廓
var draw_fill:bool = true # 是否绘制填充
var draw_points:bool = false # 是否绘制顶点
var draw_position:bool = false # 是否绘制局部坐标系原点
var draw_rect2:bool = false # 是否绘制矩形范围
# 顶点绘制参数
var point_r = 3
var point_border_width:int = 1 # 顶点轮廓线宽度
var point_border_color:Color = Color.BLACK # 顶点轮廓线颜色
var point_fill_color:Color = Color.WHITE # 顶点填充颜色
# 矩形范围绘制参数
var rect2_border_width:int = 1 # 矩形轮廓线宽度
var rect2_border_color:Color = Color.AQUAMARINE # 矩形轮廓线颜色
# ============================== 方法 ==============================
func draw(canvas:CanvasItem) -> void:
# 对所有点进行位移变换
var points = Transform2D().translated(position) * points
# 1.绘制阴影
if has_shadow:
var shadow_points = Transform2D().translated(shadow_offset) * points
canvas.draw_colored_polygon(shadow_points,shadow_color)
# 2.绘制矩形范围
if draw_rect2:
canvas.draw_rect(get_rect(),Color(rect2_border_color,0.5),false,rect2_border_width)
# 3.绘制填充
if draw_fill and fill_color:
canvas.draw_colored_polygon(points,fill_color)
# 4.绘制轮廓
if draw_border and border_width!=0 and border_color!=null:
var close_points = points.duplicate()
# 闭合
if close_path:
close_points.append(close_points[0])
if dash>0.0: # 虚线间隔大于0
for seg in segments(close_points):
canvas.draw_dashed_line(seg[0],seg[1],border_color,border_width,dash)
pass
else:
canvas.draw_polyline(close_points,border_color,border_width) # 绘制实现闭合轮廓
# 5.绘制顶点
if draw_points:
for p in points:
# 绘制圆点
canvas.draw_circle(p,point_r,point_fill_color)
# 绘制边线
canvas.draw_arc(p,point_r,0,TAU,TAU * point_r,point_border_color,point_border_width/2.0,true)
# 6.绘制位置点
if draw_position:
var line_count = 4
var ang = 360.0/float(line_count) # 每次旋转角度
for i in range(line_count):
var p1 = position
var p2 = p1 + pVector2(ang * i,5)
canvas.draw_line(p1,p2,border_color,border_width)
# 获取图形的矩形
func get_rect() -> Rect2:
# 对所有点进行位移变换
var points = Transform2D().translated(position) * points
# 拆分出X坐标和Y坐标数组
var x_arr = []
var y_arr = []
for p in points:
x_arr.append(p.x)
y_arr.append(p.y)
# 最小值构成Rect2的position
var pos = Vector2(x_arr.min(),y_arr.min())
# 最大值 - pos = Rect2 的 size
var siz = Vector2(x_arr.max(),y_arr.max()) - pos
print(pos," ",siz)
return Rect2(pos,siz)
# ============================== 子类通用函数 ==============================
# 极坐标点函数 - 通过角度和长度定义一个点
func pVector2(angle:float = 0.0,length:float =0.0) -> Vector2:
var dir = Vector2.RIGHT.rotated(deg_to_rad(angle))
return dir * length
# points的点按顺序两两相连的所有线段
func segments(points:PackedVector2Array) -> Array[PackedVector2Array]:
var arr:Array[PackedVector2Array]
if points.size() >1: # 至少有两个点
for i in range(points.size() -1):
var seg:PackedVector2Array = [points[i],points[i+1]]
arr.append(seg)
return arr
绘制折线
extends Node2D
func _draw() -> void:
var shape = CanvasShape.new()
shape.points = [Vector2(0,0),Vector2(50,50),Vector2(150,10),Vector2(250,100)] # 绘制的点集合
# 形状参数
shape.position = Vector2(200,200)
shape.close_path = false # 不闭合
# 绘制参数
shape.border_width=2
shape.border_color = Color.ORANGE_RED
# 绘制选项
shape.draw_fill = false
shape.draw_points = true
shape.draw_position = true
shape.draw_rect2 = true
# 绘制
shape.draw(self)
绘制多边形
extends Node2D
func _draw() -> void:
var shape = CanvasShape.new()
shape.points = [Vector2(0,0),Vector2(50,50),Vector2(150,10),Vector2(250,10),Vector2(50,-50)] # 绘制的点集合
# 形状参数
shape.position = Vector2(200,200)
shape.close_path = true # 闭合
# 绘制参数
shape.border_width=2
shape.border_color = Color.ORANGE_RED
shape.fill_color = Color.AQUAMARINE
# 绘制选项
shape.has_shadow = true
# 绘制
shape.draw(self)
结合ShapePoints使用
ShapePoints
本身就是一个常见几何图形顶点求取函数库。通过将求得的图形顶点赋值给CanvasShape
实例的points
属性,然后就可以调用绘制函数绘制了。
extends Node2D
func _draw() -> void:
var shape = CanvasShape.new()
# 为CanvasShape实例指定求取的圆角矩形顶点
shape.points = ShapePoints.round_rect(Vector2(150,100),[5,5,5,5])
draw_set_transform(Vector2(200,200)) # 设定绘制原点
shape.draw(self)
另一个实例是绘制五角星:
extends Node2D
func _draw() -> void:
var shape = CanvasShape.new()
# 为CanvasShape实例指定求取的圆角矩形顶点
shape.points = ShapePoints.star()
shape.position = Vector2(300,300) # 设定绘制原点
shape.draw_points = true
shape.draw_position = true
shape.draw(self)
CanvasShapeGroup
以往的CanvasItem
绘图,都是在_draw()
中调用低级的绘图函数绘制。先调用的绘图函数先绘制。在实现CanvasShape
之后,第一次有了图形的参数化和对象化。在此基础之上,可以创建一个列表形式,用于管理多个图形,按顺序绘制。并且我们将可以随时修改图形的绘制顺序。
基于这样的想法,我设计了一个名为CanvasShapeGroup
的类,它的内部维护一个对象数组_shapes
,用来存储CanvasShape
或CanvasShapeGroup
实例。CanvasShapeGroup
也拥有draw()
方法,不过其调用后是遍历并调用每个子元素CanvasShape
或CanvasShapeGroup
的draw()
方法。
其实基于CanvasShapeGroup
,已经可以实现树状的父子结构,可以用于创建和管理复杂的图形文档。
源代码
# =============================================
# 名称:CanvasShapeGroup
# 类型:类
# 描述:存放CanvasShape或CanvasShapeGroup的列表形式
# 多层嵌套可以组成树状结构
# 作者:巽星石
# 创建时间:2024年7月25日22:55:00
# 最后修改时间:2024年7月25日23:27:31
# =============================================
class_name CanvasShapeGroup
# ============================== 信号 ==============================
signal order_changed() # 元素顺序发生改变时触发
# ============================== 属性 ==============================
var _shapes:Array # 图形或图形组的列表
func _init() -> void:
_shapes = []
# ============================== 方法 ==============================
# ---------------------------- 添加
# 追加形状
func append_shape(shape:CanvasShape) -> void:
_shapes.append(shape)
# 追加形状分组
func append_shape_group(gup:CanvasShapeGroup) -> void:
_shapes.append(gup)
# ---------------------------- 移动
# 移动至最上层
func move_to_topst(index:int) -> void:
var ele = _shapes[index] # 暂存元素
_shapes.remove_at(index) # 移除
_shapes.push_back(ele) # 添加到最后面
emit_signal("order_changed")
# 移动至最下层
func move_to_bottomst(index:int) -> void:
var ele = _shapes[index] # 暂存元素
_shapes.remove_at(index) # 移除
_shapes.push_front(ele) # 添加到最前面
emit_signal("order_changed")
# 往上一层
func move_to_top(index:int) -> void:
var ele = _shapes[index] # 暂存元素
_shapes.remove_at(index) # 移除
_shapes.push_back(ele) # 添加到最后面
emit_signal("order_changed")
# 往下一层
func move_to_bottom(index:int) -> void:
var ele = _shapes[index] # 暂存元素
_shapes.remove_at(index) # 移除
_shapes.push_back(ele) # 添加到最后面
emit_signal("order_changed")
# ---------------------------- 绘制
# 遍历调用图形和分组的draw()方法
func draw(canvas:CanvasItem) -> void:
for shape in _shapes:
shape.draw(canvas)
绘制测试
extends Node2D
var shapes = CanvasShapeGroup.new() # 图形列表
func _ready() -> void:
# 处理 CanvasShapeGroup 的 order_changed 信号
shapes.order_changed.connect(func():
queue_redraw()
)
# 六边形
var shape1 = CanvasRegularPolygon.new()
shape1.position = Vector2(200,200)
shape1.edges = 6
shape1.r = 50
shape1.close_path = true
shapes.append_shape(shape1)
# 五边形
var shape2 = CanvasRegularPolygon.new()
shape2.position = Vector2(230,250)
shape2.edges = 5
shape2.r = 50
shape2.close_path = true
shapes.append_shape(shape2)
func _draw() -> void:
# 绘制
shapes.draw(self)
func _on_top_btn_pressed() -> void:
shapes.move_to_topst(0)
pass
我们创建了包含两个图形(一个正六边形,一个正五边形)的CanvasShapeGroup
,默认会按先后顺序进行绘制。
我们用一个按钮测试,对CanvasShapeGroup
中最前面的图形,执行置顶操作。因为只有两个图形,所以两个图形的顺序会反复交替。
后期设想
- 通过矩形检测鼠标位置是否在矩形内,可以判断图形是否被选择
- 通过绘制矩形选框,可以判断图形的矩形是否完全在选框内,或者与选框有部分交集,从而实现矩形框的选择形式
- 通过右键菜单或快捷键形式,可以设定图形的绘制顺序
CanvasShapeGroup
可以实现嵌套的树形结构,可以通过遍历和递归来获取图形的SVG或自定义XML、JSON或ConfigFile形式- 可以用Tree控件查看和编辑整个页面的元素。