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

字玩FontPlayer开发笔记14 Vue3实现多边形工具

目录

  • 字玩FontPlayer开发笔记14 Vue3实现多边形工具
      • 笔记
        • 整体流程
        • 临时变量
        • 多边形组件数据结构
        • 初始化多边形工具
        • mousedown事件
        • mousemove事件
        • 监听mouseup事件
        • 渲染控件
        • 将多边形转换为平滑的钢笔路径

字玩FontPlayer开发笔记14 Vue3实现多边形工具

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

笔记

多变形工具允许用户创建自定义多边形形状,实现效果:
请添加图片描述

整体流程
  1. 使用points临时变量记录创建时多边形的顶点
  2. 监听mousedown事件,第一次点击时在points数组中添加首个顶点
  3. 监听mousemove事件,每次鼠标按下后第一次移动时在points数组中添加顶点,非第一次移动则改变points中最后一个顶点的位置,使其移动到鼠标当前位置
  4. 监听mouseup事件,如果路径闭合,则创建多边形组件,并重置临时变量
  5. 使用renderPolygonEditor渲染控件,每次变量更新时重新渲染控件
  6. 字玩支持将多边形路径转换为平滑的钢笔路径,使用paper.js实现
临时变量
  1. points
    记录创建时多边形的顶点数组

  2. editing
    记录当前是否在创建顶点过程中

  3. mousedown
    记录当前鼠标是否按下

  4. mousemove
    记录当前鼠标是否移动

多边形组件数据结构

每个组件最外层数据结构如下:

// 字符组件数据结构,包含变换等基础信息,与包含图形信息的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,
}

对于多边形组件,IPolygonComponent数据格式如下:

// 多边形组件
// polygon component
export interface IPolygonComponent {
	points: any;
	strokeColor: string;
	fillColor: string;
	closePath: boolean;
	contour?: Array<ILine | IQuadraticBezierCurve | ICubicBezierCurve>;
	preview?: Array<ILine | IQuadraticBezierCurve | ICubicBezierCurve>;
}

生成多边形组件代码:

