当前位置: 首页 > news >正文

字玩FontPlayer开发笔记13 Vue3实现钢笔工具

目录

  • 字玩FontPlayer开发笔记13 Vue3实现钢笔工具
      • 笔记
        • 钢笔工具的实现效果:
        • 钢笔组件的创建
          • 整体逻辑
          • 临时变量
          • 钢笔组件数据结构
          • 钢笔工具初始化
          • mousedown事件
          • mousemove事件
          • mouseup事件
          • 绘制钢笔组件控件
        • 钢笔组件的编辑
          • 整体逻辑
          • 临时变量
          • 编辑钢笔初始化
          • 事件监听器
          • 使用renderSelectPenEditor渲染控件,每次数值变化时,重新渲染

字玩FontPlayer开发笔记13 Vue3实现钢笔工具

字玩FontPlayer是笔者开源的一款字体设计工具,使用Vue3 + ElementUI开发,源代码:github | gitee

笔记

钢笔工具是设计工具很重要的一个功能。钢笔工具通过创建一组贝塞尔曲线,使得用户可以编辑创建任意不规则的曲线形状。钢笔工具使用三次贝塞尔曲线,每段贝塞尔曲线包含两个锚点定义起点和终点,两个控制点分别定义起始切线和收尾切线,通过特定算法形成曲线线段,基本可以拟合任意形状。

钢笔工具的实现效果:

请添加图片描述

钢笔组件的创建
整体逻辑
  1. 使用points数组变量记录路径(一组贝塞尔曲线)数值,point类型可以为锚点或控制点
  2. 监听mousedown事件:
    2.1. 第一次按下鼠标时再points数组中添加一个锚点和对应控制点
    2.2. 设置当前编辑锚点为最后一个锚点
  3. 监听mousemove事件:
    3.1. 对于非第一组锚点控制点的后续节点,在每次鼠标松开后第一次移动鼠标时,添加一组锚点和控制点
    3.2. 当鼠标没有按下,也就是仅移动鼠标时,改变当前锚点为鼠标移动位置
    3.3. 当鼠标按下时,也就是拖拽状态中,改变当前控制点位置为鼠标拖拽位置
    3.4. 当鼠标移动至第一个锚点附件,自动吸附,并设置闭合路径标志为true
  4. 监听mouseup事件:
    4.1. 重置相关变量
    4.2. 当闭合路径标志为true时创建组件
  5. 监听points等相关变量变化,相关变量改变时,重新绘制控制组件
临时变量

临时变量用于记录钢笔创建过程中的数据,组件创建完即重置。

  1. points
    使用points变量记录路径中的点,其中type可以设置为anchorcontrol,对于控制点,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
}
  1. editing
    editing标识是否正在编辑钢笔路径
// 是否正在编辑钢笔路径
// whether on editing
const editing: Ref<boolean> = ref(false)
const setEditing = (status: boolean) => {
	editing.value = status
}
  1. mousedown
    mousedonw变量记录当前鼠标是否按下
  2. mousemove
    mousemove变量记录当前鼠标是否移动
  3. editAnchor
    editAnchor变量记录当前编辑的锚点
  4. 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()
		}
	}
}
钢笔组件的编辑

钢笔组件创建后,用户可以随时编辑,所以编辑钢笔组件也是一个很重要的功能。

整体逻辑
  1. 通过临时变量selectPenPoint, hoverPenPoint等控制当前选择或鼠标划过的点
  2. 监听mousedown事件,如果鼠标距离某个点距离达到一定阈值,则记录该点为选中点selectPenPoint
  3. 监听mousemove事件,如果鼠标按下,则移动选中点,如果鼠标未按下,则检查是否划过某点,记录该点为hoverPenPoint
  4. 监听mouseup事件,清除临时变量
  5. 使用renderSelectPenEditor渲染控件,每次数值变化时,重新渲染
临时变量

临时变量用于记录钢笔编辑过程中的数据,组件编辑完即重置。

  1. selectAnchor
    当前选择的锚点
  2. selectPenPoint
    当前选择的点,可能为锚点或控制点
  3. 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)
}

相关文章:

  • 【设计模式】【行为型模式】访问者模式(Visitor)
  • 《Stable Diffusion绘画完全指南:从入门到精通的Prompt设计艺术》 第二章
  • Python的那些事第十八篇:框架与算法应用研究,人工智能与机器学习
  • 深度学习算法​:ocr营业执照识别可提取字段、接口识别
  • Hello Robot 推出Stretch 3移动操作机器人,赋能研究与商业应用
  • vue3常见面试题
  • Python 面向对象(类,对象,方法,属性,魔术方法)
  • 华为防火墙pppoe拨号接入互联网配置案例
  • Java常用设计模式面试题总结(内容详细,简单易懂)
  • Linux 配置 MySQL 定时自动备份到另一台服务器
  • 《open3d+pyqt》第二章——均匀下采样
  • Spring Boot 中 “约定优于配置” 原则的理解
  • 每日温度问题:如何高效解决?
  • Java轻量级代码工程
  • C#(Winform)通过添加AForge添加并使用系统摄像机
  • Git学习使用笔记
  • JENKINS(全面)
  • 使用API有效率地管理Dynadot域名,清除某一文件夹中域名的默认DNS设置
  • 如何实现华为云+deepseek?
  • webshell通信流量分析
  • 范志毅跨界归来做青训,探索中国足球人才培养新模式
  • 万玲、胡春平调任江西省鹰潭市副市长
  • 光大华夏:近代中国私立大学遥不可及的梦想
  • 抗战回望21︱《“良民”日记》:一个“良民”在沦陷区的见闻与感受
  • 酒店取消订单加价卖何以屡禁不绝?专家建议建立黑名单并在商家页面醒目标注
  • 云南一男子酒后经常殴打七旬母亲,被警方拘14日罚600元