探秘 yft-design:强大的在线图片设计神器

1,105 阅读10分钟

yft-design是什么

Github: github.com/dromara/yft…
Gitee: gitee.com/dromara/yft…
预览: pro.yft.design/editor
微信: 15972699417 (如有需要可以添加作者微信进yft-design开发群) image.webp

yft-design 是一个基于Vue3、Typescript和fabric.js的开源图片设计平台。它具备丰富的图形编辑功能,支持文字,图片、形状,线条、二维码和条形码等多种元素类型的编辑与创建。每一种元素都具有高度可编辑能力,用户可以轻松调整文字样式、对图像应用滤镜、自定义形状和线条,满足多样化的设计需求。 该平台还支持缩略图显示,方便用户快速浏览和选择已编辑的内容。同时,它还拥有内建的模版库,为用户提供了快速开始设计工作的途径。无论是个人进行平面设计、教育学习中制作教学素材、企业宣传时创建海报和社交媒体封面,还是产品经理和设计师构建产品原型,yft-design都能广泛适用。

image.png 在文件导出方面,yft-design支持多种格式,包括json,svg,image,mp4,pdf(支持CMYK格式),方便用户进一步处理和存储。用户在编辑过程中,所有的更改都会即时在画布中展示,实现所见即所得的实时预览效果。此外,它还支持撤销和重做操作,通过历史记录功能保存创作不受意外影响。并且,该平台还优化了计算密集型任务,通过web worker支持保证流畅的用户体验。

想要亲自尝试一下这个强大的在线图片设计平台吗?直接访问pro.yft.design/editor,开启你的在线设计之旅吧!

yft-design的技术分析

图片1.png

1. Vue3 的作用

利用 Vue3 的特性,提供高性能和易于维护的前端开发环境。 Vue3 作为 yft-design 的重要技术之一,发挥着关键作用。Vue3 具有许多特性,为前端开发带来了诸多优势。 一方面,Vue3 的性能得到了显著提升。其采用了优化后的虚拟 DOM 算法(Fragments),减少了渲染时的内存开销,并且支持根据变更的部分进行局部更新,从而大大提高了应用程序的渲染性能。这对于 YFT-Design 这样的图片设计平台来说至关重要,能够确保用户在进行图形编辑等操作时,获得流畅的体验。 另一方面,Vue3 易于维护。它提供了一些新的开发工具和 API,使组件的逻辑更容易测试和重用。例如,Composition API 是 Vue3 引入的一种新的编写组件逻辑的方式,使得开发者能够更好地组织和管理组件的逻辑,提高代码的可维护性。

psd图层 QQ20241127-201113-HD.gif

<template>
  <div 
    ref="wrapperRef" 
    @mousedown="addDrawAreaFocus"
    v-contextmenu="contextMenus" 
    v-click-outside="remDrawAreaFocus"
  >
    <canvas ref="canvasRef" class="background-grid"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted } from 'vue'
import { useFabricStore, useMainStore, useTemplatesStore } from '@/store'
import { useRouter } from 'vue-router'
import { unzip } from "@/utils/crypto"
import { getTemplateData } from '@/api/template'
import { contextMenus } from '@/configs/contextMenu'
import { initEditor } from '@/views/Canvas/useCanvas'
import { initPixi } from '@/views/Canvas/usePixi'
import { ElMessage, ElLoading } from 'element-plus'
import useCanvasHotkey from '@/hooks/useCanvasHotkey'
const fabricStore = useFabricStore()
const mainStore = useMainStore()
const router = useRouter()
const templatesStore = useTemplatesStore()
const { wrapperRef, canvasRef } = storeToRefs(fabricStore)
const { drawAreaFocus } = storeToRefs(mainStore)
const { keydownListener, keyupListener, pasteListener } = useCanvasHotkey()


const addDrawAreaFocus = () => {
  if (!drawAreaFocus.value) mainStore.setDrawAreaFocus(true)
}

const remDrawAreaFocus = () => {
  if (drawAreaFocus.value) mainStore.setDrawAreaFocus(false)
}

