前端推荐!支持辅助线的开源图片编辑器

本文介绍了国内开发者开发的Fast-Image-Editor,一个基于React和Konva的可视化图片编辑器。文章详细阐述了如何使用该工具以及其主要特性,包括布局编辑、文字编辑、图片操作等功能。此外,还深入探讨了辅助线的实现原理和代码实现,帮助读者理解项目的内部工作机制。
摘要由CSDN通过智能技术生成

教师节快乐呀

关注并将「趣谈前端」设为星标

每天定时分享技术干货/优秀开源/技术思维

今天和大家分享一款国内大佬开发的图片编辑器 fast-image-editor。文章将从如何使用技术分析, 详细和大家介绍一下这款可视化工具的实现, 我相信大家可以从这篇文章的实现方案中受益匪浅。如果你觉得这个项目对你有帮助, 也可以在 github 上点个 star, 支持一下作者。(文末会附上github地址)

案例演示

快速启动

我们可以按照以下方式来获取并启动项目:

git clone git@github.com:jiechud/fast-image-editor.git
yarn install || npm install
yarn dev 启动服务
打开浏览器访问

功能特性

目前已支持的功能有:

  • layout布局

  • 文字编辑组件

  • 图片编辑组件

  • 画布放大缩小

  • 画布右键菜单

  • 图片下载

  • 背景图支持

  • 画布参考线

  • 模版库

  • 导出图片json

技术实现

项目采用 React umi 开发框架,采用 typescript 编写,图片编辑功能用的是 react-konva,考虑后期可能核心的编辑功能整体做成一个组件,所以没有 umi 里提供的 useModel 去做状态处理,采用的是flooks。技术栈如下:

img.png

大部分工具类的软件都有辅助线,方便拖拽元素的时候对齐,能让我们快速的做出漂亮的图片。辅助线实现过程稍微有些复杂,我们一步步说下实现过程。

原理讲解

左侧辅助线出现时机:

我们以节点2为移动的元素,通过上面的图观察我们可以看出,当左侧辅助线出现的时候,节点1的x坐标和节点2的x坐标相等的时候辅助线就会出现,我们移动节点2的时候动态去判断。

右侧辅助线出现时机:

我们以节点2为移动的元素,通过上面的图观察我们可以看出,当右侧辅助线出现的时候,节点1的x+width(坐标x+节点的宽度)和节点2的x坐标相等的时候辅助线就会出现,我们移动节点2的时候动态去判断。

辅助线规则

  • 左侧辅助线 x1(x) = x2(x)

  • 右侧辅助线 x1(x+width) = x2(x)

  • 水平中间辅助线 x1(x+width/2) = x2(x+ width / 2)

  • 顶部辅助线 x1(y) = x2(y)

  • 底部辅助线 x1(y+height) = x(y)

  • 垂直中间辅助线 x1(y+height/2) = x2(+height/2)

上面的公式我们以节点2为拖动的元素,节点1为目标元素。当我们以节点1为拖动元素,节点2为目标元素,公式会有变化,大家可以自行尝试一下。

代码实现

上面我们分析出了一个节点的对比规则,画布上可能会有很多节点,让当前移动的节点去和剩下的元素去做比较。然后通过 Knova 的 layer 下的 children 获取所有元素,并记录位置,代码如下:

// 获取单个节点的位置信息
export const getLocationItem = (shapeObject: Konva.Shape) => {
  const id = shapeObject.id();
  const width = shapeObject.width();
  const height = shapeObject.height();
  const x = shapeObject.x();
  const y = shapeObject.y();

  const locationItem: LocationItem = {
    id,
    w: width,
    h: height,
    x, // x坐标
    y, // y坐标
    l: x, // 左侧方向                      
    r: x + width, // 右侧方向
    t: y,  // 顶部方向
    b: y + height, // 底部方向
    lc: x + (width / 2), // 水平居中
    tc: y + (height / 2) // 垂直居中
  }
  return locationItem;
  // console.log('locationItem=>', locationItem);
}

// 设置所有节点的信息
export const setLocationItems = (layer: Konva.Layer) => {
  locationItems = [];
  layer.children?.forEach(item => {
    if (item.className !== 'Transformer') {
      locationItems.push(getLocationItem(item));
    }
  });
}

在拖动节点的时候,调用detectionToLine方法根据计算规则画线:

/**
 * 拖动节点,shape代表当前拖动的节点
 */