// 生成多边形组件
// generate polygon component
const genPolygonComponent = (points: Array<IPoint>, closePath: boolean) => {
	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 = genPolygonContour(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 = genPolygonContour(preview_points)

	return {
		uuid: genUUID(),
		type: 'polygon',
		name: 'polygon',
		lock: false,
		visible: true,
		value: {
			points: points,
			fillColor: '',
			strokeColor: '#000',
			closePath,
			preview: preview_contour,
			contour: contour,
		} as unknown as IComponentValue,
		x,
		y,
		w,
		h,
		rotation: 0,
		flipX: false,
		flipY: false,
		usedInCharacter: true,
	}
}
初始化多边形工具

每次切换至多边形工具时,首先进行工具的初始化,包括添加事件监听器,并定义关闭工具回调方法等。

// 多边形工具初始化方法
// initializer for polygon tool
const initPolygon = (canvas: HTMLCanvasElement, glyph: boolean = false) => {
	mousedown.value = false
	mousemove.value = false
	const nearD = 5
	let closePath = false
	const onMouseDown = (e: MouseEvent) => {
    //...
	}
	const onMouseMove = (e: MouseEvent) => {
    //...
	}
	const onMouseUp = (e: MouseEvent) => {
    //...
	}
	const onEnter = (e: KeyboardEvent) => {
    //...
	}
	const onKeyDown = (e: KeyboardEvent) => {
    //...
	}
	canvas.addEventListener('mousedown', onMouseDown)
	window.addEventListener('mousemove', onMouseMove)
	window.addEventListener('mouseup', onMouseUp)
	window.addEventListener('keydown', onKeyDown)
	const closePolygon = () => {
		canvas.removeEventListener('mousedown', onMouseDown)
		window.removeEventListener('mouseup', onMouseUp)
		window.removeEventListener('keydown', onKeyDown)
		window.removeEventListener('mousemove', onMouseMove)
		setEditing(false)
		setPoints([])
		closePath = false
	}
	return closePolygon
}
mousedown事件

监听mousedown事件,第一次点击时在points数组中添加首个顶点

const onMouseDown = (e: MouseEvent) => {
  if (!points.value.length) {
    // 保存状态
    saveState('创建多边形组件', [
      StoreType.Polygon,
      glyph ? StoreType.EditGlyph : StoreType.EditCharacter
    ],
      OpType.Undo
    )
  }
  setEditing(true)
  mousedown.value = true
  if (!points.value.length) {
    const _point: IPoint = {
      uuid: genUUID(),
      x: getCoord(e.offsetX),
      y: getCoord(e.offsetY),
    }
    const _points = R.clone(points.value)
    _points.push(_point)
    setPoints(_points)
  }
}
mousemove事件

监听mousemove事件,每次鼠标按下后第一次移动时在points数组中添加顶点,非第一次移动则改变points中最后一个顶点的位置,使其移动到鼠标当前位置

const onMouseMove = (e: MouseEvent) => {
  if (!points.value.length || !editing) return
  const _points = R.clone(points.value)
  if (!mousedown.value) {
    if (!mousemove.value) {
      // 保存状态
      saveState('创建多边形组件', [
        StoreType.Polygon,
        glyph ? StoreType.EditGlyph : StoreType.EditCharacter
      ],
        OpType.Undo
      )
      // 第一次移动鼠标
      const _point = {
        uuid: genUUID(),
        x: getCoord(e.offsetX),
        y: getCoord(e.offsetY),
      }
      _points.push(_point)
      setPoints(_points)
      mousemove.value = true
    } else {
      // 移动鼠标
      const _point = _points[_points.length - 1]
      _point.x = getCoord(e.offsetX)
      _point.y = getCoord(e.offsetY)
      closePath = false
      if (isNearPoint(getCoord(e.offsetX), getCoord(e.offsetY), points.value[0].x, points.value[0].y, nearD)) {
        _point.x = points.value[0].x
        _point.y = points.value[0].y
        closePath = true
      }
      setPoints(_points)
      mousemove.value = true
    }
  }
}
监听mouseup事件

监听mouseup事件,如果路径闭合,则创建多边形组件,并重置临时变量

const onMouseUp = (e: MouseEvent) => {
  if (!points.value.length || !editing) return
  mousedown.value = false
  mousemove.value = false
  if (closePath) {
    setEditing(false)
    if (!glyph) {
      addComponentForCurrentCharacterFile(genPolygonComponent(R.clone(points.value), true))
    } else {
      addComponentForCurrentGlyph(genPolygonComponent(R.clone(points.value), true))
    }
    setPoints([])
    closePath = false
  }
}
渲染控件
// 渲染多边形编辑工具
// render polygon editor
const renderPolygonEditor = (points: IPoints, canvas: HTMLCanvasElement) => {
	const ctx: CanvasRenderingContext2D = (canvas as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D
	const _points = points.value.map((point: IPoint) => {
		return 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)
	for (let i = 1; i < _points.length; i ++) {
		ctx.lineTo(_points[i].x, _points[i].y)
	}
	ctx.stroke()
	ctx.closePath()
	for (let i = 0; i < _points.length; i++) {
		ctx.fillRect(_points[i].x - w / 2, _points[i].y - w / 2, w, w)
	}
}
将多边形转换为平滑的钢笔路径

实现效果:
请添加图片描述

const transformToPath = () => {
  savePolygonEditState()
  const polygonComponent = selectedComponent.value.value
  const { x, y, w, h, rotation, flipX, flipY } = selectedComponent.value
  const points: Array<{
    x: number,
    y: number,
  }> = transformPoints(polygonComponent.points.map((point: IPoint) => {
    return {
      x: point.x,
      y: point.y,
    }
  }), {
    x, y, w, h, rotation, flipX, flipY,
  })
  let penPoints: Array<IPenPoint> = []

  // 创建一个闭合多边形
  const segments = []
  for(let i = 0; i < points.length - 1; i++) {
    segments.push([points[i].x, points[i].y])
  }
  // 如果收尾节点和起始节点重合,则不添加
  if (points[points.length - 1].x !== points[0].x || points[points.length - 1].y !== points[0].y) {
    segments.push([points[points.length - 1].x, points[points.length - 1].y])
  }

  let path = new paper.Path({
    segments,
    closed: true,
  })
  path.smooth()
  let uuid1 = genUUID()
  for (let i = 0; i < path.curves.length; i++) {
    const curve = path.curves[i]
    const uuid2 = genUUID()
    const uuid3 = genUUID()
    penPoints.push({
      uuid: uuid1,
      x: curve.points[0].x,
      y: curve.points[0].y,
      type: 'anchor',
      origin: null,
      isShow: true,
    })
    penPoints.push({
      uuid: uuid2,
      x: curve.points[1].x,
      y: curve.points[1].y,
      type: 'control',
      origin: uuid1,
      isShow: false,
    })
    uuid1 = genUUID()
    penPoints.push({
      uuid: uuid3,
      x: curve.points[2].x,
      y: curve.points[2].y,
      type: 'control',
      origin: uuid1,
      isShow: false,
    })
    if (i >= path.curves.length - 1) {
      penPoints.push({
        uuid: uuid1,
        x: curve.points[3].x,
        y: curve.points[3].y,
        type: 'anchor',
        origin: null,
        isShow: true,
      })
    }
  }

  const { x: penX, y: penY, w: penW, h: penH } = getBound(penPoints)
  if (editStatus.value === Status.Edit) {
    modifyComponentForCurrentCharacterFile(selectedComponentUUID.value, {
      value: {
        points: penPoints,
        editMode: false,
      },
      type: 'pen',
      x: penX,
      y: penY,
      w: penW,
      h: penH,
      rotation: 0,
    })
  } else if (editStatus.value === Status.Glyph) {
    modifyComponentForCurrentGlyph(selectedComponentUUID_Glyph.value, {
      value: {
        points: penPoints,
        editMode: false,
      },
      type: 'pen',
      x: penX,
      y: penY,
      w: penW,
      h: penH,
      rotation: 0,
    })
  }
}

http://www.kler.cn/a/547062.html

相关文章:

  • wps接入DeepSeek教程
  • uniapp语音时的动态音波的实现
  • SpringBoot中Mybatis记录执行sql日志
  • 基于 STM32 平台的音频特征提取与歌曲风格智能识别系统
  • TypeScript装饰器 ------- 学习笔记分享
  • DeepSeek 模型部署与使用技术评测(基于阿里云零门槛解决方案)
  • 部署onlyoffice后,php版的callback及小魔改(logo和关于)
  • Java项目引入DeepSeek搭建私有AI
  • React历代主要更新
  • 使用EVE-NG-锐捷实现NAT
  • 尚硅谷爬虫note003
  • 微软AutoGen介绍——Managing State保存并加载持续会话的Agents和Teams
  • ML.NET库学习006:成人人口普查数据分析与分类预测
  • 第十一篇:EMC的“电磁护盾”——三电系统干扰抑制实战
  • uniapp中对于文件和文件夹的处理,内存的查询
  • 132,【1】 buuctf web [EIS 2019]EzPOP
  • Scrapy:任务队列底层设计详解
  • Unity 接入Tripo API 文生模型,模型制作动画并下载使用
  • 提供可传递的易受攻击的依赖项
  • 最新国内 ChatGPT Plus/Pro 获取教程