const getTemplateDetail = async (pk: number) => {
  const result = await getTemplateData(pk)
  if (result.data && result.data.code === 200 && result.data.data) {
    try {
      router.push(`${router.currentRoute.value.path}?template=${pk}`)
      console.log('result.data.data.id:', result.data.data.id)
      const data = unzip(result.data.data.data)
      await templatesStore.changeTemplate(data)
    } 
    catch (error) {
      ElMessage({
        type: 'error',
        message: '模板加载失败,请联系管理员修改bug了',
      })
    }
  }
}

const initRouter = async (templateId?: number) => {
  if (templateId) {
    templatesStore.setTemplateId(templateId)
    const loadingInstance = ElLoading.service({ fullscreen: true, background: 'rgba(122, 122, 122, 0.5)' })
    await getTemplateDetail(templateId)
    nextTick(() => loadingInstance.close())
  }
}

onMounted(async () => {
  const query = router.currentRoute.value.query
  initRouter(query.template)
  initEditor(query.template)
  initPixi()
  document.addEventListener('keydown', keydownListener)
  document.addEventListener('keyup', keyupListener)
  window.addEventListener('blur', keyupListener)
  window.addEventListener('paste', pasteListener as any)
})

onUnmounted(() => {
  document.removeEventListener('keydown', keydownListener)
  document.removeEventListener('keyup', keyupListener)
  window.removeEventListener('blur', keyupListener)
  window.removeEventListener('paste', pasteListener as any)
})

</script>

<style lang="scss" scoped>
.full-size {
  height: 100%;
  width: 100%;
}
.background-grid {
  --offsetX: 0px;
  --offsetY: 0px;
  --size: 8px;
  --color: #dedcdc;
  background-image: 
    linear-gradient(45deg, var(--color) 25%, transparent 0, transparent 75%, var(--color) 0), 
    linear-gradient(45deg, var(--color) 25%, transparent 0, transparent 75%, var(--color) 0);
  background-position: var(--offsetX) var(--offsetY), calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
  background-size: calc(var(--size) * 2) calc(var(--size) * 2);
}
</style>

2. TypeScript 的优势

在 yft-design 中,TypeScript 的引入带来了多方面的优势。 首先,TypeScript 能够提高代码质量。通过静态类型检查,开发者能够在编译阶段发现潜在的错误,避免在运行时出现问题。这有助于减少调试时间,提高代码的可靠性。 其次,确保类型安全是 TypeScript 的重要特点之一。在大型项目中,类型安全可以防止数据类型不匹配等错误,提高代码的稳定性。对于 yft-design 这样的复杂应用,类型安全尤为重要,能够保障各个模块之间的数据交互正确无误。 此外,TypeScript 还提升了开发效率。它提供了丰富的类型定义和代码提示,使得开发者能够更快地理解代码结构和功能,减少开发过程中的错误和不确定性。同时,TypeScript 的模块化特性也使得代码的组织更加清晰,便于团队协作开发。

import type { Gradient, Pattern, Textbox, SerializedImageProps, Path, Rect, Image, Point, Polygon, Group, Line, FabricObject, ImageProps, IText, SerializedObjectProps } from "fabric"
import { ColorStop } from "./elements"
import JsBarcode from "jsbarcode"
import { EffectItem } from "./common"
export type LineOption = [number, number, number, number]
export type TPatternRepeat = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat'
export type ImageSource = HTMLImageElement | HTMLVideoElement | HTMLCanvasElement
export type QRCodeType = 'A1' | 'A2' | 'A3' | 'SP1' | 'SP2' | 'SP3' | 'B1'| 'C1'| 'A_a1'| 'A_a2'| 'A_b1'| 'A_b2'
export interface QRCodeOption {
  codeStyle: QRCodeType
  codeSpace: boolean
  codeError: number
}

export interface CommenElement {
  id: string
  name: string
  version: string 
  left: number
  top: number
  fillType: number
  background?: BackgroundElement
  isRotate?: boolean
}
export interface GradientElement extends Gradient<'linear' | 'radial'> {
  gradientName: string
}



export interface Template {
  id: string
  version: string
  workSpace: WorkSpaceElement
  background?: string
  backgroundImage?: SerializedImageProps
  zoom: number
  width: number
  height: number
  clip: number,
  objects: SerializedObjectProps[]
}

