字玩FontPlayer开发笔记14 Vue3实现多边形工具
目录
- 字玩FontPlayer开发笔记14 Vue3实现多边形工具
- 笔记
- 整体流程
- 临时变量
- 多边形组件数据结构
- 初始化多边形工具
- mousedown事件
- mousemove事件
- 监听mouseup事件
- 渲染控件
- 将多边形转换为平滑的钢笔路径
字玩FontPlayer开发笔记14 Vue3实现多边形工具
字玩FontPlayer是笔者开源的一款字体设计工具,使用Vue3 + ElementUI开发,源代码:github | gitee
笔记
多变形工具允许用户创建自定义多边形形状,实现效果:

整体流程
- 使用points临时变量记录创建时多边形的顶点
- 监听mousedown事件,第一次点击时在points数组中添加首个顶点
- 监听mousemove事件,每次鼠标按下后第一次移动时在points数组中添加顶点,非第一次移动则改变points中最后一个顶点的位置,使其移动到鼠标当前位置
- 监听mouseup事件,如果路径闭合,则创建多边形组件,并重置临时变量
- 使用renderPolygonEditor渲染控件,每次变量更新时重新渲染控件
- 字玩支持将多边形路径转换为平滑的钢笔路径,使用paper.js实现
临时变量
-
points
记录创建时多边形的顶点数组 -
editing
记录当前是否在创建顶点过程中 -
mousedown
记录当前鼠标是否按下 -
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,})}
}
相关文章:
字玩FontPlayer开发笔记14 Vue3实现多边形工具
目录 字玩FontPlayer开发笔记14 Vue3实现多边形工具笔记整体流程临时变量多边形组件数据结构初始化多边形工具mousedown事件mousemove事件监听mouseup事件渲染控件将多边形转换为平滑的钢笔路径 字玩FontPlayer开发笔记14 Vue3实现多边形工具 字玩FontPlayer是笔者开源的一款字…...
低代码与 Vue.js:技术选型与架构设计
在当下数字化转型的浪潮中,企业对应用开发的效率和质量有着极高的追求。低代码开发平台的兴起,为企业提供了一条快速构建应用的捷径,而 Vue.js 作为热门的前端框架,与低代码开发平台的结合备受关注。如何做好两者的技术选型与架构…...
比较循环与迭代器的性能:Rust 零成本抽象的威力
一、引言 在早期的 I/O 项目中,我们通过对 String 切片的索引和 clone 操作来构造配置结构体,这种方法虽然能确保数据所有权的正确传递,但既显得冗长,又引入了不必要的内存分配。随着对 Rust 迭代器特性的深入了解,我…...
一文了解zookeeper
1.ZooKeeper是什么 简单来说,她是一个分布式的,开放源码的分布式应用程序协调服务 具体来说,他可以做如下事情: 分布式配置管理:ZooKeeper可以存储配置信息,应用程序可以动态读取配置信息。分布式同步&a…...
算法题(67):最长连续序列
审题: 需要我们在O(n)的时间复杂度下找到最长的连续序列长度 思路: 我们可以用两层for循环: 第一层是依次对每个数据遍历,让他们当序列的首元素。 第二层是访问除了该元素的其他元素 但是此时时间复杂度来到…...
大中型企业专用数据安全系统 | 天锐蓝盾终端安全 数据安全
天锐蓝盾系列产品是专门为大中型企业量身定制的数据安全防护产品体系,涵盖天锐蓝盾DLP、天锐蓝盾终端安全管理系统、天锐蓝盾NAC以及其他搭配产品,致力于实现卓越的数据安全防护、施行严格的网络准入控制以及构建稳固的终端安全管理体系。通过全方位的防…...
Deepseek解读 | UE像素流送与实时云渲染技术的差别
为了实现UE引擎开发的3D/XR程序推流,绝大多数开发者会研究像素流送(Pixel Streaming)的使用方法,并尝试将插件集成在程序中。对于短时、少并发、演示场景而言,像素流送可以满足基本需求。当3D/XR项目进入落地交付周期后…...
CTFSHOW-WEB入门-PHP特性109-115
题目:web 109 1. 题目: 2. 解题思路:题目要求获得两个参数,v1 v2,if语句中的意思是要求两个参数都包含字母,条件满足的话,执行 echo new 类名(方法()…...
模糊综合评价法:原理、步骤与MATLAB实现
引言 在复杂决策场景中,评价对象往往涉及多个相互关联的模糊因素。模糊综合评价法通过建立模糊关系矩阵,结合权重分配与合成算子,实现对多因素系统的科学评价。本文详细讲解模糊综合评价法的数学原理、操作步骤,并辅以MATLAB代码…...
【数据结构-红黑树】
文章目录 红黑树红黑树介绍红黑树的五个基本性质红黑树的平衡原理红黑树的操作红黑树的操作 代码实现节点实现插入和查询操作 红黑树 红黑树介绍 红黑树(Red-Black Tree)是一种自平衡的二叉查找树(Binary Search Tree, BST)&…...
【STM32】舵机SG90
1.舵机原理 舵机内部有一个电位器,当转轴随电机旋转,电位器的电压会发生改变,电压会带动转一定的角度,舵机中的控制板就会电位器输出的电压所代表的角度,与输入的PWM所代表的角度进行比较,从而得出一个旋转…...
【Linux】Socket编程—TCP
🔥 个人主页:大耳朵土土垚 🔥 所属专栏:Linux系统编程 这里将会不定期更新有关Linux的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉 文章目…...
c++11 for auto不定参数
数量不定的模板参数。参数分为一个和一包两部分。 冒号的左边声明一个变量。右手边必须是一个容器。从容器(某种数据结构)中找出每一个元素设置到左边这个变量。11之前可以用容器的迭代器去取数据。或者标准库里的foreach...
C#+redis实现消息队列的发布订阅功能
代码 参考c#redis stream实现消息队列以及ack机制文章的思路,实现 SubscribeAttribute.cs using System;namespace DotnetQueue.Attributes {/// <summary>/// 订阅特性/// </summary>[AttributeUsage(AttributeTargets.Method, Inherited false)]pu…...
Docker容器基本操作
容器的基本操作 操作命令(全)命令(简)容器的创建docker container run <image name>docker run <image name>容器的列出(up)docker container lsdocker ps容器的列出(up和exit&…...
从无序到有序:上北智信通过深度数据分析改善会议室资源配置
当前企业普遍面临会议室资源管理难题,预约机制不完善和临时会议多导致资源调度不合理,既有空置又有过度拥挤现象。 针对上述问题,上北智信采用了专业数据分析手段,巧妙融合楼层平面图、环形图、折线图和柱形图等多种可视化工具&a…...
总结:使用JDK原生HttpsURLConnection,封装HttpsUtil工具类,加载自定义证书验证,忽略ssl证书验证
总结:使用JDK原生HttpsURLConnection,封装HttpsUtil工具类,加载自定义证书验证,忽略ssl证书验证 一HttpsUtil工具类二SSLUtil工具类 一HttpsUtil工具类 package com.example.util;import javax.net.ssl.HttpsURLConnection; impo…...
重新定义人机关系边界,Soul以AI社交构建多元社交元宇宙
近年来,AI Native应用的兴起已逐渐成为大众关注的焦点。在此背景下,Soul App的首席技术官陶明在极客公园IF2025创新大会上,发表了一场主题为“人机关系的新边界,Soul如何定义AI社交未来”的演讲。他分享了Soul在人工智能领域内的最新技术进展和战略规划,同时也将Soul社交元宇宙…...
HTTP 参数污染(HPP)详解
1. 什么是 HTTP 参数污染(HPP)? HTTP 参数污染(HTTP Parameter Pollution,简称 HPP)是一种 Web 应用攻击技术,攻击者通过在 HTTP 请求中注入多个相同的参数来绕过安全控制或篡改应用逻辑&#…...
阿里云轻量服务器docker部署nginx
拉取nginx docker镜像 sudo docker pull nginx创建以下挂载目录及文件 用户目录下:conf html logs conf: conf.d nginx.conf html: index.html conf.d: default.confnginx.conf添加文件内容 events {worker_connections 1024; }http {include /etc/ngi…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
Yolov8 目标检测蒸馏学习记录
yolov8系列模型蒸馏基本流程,代码下载:这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中,**知识蒸馏(Knowledge Distillation)**被广泛应用,作为提升模型…...