export const detectionToLine = (layer: Konva.Layer, shape: Konva.Shape) => {
  const locationItem = getLocationItem(shape); // 当前节点的位置信息
  // 过滤当前节点,和剩下的节点做比较
  const compareLocations = locationItems.filter((item: LocationItem) => item.id !== locationItem.id);
  removeLines(layer); // 移除之前划过的线
  compareLocations.forEach((item: LocationItem) => {
    if ((Math.abs(locationItem.x - item.x) <= threshold)) { // 处理左侧方向
      shape.setPosition({ x: item.x, y: locationItem.y })
      addLine(layer, locationItem, item, DIRECTION.left)
    }
    if ((Math.abs(locationItem.x - item.r) <= threshold)) { // 处理右侧
      shape.setPosition({ x: item.r, y: locationItem.y })
      addLine(layer, locationItem, item, DIRECTION.right);
    }

    if ((Math.abs(locationItem.lc - item.lc) <= threshold)) { // 处理水平居中
      shape.setPosition({ x: item.lc - (locationItem.w / 2), y: locationItem.y })
      addLine(layer, locationItem, item, DIRECTION.leftCenter);
    }

    // 拖动节点和目标节点互换的判断条件
    if ((Math.abs(locationItem.r - item.x) <= threshold)) {
      shape.setPosition({ x: item.l - locationItem.w, y: locationItem.t })
      addLine(layer,item,locationItem, DIRECTION.right)
    }
    if ((Math.abs(locationItem.r - item.r) <= threshold)) { // 右侧相等
      shape.setPosition({ x: item.r - locationItem.w, y: locationItem.t })
      addLine(layer,item,locationItem, DIRECTION.right)
    }


    if ((Math.abs(locationItem.y - item.y) <= threshold)) { // 处理垂直方向顶部
      shape.setPosition({ x: locationItem.x, y: item.y })
      addLine(layer, locationItem, item, DIRECTION.top);
    }

    if ((Math.abs(locationItem.y - item.b) <= threshold)) { // 处理底部
      shape.setPosition({ x: locationItem.x, y: item.b })
      addLine(layer, locationItem, item, DIRECTION.bottom);
    }

    if ((Math.abs(locationItem.tc - item.tc) <= threshold)) { // 处理垂直顶部居中
      shape.setPosition({ x: locationItem.x, y: item.tc - (locationItem.h /2 ) })
      addLine(layer, locationItem, item, DIRECTION.topCenter);
    }

     // 拖动节点和目标节点互换的判断条件
    if ((Math.abs(locationItem.b - item.t) <= threshold)) { // 处理垂底部方向
      shape.setPosition({ x: locationItem.l, y: item.t - locationItem.h })
      addLine(layer,item,locationItem, DIRECTION.bottom)
    }

    if ((Math.abs(locationItem.b - item.b) <= threshold)) { // 右侧相等
      shape.setPosition({ x: locationItem.l, y: item.b - locationItem.h })
      addLine(layer,item,locationItem, DIRECTION.bottom)
    }
  });
}

达到阈值,添加辅助线

我们可以看到在对比的时候有这样的代码:

Math.abs(locationItem.b - item.b) <= threshold)

这块主要是用来判断两个节点之间的距离小于设定的阈值,触发添加辅助线。

还有一段设置当前节点位置的代码,如下:

 shape.setPosition({ x: locationItem.l, y: item.t - locationItem.h })

这块的主要作用是辅助线出现的是,节点移动的位置不超过阈值,节点不会动。

添加辅助线

添加辅助线会传入拖动的元素和目标元素,以及哪个方向要出现辅助线。

 addLine(layer, locationItem, item, DIRECTION.left)

根据拖动的元素和目标元素以及方向计算出辅助线出现的位置:

/**
 *
 * @param sourceItem 拖动的图形
 * @param targetItem 目标图形
 * @param targetItem 方向
 */
const getPoints = (sourceItem: LocationItem, targetItem: LocationItem, direction: DIRECTION) => {

  let minItem: LocationItem, maxItem: LocationItem;
  let points: any = [];

  let po = {
    [DIRECTION.left]: [
      [targetItem.l, sourceItem.b, targetItem.l, targetItem.t],
      [targetItem.l, targetItem.b, targetItem.l, sourceItem.t]
    ],
    [DIRECTION.right]: [
      [targetItem.r, sourceItem.b, targetItem.r, targetItem.t],
      [targetItem.r, targetItem.b, targetItem.r, sourceItem.t]
    ],
    [DIRECTION.leftCenter]: [
      [targetItem.lc, sourceItem.b, targetItem.lc, targetItem.t],
      [targetItem.lc, targetItem.b, targetItem.lc, sourceItem.t]
    ],
    [DIRECTION.top]: [
      [sourceItem.r, targetItem.t, targetItem.l, targetItem.t],
      [targetItem.r, targetItem.t, sourceItem.l, targetItem.t]
    ],
    [DIRECTION.bottom]: [
      [sourceItem.r, targetItem.b, targetItem.l, targetItem.b],
      [targetItem.r, targetItem.b, sourceItem.l, targetItem.b]
    ],
    [DIRECTION.topCenter]: [
      [sourceItem.r, targetItem.tc, targetItem.l, targetItem.tc],
      [targetItem.r, targetItem.tc, sourceItem.l, targetItem.tc]
    ]
  }

  switch (direction) {
    case DIRECTION.left:
      return sourceItem.y < targetItem.y ? po[DIRECTION.left][0] : po[DIRECTION.left][1];

    case DIRECTION.right:
      // 目标图形是否在上边
      return sourceItem.y < targetItem.y ? po[DIRECTION.right][0] : po[DIRECTION.right][1];

    case DIRECTION.leftCenter:
      return sourceItem.y < targetItem.y ? po[DIRECTION.leftCenter][0] : po[DIRECTION.leftCenter][1];

    case DIRECTION.top:
      return sourceItem.x < targetItem.x ? po[DIRECTION.top][0] : po[DIRECTION.top][1];

    case DIRECTION.bottom:
      return sourceItem.x < targetItem.x ? po[DIRECTION.bottom][0] : po[DIRECTION.bottom][1];

    case DIRECTION.topCenter:
      return sourceItem.x < targetItem.x ? po[DIRECTION.topCenter][0] : po[DIRECTION.topCenter][1];
    default:
      break;
  }
  return points;
}