export interface WorkSpaceElement {
  fill?: string | Gradient<'linear' | 'radial'> | Pattern
  left: number
  top: number
  fillType: number
  angle: number
  scaleX: number
  scaleY: number
  color?: string
  opacity?: number
  imageURL?: string
  imageSize?: 'cover' | 'contain' | 'repeat'
  gaidImageURL?: string
  gaidImageMode?: string
  shadingImageURL?: string
  gradientType?: 'linear' | 'radial'
  gradientName?: string
  gradientColor?: ColorStop[]
  gradientRotate?: number
  backgroundColor?: string
}

export interface BackgroundElement {
  fill: string | Gradient<'linear' | 'radial'> | Pattern
  color: string
  fillType: number
  opacity: number
  imageURL?: string
  imageSize?: 'cover' | 'contain' | 'repeat'
  gaidImageURL?: string
  gaidImageMode?: string
  shadingImageURL?: string
  gradientType?: 'linear' | 'radial'
  gradientName?: string
  gradientColor?: ColorStop[]
  gradientRotate?: number
  gradientOffsetX?: number
  gradientOffsetY?: number
  backgroundColor?: string
}

export interface TextboxElement extends Textbox, CommenElement {
  fontFamily: string
  color: string
  fillRepeat: TPatternRepeat
  fillURL: string
  editable: boolean
}

export interface ITextElement extends IText, CommenElement {
  fontFamily: string
  color: string
  fillRepeat: TPatternRepeat
  fillURL: string
  editable: boolean
}

export interface PathElement extends Path, CommenElement {
  fill: string | Gradient<'linear'> | Gradient<'radial'>
  type: string
}

export interface RectElement extends Rect, CommenElement {
  type: string
}

export interface LineElement extends Line, CommenElement {
  startStyle?: string | null
  endStyle?: string | null
  type: string
}

export interface PolygonElement extends Polygon, CommenElement {
  type: string
  points: Point[]
}

export interface QRCodeElement extends Image, CommenElement {
  type: string
  codeContent: string
  codeOption: QRCodeOption
}

export interface BarCodeElement extends Image, CommenElement {
  type: string
  codeContent: string
  codeOption: JsBarcode.BaseOptions     
}

export interface BarcodeProps extends ImageProps {
  type: string
  codeContent: string
  codeOption: JsBarcode.BaseOptions 
}

export interface QRCodeProps extends ImageProps {
  type: string
  codeContent: string
  codeOption: QRCodeOption
}

export interface ReferenceLineProps extends Line {
  type: string
  axis: 'horizontal' | 'vertical' | ''
}

export interface ImageElement extends SerializedImageProps, CommenElement {
  type: string
  effects?: EffectItem[]
  pixiFilters?: any[]
  mask?: FabricObject
  originSrc?: string 
  isCropping?: boolean
  originId?: string
  cropPath?: FabricObject
  originLeft?: number
  originTop?: number
  originCropX?: number
  originCropY?: number
}


export interface GroupElement extends Group, CommenElement {
  type: string
  isShow: boolean
  objects: FabricObject[]
  _objects: FabricObject[]
}

export type CanvasElement = TextboxElement | LineElement | QRCodeElement | BarCodeElement | ImageElement | PathElement | GroupElement | PolygonElement | RectElement
3. fabric.js 的强大功能

fabric.js 是 yft-design 的核心技术之一,它作为强大的 HTML5 canvas 库,为图形编辑提供了坚实的基础。 fabric.js 提供了丰富的绘图功能,包括绘制基本形状(如矩形、圆形、椭圆等)、绘制路径、绘制文本、绘制图像等。同时,它还支持填充、描边、阴影、渐变等样式设置,让开发者能够轻松创建出各种复杂的图形效果。 在 yft-design 中,fabric.js 支持各种元素的动态操作。用户可以对绘制的图形进行交互和编辑,如通过拖动、缩放、旋转等操作改变图形的位置和大小,甚至可以通过编辑框直接修改文本内容。此外,fabric.js 还支持图形的选择、移动、删除、复制等操作,极大地提高了图形编辑的灵活性。 fabric.js 还支持多层图形管理,可以创建多个图层,并在图层之间进行切换和操作。这使得开发者能够更好地管理和控制图形对象,实现更加复杂和精细的图形效果。

psd导入编辑,支持对文本,图片,形状及智能元素的解析,支持蒙版和裁切的解析。 QQ20241127-195738-HD.gif

