汝州网站建设营销渠道的三个类型
目录
- 字玩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,})}
}