yft-design是什么
Github: github.com/dromara/yft…
Gitee: gitee.com/dromara/yft…
预览: pro.yft.design/editor
微信: 15972699417 (如有需要可以添加作者微信进yft-design开发群)
yft-design 是一个基于Vue3、Typescript和fabric.js的开源图片设计平台。它具备丰富的图形编辑功能,支持文字,图片、形状,线条、二维码和条形码等多种元素类型的编辑与创建。每一种元素都具有高度可编辑能力,用户可以轻松调整文字样式、对图像应用滤镜、自定义形状和线条,满足多样化的设计需求。 该平台还支持缩略图显示,方便用户快速浏览和选择已编辑的内容。同时,它还拥有内建的模版库,为用户提供了快速开始设计工作的途径。无论是个人进行平面设计、教育学习中制作教学素材、企业宣传时创建海报和社交媒体封面,还是产品经理和设计师构建产品原型,yft-design都能广泛适用。
在文件导出方面,yft-design支持多种格式,包括json,svg,image,mp4,pdf(支持CMYK格式),方便用户进一步处理和存储。用户在编辑过程中,所有的更改都会即时在画布中展示,实现所见即所得的实时预览效果。此外,它还支持撤销和重做操作,通过历史记录功能保存创作不受意外影响。并且,该平台还优化了计算密集型任务,通过web worker支持保证流畅的用户体验。
想要亲自尝试一下这个强大的在线图片设计平台吗?直接访问pro.yft.design/editor,开启你的在线设计之旅吧!
yft-design的技术分析
1. Vue3 的作用
利用 Vue3 的特性,提供高性能和易于维护的前端开发环境。 Vue3 作为 yft-design 的重要技术之一,发挥着关键作用。Vue3 具有许多特性,为前端开发带来了诸多优势。 一方面,Vue3 的性能得到了显著提升。其采用了优化后的虚拟 DOM 算法(Fragments),减少了渲染时的内存开销,并且支持根据变更的部分进行局部更新,从而大大提高了应用程序的渲染性能。这对于 YFT-Design 这样的图片设计平台来说至关重要,能够确保用户在进行图形编辑等操作时,获得流畅的体验。 另一方面,Vue3 易于维护。它提供了一些新的开发工具和 API,使组件的逻辑更容易测试和重用。例如,Composition API 是 Vue3 引入的一种新的编写组件逻辑的方式,使得开发者能够更好地组织和管理组件的逻辑,提高代码的可维护性。
psd图层
<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导入编辑,支持对文本,图片,形状及智能元素的解析,支持蒙版和裁切的解析。
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渲染
pdf导出
yft-design的优势
yft-design 具有诸多显著优势。首先,它完全免费,这在众多在线设计工具中脱颖而出。现在很多在线设计软件平台都采用会员机制,免费用户导出往往有水印,且很多模板无法使用,而 yft-design 不仅没有这些限制,还提供了超多设计模版可直接使用。 其次,yft-design 功能丰富多样。它支持海报生成、电商产品图制作等多种功能,无论是个人进行平面设计、教育学习中制作教学素材、企业宣传时创建海报和社交媒体封面,还是产品经理和设计师构建产品原型,都能满足不同用户的需求。 在设计完成后,yft-design 可导出多种格式,方便用户进行二次设计。用户可以直接导出图片、SVG、PDF、JSON 等格式,这些格式方便大家导入到其他平台进行进一步的处理和编辑。
此外,yft-design 无需下载注册即可使用,为用户提供了极大的便利。用户可以直接访问pro.yft.design/editor或 github.com/dromara/yft…, 开启在线设计之旅,无需担心软件安装和注册流程的繁琐。