import { watch } from 'vue'
import { storeToRefs } from 'pinia'
import { Canvas, FabricObject, Textbox, Group, Point, IText, Line, ModifiedEvent } from 'fabric'
import { WorkSpaceThumbType, WorkSpaceDrawType, propertiesToInclude } from "@/configs/canvas"
import { useFabricStore } from '@/store/modules/fabric'
import { useElementBounding } from '@vueuse/core'
import { FabricTool } from '@/app/fabricTool'
import { FabricGuide } from '@/app/fabricGuide'
import { HoverBorders } from '@/app/hoverBorders'
import { WheelScroll } from '@/app/wheelScroll'
import { FabricRuler } from '@/app/fabricRuler'
import { FabricTouch } from '@/app/fabricTouch'
import { isMobile } from '@/utils/common'
import { FabricCanvas } from '@/app/fabricCanvas'
import { Keybinding } from '@/app/keybinding'
import { defaultControls, textboxControls } from '@/app/fabricControls'
import { getObjectsBoundingBox } from '@/extension/util/common'
import { useTemplatesStore } from '@/store'
import useCommon from './useCommon'
import { SnapshotType, Snapshot } from '@/types/history'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'




let canvas: null | FabricCanvas = null

// 初始化配置
const initConf = () => {
  FabricObject.ownDefaults.objectCaching = false
  FabricObject.ownDefaults.borderColor = 'blue'
  FabricObject.ownDefaults.cornerColor = 'white'
  FabricObject.ownDefaults.cornerStrokeColor = '#c0c0c0'
  FabricObject.ownDefaults.borderOpacityWhenMoving = 1
  FabricObject.ownDefaults.borderScaleFactor = 1
  FabricObject.ownDefaults.cornerSize = 8
  FabricObject.ownDefaults.cornerStyle = 'rect'
  FabricObject.ownDefaults.centeredScaling = false
  FabricObject.ownDefaults.centeredRotation = true
  FabricObject.ownDefaults.transparentCorners = false
  // FabricObject.ownDefaults.rotatingPointOffset = 1
  // FabricObject.ownDefaults.lockUniScaling = true
  // FabricObject.ownDefaults.hasRotatingPoint = false
  FabricObject.ownDefaults.controls = defaultControls()

  Object.assign(Textbox.ownDefaults, { controls: textboxControls() })
  Object.assign(IText.ownDefaults, { controls: textboxControls() })

  const mixin = {
    getWidthHeight(noFixed = false): Point {
      const objScale = (this as FabricObject).getObjectScaling()
      const point = (this as FabricObject)._getTransformedDimensions({
        scaleX: objScale.x,
        scaleY: objScale.y,
      })
      if (!noFixed) {
        point.setX(point.x)
        point.setY(point.y)
      }
      return point
    },
    getHeight() {
      return this.getWidthHeight().y
    },
    getWidth() {
      return this.getWidthHeight().x
    },
  }

  Object.assign(FabricObject.prototype, mixin)
}

// 更新视图区长宽
const setCanvasTransform = () => {
  if (!canvas) return
  const fabricStore = useFabricStore()
  const { zoom, wrapperRef, scalePercentage } = storeToRefs(fabricStore)
  const { width, height } = useElementBounding(wrapperRef.value)
  canvas.setDimensions({width: width.value, height: height.value})
  const objects = canvas.getObjects().filter(ele => !WorkSpaceThumbType.includes(ele.id))
  const boundingBox = getObjectsBoundingBox(objects)
  if (!boundingBox) return
  let boxWidth = boundingBox.width, boxHeight = boundingBox.height
  let centerX = boundingBox.centerX, centerY = boundingBox.centerY
  const workSpaceDraw = canvas.getObjects().filter(item => item.id === WorkSpaceDrawType)[0]
  if (workSpaceDraw) {
    boxWidth = workSpaceDraw.width
    boxHeight = workSpaceDraw.height
    centerX = workSpaceDraw.left + workSpaceDraw.width / 2
    centerY = workSpaceDraw.top + workSpaceDraw.height / 2 
  }
  zoom.value = Math.min(canvas.getWidth() / boxWidth, canvas.getHeight() / boxHeight) * scalePercentage.value / 100
  canvas.setZoom(zoom.value)
  canvas.absolutePan(new Point(centerX, centerY).scalarMultiply(zoom.value).subtract(canvas.getCenterPoint()), true)
}