添加辅助线方法,比较简单:

export const addLine = (layer: Konva.Layer, sourceItem: LocationItem, targetItem: LocationItem, direction: DIRECTION) => {
// 计算出辅助线的位置新新
  const points = getPoints(sourceItem, targetItem, direction);
  var greenLine = new Konva.Line({
    points: points,
    stroke: 'green',
    strokeWidth: 1,
    lineJoin: 'round',
    dash: [10, 10]
  })
  // greenLine.direction = direction

  lines.push(greenLine);
  layer.add(greenLine);
  layer.draw();
}

好啦, 今天的内容就到这里了, 如果觉得文章对你有帮助, 记得点赞 + 再看, 让更多的朋友从中受益~

github: https://github.com/jiechud/fast-image-editor
作者: 杰出D


从零搭建全栈可视化大屏制作平台V6.Dooring

从零设计可视化大屏搭建引擎

Dooring可视化搭建平台数据源设计剖析

可视化搭建的一些思考和实践

基于Koa + React + TS从零开发全栈文档编辑器(进阶实战

创作不易,加个点赞、在看 支持一下哦!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Pixie Image Editor 在线图片编辑器 中文版 功能 集成 - 轻松将pixie集成到任何现有项目或应用程序中。 可扩展 - Pixie接口和API可以使用新功能进行扩展。 移动 - Pixie拥有完整的移动支持,并可自动调整其界面以适应任何设备的大小。 可自定义的UI - 通过显示,隐藏或添加新菜单项,更改工具栏位置或使用不同的主题来自定义UI。 可翻译 - Pixie的界面可通过配置完全翻译。 水印 - 保存的照片可以使用指定的文本轻松加水印。 模式 - 在叠加(模态),内联或全屏模式之间进行选择。 工具API - 通过API使用所有精灵工具(调整大小,裁剪,框架等),而无需打开精灵界面。 可自定义的工具 - 所有工具都可完全自定义,您可以删除或修改和添加自定义贴纸,形状,字体,框架等。 状态 - 以json格式保存当前编辑器状态,允许使用预构建模板等功能。 照片处理 - 通过界面或API调整大小,裁剪,转换等。 滤镜 - Pixie配有许多内置滤镜,如灰度,模糊,黑白,复古等。可以通过API添加更多过滤器.. 框架 - 为任何大小的照片添加内置响应帧或添加自己的帧。 裁剪 - 将照片裁剪为指定宽高比之一,或让用户通过UI选择自定义裁剪区域。 绘图 - 功能强大的免费绘图工具支持鼠标和触摸,具有多种画笔类型,颜色等。 文本 - 完全支持向图像添加文本。可以使用数百种谷歌字体或仅使用自定义添加的字体。 形状 - 只需指定svg图像路径,即可轻松添加自定义形状。 贴纸 - 可以添加或删除自定义贴纸。任何类型的图像都可以用作贴纸。 角落 - 只需单击一下或API调用即可对图像角进行四舍五入。 空画布 - Pixie不必编辑现有照片,也可以从头开始轻松创建自定义图像。 历史记录 - 所有编辑器操作都是非破坏性的,可以通过历史记录工具轻松撤消和重做。 对象 - 所有对象(如贴纸,形状和文本)都在自己的图层上,可以通过更改颜色,添加阴影,背景等来轻松移动,调整大小,删除和修改。 图案和渐变 - 所有对象都可以使用许多内置或自定义图案和渐变填充。 保存 - 修改后的图像可以通过API或接口轻松保存在本地设备或服务器上。 缩放和平移 - 可以使用鼠标,鼠标滚轮或移动设备上的触摸和捏合手势来缩放和平移画布。 HTML5 - Pixie使用原生HTML5,这意味着它可以在每个设备上运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值