字玩FontPlayer开发笔记13 Vue3实现钢笔工具
目录
- 字玩FontPlayer开发笔记13 Vue3实现钢笔工具
- 笔记
- 钢笔工具的实现效果:
- 钢笔组件的创建
- 整体逻辑
- 临时变量
- 钢笔组件数据结构
- 钢笔工具初始化
- mousedown事件
- mousemove事件
- mouseup事件
- 绘制钢笔组件控件
- 钢笔组件的编辑
- 整体逻辑
- 临时变量
- 编辑钢笔初始化
- 事件监听器
- 使用renderSelectPenEditor渲染控件,每次数值变化时,重新渲染
字玩FontPlayer开发笔记13 Vue3实现钢笔工具
字玩FontPlayer是笔者开源的一款字体设计工具,使用Vue3 + ElementUI开发,源代码:github | gitee
笔记
钢笔工具是设计工具很重要的一个功能。钢笔工具通过创建一组贝塞尔曲线,使得用户可以编辑创建任意不规则的曲线形状。钢笔工具使用三次贝塞尔曲线,每段贝塞尔曲线包含两个锚点定义起点和终点,两个控制点分别定义起始切线和收尾切线,通过特定算法形成曲线线段,基本可以拟合任意形状。
钢笔工具的实现效果:
钢笔组件的创建
整体逻辑
- 使用points数组变量记录路径(一组贝塞尔曲线)数值,point类型可以为锚点或控制点
- 监听mousedown事件:
2.1. 第一次按下鼠标时再points数组中添加一个锚点和对应控制点
2.2. 设置当前编辑锚点为最后一个锚点 - 监听mousemove事件:
3.1. 对于非第一组锚点控制点的后续节点,在每次鼠标松开后第一次移动鼠标时,添加一组锚点和控制点
3.2. 当鼠标没有按下,也就是仅移动鼠标时,改变当前锚点为鼠标移动位置
3.3. 当鼠标按下时,也就是拖拽状态中,改变当前控制点位置为鼠标拖拽位置
3.4. 当鼠标移动至第一个锚点附件,自动吸附,并设置闭合路径标志为true - 监听mouseup事件:
4.1. 重置相关变量
4.2. 当闭合路径标志为true时创建组件 - 监听points等相关变量变化,相关变量改变时,重新绘制控制组件
临时变量
临时变量用于记录钢笔创建过程中的数据,组件创建完即重置。
- points
使用points变量记录路径中的点,其中type可以设置为anchor
或control
,对于控制点,origin为其对应的锚点uuid
// 点的数据结构
// point data struct
export interface IPoint {
uuid: string;
x: number;
y: number;
type: string;
origin: string | null;
isShow?: boolean;
}
export interface IPoints {
value: Array<IPoint>;
}
// 钢笔路径包含的点
// points for pen path
const points: IPoints = reactive({
value: []
})
const setPoints = (value: Array<IPoint>) => {
points.value = value
}
- editing
editing标识是否正在编辑钢笔路径
// 是否正在编辑钢笔路径
// whether on editing
const editing: Ref<boolean> = ref(false)
const setEditing = (status: boolean) => {
editing.value = status
}
- mousedown
mousedonw变量记录当前鼠标是否按下 - mousemove
mousemove变量记录当前鼠标是否移动 - editAnchor
editAnchor变量记录当前编辑的锚点 - closePath
closePath变量记录路径是否闭合,由于字体中不含有开放路径,只有路径闭合时,才能创建钢笔组件
钢笔组件数据结构
每个组件最外层数据结构如下:
// 字符组件数据结构,包含变换等基础信息,与包含图形信息的IComponentValue不同
// component data struct, contains transform info, etc, different with IComponentValue
export interface IComponent {
uuid: string;
type: string;
name: string;
lock: boolean;
visible: boolean;
value: IComponentValue;
x: number;
y: number;
w: number;
h: number;
rotation: number;
flipX: boolean;
flipY: boolean;
usedInCharacter: boolean;
opacity?: number;
}
对于每个不同的组件,记录相应数据在IComponent的value字段中,IComponentValue枚举定义如下:
// 字符图形组件信息枚举
// enum of basic element info for component
export enum IComponentValue {
IPenComponent,
IPolygonComponent,
IRectangleComponent,
IEllipseComponent,
IPictureComponent,
ICustomGlyph,
}
对于钢笔组件,IPenComponent数据格式如下:
// 钢笔组件
// pen component
export interface IPenComponent {
points: any;
strokeColor: string;
fillColor: string;
closePath: boolean;
editMode: boolean;
contour?: Array<ILine | IQuadraticBezierCurve | ICubicBezierCurve>;
preview?: Array<ILine | IQuadraticBezierCurve | ICubicBezierCurve>;
}
生成钢笔代码:
// 生成钢笔组件
// generate pen component
const genPenComponent = (
points: Array<IPoint>,
closePath: boolean,
fillColor: string = '',
strokeColor: string = '#000',
) => {
const { x, y, w, h } = getBound(points.reduce((arr: Array<{x: number, y: number }>, point: IPoint) => {
arr.push({
x: point.x,
y: point.y,
})
return arr
}, []))
const rotation = 0
const flipX = false
const flipY = false
let options = {
unitsPerEm: 1000,
descender: -200,
advanceWidth: 1000,
}
if (editStatus.value === Status.Edit) {
options.unitsPerEm = selectedFile.value.fontSettings.unitsPerEm
options.descender = selectedFile.value.fontSettings.descender
options.advanceWidth = selectedFile.value.fontSettings.unitsPerEm
}
let transformed_points = transformPoints(points, {
x, y, w, h, rotation, flipX, flipY,
})
const contour_points = formatPoints(transformed_points, options, 1)
const contour = genPenContour(contour_points)
const scale = 100 / (options.unitsPerEm as number)
const preview_points = transformed_points.map((point) => {
return Object.assign({}, point, {
x: point.x * scale,
y: point.y * scale,
})
})
const preview_contour = genPenContour(preview_points)
return {
uuid: genUUID(),
type: 'pen',
name: 'pen',
lock: false,
visible: true,
value: {
points: points,
fillColor,
strokeColor,
closePath,
editMode: false,
preview: preview_contour,
contour: contour,
} as unknown as IComponentValue,
x,
y,
w,
h,
rotation: 0,
flipX: false,
flipY: false,
usedInCharacter: true,
}
}
钢笔工具初始化
设置相关变量:
mousedown.value = false
mousemove.value = false
eventListenersMap = {}
let editAnchor: any = null
const controlScale = 0.35
const nearD = 5
let closePath = false
let _lastControl: IPoint
let _controlIndex: number
设置事件监听器:
canvas.addEventListener('mousedown', onMouseDown)
canvas.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
window.addEventListener('keydown', onKeyDown)
设置关闭工具回调函数,该函数在关闭钢笔工具时被执行:
const closePen = () => {
canvas.removeEventListener('mousedown', onMouseDown)
canvas.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
window.removeEventListener('keydown', onKeyDown)
setEditing(false)
setPoints([])
_lastControl = undefined
_controlIndex = undefined
closePath = false
editAnchor = null
}
mousedown事件
第一次按下鼠标时再points数组中添加一个锚点和对应控制点:
if (!points.value.length) {
// 第一个锚点
const _anchor: IPoint = {
uuid: genUUID(),
type: 'anchor',
x: getCoord(e.offsetX),
y: getCoord(e.offsetY),
origin: null,
isShow: true,
}
const _control: IPoint = {
uuid: genUUID(),
type: 'control',
x: getCoord(e.offsetX),
y: getCoord(e.offsetY),
origin: _anchor.uuid,
isShow: true,
}
editAnchor = {
uuid: _anchor.uuid,
index: 0,
}
setPoints([_anchor, _control])
editAnchor = {
uuid: points.value[0].uuid,
index: 0,
}
}
非第一次按下鼠标,设置editAnchor值:
else {
editAnchor = {
uuid: points.value[points.value.length - 2].uuid,
index: points.value.length - 2,
}
}
监听器完整代码:
const onMouseDown = (e: MouseEvent) => {
if (!points.value.length) {
// 保存状态
saveState('新建钢笔组件锚点', [
StoreType.Pen,
glyph ? StoreType.EditGlyph : StoreType.EditCharacter],
OpType.Undo)
}
mousedown.value = true
setEditing(true)
if (!points.value.length) {
// 第一个锚点
const _anchor: IPoint = {
uuid: genUUID(),
type: 'anchor',
x: getCoord(e.offsetX),
y: getCoord(e.offsetY),
origin: null,
isShow: true,
}
const _control: IPoint = {
uuid: genUUID(),
type: 'control',
x: getCoord(e.offsetX),
y: getCoord(e.offsetY),
origin: _anchor.uuid,
isShow: true,
}
editAnchor = {
uuid: _anchor.uuid,
index: 0,
}
setPoints([_anchor, _control])
editAnchor = {
uuid: points.value[0].uuid,
index: 0,
}
} else {
editAnchor = {
uuid: points.value[points.value.length - 2].uuid,
index: points.value.length - 2,
}
}
}
mousemove事件
对于非第一组锚点控制点的后续节点,在每次鼠标松开后第一次移动鼠标时,添加一组锚点和控制点:
if (!mousedown.value) {
if (!mousemove.value && _points.length) {
// 第一次移动鼠标
// 保存状态
saveState('新建钢笔组件锚点', [
StoreType.Pen,
glyph ? StoreType.EditGlyph : StoreType.EditCharacter],
OpType.Undo)
_lastControl = Object.assign({}, _points[_points.length - 1])
_controlIndex = _points.length - 1
const _anchor = {
uuid: genUUID(),
type: 'anchor',
x: getCoord(e.offsetX),
y: getCoord(e.offsetY),
origin: null,
isShow: true,
}
const _control1 = {
uuid: genUUID(),
type: 'control',
x: _anchor.x,
y: _anchor.y,
origin: _anchor.uuid,
isShow: false,
}
const _control2 = {
uuid: genUUID(),
type: 'control',
x: _anchor.x,
y: _anchor.y,
origin: _anchor.uuid,
isShow: false,
}
_points.push(_control1, _anchor, _control2)
setPoints(_points)
mousemove.value = true
}
}
当鼠标没有按下,也就是仅移动鼠标时,改变当前锚点为鼠标移动位置:
else if (_points.length) {
// 移动鼠标
_controlIndex = _points.length - 4
const _anchor = _points[_points.length - 2]
const _control1 = _points[_points.length - 3]
const _control2 = _points[_points.length - 1]
_anchor.x = getCoord(e.offsetX)
_anchor.y = getCoord(e.offsetY)
_control2.x = getCoord(e.offsetX)
_control2.y = getCoord(e.offsetY)
closePath = false
// 当鼠标移动至第一个锚点所在位置附近时,自动闭合路径
if (isNearPoint(getCoord(e.offsetX), getCoord(e.offsetY), points.value[0].x, points.value[0].y, nearD)) {
// 将最后一个锚点位置设置为第一个锚点位置
_anchor.x = points.value[0].x
_anchor.y = points.value[0].y
// 自动延切线与第一条贝塞尔曲线进行连接
_control2.x = points.value[1].x
_control2.y = points.value[1].y
_control1.x = points.value[0].x - (points.value[1].x - points.value[0].x)
_control1.y = points.value[0].y - (points.value[1].y - points.value[0].y)
closePath = true
}
setPoints(_points)
mousemove.value = true
}
当鼠标按下时,也就是拖拽状态中,改变当前控制点位置为鼠标拖拽位置:
if (mousedown.value) {
if (_lastControl) _points[_controlIndex] = _lastControl
// 长按鼠标
if (editAnchor.index === 0) {
//第一个锚点
let _anchor = _points[editAnchor.index]
let _control = _points[editAnchor.index + 1]
//将第一个锚点对应的控制点设置为鼠标移动位置
_control.x = getCoord(e.offsetX)
_control.y = getCoord(e.offsetY)
_control.isShow = true
} else {
// 后续锚点
let _anchor = _points[editAnchor.index]
let _control1 = _points[editAnchor.index - 1]
let _control2 = _points[editAnchor.index + 1]
//将锚点对应的后续控制点设置为鼠标移动位置
_control2.x = getCoord(e.offsetX)
_control2.y = getCoord(e.offsetY)
_control2.isShow = true
//将锚点对应的前接控制点设置为与后续控制点对称的位置
_control1.x = _anchor.x - (getCoord(e.offsetX) - _anchor.x)
_control1.y = _anchor.y - (getCoord(e.offsetY) - _anchor.y)
_control1.isShow = true
}
mousemove.value = true
setPoints(_points)
}
当鼠标移动至第一个锚点附件,自动吸附,并设置闭合路径标志为true:
// 当鼠标移动至第一个锚点所在位置附近时,自动闭合路径
if (isNearPoint(getCoord(e.offsetX), getCoord(e.offsetY), points.value[0].x, points.value[0].y, nearD)) {
// 将最后一个锚点位置设置为第一个锚点位置
_anchor.x = points.value[0].x
_anchor.y = points.value[0].y
// 自动延切线与第一条贝塞尔曲线进行连接
_control2.x = points.value[1].x
_control2.y = points.value[1].y
_control1.x = points.value[0].x - (points.value[1].x - points.value[0].x)
_control1.y = points.value[0].y - (points.value[1].y - points.value[0].y)
closePath = true
}
监听器完整代码:
const onMouseMove = (e: MouseEvent) => {
if (!points.value.length || !editing) return
const _points = R.clone(points.value)
if (mousedown.value) {
if (_lastControl) _points[_controlIndex] = _lastControl
// 长按鼠标
if (editAnchor.index === 0) {
//第一个锚点
let _anchor = _points[editAnchor.index]
let _control = _points[editAnchor.index + 1]
//将第一个锚点对应的控制点设置为鼠标移动位置
_control.x = getCoord(e.offsetX)
_control.y = getCoord(e.offsetY)
_control.isShow = true
} else {
// 后续锚点
let _anchor = _points[editAnchor.index]
let _control1 = _points[editAnchor.index - 1]
let _control2 = _points[editAnchor.index + 1]
//将锚点对应的后续控制点设置为鼠标移动位置
_control2.x = getCoord(e.offsetX)
_control2.y = getCoord(e.offsetY)
_control2.isShow = true
//将锚点对应的前接控制点设置为与后续控制点对称的位置
_control1.x = _anchor.x - (getCoord(e.offsetX) - _anchor.x)
_control1.y = _anchor.y - (getCoord(e.offsetY) - _anchor.y)
_control1.isShow = true
}
mousemove.value = true
setPoints(_points)
}
if (!mousedown.value) {
if (!mousemove.value && _points.length) {
// 第一次移动鼠标
// 保存状态
saveState('新建钢笔组件锚点', [
StoreType.Pen,
glyph ? StoreType.EditGlyph : StoreType.EditCharacter],
OpType.Undo)
_lastControl = Object.assign({}, _points[_points.length - 1])
_controlIndex = _points.length - 1
const _anchor = {
uuid: genUUID(),
type: 'anchor',
x: getCoord(e.offsetX),
y: getCoord(e.offsetY),
origin: null,
isShow: true,
}
const _control1 = {
uuid: genUUID(),
type: 'control',
x: _anchor.x,
y: _anchor.y,
origin: _anchor.uuid,
isShow: false,
}
const _control2 = {
uuid: genUUID(),
type: 'control',
x: _anchor.x,
y: _anchor.y,
origin: _anchor.uuid,
isShow: false,
}
_points.push(_control1, _anchor, _control2)
setPoints(_points)
mousemove.value = true
} else if (_points.length) {
// 移动鼠标
_controlIndex = _points.length - 4
const _anchor = _points[_points.length - 2]
const _control1 = _points[_points.length - 3]
const _control2 = _points[_points.length - 1]
_anchor.x = getCoord(e.offsetX)
_anchor.y = getCoord(e.offsetY)
_control2.x = getCoord(e.offsetX)
_control2.y = getCoord(e.offsetY)
closePath = false
// 当鼠标移动至第一个锚点所在位置附近时,自动闭合路径
if (isNearPoint(getCoord(e.offsetX), getCoord(e.offsetY), points.value[0].x, points.value[0].y, nearD)) {
// 将最后一个锚点位置设置为第一个锚点位置
_anchor.x = points.value[0].x
_anchor.y = points.value[0].y
// 自动延切线与第一条贝塞尔曲线进行连接
_control2.x = points.value[1].x
_control2.y = points.value[1].y
_control1.x = points.value[0].x - (points.value[1].x - points.value[0].x)
_control1.y = points.value[0].y - (points.value[1].y - points.value[0].y)
closePath = true
}
setPoints(_points)
mousemove.value = true
}
}
}
mouseup事件
mouseup事件中重置相关变量,如果closePath为true则创建组件
const onMouseUp = (e: MouseEvent) => {
if (!points.value.length || !editing) return
mousedown.value = false
mousemove.value = false
editAnchor = null
if (closePath) {
setEditing(false)
const component = genPenComponent(R.clone(points).value, true)
setPoints([])
if (!glyph) {
addComponentForCurrentCharacterFile(component)
} else {
addComponentForCurrentGlyph(component)
}
_lastControl = undefined
_controlIndex = undefined
closePath = false
editAnchor = null
}
}
绘制钢笔组件控件
在钢笔组件创建过程中,编辑控件需要全程渲染辅助用户进行操作。程序需要监听points等相关变量变化,相关变量改变时,重新绘制控制组件。
监听事件:
watch([
penPoints,
penEditing,
], () => {
render()
tool.value === 'select' && renderSelectEditor(canvas.value)
tool.value === 'pen' && renderPenEditor(canvas.value)
if (!penEditing.value) return
renderPenEditor(canvas.value)
})
控件绘制函数
const renderPenEditor = (canvas: HTMLCanvasElement) => {
const ctx: CanvasRenderingContext2D = (canvas as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D
const _points = points.value.map((point: IPoint) => {
return {
isShow: point.isShow,
...mapCanvasCoords({
x: point.x,
y: point.y,
}),
}
})
if (!_points.length) return
const w = 10
ctx.strokeStyle = '#000'
ctx.fillStyle = '#000'
ctx.beginPath()
ctx.moveTo(_points[0].x, _points[0].y)
if (_points.length >= 4) {
ctx.bezierCurveTo(_points[1].x, _points[1].y, _points[2].x, _points[2].y, _points[3].x, _points[3].y)
}
for (let i = 3; i < _points.length - 1; i += 3) {
if (i + 3 >= _points.length) break
ctx.bezierCurveTo(_points[i + 1].x, _points[i + 1].y, _points[i + 2].x, _points[i + 2].y, _points[i + 3].x, _points[i + 3].y)
}
ctx.stroke()
ctx.closePath()
for (let i = 0; i < _points.length - 1; i += 3) {
_points[i].isShow && ctx.fillRect(_points[i].x - w / 2, _points[i].y - w / 2, w, w)
_points[i + 1].isShow && ctx.strokeRect(_points[i + 1].x - w / 2, _points[i + 1].y - w / 2, w, w)
if ((i + 2) > _points.length - 1) break
_points[i + 2].isShow && ctx.strokeRect(_points[i + 2].x - w / 2, _points[i + 2].y - w / 2, w, w)
}
ctx.beginPath()
ctx.moveTo(_points[0].x, _points[0].y)
ctx.lineTo(_points[1].x, _points[1].y)
ctx.stroke()
ctx.closePath()
for (let i = 3; i < _points.length - 1; i += 3) {
if (_points[i - 1].isShow) {
ctx.beginPath()
ctx.moveTo(_points[i].x, _points[i].y)
ctx.lineTo(_points[i - 1].x, _points[i - 1].y)
ctx.stroke()
ctx.closePath()
}
if (_points[i + 1].isShow) {
ctx.beginPath()
ctx.moveTo(_points[i].x, _points[i].y)
ctx.lineTo(_points[i + 1].x, _points[i + 1].y)
ctx.stroke()
ctx.closePath()
}
}
}
钢笔组件的编辑
钢笔组件创建后,用户可以随时编辑,所以编辑钢笔组件也是一个很重要的功能。
整体逻辑
- 通过临时变量selectPenPoint, hoverPenPoint等控制当前选择或鼠标划过的点
- 监听mousedown事件,如果鼠标距离某个点距离达到一定阈值,则记录该点为选中点selectPenPoint
- 监听mousemove事件,如果鼠标按下,则移动选中点,如果鼠标未按下,则检查是否划过某点,记录该点为hoverPenPoint
- 监听mouseup事件,清除临时变量
- 使用renderSelectPenEditor渲染控件,每次数值变化时,重新渲染
临时变量
临时变量用于记录钢笔编辑过程中的数据,组件编辑完即重置。
- selectAnchor
当前选择的锚点 - selectPenPoint
当前选择的点,可能为锚点或控制点 - hoverPenPoint
当前鼠标移动至的点
编辑钢笔初始化
当用户切换到编辑模式下,首先调用initPenEditMode方法,该方法初始化事件监听器,和一些变量,并定义关闭编辑模式的方法回调。
// 选择钢笔组件时,初始化方法
// initializier for pen component selection
const initPenEditMode = (canvas: HTMLCanvasElement, d: number = 5, glyph: boolean = false) => {
let lastX = -1
let lastY = -1
let mousedown = false
const onMouseDown = (e: MouseEvent) => {
//...
}
const onMouseMove = (e: MouseEvent) => {
//...
}
const onMouseUp = (e: MouseEvent) => {
//...
}
//...
canvas?.addEventListener('mousedown', onMouseDown)
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
canvas.addEventListener('keydown', onKeyDown)
const closeSelect = () => {
canvas?.removeEventListener('mousedown', onMouseDown)
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
canvas.removeEventListener('keydown', onKeyDown)
selectAnchor.value = ''
selectPenPoint.value = ''
hoverPenPoint.value = ''
}
return closeSelect
}
事件监听器
监听mousedown事件,如果鼠标距离某个点距离达到一定阈值,则记录该点为选中点selectPenPoint
const onMouseDown = (e: MouseEvent) => {
mousedown = true
for (let i = orderedListWithItemsForCurrentCharacterFile.value.length - 1; i >= 0; i--) {
const component = orderedListWithItemsForCurrentCharacterFile.value[i]
const { x: _x, y: _y } = rotatePoint(
{ x: getCoord(e.offsetX), y: getCoord(e.offsetY) },
{ x: component.x + component.w / 2, y: component.y + component.h / 2 },
-component.rotation
)
if (selectedComponentUUID.value === component.uuid && component.visible) {
const _points = transformPenPoints(selectedComponent.value, false)
for (let i = 0; i < _points.length - 1; i++) {
const point = _points[i]
// 鼠标移动至point
if (distance(_x, _y, point.x, point.y) <= d) {
if (point.type === 'anchor') {
// 选择锚点
selectAnchor.value = point.uuid
selectPenPoint.value = point.uuid
return
} else if (selectAnchor.value) {
// 选择控制点
const _index: number = (() => {
for (let j = 0; j < _points.length; j++) {
if (_points[j].uuid === selectAnchor.value) {
return j
}
}
return -1
})()
if (i <= _index + 4 && i >= _index - 4) {
selectPenPoint.value = point.uuid
} else if (i === 1 && _index === _points.length - 1) {
// 最后一个锚点(和第一个锚点重合),第二个控制点为第一个锚点的第一个控制点
selectPenPoint.value = point.uuid
}
}
}
}
return
}
}
if (!selectedComponent.value.visible) return
const { x, y, w, h, rotation, uuid} = selectedComponent.value
const { x: _x, y: _y } = rotatePoint(
{ x: getCoord(e.offsetX), y: getCoord(e.offsetY) },
{ x: x + w / 2, y: y + h / 2 },
-rotation
)
lastX = _x
lastY = _y
setSelectionForCurrentCharacterFile('')
}
监听mousemove事件,如果鼠标按下,则移动选中点,如果鼠标未按下,则检查是否划过某点,记录该点为hoverPenPoint
const onMouseMove = (e: MouseEvent) => {
if (!selectedComponent.value || !selectedComponent.value?.visible) return
const { x, y, w, h, rotation, uuid} = selectedComponent.value
const { x: _x, y: _y } = rotatePoint(
{ x: getCoord(e.offsetX), y: getCoord(e.offsetY) },
{ x: x + w / 2, y: y + h / 2 },
-rotation
)
const penComponentValue = selectedComponent.value.value as unknown as IPenComponent
const { points, closePath } = penComponentValue
if (mousedown) {
const _points = R.clone(points)
_points.map((point: IPoint, index: number) => {
if (selectPenPoint.value === point.uuid) {
// 对于闭合路径,起始锚点和收尾锚点重合,应该一致移动
if (point.type === 'anchor' && closePath && ( index < 2 || index > _points.length - 3 )) {
if (index < 2) {
for(let i = _points.length - 2; i < _points.length; i++) {
if (_points[i].type === 'anchor' && _points[i].x === point.x && _points[i].y === point.y) {
_points[i].x = _x
_points[i].y = _y
}
}
} else if (index > _points.length - 3) {
for(let i = 0; i < 2; i++) {
if (points[i].type === 'anchor' && _points[i].x === point.x && _points[i].y === point.y) {
_points[i].x = _x
_points[i].y = _y
}
}
}
}
// 对于闭合路径,起始控制点和收尾控制点重合,应该一致移动
// TODO
point.x = _x
point.y = _y
}
return point
})
modifyComponentForCurrentCharacterFile(uuid, {
value: {
points: _points
}
})
}
if (!mousedown) {
points.map((point: IPoint, index) => {
if (distance(_x, _y, point.x, point.y) <= d) {
if (point.type === 'control' && index === points.length - 1 && points.length >= 2 && point.x === points[1].x && point.y === points[1].y) {
// 如果未闭合路径,且最后一个控制点和第一个控制点重合,改变第一个控制点
return
} else {
hoverPenPoint.value = point.uuid
}
}
})
}
lastX = _x
lastY = _y
}
监听mouseup事件,清除临时变量
const onMouseUp = (e: MouseEvent) => {
const comp = glyph ? selectedComponent_glyph.value : selectedComponent.value
if (!comp || !comp.visible) return
modifyComponentValue()
mousedown = false
selectControl.value = 'null'
}
使用renderSelectPenEditor渲染控件,每次数值变化时,重新渲染
// 渲染钢笔组件选择编辑器
// render selection editor for selected pen component
const renderSelectPenEditor = (canvas: HTMLCanvasElement, d: number = 10, glyph: boolean = false) => {
if (!glyph && !selectedComponentUUID.value) return
if (glyph && !selectedComponentUUID_glyph.value) return
const comp = glyph ? selectedComponent_glyph.value : selectedComponent.value
if (!comp.visible) return
const { x, y, w, h, rotation, flipX, flipY, value: penComponentValue } = comp
const _x = mapCanvasX(x)
const _y = mapCanvasY(y)
const _w = mapCanvasWidth(w)
const _h = mapCanvasHeight(h)
const ctx: CanvasRenderingContext2D = canvas?.getContext('2d') as CanvasRenderingContext2D
const _points = transformPenPoints(glyph ? selectedComponent_glyph.value : selectedComponent.value, true)
const _map = listToMap(_points, 'uuid')
// index为selectAnchor对应的索引数值
const { index, pointType } = (() => {
for (let i = 0; i < _points.length; i++) {
if (_points[i].uuid === selectAnchor.value) {
return {
index: i,
pointType: _points[i].type
}
}
}
return { index: -1, pointType: '' }
})()
//ctx.strokeStyle = '#79bbff'
ctx.strokeStyle = '#153063'
ctx.translate(_x + _w / 2, _y + _h / 2)
ctx.rotate(rotation * Math.PI / 180)
ctx.translate(-(_x + _w / 2), -(_y + _h / 2))
if (!selectAnchor.value) {
for (let i = 0; i < _points.length; i++) {
if (_points[i].type === 'anchor') {
ctx.beginPath()
ctx.ellipse(_points[i].x, _points[i].y, d, d, 0, 0, 2 * Math.PI);
ctx.stroke()
ctx.closePath()
}
}
} else {
for (let i = 0; i < _points.length - 1; i++) {
if (_points[i].type === 'anchor') {
ctx.beginPath()
ctx.ellipse(_points[i].x, _points[i].y, d, d, 0, 0, 2 * Math.PI);
ctx.stroke()
ctx.closePath()
}
if (_points[i].type === 'control' && i >= index - 4 && i <= index + 4) {
const originUUID = _points[i].origin as string
// @ts-ignore
const originAnchor: IPoint = _map[originUUID]
ctx.strokeRect(_points[i].x - d, _points[i].y - d, 2 * d, 2 * d)
ctx.beginPath()
ctx.moveTo(_points[i].x, _points[i].y)
ctx.lineTo(originAnchor.x, originAnchor.y)
ctx.stroke()
ctx.closePath()
}
if (index === _points.length - 1 && _points[i].type === 'control' && i === 1) {
// 最后一个锚点(和第一个锚点重合),第二个控制点为第一个锚点的第一个控制点
const originUUID = _points[i].origin as string
// @ts-ignore
const originAnchor: IPoint = _map[originUUID]
ctx.strokeRect(_points[i].x - d, _points[i].y - d, 2 * d, 2 * d)
ctx.beginPath()
ctx.moveTo(_points[i].x, _points[i].y)
ctx.lineTo(originAnchor.x, originAnchor.y)
ctx.stroke()
ctx.closePath()
}
}
}
for (let i = 0; i < _points.length; i++) {
if (hoverPenPoint.value === _points[i].uuid) {
ctx.fillStyle = '#79bbff'
if (_points[i].type === 'anchor') {
ctx.beginPath()
ctx.ellipse(_points[i].x, _points[i].y, d, d, 0, 0, 2 * Math.PI);
ctx.fill()
ctx.closePath()
}
// 只显示选中锚点前后控制点
if (_points[i].type === 'control' && i >= index - 4 && i <= index + 4) {
ctx.fillRect(_points[i].x - d, _points[i].y - d, 2 * d, 2 * d)
}
ctx.fillStyle = '#fff'
}
}
ctx.setTransform(1, 0, 0, 1, 0, 0)
}