const initCanvas = () => {
  const fabricStore = useFabricStore()
  const { canvasRef } = storeToRefs(fabricStore)
  const fabricWidth = fabricStore.getWidth()
  const fabricHeight = fabricStore.getHeight()
  if (!canvasRef.value) return
  canvas = new FabricCanvas(canvasRef.value, {
    width: fabricWidth,
    height: fabricHeight
  })
  // const keybinding = new Keybinding()
  new FabricTool(canvas)
  new FabricGuide(canvas)
  new HoverBorders(canvas)
  new WheelScroll(canvas)
  new FabricRuler(canvas)
  new FabricTouch(canvas)
  canvas.preserveObjectStacking = true
  canvas.renderAll()
}

const initEvent = () => {
  if (!canvas) return
  const templatesStore = useTemplatesStore()
  const { templateId } = storeToRefs(templatesStore)
  canvas.on('object:modified', (e: ModifiedEvent) => {
    const { transform, action } = e; 
    const target = canvas?._activeObject?.toObject(propertiesToInclude)
    const index = canvas?._objects.findIndex(item => item.id === target.id)
    if (!index) return
    const data: Snapshot = {
      type: SnapshotType.MODIFY,
      index,
      target,
      transform,
      action,
      tid: templateId.value
    };
    const { addHistorySnapshot } = useHistorySnapshot()
    addHistorySnapshot(data)
  })
}

// 初始化模板
const initTemplate = async (templateId?: number) => {
  if (!canvas) return
  const { initCommon } = useCommon()
  const templatesStore = useTemplatesStore()
  const { currentTemplate } = storeToRefs(templatesStore)
  if (templateId && Number(templateId) > 0) return
  await canvas.loadFromJSON(currentTemplate.value)
  setCanvasTransform()
  initCommon()
  initEvent()
}

export const initEditor = async (templateId?: number) => {
  const fabricStore = useFabricStore()
  const { wrapperRef } = storeToRefs(fabricStore)
  initConf()
  initCanvas()
  initTemplate(templateId)
  const { width, height } = useElementBounding(wrapperRef.value)
  watch([width, height], () => {
    setCanvasTransform()
  })
}

export default (): [FabricCanvas] => [canvas as FabricCanvas]
4. Element-Plus 的贡献

yft-design 采用了 Element-Plus UI 库,为用户带来了美观且一致的界面体验。 Element-Plus 提供了一系列高质量的界面组件,如按钮、输入框、下拉菜单等。这些组件具有简洁美观的设计风格,能够与 yft-design 的整体风格相融合,为用户提供舒适的视觉感受。 同时,Element-Plus 的组件具有高度的一致性。无论是在颜色、字体、大小等方面,还是在交互效果上,都保持了统一的风格。这使得用户在使用 yft-design 时,能够轻松熟悉和掌握各个界面元素的操作方法,提高了用户的使用效率。 此外,Element-Plus 还具有良好的可扩展性和定制性。开发者可以根据 yft-design 的特定需求,对 Element-Plus 的组件进行定制和扩展,以满足不同用户的个性化需求。

psd渲染 image.png

pdf导出 image.png

yft-design的优势

yft-design 具有诸多显著优势。首先,它完全免费,这在众多在线设计工具中脱颖而出。现在很多在线设计软件平台都采用会员机制,免费用户导出往往有水印,且很多模板无法使用,而 yft-design 不仅没有这些限制,还提供了超多设计模版可直接使用。 其次,yft-design 功能丰富多样。它支持海报生成、电商产品图制作等多种功能,无论是个人进行平面设计、教育学习中制作教学素材、企业宣传时创建海报和社交媒体封面,还是产品经理和设计师构建产品原型,都能满足不同用户的需求。 在设计完成后,yft-design 可导出多种格式,方便用户进行二次设计。用户可以直接导出图片、SVG、PDF、JSON 等格式,这些格式方便大家导入到其他平台进行进一步的处理和编辑。

此外,yft-design 无需下载注册即可使用,为用户提供了极大的便利。用户可以直接访问pro.yft.design/editorgithub.com/dromara/yft…, 开启在线设计之旅,无需担心软件安装和注册流程的繁琐。