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

字玩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 = 0const flipX = falseconst flipY = falselet options = {unitsPerEm: 1000,descender: -200,advanceWidth: 1000,}if (editStatus.value === Status.Edit) {options.unitsPerEm = selectedFile.value.fontSettings.unitsPerEmoptions.descender = selectedFile.value.fontSettings.descenderoptions.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 = falsemousemove.value = falseconst nearD = 5let closePath = falseconst 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 = trueif (!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) returnconst _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 = falseif (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].yclosePath = true}setPoints(_points)mousemove.value = true}}
}
监听mouseup事件

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

const onMouseUp = (e: MouseEvent) => {if (!points.value.length || !editing) returnmousedown.value = falsemousemove.value = falseif (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 CanvasRenderingContext2Dconst _points = points.value.map((point: IPoint) => {return mapCanvasCoords({x: point.x,y: point.y,})})if (!_points.length) returnconst w = 10ctx.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.valueconst { x, y, w, h, rotation, flipX, flipY } = selectedComponent.valueconst 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.lryc.cn/news/536802.html

相关文章:

  • 低代码与 Vue.js:技术选型与架构设计
  • 比较循环与迭代器的性能:Rust 零成本抽象的威力
  • 一文了解zookeeper
  • 算法题(67):最长连续序列
  • 大中型企业专用数据安全系统 | 天锐蓝盾终端安全 数据安全
  • Deepseek解读 | UE像素流送与实时云渲染技术的差别
  • CTFSHOW-WEB入门-PHP特性109-115
  • 模糊综合评价法:原理、步骤与MATLAB实现
  • 【数据结构-红黑树】
  • 【STM32】舵机SG90
  • 【Linux】Socket编程—TCP
  • c++11 for auto不定参数
  • C#+redis实现消息队列的发布订阅功能
  • Docker容器基本操作
  • 从无序到有序:上北智信通过深度数据分析改善会议室资源配置
  • 总结:使用JDK原生HttpsURLConnection,封装HttpsUtil工具类,加载自定义证书验证,忽略ssl证书验证
  • 重新定义人机关系边界,Soul以AI社交构建多元社交元宇宙
  • HTTP 参数污染(HPP)详解
  • 阿里云轻量服务器docker部署nginx
  • (萌新入门)如何从起步阶段开始学习STM32 —— 我应该学习HAL库还是寄存器库?
  • Windchill开发-电子仓相关对象信息查询SQL
  • MySQL 数据库定时任务及进阶学习
  • DeepSeek教unity------MessagePack-01
  • 知识拓展:Python序列化模块 marshal 模块详解
  • leetcode 2684. 矩阵中移动的最大次数
  • 机械学习基础-6.更多分类-数据建模与机械智能课程自留
  • 自动化测试实战
  • qt QPlainTextEdit总结
  • AWS SES 邮件服务退信/投诉处理与最佳实践指南
  • 理解WebGPU 中的 GPUAdapter :连接浏览器与 GPU 的桥梁